From d01fcba44c370cbcf976fd8b6849b1851a7b7a6c Mon Sep 17 00:00:00 2001 From: pauhull Date: Tue, 12 Dec 2023 11:49:47 +0100 Subject: [PATCH] feat: allow YAML output --- go.mod | 6 ++-- go.sum | 22 +++++++++---- internal/cmd/all/list.go | 19 ++++++----- internal/cmd/all/list_test.go | 2 +- internal/cmd/base/create.go | 24 ++++++++------ internal/cmd/base/describe.go | 20 ++++++------ internal/cmd/base/list.go | 13 +++++--- internal/cmd/certificate/list.go | 2 +- internal/cmd/datacenter/list.go | 2 +- internal/cmd/firewall/list.go | 2 +- internal/cmd/floatingip/list.go | 2 +- internal/cmd/image/list.go | 2 +- internal/cmd/iso/list.go | 2 +- internal/cmd/loadbalancer/list.go | 2 +- internal/cmd/loadbalancer/metrics.go | 14 +++++--- internal/cmd/loadbalancertype/list.go | 2 +- internal/cmd/location/list.go | 2 +- internal/cmd/network/list.go | 2 +- internal/cmd/output/output.go | 44 ++++++++++++++------------ internal/cmd/placementgroup/list.go | 2 +- internal/cmd/primaryip/list.go | 2 +- internal/cmd/server/list.go | 2 +- internal/cmd/server/metrics.go | 14 +++++--- internal/cmd/server/request_console.go | 10 ++++-- internal/cmd/servertype/list.go | 2 +- internal/cmd/sshkey/list.go | 2 +- internal/cmd/util/util.go | 6 ++++ internal/cmd/volume/list.go | 2 +- 28 files changed, 135 insertions(+), 91 deletions(-) diff --git a/go.mod b/go.mod index 9b7ff11e..8cabd511 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/cheggaaa/pb/v3 v3.1.4 github.com/dustin/go-humanize v1.0.1 github.com/fatih/structs v1.1.0 + github.com/goccy/go-yaml v1.11.2 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 github.com/guptarohit/asciigraph v0.5.6 @@ -25,11 +26,11 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -43,6 +44,7 @@ require ( golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.6.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 020ab904..32a10a28 100644 --- a/go.sum +++ b/go.sum @@ -14,10 +14,18 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.11.2 h1:joq77SxuyIs9zzxEjgyLBugMQ9NEgTWxXfz2wVqwAaQ= +github.com/goccy/go-yaml v1.11.2/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -37,17 +45,17 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -127,6 +135,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= diff --git a/internal/cmd/all/list.go b/internal/cmd/all/list.go index a3bbb105..86dbf97c 100644 --- a/internal/cmd/all/list.go +++ b/internal/cmd/all/list.go @@ -2,7 +2,6 @@ package all import ( "context" - "encoding/json" "strings" "github.com/spf13/cobra" @@ -21,6 +20,7 @@ import ( "github.com/hetznercloud/cli/internal/cmd/primaryip" "github.com/hetznercloud/cli/internal/cmd/server" "github.com/hetznercloud/cli/internal/cmd/sshkey" + "github.com/hetznercloud/cli/internal/cmd/util" "github.com/hetznercloud/cli/internal/cmd/volume" "github.com/hetznercloud/cli/internal/hcapi2" "github.com/hetznercloud/cli/internal/state" @@ -63,7 +63,7 @@ Listed resources are: cmd.Flags().Bool("paid", false, "Only list resources that cost money") - output.AddFlag(cmd, output.OptionJSON()) + output.AddFlag(cmd, output.OptionJSON(), output.OptionYAML()) return cmd }, @@ -148,17 +148,16 @@ Listed resources are: resources[i] = response.result } - if outOpts.IsSet("json") { - jsonSchema := make(map[string]any) + if outOpts.IsSet("json") || outOpts.IsSet("yaml") { + schema := make(map[string]any) for i, lc := range cmds { - jsonSchema[lc.JSONKeyGetByName] = lc.JSONSchema(resources[i]) + schema[lc.JSONKeyGetByName] = lc.Schema(resources[i]) } - jsonBytes, err := json.Marshal(jsonSchema) - if err != nil { - return err + if outOpts.IsSet("json") { + return util.DescribeJSON(schema) + } else { + return util.DescribeYAML(schema) } - cmd.Printf("%s\n", jsonBytes) - return nil } for i, lc := range cmds { diff --git a/internal/cmd/all/list_test.go b/internal/cmd/all/list_test.go index f3562db7..bf5f4722 100644 --- a/internal/cmd/all/list_test.go +++ b/internal/cmd/all/list_test.go @@ -297,5 +297,5 @@ func TestListAllPaidJSON(t *testing.T) { "\"primary_ips\":[],\"servers\":[],\"volumes\":[]}\n" assert.NoError(t, err) - assert.Equal(t, expOut, out) + assert.JSONEq(t, expOut, out) } diff --git a/internal/cmd/base/create.go b/internal/cmd/base/create.go index ac1b9289..b379c35e 100644 --- a/internal/cmd/base/create.go +++ b/internal/cmd/base/create.go @@ -28,7 +28,7 @@ func (cc *CreateCmd) CobraCommand( ) *cobra.Command { cmd := cc.BaseCobraCommand(client) - output.AddFlag(cmd, output.OptionJSON()) + output.AddFlag(cmd, output.OptionJSON(), output.OptionYAML()) if cmd.Args == nil { cmd.Args = cobra.NoArgs @@ -46,8 +46,8 @@ func (cc *CreateCmd) CobraCommand( cmd.RunE = func(cmd *cobra.Command, args []string) error { outputFlags := output.FlagsForCommand(cmd) - isJson := outputFlags.IsSet("json") - if isJson { + isSchema := outputFlags.IsSet("json") || outputFlags.IsSet("yaml") + if isSchema { cmd.SetOut(os.Stderr) } else { cmd.SetOut(os.Stdout) @@ -58,19 +58,23 @@ func (cc *CreateCmd) CobraCommand( return err } - if isJson { + if isSchema { bytes, _ := io.ReadAll(response.Body) - var data map[string]any - if err := json.Unmarshal(bytes, &data); err != nil { + var schema map[string]any + if err := json.Unmarshal(bytes, &schema); err != nil { return err } - delete(data, "action") - delete(data, "actions") - delete(data, "next_actions") + delete(schema, "action") + delete(schema, "actions") + delete(schema, "next_actions") - return util.DescribeJSON(data) + if outputFlags.IsSet("json") { + return util.DescribeJSON(schema) + } else { + return util.DescribeYAML(schema) + } } else if resource != nil { cc.PrintResource(ctx, client, cmd, resource) } diff --git a/internal/cmd/base/describe.go b/internal/cmd/base/describe.go index dc86292d..641e48c5 100644 --- a/internal/cmd/base/describe.go +++ b/internal/cmd/base/describe.go @@ -47,7 +47,7 @@ func (dc *DescribeCmd) CobraCommand( return dc.Run(ctx, client, cmd, args) }, } - output.AddFlag(cmd, output.OptionJSON(), output.OptionFormat()) + output.AddFlag(cmd, output.OptionJSON(), output.OptionYAML(), output.OptionFormat()) if dc.AdditionalFlags != nil { dc.AdditionalFlags(cmd) } @@ -72,7 +72,9 @@ func (dc *DescribeCmd) Run(ctx context.Context, client hcapi2.Client, cmd *cobra switch { case outputFlags.IsSet("json"): - return dc.describeJSON(resp.Body) + return dc.describe(resp.Body, util.DescribeJSON) + case outputFlags.IsSet("yaml"): + return dc.describe(resp.Body, util.DescribeYAML) case outputFlags.IsSet("format"): return util.DescribeFormat(resource, outputFlags["format"][0]) default: @@ -80,18 +82,18 @@ func (dc *DescribeCmd) Run(ctx context.Context, client hcapi2.Client, cmd *cobra } } -func (dc *DescribeCmd) describeJSON(body io.ReadCloser) error { - var data map[string]interface{} - if err := json.NewDecoder(body).Decode(&data); err != nil { +func (dc *DescribeCmd) describe(body io.ReadCloser, describeFunc func(interface{}) error) error { + var schema map[string]interface{} + if err := json.NewDecoder(body).Decode(&schema); err != nil { return err } - if resource, ok := data[dc.JSONKeyGetByID]; ok { - return util.DescribeJSON(resource) + if resource, ok := schema[dc.JSONKeyGetByID]; ok { + return describeFunc(resource) } - if resources, ok := data[dc.JSONKeyGetByName].([]interface{}); ok { + if resources, ok := schema[dc.JSONKeyGetByName].([]interface{}); ok { // We check whether we got a resource at all above (see reflect-based nil check), so it's // ok to assume there's an element in resources. - return util.DescribeJSON(resources[0]) + return describeFunc(resources[0]) } return fmt.Errorf("got invalid JSON response") } diff --git a/internal/cmd/base/list.go b/internal/cmd/base/list.go index 94e55433..52a4b4c8 100644 --- a/internal/cmd/base/list.go +++ b/internal/cmd/base/list.go @@ -22,7 +22,7 @@ type ListCmd struct { Fetch func(context.Context, hcapi2.Client, *pflag.FlagSet, hcloud.ListOpts, []string) ([]interface{}, error) AdditionalFlags func(*cobra.Command) OutputTable func(client hcapi2.Client) *output.Table - JSONSchema func([]interface{}) interface{} + Schema func([]interface{}) interface{} } // CobraCommand creates a command that can be registered with cobra. @@ -45,7 +45,7 @@ func (lc *ListCmd) CobraCommand( return lc.Run(ctx, client, cmd) }, } - output.AddFlag(cmd, output.OptionNoHeader(), output.OptionColumns(outputColumns), output.OptionJSON()) + output.AddFlag(cmd, output.OptionNoHeader(), output.OptionColumns(outputColumns), output.OptionJSON(), output.OptionYAML()) cmd.Flags().StringP("selector", "l", "", "Selector to filter by labels") if lc.AdditionalFlags != nil { lc.AdditionalFlags(cmd) @@ -70,8 +70,13 @@ func (lc *ListCmd) Run(ctx context.Context, client hcapi2.Client, cmd *cobra.Com return err } - if outOpts.IsSet("json") { - return util.DescribeJSON(lc.JSONSchema(resources)) + if outOpts.IsSet("json") || outOpts.IsSet("yaml") { + schema := lc.Schema(resources) + if outOpts.IsSet("json") { + return util.DescribeJSON(schema) + } else { + return util.DescribeYAML(schema) + } } cols := lc.DefaultColumns diff --git a/internal/cmd/certificate/list.go b/internal/cmd/certificate/list.go index cd834214..b1ec9cc1 100644 --- a/internal/cmd/certificate/list.go +++ b/internal/cmd/certificate/list.go @@ -79,7 +79,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { certSchemas := make([]schema.Certificate, 0, len(resources)) for _, resource := range resources { cert := resource.(*hcloud.Certificate) diff --git a/internal/cmd/datacenter/list.go b/internal/cmd/datacenter/list.go index 4b3bc7ed..23c9bde3 100644 --- a/internal/cmd/datacenter/list.go +++ b/internal/cmd/datacenter/list.go @@ -40,7 +40,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { dcSchemas := make([]schema.Datacenter, 0, len(resources)) for _, resource := range resources { dc := resource.(*hcloud.Datacenter) diff --git a/internal/cmd/firewall/list.go b/internal/cmd/firewall/list.go index 1a20cd6e..9bb32f99 100644 --- a/internal/cmd/firewall/list.go +++ b/internal/cmd/firewall/list.go @@ -66,7 +66,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { firewallSchemas := make([]schema.Firewall, 0, len(resources)) for _, resource := range resources { firewall := resource.(*hcloud.Firewall) diff --git a/internal/cmd/floatingip/list.go b/internal/cmd/floatingip/list.go index 5d379cee..fe3b7d31 100644 --- a/internal/cmd/floatingip/list.go +++ b/internal/cmd/floatingip/list.go @@ -93,7 +93,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { floatingIPSchemas := make([]schema.FloatingIP, 0, len(resources)) for _, resource := range resources { floatingIP := resource.(*hcloud.FloatingIP) diff --git a/internal/cmd/image/list.go b/internal/cmd/image/list.go index c01ad949..73ef3603 100644 --- a/internal/cmd/image/list.go +++ b/internal/cmd/image/list.go @@ -141,7 +141,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { imageSchemas := make([]schema.Image, 0, len(resources)) for _, resource := range resources { image := resource.(*hcloud.Image) diff --git a/internal/cmd/iso/list.go b/internal/cmd/iso/list.go index 5d8f5514..95cf41b2 100644 --- a/internal/cmd/iso/list.go +++ b/internal/cmd/iso/list.go @@ -90,7 +90,7 @@ var ListCmd = base.ListCmd{ }) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { isoSchemas := make([]schema.ISO, 0, len(resources)) for _, resource := range resources { iso := resource.(*hcloud.ISO) diff --git a/internal/cmd/loadbalancer/list.go b/internal/cmd/loadbalancer/list.go index e85994d0..b607c8a7 100644 --- a/internal/cmd/loadbalancer/list.go +++ b/internal/cmd/loadbalancer/list.go @@ -82,7 +82,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { loadBalancerSchemas := make([]schema.LoadBalancer, 0, len(resources)) for _, resource := range resources { loadBalancer := resource.(*hcloud.LoadBalancer) diff --git a/internal/cmd/loadbalancer/metrics.go b/internal/cmd/loadbalancer/metrics.go index 59207c48..0bd1fb8f 100644 --- a/internal/cmd/loadbalancer/metrics.go +++ b/internal/cmd/loadbalancer/metrics.go @@ -38,7 +38,7 @@ var MetricsCmd = base.Cmd{ cmd.Flags().String("start", "", "ISO 8601 timestamp") cmd.Flags().String("end", "", "ISO 8601 timestamp") - output.AddFlag(cmd, output.OptionJSON()) + output.AddFlag(cmd, output.OptionJSON(), output.OptionYAML()) return cmd }, Run: func(ctx context.Context, client hcapi2.Client, waiter state.ActionWaiter, cmd *cobra.Command, args []string) error { @@ -83,12 +83,16 @@ var MetricsCmd = base.Cmd{ return err } switch { - case outputFlags.IsSet("json"): - var data map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + case outputFlags.IsSet("json") || outputFlags.IsSet("yaml"): + var schema map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&schema); err != nil { return err } - return util.DescribeJSON(data) + if outputFlags.IsSet("json") { + return util.DescribeJSON(schema) + } else { + return util.DescribeYAML(schema) + } default: var keys []string for k := range m.TimeSeries { diff --git a/internal/cmd/loadbalancertype/list.go b/internal/cmd/loadbalancertype/list.go index 1eaf7dbc..8102241d 100644 --- a/internal/cmd/loadbalancertype/list.go +++ b/internal/cmd/loadbalancertype/list.go @@ -38,7 +38,7 @@ var ListCmd = base.ListCmd{ AddAllowedFields(hcloud.LoadBalancerType{}) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { loadBalancerTypeSchemas := make([]schema.LoadBalancerType, 0, len(resources)) for _, resource := range resources { loadBalancerType := resource.(*hcloud.LoadBalancerType) diff --git a/internal/cmd/location/list.go b/internal/cmd/location/list.go index 87369a82..cdd306ce 100644 --- a/internal/cmd/location/list.go +++ b/internal/cmd/location/list.go @@ -37,7 +37,7 @@ var ListCmd = base.ListCmd{ AddAllowedFields(hcloud.Location{}) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { locationSchemas := make([]schema.Location, 0, len(resources)) for _, resource := range resources { location := resource.(*hcloud.Location) diff --git a/internal/cmd/network/list.go b/internal/cmd/network/list.go index 46eea6b8..bc508b98 100644 --- a/internal/cmd/network/list.go +++ b/internal/cmd/network/list.go @@ -71,7 +71,7 @@ var ListCmd = base.ListCmd{ return util.Age(network.Created, time.Now()) })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { networkSchemas := make([]schema.Network, 0, len(resources)) for _, resource := range resources { network := resource.(*hcloud.Network) diff --git a/internal/cmd/output/output.go b/internal/cmd/output/output.go index 5c494f6c..f2042dd3 100644 --- a/internal/cmd/output/output.go +++ b/internal/cmd/output/output.go @@ -19,28 +19,32 @@ import ( const flagName = "output" -type outputOption struct { +type Option struct { Name string Values []string } -func OptionNoHeader() outputOption { - return outputOption{Name: "noheader"} +func OptionNoHeader() Option { + return Option{Name: "noheader"} } -func OptionJSON() outputOption { - return outputOption{Name: "json"} +func OptionJSON() Option { + return Option{Name: "json"} } -func OptionFormat() outputOption { - return outputOption{Name: "format"} +func OptionYAML() Option { + return Option{Name: "yaml"} } -func OptionColumns(columns []string) outputOption { - return outputOption{Name: "columns", Values: columns} +func OptionFormat() Option { + return Option{Name: "format"} } -func AddFlag(cmd *cobra.Command, options ...outputOption) { +func OptionColumns(columns []string) Option { + return Option{Name: "columns", Values: columns} +} + +func AddFlag(cmd *cobra.Command, options ...Option) { var ( names []string values []string @@ -63,7 +67,7 @@ func AddFlag(cmd *cobra.Command, options ...outputOption) { cmd.PreRunE = util.ChainRunE(cmd.PreRunE, validateOutputFlag(options)) } -func validateOutputFlag(options []outputOption) func(cmd *cobra.Command, args []string) error { +func validateOutputFlag(options []Option) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { validOptions := map[string]map[string]bool{} for _, option := range options { @@ -105,34 +109,34 @@ func validateOutputFlag(options []outputOption) func(cmd *cobra.Command, args [] } } -func FlagsForCommand(cmd *cobra.Command) outputOpts { +func FlagsForCommand(cmd *cobra.Command) Opts { opts, _ := cmd.Flags().GetStringArray(flagName) return parseOutputFlags(opts) } -type outputOpts map[string][]string +type Opts map[string][]string // Set sets the key to value. It replaces any existing // values. -func (o outputOpts) Set(key, value string) { +func (o Opts) Set(key, value string) { o[key] = []string{value} } // Add adds the value to key. It appends to any existing // values associated with key. -func (o outputOpts) Add(key, value string) { +func (o Opts) Add(key, value string) { o[key] = append(o[key], value) } -func (o outputOpts) IsSet(key string) bool { +func (o Opts) IsSet(key string) bool { if values, ok := o[key]; ok && len(values) > 0 { return true } return false } -func parseOutputFlags(in []string) outputOpts { - o := outputOpts{} +func parseOutputFlags(in []string) Opts { + o := Opts{} for _, param := range in { parts := strings.SplitN(param, "=", 2) if len(parts) == 2 { @@ -252,7 +256,7 @@ func (o *Table) WriteHeader(columns []string) { } header = append(header, strings.Replace(strings.ToUpper(col), "_", " ", -1)) } - fmt.Fprintln(o.w, strings.Join(header, "\t")) + _, _ = fmt.Fprintln(o.w, strings.Join(header, "\t")) } func (o *Table) Flush() error { @@ -296,7 +300,7 @@ func (o *Table) Write(columns []string, obj interface{}) { out = append(out, fmt.Sprintf("%v", value)) } } - fmt.Fprintln(o.w, strings.Join(out, "\t")) + _, _ = fmt.Fprintln(o.w, strings.Join(out, "\t")) } func fieldName(name string) string { diff --git a/internal/cmd/placementgroup/list.go b/internal/cmd/placementgroup/list.go index b0529454..f3c5fdb9 100644 --- a/internal/cmd/placementgroup/list.go +++ b/internal/cmd/placementgroup/list.go @@ -55,7 +55,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { placementGroupSchemas := make([]schema.PlacementGroup, 0, len(resources)) for _, resource := range resources { placementGroup := resource.(*hcloud.PlacementGroup) diff --git a/internal/cmd/primaryip/list.go b/internal/cmd/primaryip/list.go index 89cfe443..826ec74c 100644 --- a/internal/cmd/primaryip/list.go +++ b/internal/cmd/primaryip/list.go @@ -96,7 +96,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { primaryIPsSchema := make([]schema.PrimaryIP, 0, len(resources)) for _, resource := range resources { primaryIP := resource.(*hcloud.PrimaryIP) diff --git a/internal/cmd/server/list.go b/internal/cmd/server/list.go index 59d169fb..1ecc9587 100644 --- a/internal/cmd/server/list.go +++ b/internal/cmd/server/list.go @@ -128,7 +128,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { serversSchema := make([]schema.Server, 0, len(resources)) for _, resource := range resources { server := resource.(*hcloud.Server) diff --git a/internal/cmd/server/metrics.go b/internal/cmd/server/metrics.go index 93616c18..a849d821 100644 --- a/internal/cmd/server/metrics.go +++ b/internal/cmd/server/metrics.go @@ -38,7 +38,7 @@ var MetricsCmd = base.Cmd{ cmd.Flags().String("start", "", "ISO 8601 timestamp") cmd.Flags().String("end", "", "ISO 8601 timestamp") - output.AddFlag(cmd, output.OptionJSON()) + output.AddFlag(cmd, output.OptionJSON(), output.OptionYAML()) return cmd }, Run: func(ctx context.Context, client hcapi2.Client, waiter state.ActionWaiter, cmd *cobra.Command, args []string) error { @@ -82,12 +82,16 @@ var MetricsCmd = base.Cmd{ return err } switch { - case outputFlags.IsSet("json"): - var data map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + case outputFlags.IsSet("json") || outputFlags.IsSet("yaml"): + var schema map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&schema); err != nil { return err } - return util.DescribeJSON(data) + if outputFlags.IsSet("json") { + return util.DescribeJSON(schema) + } else { + return util.DescribeYAML(schema) + } default: var keys []string for k := range m.TimeSeries { diff --git a/internal/cmd/server/request_console.go b/internal/cmd/server/request_console.go index 503a009a..feff1ee8 100644 --- a/internal/cmd/server/request_console.go +++ b/internal/cmd/server/request_console.go @@ -24,7 +24,7 @@ var RequestConsoleCmd = base.Cmd{ TraverseChildren: true, DisableFlagsInUseLine: true, } - output.AddFlag(cmd, output.OptionJSON()) + output.AddFlag(cmd, output.OptionJSON(), output.OptionYAML()) return cmd }, Run: func(ctx context.Context, client hcapi2.Client, waiter state.ActionWaiter, cmd *cobra.Command, args []string) error { @@ -47,7 +47,7 @@ var RequestConsoleCmd = base.Cmd{ return err } - if outOpts.IsSet("json") { + if outOpts.IsSet("json") || outOpts.IsSet("yaml") { schema := struct { WSSURL string `json:"wss_url"` Password string `json:"password"` @@ -56,7 +56,11 @@ var RequestConsoleCmd = base.Cmd{ Password: result.Password, } - return util.DescribeJSON(schema) + if outOpts.IsSet("json") { + return util.DescribeJSON(schema) + } else { + return util.DescribeYAML(schema) + } } cmd.Printf("Console for server %d:\n", server.ID) diff --git a/internal/cmd/servertype/list.go b/internal/cmd/servertype/list.go index 4330660f..a9f8f613 100644 --- a/internal/cmd/servertype/list.go +++ b/internal/cmd/servertype/list.go @@ -52,7 +52,7 @@ var ListCmd = base.ListCmd{ }) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { serverTypeSchemas := make([]schema.ServerType, 0, len(resources)) for _, resource := range resources { serverType := resource.(*hcloud.ServerType) diff --git a/internal/cmd/sshkey/list.go b/internal/cmd/sshkey/list.go index 1fdf800d..f8e5fd86 100644 --- a/internal/cmd/sshkey/list.go +++ b/internal/cmd/sshkey/list.go @@ -50,7 +50,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { sshKeySchemas := make([]schema.SSHKey, 0, len(resources)) for _, resource := range resources { sshKey := resource.(*hcloud.SSHKey) diff --git a/internal/cmd/util/util.go b/internal/cmd/util/util.go index 05d44149..f3ccb77f 100644 --- a/internal/cmd/util/util.go +++ b/internal/cmd/util/util.go @@ -9,6 +9,7 @@ import ( "text/template" "time" + "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -162,6 +163,11 @@ func DescribeJSON(object interface{}) error { return enc.Encode(object) } +func DescribeYAML(object interface{}) error { + enc := yaml.NewEncoder(os.Stdout) + return enc.Encode(object) +} + func LocationToSchema(location hcloud.Location) schema.Location { return schema.Location{ ID: location.ID, diff --git a/internal/cmd/volume/list.go b/internal/cmd/volume/list.go index fed1f483..4a2d91f0 100644 --- a/internal/cmd/volume/list.go +++ b/internal/cmd/volume/list.go @@ -76,7 +76,7 @@ var ListCmd = base.ListCmd{ })) }, - JSONSchema: func(resources []interface{}) interface{} { + Schema: func(resources []interface{}) interface{} { volumesSchema := make([]schema.Volume, 0, len(resources)) for _, resource := range resources { volume := resource.(*hcloud.Volume)