diff --git a/cmd/cluster/list.go b/cmd/cluster/list.go index 9243f7d79..ee424be3b 100755 --- a/cmd/cluster/list.go +++ b/cmd/cluster/list.go @@ -26,6 +26,8 @@ const ( listExample = ` # Retrieve cluster list airshipctl cluster list --airshipconf /tmp/airconfig +airshipctl cluster list -o table +airshipctl cluster list -o name ` ) @@ -38,6 +40,11 @@ func NewListCommand(cfgFactory config.Factory) *cobra.Command { Example: listExample[1:], RunE: listRunE(o), } + flags := cmd.Flags() + flags.StringVarP(&o.Format, + "output", "o", "name", "'table' "+ + "and 'name' are available "+ + "output formats") return cmd } diff --git a/cmd/cluster/testdata/TestNewListCommandGoldenOutput/cluster-list-cmd-with-help.golden b/cmd/cluster/testdata/TestNewListCommandGoldenOutput/cluster-list-cmd-with-help.golden index 9cd4ebb9f..ab18d64f9 100644 --- a/cmd/cluster/testdata/TestNewListCommandGoldenOutput/cluster-list-cmd-with-help.golden +++ b/cmd/cluster/testdata/TestNewListCommandGoldenOutput/cluster-list-cmd-with-help.golden @@ -6,7 +6,10 @@ Usage: Examples: # Retrieve cluster list airshipctl cluster list --airshipconf /tmp/airconfig +airshipctl cluster list -o table +airshipctl cluster list -o name Flags: - -h, --help help for list + -h, --help help for list + -o, --output string 'table' and 'name' are available output formats (default "name") diff --git a/docs/source/cli/airshipctl_cluster_list.md b/docs/source/cli/airshipctl_cluster_list.md index 4cf7acd04..a7fdc2df9 100644 --- a/docs/source/cli/airshipctl_cluster_list.md +++ b/docs/source/cli/airshipctl_cluster_list.md @@ -15,13 +15,16 @@ airshipctl cluster list [flags] ``` # Retrieve cluster list airshipctl cluster list --airshipconf /tmp/airconfig +airshipctl cluster list -o table +airshipctl cluster list -o name ``` ### Options ``` - -h, --help help for list + -h, --help help for list + -o, --output string 'table' and 'name' are available output formats (default "name") ``` ### Options inherited from parent commands diff --git a/pkg/cluster/clustermap/map.go b/pkg/cluster/clustermap/map.go index 4d0d4971f..0f7c1c893 100644 --- a/pkg/cluster/clustermap/map.go +++ b/pkg/cluster/clustermap/map.go @@ -15,12 +15,22 @@ package clustermap import ( + "fmt" + "io" + "os" + "text/tabwriter" + "opendev.org/airship/airshipctl/pkg/api/v1alpha1" ) // DefaultClusterAPIObjNamespace is a default namespace used for cluster-api cluster object const DefaultClusterAPIObjNamespace = "default" +// WriteOptions has format in which we want to print the output(table/yaml/cluster name) +type WriteOptions struct { + Format string +} + // ClusterMap interface that allows to list all clusters, find its parent, namespace, // check if dynamic kubeconfig is enabled. // TODO use typed cluster names @@ -29,6 +39,7 @@ type ClusterMap interface { AllClusters() []string ClusterKubeconfigContext(string) (string, error) Sources(string) ([]v1alpha1.KubeconfigSource, error) + Write(io.Writer, WriteOptions) error } // clusterMap allows to view clusters and relationship between them @@ -82,3 +93,28 @@ func (cm clusterMap) Sources(clusterName string) ([]v1alpha1.KubeconfigSource, e } return cluster.Sources, nil } + +// Write prints the cluster list in table/name output format +func (cm clusterMap) Write(writer io.Writer, wo WriteOptions) error { + if wo.Format == "table" { + w := tabwriter.NewWriter(os.Stdout, 20, 8, 1, ' ', 0) + fmt.Fprintf(w, "NAME\tKUBECONFIG CONTEXT\tPARENT CLUSTER\n") + for clustername, cluster := range cm.apiMap.Map { + kubeconfig, err := cm.ClusterKubeconfigContext(clustername) + if err != nil { + return err + } + fmt.Fprintf(w, "%s\t%s\t%s\n", + clustername, kubeconfig, cluster.Parent) + } + w.Flush() + } else if wo.Format == "name" { + clusterList := cm.AllClusters() + for _, clusterName := range clusterList { + if _, err := writer.Write([]byte(clusterName + "\n")); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/cluster/clustermap/map_test.go b/pkg/cluster/clustermap/map_test.go index f5a31d334..96ceaff06 100644 --- a/pkg/cluster/clustermap/map_test.go +++ b/pkg/cluster/clustermap/map_test.go @@ -15,6 +15,11 @@ package clustermap_test import ( + "bufio" + "bytes" + "io" + "io/ioutil" + "os" "testing" "github.com/stretchr/testify/assert" @@ -65,7 +70,6 @@ func TestClusterMap(t *testing.T) { }, }, } - cMap := clustermap.NewClusterMap(apiMap) require.NotNil(t, cMap) @@ -121,3 +125,64 @@ func TestClusterMap(t *testing.T) { assert.Error(t, err) }) } + +func Test_clusterMap_Write(t *testing.T) { + var b bytes.Buffer + wr := bufio.NewWriter(&b) + targetCluster := "target" + ephemeraCluster := "ephemeral" + apiMap := &v1alpha1.ClusterMap{ + Map: map[string]*v1alpha1.Cluster{ + targetCluster: { + Parent: ephemeraCluster, + }, + }, + } + tests := []struct { + name string + wo clustermap.WriteOptions + wantWriter string + expectedOut string + expectedErr string + writer io.Writer + }{ + { + name: "success table", + wo: clustermap.WriteOptions{Format: "table"}, + expectedOut: "NAME KUBECONFIG CONTEXT PARENT CLUSTER" + + "\ntarget target ephemeral\n", + writer: wr, + }, + { + name: "writer nil", + wo: clustermap.WriteOptions{Format: "table"}, + writer: nil, + expectedOut: "", + }, + } + rStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + require.Error(t, err) + } + os.Stdout = w + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cMap := clustermap.NewClusterMap(apiMap) + err := cMap.Write(tt.writer, tt.wo) + w.Close() + if tt.expectedErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + } else { + assert.NoError(t, err) + } + out, err := ioutil.ReadAll(r) + if err != nil { + require.Error(t, err) + } + os.Stdout = rStdout + assert.Equal(t, tt.expectedOut, string(out)) + }) + } +} diff --git a/pkg/phase/command.go b/pkg/phase/command.go index ad881d8d3..15e41b477 100644 --- a/pkg/phase/command.go +++ b/pkg/phase/command.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/cli-utils/pkg/print/table" "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/cluster/clustermap" "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/document" phaseerrors "opendev.org/airship/airshipctl/pkg/phase/errors" @@ -247,10 +248,14 @@ func (c *PlanRunCommand) RunE() error { type ClusterListCommand struct { Factory config.Factory Writer io.Writer + Format string } // RunE executes cluster list command func (c *ClusterListCommand) RunE() error { + if c.Format != "table" && c.Format != "name" { + return phaseerrors.ErrInvalidOutputFormat{RequestedFormat: c.Format} + } cfg, err := c.Factory() if err != nil { return err @@ -263,12 +268,9 @@ func (c *ClusterListCommand) RunE() error { if err != nil { return err } - - clusterList := clusterMap.AllClusters() - for _, clusterName := range clusterList { - if _, err := c.Writer.Write([]byte(clusterName + "\n")); err != nil { - return err - } + err = clusterMap.Write(c.Writer, clustermap.WriteOptions{Format: c.Format}) + if err != nil { + return err } return nil } diff --git a/pkg/phase/command_test.go b/pkg/phase/command_test.go index 59b993c22..451946a4c 100644 --- a/pkg/phase/command_test.go +++ b/pkg/phase/command_test.go @@ -456,6 +456,7 @@ func TestClusterListCommand_RunE(t *testing.T) { name string factory config.Factory expectedErr string + Format string }{ { name: "Error config factory", @@ -463,6 +464,7 @@ func TestClusterListCommand_RunE(t *testing.T) { return nil, testErr }, expectedErr: testFactoryErr, + Format: "name", }, { name: "Error new helper", @@ -473,9 +475,11 @@ func TestClusterListCommand_RunE(t *testing.T) { }, nil }, expectedErr: "missing configuration: context with name 'does not exist'", + Format: "name", }, { - name: "No error", + name: "No error", + Format: "name", factory: func() (*config.Config, error) { conf := config.NewConfig() conf.Manifests = map[string]*config.Manifest{ @@ -506,6 +510,7 @@ func TestClusterListCommand_RunE(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cmd := phase.ClusterListCommand{ Factory: tt.factory, + Format: tt.Format, Writer: bytes.NewBuffer(nil), } err := cmd.RunE() diff --git a/pkg/phase/errors/errors.go b/pkg/phase/errors/errors.go index 499c23ad2..735362858 100644 --- a/pkg/phase/errors/errors.go +++ b/pkg/phase/errors/errors.go @@ -69,3 +69,12 @@ type ErrInvalidPhase struct { func (e ErrInvalidPhase) Error() string { return fmt.Sprintf("invalid phase: %s", e.Reason) } + +// ErrInvalidOutputFormat is called when the user provides format other than name/table +type ErrInvalidOutputFormat struct { + RequestedFormat string +} + +func (e ErrInvalidOutputFormat) Error() string { + return fmt.Sprintf("invalid output format specified %s. Allowed values are table|name", e.RequestedFormat) +}