Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Add JSON output for fluxctl list-images
Browse files Browse the repository at this point in the history
  • Loading branch information
trevrosen authored and hiddeco committed Feb 12, 2020
1 parent a9e821b commit 914deac
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 98 deletions.
1 change: 1 addition & 0 deletions cmd/fluxctl/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ func checkExactlyOne(optsDescription string, supplied ...bool) error {
}

var errorWantedNoArgs = newUsageError("expected no (non-flag) arguments")
var errorInvalidOutputFormat = newUsageError("invalid output format specified")
157 changes: 157 additions & 0 deletions cmd/fluxctl/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"time"

"github.com/pkg/errors"

"github.com/fluxcd/flux/pkg/registry"

v6 "github.com/fluxcd/flux/pkg/api/v6"

"github.com/spf13/cobra"
)
Expand All @@ -14,6 +23,13 @@ type outputOpts struct {
verbosity int
}

const (
outputFormatJson = "json"
outputFormatTab = "tab"
)

var validOutputFormats = []string{outputFormatJson, outputFormatTab}

func AddOutputFlags(cmd *cobra.Command, opts *outputOpts) {
cmd.Flags().CountVarP(&opts.verbosity, "verbose", "v", "include skipped (and ignored, with -vv) workloads in output")
}
Expand All @@ -29,3 +45,144 @@ func makeExample(examples ...string) string {
}
return strings.TrimSuffix(buf.String(), "\n")
}

func outputFormatIsValid(format string) bool {
for _, f := range validOutputFormats {
if f == format {
return true
}
}
return false
}

// outputImagesJson sends the provided ImageStatus info to the io.Writer in JSON formatting, honoring limits in opts
func outputImagesJson(images []v6.ImageStatus, out io.Writer, opts *imageListOpts) error {
if opts.limit < 0 {
return errors.New("opts.limit cannot be less than 0")
}
var sliceLimit int

// Truncate the Available container images to honor the lesser of
// opts.limit or the total number of Available
for i := 0; i < len(images); i++ {
containerImages := images[i]
for i := 0; i < len(containerImages.Containers); i++ {
if opts.limit != 0 {
available := containerImages.Containers[i].Available
if len(available) < opts.limit {
sliceLimit = len(available)
} else {
sliceLimit = opts.limit
}
containerImages.Containers[i].Available = containerImages.Containers[i].Available[:sliceLimit]
}
}
}

e := json.NewEncoder(out)
if err := e.Encode(images); err != nil {
return err
}
return nil
}

// outputImagesTab sends the provided ImageStatus info to os.Stdout in tab formatting, honoring limits in opts
func outputImagesTab(images []v6.ImageStatus, opts *imageListOpts) {
out := newTabwriter()

if !opts.noHeaders {
fmt.Fprintln(out, "WORKLOAD\tCONTAINER\tIMAGE\tCREATED")
}

for _, image := range images {
if len(image.Containers) == 0 {
fmt.Fprintf(out, "%s\t\t\t\n", image.ID)
continue
}

imageName := image.ID.String()
for _, container := range image.Containers {
var lineCount int
containerName := container.Name
reg, repo, currentTag := container.Current.ID.Components()
if reg != "" {
reg += "/"
}
if len(container.Available) == 0 {
availableErr := container.AvailableError
if availableErr == "" {
availableErr = registry.ErrNoImageData.Error()
}
fmt.Fprintf(out, "%s\t%s\t%s%s\t%s\n", imageName, containerName, reg, repo, availableErr)
} else {
fmt.Fprintf(out, "%s\t%s\t%s%s\t\n", imageName, containerName, reg, repo)
}
foundRunning := false
for _, available := range container.Available {
running := "| "
_, _, tag := available.ID.Components()
if currentTag == tag {
running = "'->"
foundRunning = true
} else if foundRunning {
running = " "
}

lineCount++
var printEllipsis, printLine bool
if opts.limit <= 0 || lineCount <= opts.limit {
printEllipsis, printLine = false, true
} else if container.Current.ID == available.ID {
printEllipsis, printLine = lineCount > (opts.limit+1), true
}
if printEllipsis {
fmt.Fprintf(out, "\t\t%s (%d image(s) omitted)\t\n", ":", lineCount-opts.limit-1)
}
if printLine {
createdAt := ""
if !available.CreatedAt.IsZero() {
createdAt = available.CreatedAt.Format(time.RFC822)
}
fmt.Fprintf(out, "\t\t%s %s\t%s\n", running, tag, createdAt)
}
}
if !foundRunning {
running := "'->"
if currentTag == "" {
currentTag = "(untagged)"
}
fmt.Fprintf(out, "\t\t%s %s\t%s\n", running, currentTag, "?")

}
imageName = ""
}
}
out.Flush()
}

// outputWorkloadsJson sends the provided Workload data to the io.Writer as JSON
func outputWorkloadsJson(workloads []v6.ControllerStatus, out io.Writer) error {
encoder := json.NewEncoder(out)
return encoder.Encode(workloads)
}

