diff --git a/cmd/plan/list.go b/cmd/plan/list.go index 6e47e7826..d4c07c32e 100644 --- a/cmd/plan/list.go +++ b/cmd/plan/list.go @@ -18,7 +18,7 @@ import ( "github.com/spf13/cobra" "opendev.org/airship/airshipctl/pkg/config" - "opendev.org/airship/airshipctl/pkg/errors" + "opendev.org/airship/airshipctl/pkg/phase" ) const ( @@ -29,12 +29,15 @@ List life-cycle plans which were defined in document model. // NewListCommand creates a command which prints available phase plans func NewListCommand(cfgFactory config.Factory) *cobra.Command { + planCmd := &phase.PlanListCommand{Factory: cfgFactory} + listCmd := &cobra.Command{ Use: "list", Short: "List plans", Long: listLong[1:], RunE: func(cmd *cobra.Command, args []string) error { - return errors.ErrNotImplemented{What: "airshipctl plan list"} + planCmd.Writer = cmd.OutOrStdout() + return planCmd.RunE() }, } return listCmd diff --git a/manifests/phases/plan.yaml b/manifests/phases/plan.yaml index 2c5848598..26c16b92f 100644 --- a/manifests/phases/plan.yaml +++ b/manifests/phases/plan.yaml @@ -2,6 +2,7 @@ apiVersion: airshipit.org/v1alpha1 kind: PhasePlan metadata: name: phasePlan +description: "Default phase plan" phaseGroups: - name: group1 phases: diff --git a/pkg/api/v1alpha1/phaseplan_types.go b/pkg/api/v1alpha1/phaseplan_types.go index 864a13b14..5c7e2543e 100644 --- a/pkg/api/v1alpha1/phaseplan_types.go +++ b/pkg/api/v1alpha1/phaseplan_types.go @@ -24,6 +24,7 @@ import ( type PhasePlan struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` + Description string `json:"description,omitempty"` PhaseGroups []PhaseGroup `json:"phaseGroups,omitempty"` } diff --git a/pkg/phase/command.go b/pkg/phase/command.go index e7d652467..1df54f841 100644 --- a/pkg/phase/command.go +++ b/pkg/phase/command.go @@ -15,12 +15,17 @@ package phase import ( + "fmt" "io" "os" "path/filepath" "strings" "time" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/cli-utils/pkg/print/table" + + "opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/phase/ifc" @@ -146,3 +151,54 @@ func (c *TreeCommand) RunE() error { t.PrintTree("") return nil } + +// PlanListCommand phase list command +type PlanListCommand struct { + Factory config.Factory + Writer io.Writer +} + +// RunE runs a phase plan command +func (c *PlanListCommand) RunE() error { + cfg, err := c.Factory() + if err != nil { + return err + } + + helper, err := NewHelper(cfg) + if err != nil { + return err + } + + phases, err := helper.ListPlans() + if err != nil { + return err + } + + rt, err := util.NewResourceTable(phases, util.DefaultStatusFunction()) + if err != nil { + return err + } + + printer := util.DefaultTablePrinter(c.Writer, nil) + descriptionCol := table.ColumnDef{ + ColumnName: "description", + ColumnHeader: "DESCRIPTION", + ColumnWidth: 40, + PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int, error) { + rs := r.ResourceStatus() + if rs == nil { + return 0, nil + } + plan := &v1alpha1.PhasePlan{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(rs.Resource.Object, plan) + if err != nil { + return 0, err + } + return fmt.Fprint(w, plan.Description) + }, + } + printer.Columns = append(printer.Columns, descriptionCol) + printer.PrintTable(rt, 0) + return nil +} diff --git a/pkg/phase/command_test.go b/pkg/phase/command_test.go index afa7dd410..541a458c8 100644 --- a/pkg/phase/command_test.go +++ b/pkg/phase/command_test.go @@ -15,7 +15,9 @@ package phase_test import ( + "bytes" "fmt" + "io/ioutil" "testing" "github.com/stretchr/testify/assert" @@ -101,7 +103,7 @@ func TestRunCommand(t *testing.T) { } } -func TestPlanCommand(t *testing.T) { +func TestListCommand(t *testing.T) { tests := []struct { name string errContains string @@ -236,3 +238,58 @@ func TestTreeCommand(t *testing.T) { }) } } + +func TestPlanListCommand(t *testing.T) { + testErr := fmt.Errorf(testFactoryErr) + testCases := []struct { + name string + factory config.Factory + expectedOut [][]byte + expectedErr string + }{ + { + name: "Error config factory", + factory: func() (*config.Config, error) { + return nil, testErr + }, + expectedErr: testFactoryErr, + expectedOut: [][]byte{{}}, + }, + { + name: "List phases", + factory: func() (*config.Config, error) { + conf := config.NewConfig() + manifest := conf.Manifests[config.AirshipDefaultManifest] + manifest.TargetPath = "testdata" + manifest.MetadataPath = "metadata.yaml" + manifest.Repositories[config.DefaultTestPhaseRepo].URLString = "" + return conf, nil + }, + expectedOut: [][]byte{ + []byte("NAMESPACE RESOURCE DESCRIPTION "), + []byte(" PhasePlan/phasePlan Default phase plan "), + {}, + }, + }, + } + for _, tc := range testCases { + tt := tc + t.Run(tt.name, func(t *testing.T) { + buf := &bytes.Buffer{} + cmd := phase.PlanListCommand{ + Factory: tt.factory, + Writer: buf, + } + err := cmd.RunE() + if tt.expectedErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + } else { + assert.NoError(t, err) + } + out, err := ioutil.ReadAll(buf) + require.NoError(t, err) + assert.Equal(t, tt.expectedOut, bytes.Split(out, []byte("\n"))) + }) + } +} diff --git a/pkg/phase/helper.go b/pkg/phase/helper.go index fd0bffedf..d6d79ff55 100644 --- a/pkg/phase/helper.go +++ b/pkg/phase/helper.go @@ -15,8 +15,6 @@ package phase import ( - "fmt" - "io" "path/filepath" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -144,6 +142,35 @@ func (helper *Helper) ListPhases() ([]*v1alpha1.Phase, error) { return phases, nil } +// ListPlans returns all phases associated with manifest +func (helper *Helper) ListPlans() ([]*v1alpha1.PhasePlan, error) { + bundle, err := document.NewBundleByPath(helper.phaseBundleRoot) + if err != nil { + return nil, err + } + + plan := &v1alpha1.PhasePlan{} + selector, err := document.NewSelector().ByObject(plan, v1alpha1.Scheme) + if err != nil { + return nil, err + } + + docs, err := bundle.Select(selector) + if err != nil { + return nil, err + } + + plans := make([]*v1alpha1.PhasePlan, len(docs)) + for i, doc := range docs { + p := &v1alpha1.PhasePlan{} + if err = doc.ToAPIObject(p, v1alpha1.Scheme); err != nil { + return nil, err + } + plans[i] = p + } + return plans, nil +} + // ClusterMapAPIobj associated with the the manifest func (helper *Helper) ClusterMapAPIobj() (*v1alpha1.ClusterMap, error) { bundle, err := document.NewBundleByPath(helper.phaseBundleRoot) @@ -236,27 +263,3 @@ func (helper *Helper) PhaseEntryPointBasePath() string { func (helper *Helper) WorkDir() (string, error) { return filepath.Join(util.UserHomeDir(), config.AirshipConfigDir), nil } - -// PrintPlan prints plan -// TODO make this more readable in the future, and move to client -func PrintPlan(plan *v1alpha1.PhasePlan, w io.Writer) error { - result := make(map[string][]string) - for _, phaseGroup := range plan.PhaseGroups { - phases := make([]string, len(phaseGroup.Phases)) - for i, phase := range phaseGroup.Phases { - phases[i] = phase.Name - } - result[phaseGroup.Name] = phases - } - - tw := util.NewTabWriter(w) - defer tw.Flush() - fmt.Fprintf(tw, "GROUP\tPHASE\n") - for group, phaseList := range result { - fmt.Fprintf(tw, "%s\t\n", group) - for _, phase := range phaseList { - fmt.Fprintf(tw, "\t%s\n", phase) - } - } - return nil -} diff --git a/pkg/phase/helper_test.go b/pkg/phase/helper_test.go index b5027f04a..c991213a0 100644 --- a/pkg/phase/helper_test.go +++ b/pkg/phase/helper_test.go @@ -15,7 +15,6 @@ package phase_test import ( - "bytes" "path/filepath" "testing" @@ -236,6 +235,57 @@ func TestHelperListPhases(t *testing.T) { } } +func TestHelperListPlans(t *testing.T) { + testCases := []struct { + name string + errContains string + expectedLen int + config func(t *testing.T) *config.Config + }{ + { + name: "Success plan list", + expectedLen: 1, + config: testConfig, + }, + { + name: "Error bundle path doesn't exist", + config: func(t *testing.T) *config.Config { + conf := testConfig(t) + conf.Manifests["dummy_manifest"].MetadataPath = brokenMetaPath + return conf + }, + errContains: "no such file or directory", + }, + { + name: "Success 0 plans", + config: func(t *testing.T) *config.Config { + conf := testConfig(t) + conf.Manifests["dummy_manifest"].MetadataPath = noPlanMetaPath + return conf + }, + expectedLen: 0, + }, + } + + for _, test := range testCases { + tt := test + t.Run(tt.name, func(t *testing.T) { + helper, err := phase.NewHelper(tt.config(t)) + require.NoError(t, err) + require.NotNil(t, helper) + + actualList, actualErr := helper.ListPlans() + if tt.errContains != "" { + require.Error(t, actualErr) + assert.Contains(t, actualErr.Error(), tt.errContains) + } else { + require.NoError(t, actualErr) + assert.Len(t, actualList, tt.expectedLen) + } + }) + } +} + func TestHelperClusterMapAPI(t *testing.T) { testCases := []struct { name string @@ -401,24 +451,6 @@ func TestHelperExecutorDoc(t *testing.T) { } } -func TestHelperPrintPlan(t *testing.T) { - helper, err := phase.NewHelper(testConfig(t)) - require.NoError(t, err) - require.NotNil(t, helper) - plan, err := helper.Plan() - require.NoError(t, err) - require.NotNil(t, plan) - buf := bytes.NewBuffer([]byte{}) - err = phase.PrintPlan(plan, buf) - require.NoError(t, err) - // easy check to make sure printed plan contains all phases in plan - assert.Contains(t, buf.String(), "remotedirect") - assert.Contains(t, buf.String(), "isogen") - assert.Contains(t, buf.String(), "initinfra") - assert.Contains(t, buf.String(), "some_phase") - assert.Contains(t, buf.String(), "capi_init") -} - func TestHelperTargetPath(t *testing.T) { helper, err := phase.NewHelper(testConfig(t)) require.NoError(t, err) diff --git a/pkg/phase/ifc/helper.go b/pkg/phase/ifc/helper.go index 8431048c6..f4409a99b 100644 --- a/pkg/phase/ifc/helper.go +++ b/pkg/phase/ifc/helper.go @@ -29,6 +29,7 @@ type Helper interface { Phase(phaseID ID) (*v1alpha1.Phase, error) Plan() (*v1alpha1.PhasePlan, error) ListPhases() ([]*v1alpha1.Phase, error) + ListPlans() ([]*v1alpha1.PhasePlan, error) ClusterMapAPIobj() (*v1alpha1.ClusterMap, error) ClusterMap() (clustermap.ClusterMap, error) ExecutorDoc(phaseID ID) (document.Document, error) diff --git a/pkg/phase/testdata/phases/kustomization.yaml b/pkg/phase/testdata/phases/kustomization.yaml index 7d5a69471..fa8e1c108 100755 --- a/pkg/phase/testdata/phases/kustomization.yaml +++ b/pkg/phase/testdata/phases/kustomization.yaml @@ -1,4 +1,5 @@ resources: - phases.yaml - executors.yaml - - cluster-map.yaml \ No newline at end of file + - cluster-map.yaml + - phaseplan.yaml \ No newline at end of file diff --git a/pkg/phase/testdata/phases/phaseplan.yaml b/pkg/phase/testdata/phases/phaseplan.yaml new file mode 100644 index 000000000..414efb0d3 --- /dev/null +++ b/pkg/phase/testdata/phases/phaseplan.yaml @@ -0,0 +1,9 @@ +apiVersion: airshipit.org/v1alpha1 +kind: PhasePlan +metadata: + name: phasePlan +description: "Default phase plan" +phaseGroups: + - name: group1 + phases: + - name: phase \ No newline at end of file