Skip to content
This repository was archived by the owner on Jun 28, 2023. It is now read-only.

Add ability to render output in json and yaml #717

Closed
wants to merge 1 commit into from
Closed
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
4 changes: 2 additions & 2 deletions cli/pkg/addon/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var ListCmd = &cobra.Command{
}

func init() {
ListCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Print metadata in format (yaml|json)")
ListCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (yaml|json|table)")
}

func list(cmd *cobra.Command, args []string) error {
Expand All @@ -31,7 +31,7 @@ func list(cmd *cobra.Command, args []string) error {
}

// list all packages known in the cluster
writer := utils.NewTableWriter(cmd.OutOrStdout(), "NAME", "VERSION", "DESCRIPTION")
writer := utils.NewOutputWriter(cmd.OutOrStdout(), outputFormat, "NAME", "VERSION", "DESCRIPTION")
for _, pkg := range pkgs {
writer.AddRow(pkg.Spec.PublicName, pkg.Spec.Version, pkg.Spec.Description)
}
Expand Down
4 changes: 3 additions & 1 deletion cli/pkg/addon/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func init() {

DeleteRepoCmd.Flags().StringVarP(&repoFilename, "file", "f", "", "Delete a repository based on a provided file")

ListRepoCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (yaml|json|table)")

RepositoryCmd.AddCommand(InstallRepoCmd)
RepositoryCmd.AddCommand(ListRepoCmd)
RepositoryCmd.AddCommand(DeleteRepoCmd)
Expand Down Expand Up @@ -87,7 +89,7 @@ func listRepository(cmd *cobra.Command, args []string) error {
return utils.NonUsageError(cmd, err, "listing repositories failed.")
}

writer := utils.NewTableWriter(cmd.OutOrStdout(), "NAME")
writer := utils.NewOutputWriter(cmd.OutOrStdout(), outputFormat, "NAME")
for _, repo := range repos.Items {
writer.AddRow(repo.ObjectMeta.Name)
}
Expand Down
146 changes: 106 additions & 40 deletions cli/pkg/utils/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,79 +4,145 @@
package utils

import (
"bytes"
"fmt"
"io"
"strings"

"github.com/helloeave/json"
"github.com/olekukonko/tablewriter"
"gopkg.in/yaml.v2"
)

const colWidth = 300
const indentation = ` `

type TableOutputWriter interface {
SetHeaders(headers ...string)
type OutputWriter interface {
SetKeys(headerKeys ...string)
AddRow(items ...interface{})
Render()
}

// convertToUpper will make sure all entries are upper cased.
func convertToUpper(headers []string) []string {
head := []string{}
for _, item := range headers {
head = append(head, strings.ToUpper(item))
}
type OutputType string

return head
}
const (
TableOutputType = "table"
YAMLOutputType = "yaml"
JSONOutputType = "json"
)

// NewTableWriter gets a new instance of our table output writer.
func NewTableWriter(output io.Writer, headers ...string) TableOutputWriter {
// NewOutputWriter gets a new instance of our table output writer.
func NewOutputWriter(output io.Writer, outputFormat string, headers ...string) OutputWriter {
// Initialize the output writer that we use under the covers
table := tablewriter.NewWriter(output)
table.SetBorder(false)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetHeaderLine(false)
table.SetColWidth(colWidth)
table.SetTablePadding("\t\t")
table.SetHeader(convertToUpper(headers))

t := &tableoutputwriter{}
t.out = output
t.table = table
ow := &outputwriter{}
ow.out = output
ow.outputFormat = outputFormat
ow.keys = headers

return t
return ow
}

// tableoutputwriter is our internal implementation.
type tableoutputwriter struct {
out io.Writer
table *tablewriter.Table
// outputwriter is our internal implementation.
type outputwriter struct {
out io.Writer
keys []string
values [][]string
outputFormat string
}

func (t *tableoutputwriter) SetHeaders(headers ...string) {
// SetKeys sets the values to use as the keys for the output values.
func (ow *outputwriter) SetKeys(headerKeys ...string) {
// Overwrite whatever was used in initialization
t.table.SetHeader(convertToUpper(headers))
ow.keys = headerKeys
}

// AddRow appends a new row to our table.
func (t *tableoutputwriter) AddRow(items ...interface{}) {
func (ow *outputwriter) AddRow(items ...interface{}) {
row := []string{}

// Make sure all values are ultimately strings
for _, item := range items {
row = append(row, fmt.Sprintf("%v", item))
}
t.table.Append(row)
ow.values = append(ow.values, row)
}

// Render emits the generated table to the output once ready
func (t *tableoutputwriter) Render() {
t.table.Render()
func (ow *outputwriter) Render() {
switch strings.ToLower(ow.outputFormat) {
case JSONOutputType:
renderJSON(ow)
case YAMLOutputType:
renderYAML(ow)
default:
renderTable(ow)
}

// ensures a break line after
fmt.Fprintln(ow.out)
}

func (ow *outputwriter) dataStruct() []map[string]string {
data := []map[string]string{}
keys := ow.keys
for i, k := range keys {
keys[i] = strings.ToLower(strings.ReplaceAll(k, " ", "_"))
}

for _, itemValues := range ow.values {
item := map[string]string{}
for i, value := range itemValues {
item[keys[i]] = value
}
data = append(data, item)
}

// ensures a break line after we flush the tabwriter
fmt.Fprintln(t.out)
return data
}

// renderJSON prints output as json
func renderJSON(ow *outputwriter) {
data := ow.dataStruct()
bytesJSON, err := json.MarshalSafeCollections(data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that github.com/helloeave/json forwards to github.com/homelight/json (probably an internal change of ownership). It looks to be a fork of go's encoding/json library but with a few additional "safe" methods for handling null, primarily through the MarshalSafeCollections method. But it hasn't been updated in ~1/2 year. Would it be worth including logic in our own writer in order to handle null cases so we can instead use the core encoding/json library?

Hmmm. But, on deeper inspection, it seems this isn't really handled well in go at all. If this gets around an imperfection in the go library, makes sense to me 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stmcginnis - Circling back on this one. I think it's reasonable to introduce this library so we don't have to deal with Go's handling of null cases in json. Let me know your thoughts and we can merge

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bringing this up again. I should mark this as WIP for now.

Based on discussion in Slack, the core repo really is a more appropriate place for something like this. Then our CLI plugins can just use that common functionality so we're consistent across all plugins.

That change has now merged to core. We just need to wait to use a more recent commit from there and then I can update this PR to switch over to using the common functionality.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Thanks for the update - good to hear it's made it's way into core!

if err != nil {
fmt.Fprint(ow.out, err)
return
}
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, bytesJSON, "", indentation)
if err != nil {
fmt.Fprint(ow.out, err)
return
}

fmt.Fprintf(ow.out, "%v", prettyJSON.String())
}

// renderYAML prints output as yaml
func renderYAML(ow *outputwriter) {
data := ow.dataStruct()
yamlInBytes, err := yaml.Marshal(data)
if err != nil {
fmt.Fprint(ow.out, err)
return
}

fmt.Fprintf(ow.out, "%s", yamlInBytes)
}

// renderTable prints output as a table
func renderTable(ow *outputwriter) {
table := tablewriter.NewWriter(ow.out)
table.SetBorder(false)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetHeaderLine(false)
table.SetColWidth(colWidth)
table.SetTablePadding("\t\t")
table.SetHeader(ow.keys)
table.AppendBulk(ow.values)
table.Render()
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/adrg/xdg v0.3.0 // indirect
github.com/ghodss/yaml v1.0.0
github.com/google/go-github v17.0.0+incompatible
github.com/helloeave/json v1.15.3
github.com/joshrosso/image/v5 v5.10.2-0.20210331180716-71545f2b27af
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.1.3
Expand All @@ -15,6 +16,7 @@ require (
github.com/vmware-tanzu/carvel-kapp-controller v0.18.1-0.20210414223504-f3d2ae4c5aeb
github.com/vmware-tanzu/carvel-vendir v0.18.0
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5
gopkg.in/yaml.v2 v2.4.0
honnef.co/go/tools v0.1.3 // indirect
k8s.io/api v0.20.1
k8s.io/apimachinery v0.20.1
Expand Down
Loading