From c36a8ea0224f5168c0b8816b5df168c54ff05ab6 Mon Sep 17 00:00:00 2001 From: Dmitry Ukov Date: Mon, 7 Dec 2020 16:39:09 +0400 Subject: [PATCH] Introduce generic table printer * Table printer is based on cli-utils approach * Rename 'phase plan' command to 'phase list' and print all Phase documents from model instead of just printing PhasePlan object Relates-To: #358 Change-Id: If3c5e2463e32f6794af4c82c12955a45583fce80 --- cmd/phase/plan.go | 4 +- .../phase-cmd-with-help.golden | 2 +- .../phase-plan-cmd-with-help.golden | 4 +- docs/source/cli/airshipctl_phase.md | 2 +- docs/source/cli/airshipctl_phase_list.md | 32 ++++ pkg/phase/command.go | 17 +- pkg/phase/command_test.go | 2 +- pkg/util/tableprinter.go | 152 ++++++++++++++++++ pkg/util/tableprinter_test.go | 108 +++++++++++++ 9 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 docs/source/cli/airshipctl_phase_list.md create mode 100644 pkg/util/tableprinter.go create mode 100644 pkg/util/tableprinter_test.go diff --git a/cmd/phase/plan.go b/cmd/phase/plan.go index 80c5b0783..d33860bd7 100644 --- a/cmd/phase/plan.go +++ b/cmd/phase/plan.go @@ -31,10 +31,10 @@ are executed in parallel. // NewPlanCommand creates a command which prints available phases func NewPlanCommand(cfgFactory config.Factory) *cobra.Command { - p := &phase.PlanCommand{Factory: cfgFactory} + p := &phase.ListCommand{Factory: cfgFactory} planCmd := &cobra.Command{ - Use: "plan", + Use: "list", Short: "List phases", Long: cmdLong[1:], RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/phase/testdata/TestNewPhaseCommandGoldenOutput/phase-cmd-with-help.golden b/cmd/phase/testdata/TestNewPhaseCommandGoldenOutput/phase-cmd-with-help.golden index 9ae36f95c..5b39e34b8 100644 --- a/cmd/phase/testdata/TestNewPhaseCommandGoldenOutput/phase-cmd-with-help.golden +++ b/cmd/phase/testdata/TestNewPhaseCommandGoldenOutput/phase-cmd-with-help.golden @@ -6,7 +6,7 @@ Usage: Available Commands: help Help about any command - plan List phases + list List phases render Render phase documents from model run Run phase tree Tree view of kustomize entrypoints of phase diff --git a/cmd/phase/testdata/TestNewPlanCommandGoldenOutput/phase-plan-cmd-with-help.golden b/cmd/phase/testdata/TestNewPlanCommandGoldenOutput/phase-plan-cmd-with-help.golden index 09fb8b73b..2c0c133d9 100644 --- a/cmd/phase/testdata/TestNewPlanCommandGoldenOutput/phase-plan-cmd-with-help.golden +++ b/cmd/phase/testdata/TestNewPlanCommandGoldenOutput/phase-plan-cmd-with-help.golden @@ -3,7 +3,7 @@ Phases within a group are executed sequentially. Multiple phase groups are executed in parallel. Usage: - plan [flags] + list [flags] Flags: - -h, --help help for plan + -h, --help help for list diff --git a/docs/source/cli/airshipctl_phase.md b/docs/source/cli/airshipctl_phase.md index d81499bb8..32a4904a5 100644 --- a/docs/source/cli/airshipctl_phase.md +++ b/docs/source/cli/airshipctl_phase.md @@ -24,7 +24,7 @@ such as getting list and applying specific one. ### SEE ALSO * [airshipctl](airshipctl.md) - A unified entrypoint to various airship components -* [airshipctl phase plan](airshipctl_phase_plan.md) - List phases +* [airshipctl phase list](airshipctl_phase_list.md) - List phases * [airshipctl phase render](airshipctl_phase_render.md) - Render phase documents from model * [airshipctl phase run](airshipctl_phase_run.md) - Run phase * [airshipctl phase tree](airshipctl_phase_tree.md) - Tree view of kustomize entrypoints of phase diff --git a/docs/source/cli/airshipctl_phase_list.md b/docs/source/cli/airshipctl_phase_list.md new file mode 100644 index 000000000..ba85107af --- /dev/null +++ b/docs/source/cli/airshipctl_phase_list.md @@ -0,0 +1,32 @@ +## airshipctl phase list + +List phases + +### Synopsis + +List life-cycle phases which were defined in document model by group. +Phases within a group are executed sequentially. Multiple phase groups +are executed in parallel. + + +``` +airshipctl phase list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --airshipconf string Path to file for airshipctl configuration. (default "$HOME/.airship/config") + --debug enable verbose output +``` + +### SEE ALSO + +* [airshipctl phase](airshipctl_phase.md) - Manage phases + diff --git a/pkg/phase/command.go b/pkg/phase/command.go index 31bab8278..e7d652467 100644 --- a/pkg/phase/command.go +++ b/pkg/phase/command.go @@ -24,6 +24,7 @@ import ( "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/phase/ifc" + "opendev.org/airship/airshipctl/pkg/util" ) // RunFlags options for phase run command @@ -63,14 +64,14 @@ func (c *RunCommand) RunE() error { return phase.Run(ifc.RunOptions{DryRun: c.Options.DryRun, Timeout: c.Options.Timeout, Progress: c.Options.Progress}) } -// PlanCommand plan command -type PlanCommand struct { +// ListCommand phase list command +type ListCommand struct { Factory config.Factory Writer io.Writer } // RunE runs a phase plan command -func (c *PlanCommand) RunE() error { +func (c *ListCommand) RunE() error { cfg, err := c.Factory() if err != nil { return err @@ -81,12 +82,18 @@ func (c *PlanCommand) RunE() error { return err } - plan, err := helper.Plan() + phases, err := helper.ListPhases() if err != nil { return err } - return PrintPlan(plan, c.Writer) + rt, err := util.NewResourceTable(phases, util.DefaultStatusFunction()) + if err != nil { + return err + } + + util.DefaultTablePrinter(c.Writer, nil).PrintTable(rt, 0) + return nil } // TreeCommand plan command diff --git a/pkg/phase/command_test.go b/pkg/phase/command_test.go index 6ddf3e835..afa7dd410 100644 --- a/pkg/phase/command_test.go +++ b/pkg/phase/command_test.go @@ -156,7 +156,7 @@ func TestPlanCommand(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - command := phase.PlanCommand{ + command := phase.ListCommand{ Factory: tt.factory, } err := command.RunE() diff --git a/pkg/util/tableprinter.go b/pkg/util/tableprinter.go new file mode 100644 index 000000000..ec7117569 --- /dev/null +++ b/pkg/util/tableprinter.go @@ -0,0 +1,152 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package util + +import ( + "fmt" + "io" + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + pe "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/print/table" +) + +// Printable is an object which has Group, Version, Kind, Name and Namespace +// fields and appropriate methods to retrieve them. All K8s api objects +// implement methods above +type Printable interface { + metav1.Object + runtime.Object +} + +var _ table.ResourceStates = &ResourceTable{} + +// ResourceTable provides information about the resources that should be printed +type ResourceTable struct { + resources []Printable + statusFunc PrintStatusFunction +} + +// NewResourceTable creates resource status table +func NewResourceTable(object interface{}, statusFunc PrintStatusFunction) (*ResourceTable, error) { + var resources []Printable + value := reflect.ValueOf(object) + switch value.Kind() { + case reflect.Slice: + resources = make([]Printable, value.Len()) + for i := 0; i < value.Len(); i++ { + printable, ok := value.Index(i).Interface().(Printable) + if !ok { + return nil, fmt.Errorf("resource %#v is not printable", value.Index(i).Interface()) + } + resources[i] = printable + } + default: + res, ok := value.Interface().(Printable) + if !ok { + return nil, fmt.Errorf("resource %#v is not printable", value.Interface()) + } + resources = []Printable{res} + } + + return &ResourceTable{ + resources: resources, + statusFunc: statusFunc, + }, nil +} + +// Resources list of table rows +func (rt *ResourceTable) Resources() []table.Resource { + result := make([]table.Resource, len(rt.resources)) + for i, obj := range rt.resources { + result[i] = &resource{Printable: obj, statusFunc: rt.statusFunc} + } + return result +} + +//Error returns error for resource table +func (rt *ResourceTable) Error() error { + return fmt.Errorf("error table printing") +} + +var _ table.Resource = &resource{} + +type resource struct { + Printable + statusFunc PrintStatusFunction +} + +// Identifier opf the resource +func (r *resource) Identifier() object.ObjMetadata { + return object.ObjMetadata{ + Namespace: r.GetNamespace(), + Name: r.GetName(), + GroupKind: r.GetObjectKind().GroupVersionKind().GroupKind(), + } +} + +// ResourceStatus returns resource status object +func (r *resource) ResourceStatus() *PrintResourceStatus { + return r.statusFunc(r.Printable) +} + +// SubResources list of subresources +func (r *resource) SubResources() []table.Resource { + return nil +} + +// DefaultTablePrinter returns basic table printer with 2 columns: Namespace +// and Name/Kind +func DefaultTablePrinter(out, errOut io.Writer) *table.BaseTablePrinter { + cols := []table.ColumnDefinition{ + table.MustColumn("namespace"), + table.MustColumn("resource"), + } + return &table.BaseTablePrinter{ + IOStreams: genericclioptions.IOStreams{ + Out: out, + ErrOut: errOut, + }, + Columns: cols, + } +} + +// PrintResourceStatus alias for ResourceStatus +type PrintResourceStatus = pe.ResourceStatus + +// PrintStatusFunction alias for status function +type PrintStatusFunction = func(Printable) *PrintResourceStatus + +// DefaultStatusFunction for resource status +func DefaultStatusFunction() PrintStatusFunction { + return func(obj Printable) *PrintResourceStatus { + unsContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + identifier := object.ObjMetadata{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + GroupKind: obj.GetObjectKind().GroupVersionKind().GroupKind(), + } + return &PrintResourceStatus{ + Identifier: identifier, + Resource: &unstructured.Unstructured{Object: unsContent}, + Error: err, + } + } +} diff --git a/pkg/util/tableprinter_test.go b/pkg/util/tableprinter_test.go new file mode 100644 index 000000000..9a80eeeb5 --- /dev/null +++ b/pkg/util/tableprinter_test.go @@ -0,0 +1,108 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package util_test + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + airapiv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/util" +) + +func TestPrintTableForList(t *testing.T) { + resources := []*airapiv1.Phase{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Phase", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "p1", + }, + }, + } + expectedOut := [][]byte{ + []byte("NAMESPACE RESOURCE "), + []byte(" Phase/p1 "), + {}, + } + + rt, err := util.NewResourceTable(resources, func(util.Printable) *util.PrintResourceStatus { return nil }) + require.NoError(t, err) + buf := &bytes.Buffer{} + util.DefaultTablePrinter(buf, nil).PrintTable(rt, 0) + out, err := ioutil.ReadAll(buf) + require.NoError(t, err) + assert.Equal(t, expectedOut, bytes.Split(out, []byte("\n"))) +} + +func TestDefaultStatusFunction(t *testing.T) { + f := util.DefaultStatusFunction() + expectedObj := map[string]interface{}{ + "kind": "Phase", + "metadata": map[string]interface{}{ + "name": "p1", + "creationTimestamp": nil, + }, + "config": map[string]interface{}{ + "documentEntryPoint": "", + "executorRef": nil, + }, + } + printable := &airapiv1.Phase{ + TypeMeta: metav1.TypeMeta{ + Kind: "Phase", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "p1", + }, + } + rs := f(printable) + assert.Equal(t, expectedObj, rs.Resource.Object) +} + +func TestNonPrintable(t *testing.T) { + _, err := util.NewResourceTable("non Printable string", util.DefaultStatusFunction()) + assert.Error(t, err) +} + +func TestPrintTableForSingleResource(t *testing.T) { + resource := &airapiv1.Phase{ + TypeMeta: metav1.TypeMeta{ + Kind: "Phase", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "p1", + }, + } + expectedOut := [][]byte{ + []byte("NAMESPACE RESOURCE "), + []byte(" Phase/p1 "), + {}, + } + rt, err := util.NewResourceTable(resource, func(util.Printable) *util.PrintResourceStatus { return nil }) + require.NoError(t, err) + buf := &bytes.Buffer{} + util.DefaultTablePrinter(buf, nil).PrintTable(rt, 0) + out, err := ioutil.ReadAll(buf) + require.NoError(t, err) + assert.Equal(t, expectedOut, bytes.Split(out, []byte("\n"))) +}