Merge "[#323] Cluster list command to output in table/clusternames."

This commit is contained in:
Zuul 2021-03-31 03:00:29 +00:00 committed by Gerrit Code Review
commit 6a62e11fea
8 changed files with 140 additions and 10 deletions

View File

@ -26,6 +26,8 @@ const (
listExample = ` listExample = `
# Retrieve cluster list # Retrieve cluster list
airshipctl cluster list --airshipconf /tmp/airconfig 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:], Example: listExample[1:],
RunE: listRunE(o), RunE: listRunE(o),
} }
flags := cmd.Flags()
flags.StringVarP(&o.Format,
"output", "o", "name", "'table' "+
"and 'name' are available "+
"output formats")
return cmd return cmd
} }

View File

@ -6,7 +6,10 @@ Usage:
Examples: Examples:
# Retrieve cluster list # Retrieve cluster list
airshipctl cluster list --airshipconf /tmp/airconfig airshipctl cluster list --airshipconf /tmp/airconfig
airshipctl cluster list -o table
airshipctl cluster list -o name
Flags: Flags:
-h, --help help for list -h, --help help for list
-o, --output string 'table' and 'name' are available output formats (default "name")

View File

@ -15,13 +15,16 @@ airshipctl cluster list [flags]
``` ```
# Retrieve cluster list # Retrieve cluster list
airshipctl cluster list --airshipconf /tmp/airconfig airshipctl cluster list --airshipconf /tmp/airconfig
airshipctl cluster list -o table
airshipctl cluster list -o name
``` ```
### Options ### 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 ### Options inherited from parent commands

View File

@ -15,12 +15,22 @@
package clustermap package clustermap
import ( import (
"fmt"
"io"
"os"
"text/tabwriter"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
) )
// DefaultClusterAPIObjNamespace is a default namespace used for cluster-api cluster object // DefaultClusterAPIObjNamespace is a default namespace used for cluster-api cluster object
const DefaultClusterAPIObjNamespace = "default" 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, // ClusterMap interface that allows to list all clusters, find its parent, namespace,
// check if dynamic kubeconfig is enabled. // check if dynamic kubeconfig is enabled.
// TODO use typed cluster names // TODO use typed cluster names
@ -29,6 +39,7 @@ type ClusterMap interface {
AllClusters() []string AllClusters() []string
ClusterKubeconfigContext(string) (string, error) ClusterKubeconfigContext(string) (string, error)
Sources(string) ([]v1alpha1.KubeconfigSource, error) Sources(string) ([]v1alpha1.KubeconfigSource, error)
Write(io.Writer, WriteOptions) error
} }
// clusterMap allows to view clusters and relationship between them // 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 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
}

View File

@ -15,6 +15,11 @@
package clustermap_test package clustermap_test
import ( import (
"bufio"
"bytes"
"io"
"io/ioutil"
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -65,7 +70,6 @@ func TestClusterMap(t *testing.T) {
}, },
}, },
} }
cMap := clustermap.NewClusterMap(apiMap) cMap := clustermap.NewClusterMap(apiMap)
require.NotNil(t, cMap) require.NotNil(t, cMap)
@ -121,3 +125,64 @@ func TestClusterMap(t *testing.T) {
assert.Error(t, err) 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))
})
}
}

View File

@ -26,6 +26,7 @@ import (
"sigs.k8s.io/cli-utils/pkg/print/table" "sigs.k8s.io/cli-utils/pkg/print/table"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1" "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/config"
"opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/document"
phaseerrors "opendev.org/airship/airshipctl/pkg/phase/errors" phaseerrors "opendev.org/airship/airshipctl/pkg/phase/errors"
@ -247,10 +248,14 @@ func (c *PlanRunCommand) RunE() error {
type ClusterListCommand struct { type ClusterListCommand struct {
Factory config.Factory Factory config.Factory
Writer io.Writer Writer io.Writer
Format string
} }
// RunE executes cluster list command // RunE executes cluster list command
func (c *ClusterListCommand) RunE() error { func (c *ClusterListCommand) RunE() error {
if c.Format != "table" && c.Format != "name" {
return phaseerrors.ErrInvalidOutputFormat{RequestedFormat: c.Format}
}
cfg, err := c.Factory() cfg, err := c.Factory()
if err != nil { if err != nil {
return err return err
@ -263,12 +268,9 @@ func (c *ClusterListCommand) RunE() error {
if err != nil { if err != nil {
return err return err
} }
err = clusterMap.Write(c.Writer, clustermap.WriteOptions{Format: c.Format})
clusterList := clusterMap.AllClusters() if err != nil {
for _, clusterName := range clusterList { return err
if _, err := c.Writer.Write([]byte(clusterName + "\n")); err != nil {
return err
}
} }
return nil return nil
} }

View File

@ -456,6 +456,7 @@ func TestClusterListCommand_RunE(t *testing.T) {
name string name string
factory config.Factory factory config.Factory
expectedErr string expectedErr string
Format string
}{ }{
{ {
name: "Error config factory", name: "Error config factory",
@ -463,6 +464,7 @@ func TestClusterListCommand_RunE(t *testing.T) {
return nil, testErr return nil, testErr
}, },
expectedErr: testFactoryErr, expectedErr: testFactoryErr,
Format: "name",
}, },
{ {
name: "Error new helper", name: "Error new helper",
@ -473,9 +475,11 @@ func TestClusterListCommand_RunE(t *testing.T) {
}, nil }, nil
}, },
expectedErr: "missing configuration: context with name 'does not exist'", expectedErr: "missing configuration: context with name 'does not exist'",
Format: "name",
}, },
{ {
name: "No error", name: "No error",
Format: "name",
factory: func() (*config.Config, error) { factory: func() (*config.Config, error) {
conf := config.NewConfig() conf := config.NewConfig()
conf.Manifests = map[string]*config.Manifest{ conf.Manifests = map[string]*config.Manifest{
@ -506,6 +510,7 @@ func TestClusterListCommand_RunE(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cmd := phase.ClusterListCommand{ cmd := phase.ClusterListCommand{
Factory: tt.factory, Factory: tt.factory,
Format: tt.Format,
Writer: bytes.NewBuffer(nil), Writer: bytes.NewBuffer(nil),
} }
err := cmd.RunE() err := cmd.RunE()

View File

@ -69,3 +69,12 @@ type ErrInvalidPhase struct {
func (e ErrInvalidPhase) Error() string { func (e ErrInvalidPhase) Error() string {
return fmt.Sprintf("invalid phase: %s", e.Reason) 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)
}