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

Add JSON output option for fluxctl's list-images and list-workloads #2834

Merged
merged 1 commit into from
Feb 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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