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

* airshipctl cluster list
* airshipctl cluster list -o table
* airshipctl cluster list -o name(lists cluster names)

Relates-To: #323
Change-Id: Ic4609b8e05984c14acf8f20053123423d5e0088a
This commit is contained in:
Niharika Bhavaraju 2021-02-17 21:58:59 +05:30 committed by niharikab
parent 310bc282f5
commit 1f90036413
8 changed files with 140 additions and 10 deletions

View File

@ -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
}

View File

@ -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")

View File

@ -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

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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
}

View File

@ -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()

View File

@ -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)
}