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
This commit is contained in:
Dmitry Ukov 2020-12-07 16:39:09 +04:00
parent 8efb786a6a
commit c36a8ea022
9 changed files with 311 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

152
pkg/util/tableprinter.go Normal file
View File

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

View File

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