Implement plan list subcommand

Change-Id: Ibcd7dbf6dc8cd9d0b018c148017244526651d8ba
Closes: #385
This commit is contained in:
Dmitry Ukov 2020-12-07 16:39:09 +04:00
parent c36a8ea022
commit 069e4069ce
10 changed files with 213 additions and 49 deletions

View File

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

View File

@ -2,6 +2,7 @@ apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
description: "Default phase plan"
phaseGroups:
- name: group1
phases:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,3 +2,4 @@ resources:
- phases.yaml
- executors.yaml
- cluster-map.yaml
- phaseplan.yaml

View File

@ -0,0 +1,9 @@
apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
description: "Default phase plan"
phaseGroups:
- name: group1
phases:
- name: phase