From 7643fa256da94b962c785b0b5af965e090ef66a4 Mon Sep 17 00:00:00 2001 From: phm07 <22707808+phm07@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:30:03 +0200 Subject: [PATCH] refactor: prepare for e2e tests (#870) This PR adds refactors that are necessary for the e2e tests. Changes: - Subcommand initialization has been moved to `internal/cli/root.go` - `output.Table`s now accept writers as output. This is needed to catch command output. If we only catch stdout we can only run one command at once or there will be interference. --- cmd/hcloud/main.go | 49 --------------------- internal/cli/root.go | 61 +++++++++++++++++++++++--- internal/cmd/all/list.go | 7 +-- internal/cmd/base/create.go | 15 +++++-- internal/cmd/base/describe.go | 17 ++++--- internal/cmd/base/list.go | 44 ++++++++++++------- internal/cmd/base/list_test.go | 4 +- internal/cmd/certificate/list.go | 4 +- internal/cmd/config/list.go | 6 +-- internal/cmd/context/active_test.go | 4 ++ internal/cmd/context/create_test.go | 4 ++ internal/cmd/context/list.go | 13 +++--- internal/cmd/context/use_test.go | 4 ++ internal/cmd/datacenter/list.go | 4 +- internal/cmd/firewall/list.go | 4 +- internal/cmd/floatingip/list.go | 4 +- internal/cmd/image/list.go | 4 +- internal/cmd/iso/list.go | 4 +- internal/cmd/loadbalancer/list.go | 4 +- internal/cmd/loadbalancer/metrics.go | 4 +- internal/cmd/loadbalancertype/list.go | 4 +- internal/cmd/location/list.go | 4 +- internal/cmd/network/list.go | 4 +- internal/cmd/output/output.go | 5 +-- internal/cmd/output/output_test.go | 3 +- internal/cmd/placementgroup/list.go | 4 +- internal/cmd/primaryip/list.go | 4 +- internal/cmd/server/list.go | 4 +- internal/cmd/server/metrics.go | 4 +- internal/cmd/server/request_console.go | 4 +- internal/cmd/server/reset_password.go | 4 +- internal/cmd/servertype/list.go | 4 +- internal/cmd/sshkey/list.go | 4 +- internal/cmd/util/util.go | 14 +++--- internal/cmd/util/util_test.go | 58 +++++++++++------------- internal/cmd/volume/list.go | 4 +- 36 files changed, 211 insertions(+), 177 deletions(-) diff --git a/cmd/hcloud/main.go b/cmd/hcloud/main.go index 0514e89d..5f0ac95a 100644 --- a/cmd/hcloud/main.go +++ b/cmd/hcloud/main.go @@ -5,28 +5,6 @@ import ( "os" "github.com/hetznercloud/cli/internal/cli" - "github.com/hetznercloud/cli/internal/cmd/all" - "github.com/hetznercloud/cli/internal/cmd/certificate" - "github.com/hetznercloud/cli/internal/cmd/completion" - configCmd "github.com/hetznercloud/cli/internal/cmd/config" - "github.com/hetznercloud/cli/internal/cmd/context" - "github.com/hetznercloud/cli/internal/cmd/datacenter" - "github.com/hetznercloud/cli/internal/cmd/firewall" - "github.com/hetznercloud/cli/internal/cmd/floatingip" - "github.com/hetznercloud/cli/internal/cmd/image" - "github.com/hetznercloud/cli/internal/cmd/iso" - "github.com/hetznercloud/cli/internal/cmd/loadbalancer" - "github.com/hetznercloud/cli/internal/cmd/loadbalancertype" - "github.com/hetznercloud/cli/internal/cmd/location" - "github.com/hetznercloud/cli/internal/cmd/network" - "github.com/hetznercloud/cli/internal/cmd/placementgroup" - "github.com/hetznercloud/cli/internal/cmd/primaryip" - "github.com/hetznercloud/cli/internal/cmd/server" - "github.com/hetznercloud/cli/internal/cmd/servertype" - "github.com/hetznercloud/cli/internal/cmd/sshkey" - "github.com/hetznercloud/cli/internal/cmd/util" - "github.com/hetznercloud/cli/internal/cmd/version" - "github.com/hetznercloud/cli/internal/cmd/volume" "github.com/hetznercloud/cli/internal/state" "github.com/hetznercloud/cli/internal/state/config" ) @@ -50,33 +28,6 @@ func main() { rootCommand := cli.NewRootCommand(s) - util.AddGroup(rootCommand, "resource", "Resources", - all.NewCommand(s), - floatingip.NewCommand(s), - image.NewCommand(s), - server.NewCommand(s), - sshkey.NewCommand(s), - servertype.NewCommand(s), - datacenter.NewCommand(s), - location.NewCommand(s), - iso.NewCommand(s), - volume.NewCommand(s), - network.NewCommand(s), - loadbalancer.NewCommand(s), - loadbalancertype.NewCommand(s), - certificate.NewCommand(s), - firewall.NewCommand(s), - placementgroup.NewCommand(s), - primaryip.NewCommand(s), - ) - - rootCommand.AddCommand( - version.NewCommand(s), - completion.NewCommand(s), - context.NewCommand(s), - configCmd.NewCommand(s), - ) - if err := rootCommand.Execute(); err != nil { log.Fatalln(err) } diff --git a/internal/cli/root.go b/internal/cli/root.go index 4a43571d..1aa93843 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -1,10 +1,32 @@ package cli import ( - "os" + "io" "github.com/spf13/cobra" + "github.com/hetznercloud/cli/internal/cmd/all" + "github.com/hetznercloud/cli/internal/cmd/certificate" + "github.com/hetznercloud/cli/internal/cmd/completion" + configCmd "github.com/hetznercloud/cli/internal/cmd/config" + "github.com/hetznercloud/cli/internal/cmd/context" + "github.com/hetznercloud/cli/internal/cmd/datacenter" + "github.com/hetznercloud/cli/internal/cmd/firewall" + "github.com/hetznercloud/cli/internal/cmd/floatingip" + "github.com/hetznercloud/cli/internal/cmd/image" + "github.com/hetznercloud/cli/internal/cmd/iso" + "github.com/hetznercloud/cli/internal/cmd/loadbalancer" + "github.com/hetznercloud/cli/internal/cmd/loadbalancertype" + "github.com/hetznercloud/cli/internal/cmd/location" + "github.com/hetznercloud/cli/internal/cmd/network" + "github.com/hetznercloud/cli/internal/cmd/placementgroup" + "github.com/hetznercloud/cli/internal/cmd/primaryip" + "github.com/hetznercloud/cli/internal/cmd/server" + "github.com/hetznercloud/cli/internal/cmd/servertype" + "github.com/hetznercloud/cli/internal/cmd/sshkey" + "github.com/hetznercloud/cli/internal/cmd/util" + "github.com/hetznercloud/cli/internal/cmd/version" + "github.com/hetznercloud/cli/internal/cmd/volume" "github.com/hetznercloud/cli/internal/state" "github.com/hetznercloud/cli/internal/state/config" ) @@ -20,6 +42,33 @@ func NewRootCommand(s state.State) *cobra.Command { DisableFlagsInUseLine: true, } + util.AddGroup(cmd, "resource", "Resources", + all.NewCommand(s), + floatingip.NewCommand(s), + image.NewCommand(s), + server.NewCommand(s), + sshkey.NewCommand(s), + servertype.NewCommand(s), + datacenter.NewCommand(s), + location.NewCommand(s), + iso.NewCommand(s), + volume.NewCommand(s), + network.NewCommand(s), + loadbalancer.NewCommand(s), + loadbalancertype.NewCommand(s), + certificate.NewCommand(s), + firewall.NewCommand(s), + placementgroup.NewCommand(s), + primaryip.NewCommand(s), + ) + + cmd.AddCommand( + version.NewCommand(s), + completion.NewCommand(s), + context.NewCommand(s), + configCmd.NewCommand(s), + ) + cmd.PersistentFlags().AddFlagSet(s.Config().FlagSet()) for _, opt := range config.Options { @@ -35,17 +84,15 @@ func NewRootCommand(s state.State) *cobra.Command { } cmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { - var err error - out := os.Stdout + out := cmd.OutOrStdout() quiet, err := config.OptionQuiet.Get(s.Config()) if err != nil { return err } if quiet { - out, err = os.Open(os.DevNull) - if err != nil { - return err - } + // We save the original output in cmd.errWriter so that we can still use it if we need it later. + cmd.SetErr(out) + out = io.Discard } cmd.SetOut(out) return nil diff --git a/internal/cmd/all/list.go b/internal/cmd/all/list.go index 3925a1fc..a21d5e2d 100644 --- a/internal/cmd/all/list.go +++ b/internal/cmd/all/list.go @@ -153,14 +153,15 @@ Listed resources are: schema[lc.JSONKeyGetByName] = lc.Schema(resources[i]) } if outOpts.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(cmd.OutOrStdout(), schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(cmd.OutOrStdout(), schema) } for i, lc := range cmds { cols := lc.DefaultColumns - table := lc.OutputTable(s.Client()) + table := output.NewTable(cmd.OutOrStdout()) + lc.OutputTable(table, s.Client()) table.WriteHeader(cols) if len(resources[i]) == 0 { diff --git a/internal/cmd/base/create.go b/internal/cmd/base/create.go index 672de2fa..f9479bdd 100644 --- a/internal/cmd/base/create.go +++ b/internal/cmd/base/create.go @@ -48,9 +48,16 @@ func (cc *CreateCmd) CobraCommand(s state.State) *cobra.Command { return err } + schemaOut := cmd.OutOrStdout() isSchema := outputFlags.IsSet("json") || outputFlags.IsSet("yaml") - if isSchema && !quiet { - cmd.SetOut(os.Stderr) + if isSchema { + if quiet { + // If we are in quiet mode, we saved the original output in cmd.errWriter. We can now restore it. + schemaOut = cmd.ErrOrStderr() + } else { + // We don't want anything other than the schema in stdout, so we set the default to stderr + cmd.SetOut(os.Stderr) + } } resource, schema, err := cc.Run(s, cmd, args) @@ -60,9 +67,9 @@ func (cc *CreateCmd) CobraCommand(s state.State) *cobra.Command { if isSchema { if outputFlags.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(schemaOut, schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(schemaOut, schema) } else if cc.PrintResource != nil && resource != nil { cc.PrintResource(s, cmd, resource) } diff --git a/internal/cmd/base/describe.go b/internal/cmd/base/describe.go index c916175b..73fa8925 100644 --- a/internal/cmd/base/describe.go +++ b/internal/cmd/base/describe.go @@ -60,9 +60,16 @@ func (dc *DescribeCmd) Run(s state.State, cmd *cobra.Command, args []string) err return err } + schemaOut := cmd.OutOrStdout() isSchema := outputFlags.IsSet("json") || outputFlags.IsSet("yaml") - if isSchema && !quiet { - cmd.SetOut(os.Stderr) + if isSchema { + if quiet { + // If we are in quiet mode, we saved the original output in cmd.errWriter. We can now restore it. + schemaOut = cmd.ErrOrStderr() + } else { + // We don't want anything other than the schema in stdout, so we set the default to stderr + cmd.SetOut(os.Stderr) + } } idOrName := args[0] @@ -79,11 +86,11 @@ func (dc *DescribeCmd) Run(s state.State, cmd *cobra.Command, args []string) err switch { case outputFlags.IsSet("json"): - return util.DescribeJSON(schema) + return util.DescribeJSON(schemaOut, schema) case outputFlags.IsSet("yaml"): - return util.DescribeYAML(schema) + return util.DescribeYAML(schemaOut, schema) case outputFlags.IsSet("format"): - return util.DescribeFormat(resource, outputFlags["format"][0]) + return util.DescribeFormat(schemaOut, resource, outputFlags["format"][0]) default: return dc.PrintText(s, cmd, resource) } diff --git a/internal/cmd/base/list.go b/internal/cmd/base/list.go index 0d5d9e2c..1e4763b7 100644 --- a/internal/cmd/base/list.go +++ b/internal/cmd/base/list.go @@ -2,6 +2,7 @@ package base import ( "fmt" + "io" "os" "github.com/spf13/cobra" @@ -23,13 +24,15 @@ type ListCmd struct { DefaultColumns []string Fetch func(state.State, *pflag.FlagSet, hcloud.ListOpts, []string) ([]interface{}, error) AdditionalFlags func(*cobra.Command) - OutputTable func(client hcapi2.Client) *output.Table + OutputTable func(t *output.Table, client hcapi2.Client) Schema func([]interface{}) interface{} } // CobraCommand creates a command that can be registered with cobra. func (lc *ListCmd) CobraCommand(s state.State) *cobra.Command { - outputColumns := lc.OutputTable(s.Client()).Columns() + t := output.NewTable(io.Discard) + lc.OutputTable(t, s.Client()) + outputColumns := t.Columns() cmd := &cobra.Command{ Use: "list [options]", @@ -59,10 +62,9 @@ func (lc *ListCmd) CobraCommand(s state.State) *cobra.Command { func (lc *ListCmd) Run(s state.State, cmd *cobra.Command) error { outOpts := output.FlagsForCommand(cmd) - labelSelector, _ := cmd.Flags().GetString("selector") - listOpts := hcloud.ListOpts{ - LabelSelector: labelSelector, - PerPage: 50, + quiet, err := config.OptionQuiet.Get(s.Config()) + if err != nil { + return err } var sorts []string @@ -80,17 +82,30 @@ func (lc *ListCmd) Run(s state.State, cmd *cobra.Command) error { } } + labelSelector, _ := cmd.Flags().GetString("selector") + listOpts := hcloud.ListOpts{ + LabelSelector: labelSelector, + PerPage: 50, + } + resources, err := lc.Fetch(s, cmd.Flags(), listOpts, sorts) if err != nil { return err } - if outOpts.IsSet("json") || outOpts.IsSet("yaml") { + out := cmd.OutOrStdout() + if quiet { + // If we are in quiet mode, we saved the original output in cmd.errWriter. We can now restore it. + out = cmd.ErrOrStderr() + } + + isSchema := outOpts.IsSet("json") || outOpts.IsSet("yaml") + if isSchema { schema := lc.Schema(resources) if outOpts.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(out, schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(out, schema) } cols := lc.DefaultColumns @@ -98,14 +113,13 @@ func (lc *ListCmd) Run(s state.State, cmd *cobra.Command) error { cols = outOpts["columns"] } - table := lc.OutputTable(s.Client()) + t := output.NewTable(out) + lc.OutputTable(t, s.Client()) if !outOpts.IsSet("noheader") { - table.WriteHeader(cols) + t.WriteHeader(cols) } for _, resource := range resources { - table.Write(cols, resource) + t.Write(cols, resource) } - table.Flush() - - return nil + return t.Flush() } diff --git a/internal/cmd/base/list_test.go b/internal/cmd/base/list_test.go index cfa9866a..09b0f704 100644 --- a/internal/cmd/base/list_test.go +++ b/internal/cmd/base/list_test.go @@ -25,8 +25,8 @@ var fakeListCmd = &base.ListCmd{ return i }, - OutputTable: func(hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.Firewall{}). AddFieldFn("id", func(obj interface{}) string { rsc := obj.(*fakeResource) diff --git a/internal/cmd/certificate/list.go b/internal/cmd/certificate/list.go index 58005e35..9a0e7f1a 100644 --- a/internal/cmd/certificate/list.go +++ b/internal/cmd/certificate/list.go @@ -36,8 +36,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(_ hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.Certificate{}). RemoveAllowedField("certificate", "chain"). AddFieldFn("labels", output.FieldFn(func(obj interface{}) string { diff --git a/internal/cmd/config/list.go b/internal/cmd/config/list.go index 0298ba2a..14f80d74 100644 --- a/internal/cmd/config/list.go +++ b/internal/cmd/config/list.go @@ -73,9 +73,9 @@ func runList(s state.State, cmd *cobra.Command, _ []string) error { if outOpts.IsSet("json") || outOpts.IsSet("yaml") { schema := util.Wrap("options", options) if outOpts.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(cmd.OutOrStdout(), schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(cmd.OutOrStdout(), schema) } cols := outputColumns @@ -83,7 +83,7 @@ func runList(s state.State, cmd *cobra.Command, _ []string) error { cols = outOpts["columns"] } - t := output.NewTable() + t := output.NewTable(cmd.OutOrStdout()) t.AddAllowedFields(option{}) if !outOpts.IsSet("noheader") { t.WriteHeader(cols) diff --git a/internal/cmd/context/active_test.go b/internal/cmd/context/active_test.go index db312be5..d3743eed 100644 --- a/internal/cmd/context/active_test.go +++ b/internal/cmd/context/active_test.go @@ -12,6 +12,10 @@ import ( ) func TestActive(t *testing.T) { + // Make sure we don't have any environment variables set that could interfere with the test + t.Setenv("HCLOUD_TOKEN", "") + t.Setenv("HCLOUD_CONTEXT", "") + testConfig := ` active_context = "my-context" diff --git a/internal/cmd/context/create_test.go b/internal/cmd/context/create_test.go index 68795ef7..4443c989 100644 --- a/internal/cmd/context/create_test.go +++ b/internal/cmd/context/create_test.go @@ -13,6 +13,10 @@ import ( ) func TestCreate(t *testing.T) { + // Make sure we don't have any environment variables set that could interfere with the test + t.Setenv("HCLOUD_TOKEN", "") + t.Setenv("HCLOUD_CONTEXT", "") + testConfig := ` active_context = "my-context" diff --git a/internal/cmd/context/list.go b/internal/cmd/context/list.go index 43f3a465..b05f6daf 100644 --- a/internal/cmd/context/list.go +++ b/internal/cmd/context/list.go @@ -1,6 +1,8 @@ package context import ( + "io" + "github.com/spf13/cobra" "github.com/hetznercloud/cli/internal/cmd/output" @@ -15,7 +17,7 @@ type presentation struct { } func NewListCommand(s state.State) *cobra.Command { - cols := newListOutputTable().Columns() + cols := newListOutputTable(io.Discard).Columns() cmd := &cobra.Command{ Use: "list [options]", Short: "List contexts", @@ -40,7 +42,7 @@ func runList(s state.State, cmd *cobra.Command, _ []string) error { cols = outOpts["columns"] } - tw := newListOutputTable() + tw := newListOutputTable(cmd.OutOrStdout()) if err := tw.ValidateColumns(cols); err != nil { return err } @@ -61,12 +63,11 @@ func runList(s state.State, cmd *cobra.Command, _ []string) error { tw.Write(cols, presentation) } - tw.Flush() - return nil + return tw.Flush() } -func newListOutputTable() *output.Table { - return output.NewTable(). +func newListOutputTable(w io.Writer) *output.Table { + return output.NewTable(w). AddAllowedFields(presentation{}). RemoveAllowedField("token") } diff --git a/internal/cmd/context/use_test.go b/internal/cmd/context/use_test.go index 40051c7e..bec15cce 100644 --- a/internal/cmd/context/use_test.go +++ b/internal/cmd/context/use_test.go @@ -11,6 +11,10 @@ import ( ) func TestUse(t *testing.T) { + // Make sure we don't have any environment variables set that could interfere with the test + t.Setenv("HCLOUD_TOKEN", "") + t.Setenv("HCLOUD_CONTEXT", "") + testConfig := `active_context = "my-context" [[contexts]] diff --git a/internal/cmd/datacenter/list.go b/internal/cmd/datacenter/list.go index a01355e7..45ff441b 100644 --- a/internal/cmd/datacenter/list.go +++ b/internal/cmd/datacenter/list.go @@ -31,8 +31,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(_ hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.Datacenter{}). AddFieldFn("location", output.FieldFn(func(obj interface{}) string { datacenter := obj.(*hcloud.Datacenter) diff --git a/internal/cmd/firewall/list.go b/internal/cmd/firewall/list.go index 4546b2b8..bd627cbf 100644 --- a/internal/cmd/firewall/list.go +++ b/internal/cmd/firewall/list.go @@ -34,8 +34,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.Firewall{}). AddFieldFn("rules_count", output.FieldFn(func(obj interface{}) string { firewall := obj.(*hcloud.Firewall) diff --git a/internal/cmd/floatingip/list.go b/internal/cmd/floatingip/list.go index a73ed540..29c1bb42 100644 --- a/internal/cmd/floatingip/list.go +++ b/internal/cmd/floatingip/list.go @@ -37,8 +37,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(client hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, client hcapi2.Client) { + t. AddAllowedFields(hcloud.FloatingIP{}). AddFieldFn("dns", output.FieldFn(func(obj interface{}) string { floatingIP := obj.(*hcloud.FloatingIP) diff --git a/internal/cmd/image/list.go b/internal/cmd/image/list.go index 4dbca601..267f4b38 100644 --- a/internal/cmd/image/list.go +++ b/internal/cmd/image/list.go @@ -72,8 +72,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(client hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, client hcapi2.Client) { + t. AddAllowedFields(hcloud.Image{}). AddFieldAlias("imagesize", "image size"). AddFieldAlias("disksize", "disk size"). diff --git a/internal/cmd/iso/list.go b/internal/cmd/iso/list.go index 0e680800..e2eb3f4e 100644 --- a/internal/cmd/iso/list.go +++ b/internal/cmd/iso/list.go @@ -78,8 +78,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(_ hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.ISO{}). AddFieldFn("architecture", func(obj interface{}) string { iso := obj.(*hcloud.ISO) diff --git a/internal/cmd/loadbalancer/list.go b/internal/cmd/loadbalancer/list.go index ee9ed869..74e6d586 100644 --- a/internal/cmd/loadbalancer/list.go +++ b/internal/cmd/loadbalancer/list.go @@ -36,8 +36,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.LoadBalancer{}). AddFieldFn("ipv4", output.FieldFn(func(obj interface{}) string { loadbalancer := obj.(*hcloud.LoadBalancer) diff --git a/internal/cmd/loadbalancer/metrics.go b/internal/cmd/loadbalancer/metrics.go index 90ef0301..e942b6cd 100644 --- a/internal/cmd/loadbalancer/metrics.go +++ b/internal/cmd/loadbalancer/metrics.go @@ -104,9 +104,9 @@ var MetricsCmd = base.Cmd{ return err } if outputFlags.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(cmd.OutOrStdout(), schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(cmd.OutOrStdout(), 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 96dbacea..fc6386b3 100644 --- a/internal/cmd/loadbalancertype/list.go +++ b/internal/cmd/loadbalancertype/list.go @@ -31,8 +31,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.LoadBalancerType{}) }, diff --git a/internal/cmd/location/list.go b/internal/cmd/location/list.go index eaf16214..1d6113f7 100644 --- a/internal/cmd/location/list.go +++ b/internal/cmd/location/list.go @@ -32,8 +32,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(_ hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.Location{}) }, diff --git a/internal/cmd/network/list.go b/internal/cmd/network/list.go index aaaebc98..acf780ea 100644 --- a/internal/cmd/network/list.go +++ b/internal/cmd/network/list.go @@ -36,8 +36,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(_ hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.Network{}). AddFieldFn("servers", output.FieldFn(func(obj interface{}) string { network := obj.(*hcloud.Network) diff --git a/internal/cmd/output/output.go b/internal/cmd/output/output.go index 4ddd79d1..6ed1d193 100644 --- a/internal/cmd/output/output.go +++ b/internal/cmd/output/output.go @@ -3,7 +3,6 @@ package output import ( "fmt" "io" - "os" "reflect" "sort" "strings" @@ -149,9 +148,9 @@ func parseOutputFlags(in []string) Opts { } // NewTable creates a new Table. -func NewTable() *Table { +func NewTable(out io.Writer) *Table { return &Table{ - w: tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0), + w: tabwriter.NewWriter(out, 0, 0, 3, ' ', 0), columns: map[string]bool{}, fieldMapping: map[string]FieldFn{}, fieldAlias: map[string]string{}, diff --git a/internal/cmd/output/output_test.go b/internal/cmd/output/output_test.go index e5ffddf3..c0304054 100644 --- a/internal/cmd/output/output_test.go +++ b/internal/cmd/output/output_test.go @@ -2,6 +2,7 @@ package output import ( "bytes" + "io" "strings" "testing" ) @@ -21,7 +22,7 @@ type testFieldsStruct struct { func TestTableOutput(t *testing.T) { var wfs writerFlusherStub - to := NewTable() + to := NewTable(io.Discard) to.w = &wfs t.Run("AddAllowedFields", func(t *testing.T) { diff --git a/internal/cmd/placementgroup/list.go b/internal/cmd/placementgroup/list.go index 98d51f70..05a80b18 100644 --- a/internal/cmd/placementgroup/list.go +++ b/internal/cmd/placementgroup/list.go @@ -36,8 +36,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.PlacementGroup{}). AddFieldFn("servers", output.FieldFn(func(obj interface{}) string { placementGroup := obj.(*hcloud.PlacementGroup) diff --git a/internal/cmd/primaryip/list.go b/internal/cmd/primaryip/list.go index a06f23c6..2adfecf4 100644 --- a/internal/cmd/primaryip/list.go +++ b/internal/cmd/primaryip/list.go @@ -37,8 +37,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(client hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, client hcapi2.Client) { + t. AddAllowedFields(hcloud.PrimaryIP{}). AddFieldFn("ip", output.FieldFn(func(obj interface{}) string { primaryIP := obj.(*hcloud.PrimaryIP) diff --git a/internal/cmd/server/list.go b/internal/cmd/server/list.go index 9ee33baa..3e73df90 100644 --- a/internal/cmd/server/list.go +++ b/internal/cmd/server/list.go @@ -70,8 +70,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(client hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, client hcapi2.Client) { + t. AddAllowedFields(hcloud.Server{}). AddFieldFn("ipv4", output.FieldFn(func(obj interface{}) string { server := obj.(*hcloud.Server) diff --git a/internal/cmd/server/metrics.go b/internal/cmd/server/metrics.go index c1313ed4..4bbb3d99 100644 --- a/internal/cmd/server/metrics.go +++ b/internal/cmd/server/metrics.go @@ -102,9 +102,9 @@ var MetricsCmd = base.Cmd{ return err } if outputFlags.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(cmd.OutOrStdout(), schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(cmd.OutOrStdout(), 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 8a2e0ab8..b6695526 100644 --- a/internal/cmd/server/request_console.go +++ b/internal/cmd/server/request_console.go @@ -55,9 +55,9 @@ var RequestConsoleCmd = base.Cmd{ } if outOpts.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(cmd.OutOrStdout(), schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(cmd.OutOrStdout(), schema) } cmd.Printf("Console for server %d:\n", server.ID) diff --git a/internal/cmd/server/reset_password.go b/internal/cmd/server/reset_password.go index ed0d6185..78dbb66a 100644 --- a/internal/cmd/server/reset_password.go +++ b/internal/cmd/server/reset_password.go @@ -50,9 +50,9 @@ var ResetPasswordCmd = base.Cmd{ schema := make(map[string]interface{}) schema["root_password"] = result.RootPassword if outputFlags.IsSet("json") { - return util.DescribeJSON(schema) + return util.DescribeJSON(cmd.OutOrStdout(), schema) } - return util.DescribeYAML(schema) + return util.DescribeYAML(cmd.OutOrStdout(), schema) } cmd.Printf("Password of server %d reset to: %s\n", server.ID, result.RootPassword) diff --git a/internal/cmd/servertype/list.go b/internal/cmd/servertype/list.go index dac892e7..4e98924c 100644 --- a/internal/cmd/servertype/list.go +++ b/internal/cmd/servertype/list.go @@ -34,8 +34,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.ServerType{}). AddFieldAlias("storagetype", "storage type"). AddFieldFn("memory", output.FieldFn(func(obj interface{}) string { diff --git a/internal/cmd/sshkey/list.go b/internal/cmd/sshkey/list.go index 79099629..741bd768 100644 --- a/internal/cmd/sshkey/list.go +++ b/internal/cmd/sshkey/list.go @@ -35,8 +35,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(_ hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, _ hcapi2.Client) { + t. AddAllowedFields(hcloud.SSHKey{}). AddFieldFn("labels", output.FieldFn(func(obj interface{}) string { sshKey := obj.(*hcloud.SSHKey) diff --git a/internal/cmd/util/util.go b/internal/cmd/util/util.go index d01afd2d..64148636 100644 --- a/internal/cmd/util/util.go +++ b/internal/cmd/util/util.go @@ -4,7 +4,7 @@ import ( "cmp" "encoding/json" "fmt" - "os" + "io" "reflect" "sort" "strings" @@ -173,7 +173,7 @@ func PrefixLines(text, prefix string) string { return strings.Join(lines, "\n") } -func DescribeFormat(object interface{}, format string) error { +func DescribeFormat(w io.Writer, object interface{}, format string) error { if !strings.HasSuffix(format, "\n") { format += "\n" } @@ -181,17 +181,17 @@ func DescribeFormat(object interface{}, format string) error { if err != nil { return err } - return t.Execute(os.Stdout, object) + return t.Execute(w, object) } -func DescribeJSON(object interface{}) error { - enc := json.NewEncoder(os.Stdout) +func DescribeJSON(w io.Writer, object interface{}) error { + enc := json.NewEncoder(w) enc.SetIndent("", " ") return enc.Encode(object) } -func DescribeYAML(object interface{}) error { - enc := yaml.NewEncoder(os.Stdout) +func DescribeYAML(w io.Writer, object interface{}) error { + enc := yaml.NewEncoder(w) return enc.Encode(object) } diff --git a/internal/cmd/util/util_test.go b/internal/cmd/util/util_test.go index 05bf557c..6b407fe4 100644 --- a/internal/cmd/util/util_test.go +++ b/internal/cmd/util/util_test.go @@ -1,6 +1,7 @@ package util_test import ( + "bytes" "encoding/json" "errors" "testing" @@ -12,7 +13,6 @@ import ( require "github.com/stretchr/testify/require" "github.com/hetznercloud/cli/internal/cmd/util" - "github.com/hetznercloud/cli/internal/testutil" "github.com/hetznercloud/hcloud-go/v2/hcloud" ) @@ -190,51 +190,45 @@ func TestPrefixLines(t *testing.T) { } func TestDescribeFormat(t *testing.T) { - stdout, stderr, err := testutil.CaptureOutStreams(func() error { - return util.DescribeFormat(struct { - Foo string - Bar string - }{ - Foo: "foo", - Bar: "bar", - }, "Foo is: {{.Foo}} Bar is: {{.Bar}}") - }) + var buf bytes.Buffer + err := util.DescribeFormat(&buf, struct { + Foo string + Bar string + }{ + Foo: "foo", + Bar: "bar", + }, "Foo is: {{.Foo}} Bar is: {{.Bar}}") require.NoError(t, err) - assert.Equal(t, "Foo is: foo Bar is: bar\n", stdout) - assert.Empty(t, stderr) + assert.Equal(t, "Foo is: foo Bar is: bar\n", buf.String()) } func TestDescribeJSON(t *testing.T) { - stdout, stderr, err := testutil.CaptureOutStreams(func() error { - return util.DescribeJSON(struct { - Foo string `json:"foo"` - Bar string `json:"bar"` - }{ - Foo: "foo", - Bar: "bar", - }) + var buf bytes.Buffer + err := util.DescribeJSON(&buf, struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + }{ + Foo: "foo", + Bar: "bar", }) require.NoError(t, err) - assert.JSONEq(t, `{"foo":"foo", "bar": "bar"}`, stdout) - assert.Empty(t, stderr) + assert.JSONEq(t, `{"foo":"foo", "bar": "bar"}`, buf.String()) } func TestDescribeYAML(t *testing.T) { - stdout, stderr, err := testutil.CaptureOutStreams(func() error { - return util.DescribeYAML(struct { - Foo string `json:"foo"` - Bar string `json:"bar"` - }{ - Foo: "foo", - Bar: "bar", - }) + var buf bytes.Buffer + err := util.DescribeYAML(&buf, struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + }{ + Foo: "foo", + Bar: "bar", }) require.NoError(t, err) - assert.YAMLEq(t, `{"foo":"foo", "bar": "bar"}`, stdout) - assert.Empty(t, stderr) + assert.YAMLEq(t, `{"foo":"foo", "bar": "bar"}`, buf.String()) } func TestWrap(t *testing.T) { diff --git a/internal/cmd/volume/list.go b/internal/cmd/volume/list.go index 2e85720b..3fa5acc3 100644 --- a/internal/cmd/volume/list.go +++ b/internal/cmd/volume/list.go @@ -37,8 +37,8 @@ var ListCmd = base.ListCmd{ return resources, err }, - OutputTable: func(client hcapi2.Client) *output.Table { - return output.NewTable(). + OutputTable: func(t *output.Table, client hcapi2.Client) { + t. AddAllowedFields(hcloud.Volume{}). AddFieldFn("server", output.FieldFn(func(obj interface{}) string { volume := obj.(*hcloud.Volume)