// outputWorkloadsTab sends the provided Workload data to STDOUT, formatted with tabs for CLI
func outputWorkloadsTab(workloads []v6.ControllerStatus, opts *workloadListOpts) {
w := newTabwriter()
if !opts.noHeaders {
fmt.Fprintf(w, "WORKLOAD\tCONTAINER\tIMAGE\tRELEASE\tPOLICY\n")
}

for _, workload := range workloads {
if len(workload.Containers) > 0 {
c := workload.Containers[0]
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", workload.ID, c.Name, c.Current.ID, workload.Status, policies(workload))
for _, c := range workload.Containers[1:] {
fmt.Fprintf(w, "\t%s\t%s\t\t\n", c.Name, c.Current.ID)
}
} else {
fmt.Fprintf(w, "%s\t\t\t%s\t%s\n", workload.ID, workload.Status, policies(workload))
}
}
w.Flush()
}
98 changes: 20 additions & 78 deletions cmd/fluxctl/list_images_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@ package main

import (
"context"
"fmt"
"os"
"sort"
"time"

"github.com/spf13/cobra"

v10 "github.com/fluxcd/flux/pkg/api/v10"
v6 "github.com/fluxcd/flux/pkg/api/v6"
"github.com/fluxcd/flux/pkg/registry"
"github.com/fluxcd/flux/pkg/resource"
"github.com/fluxcd/flux/pkg/update"
)

type imageListOpts struct {
*rootOpts
namespace string
workload string
limit int
noHeaders bool
namespace string
workload string
limit int
noHeaders bool
outputFormat string

// Deprecated
controller string
Expand All @@ -41,6 +40,7 @@ func (opts *imageListOpts) Command() *cobra.Command {
cmd.Flags().StringVarP(&opts.workload, "workload", "w", "", "Show images for this workload")
cmd.Flags().IntVarP(&opts.limit, "limit", "l", 10, "Number of images to show (0 for all)")
cmd.Flags().BoolVar(&opts.noHeaders, "no-headers", false, "Don't print headers (default print headers)")
cmd.Flags().StringVarP(&opts.outputFormat, "output-format", "o", "tab", "Output format (tab or json)")

// Deprecated
cmd.Flags().StringVarP(&opts.controller, "controller", "c", "", "Show images for this controller")
Expand All @@ -53,6 +53,11 @@ func (opts *imageListOpts) RunE(cmd *cobra.Command, args []string) error {
if len(args) != 0 {
return errorWantedNoArgs
}

if !outputFormatIsValid(opts.outputFormat) {
return errorInvalidOutputFormat
}

ns := getKubeConfigContextNamespaceOrDefault(opts.namespace, "default", opts.Context)
imageOpts := v10.ListImagesOptions{
Spec: update.ResourceSpecAll,
Expand All @@ -61,7 +66,7 @@ func (opts *imageListOpts) RunE(cmd *cobra.Command, args []string) error {
// Backwards compatibility with --controller until we remove it
switch {
case opts.workload != "" && opts.controller != "":
return newUsageError("can't specify both the controller and workload")
return newUsageError("can't specify both the controller and image")
case opts.controller != "":
opts.workload = opts.controller
}
Expand All @@ -76,83 +81,20 @@ func (opts *imageListOpts) RunE(cmd *cobra.Command, args []string) error {

ctx := context.Background()

workloads, err := opts.API.ListImagesWithOptions(ctx, imageOpts)
images, err := opts.API.ListImagesWithOptions(ctx, imageOpts)
if err != nil {
return err
}

sort.Sort(imageStatusByName(workloads))

out := newTabwriter()
sort.Sort(imageStatusByName(images))

if !opts.noHeaders {
fmt.Fprintln(out, "WORKLOAD\tCONTAINER\tIMAGE\tCREATED")
switch opts.outputFormat {
case outputFormatJson:
return outputImagesJson(images, os.Stdout, opts)
default:
outputImagesTab(images, opts)
}

for _, workload := range workloads {
if len(workload.Containers) == 0 {
fmt.Fprintf(out, "%s\t\t\t\n", workload.ID)
continue
}

workloadName := workload.ID.String()
for _, container := range workload.Containers {
var lineCount int
containerName := container.Name
reg, repo, currentTag := container.Current.ID.Components()
if reg != "" {
reg += "/"
}
if len(container.Available) == 0 {
availableErr := container.AvailableError
if availableErr == "" {
availableErr = registry.ErrNoImageData.Error()
}
fmt.Fprintf(out, "%s\t%s\t%s%s\t%s\n", workloadName, containerName, reg, repo, availableErr)
} else {
fmt.Fprintf(out, "%s\t%s\t%s%s\t\n", workloadName, containerName, reg, repo)
}
foundRunning := false
for _, available := range container.Available {
running := "| "
_, _, tag := available.ID.Components()
if currentTag == tag {
running = "'->"
foundRunning = true
} else if foundRunning {
running = " "
}

lineCount++
var printEllipsis, printLine bool
if opts.limit <= 0 || lineCount <= opts.limit {
printEllipsis, printLine = false, true
} else if container.Current.ID == available.ID {
printEllipsis, printLine = lineCount > (opts.limit+1), true
}
if printEllipsis {
fmt.Fprintf(out, "\t\t%s (%d image(s) omitted)\t\n", ":", lineCount-opts.limit-1)
}
if printLine {
createdAt := ""
if !available.CreatedAt.IsZero() {
createdAt = available.CreatedAt.Format(time.RFC822)
}
fmt.Fprintf(out, "\t\t%s %s\t%s\n", running, tag, createdAt)
}
}
if !foundRunning {
running := "'->"
if currentTag == "" {
currentTag = "(untagged)"
}
fmt.Fprintf(out, "\t\t%s %s\t%s\n", running, currentTag, "?")

}
workloadName = ""
}
}
out.Flush()
return nil
}

Expand Down
Loading

0 comments on commit 914deac

Please sign in to comment.