[#358] Introduce Phase List command to output phase list(pkg module).
* Phase list command lists phases of current documentset/plan in table or yaml format. airshipctl phase list airshipctl phase list --plan planName airshipctl phase list --plan planName -o yaml airshipctl phase list --plan planName -o table(default format) Relates-To: #358 Co-Authored-By: Niharika Bhavaraju <niha.twinkle@gmail.com> Change-Id: I37add2fc9dca2433de525bac8c2cc9e56fe39621
This commit is contained in:
parent
4e296dacab
commit
9bf40366a5
@ -30,5 +30,6 @@ type PhasePlan struct {
|
||||
|
||||
// PhaseStep represents phase (or step) within a phase plan
|
||||
type PhaseStep struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
}
|
||||
|
@ -28,8 +28,10 @@ import (
|
||||
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
|
||||
"opendev.org/airship/airshipctl/pkg/config"
|
||||
"opendev.org/airship/airshipctl/pkg/document"
|
||||
phaseerrors "opendev.org/airship/airshipctl/pkg/phase/errors"
|
||||
"opendev.org/airship/airshipctl/pkg/phase/ifc"
|
||||
"opendev.org/airship/airshipctl/pkg/util"
|
||||
"opendev.org/airship/airshipctl/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// GenericRunFlags generic options for run command
|
||||
@ -73,14 +75,18 @@ func (c *RunCommand) RunE() error {
|
||||
|
||||
// ListCommand phase list command
|
||||
type ListCommand struct {
|
||||
Factory config.Factory
|
||||
Writer io.Writer
|
||||
ClusterName string
|
||||
PlanID ifc.ID
|
||||
Factory config.Factory
|
||||
Writer io.Writer
|
||||
ClusterName string
|
||||
PlanID ifc.ID
|
||||
OutputFormat string
|
||||
}
|
||||
|
||||
// RunE runs a phase plan command
|
||||
// RunE runs a phase list command
|
||||
func (c *ListCommand) RunE() error {
|
||||
if c.OutputFormat != "table" && c.OutputFormat != "yaml" {
|
||||
return phaseerrors.ErrInvalidFormat{RequestedFormat: c.OutputFormat}
|
||||
}
|
||||
cfg, err := c.Factory()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -92,18 +98,14 @@ func (c *ListCommand) RunE() error {
|
||||
}
|
||||
|
||||
o := ifc.ListPhaseOptions{ClusterName: c.ClusterName, PlanID: c.PlanID}
|
||||
phases, err := helper.ListPhases(o)
|
||||
phaseList, err := helper.ListPhases(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rt, err := util.NewResourceTable(phases, util.DefaultStatusFunction())
|
||||
if err != nil {
|
||||
return err
|
||||
if c.OutputFormat == "table" {
|
||||
return PrintPhaseListTable(c.Writer, phaseList)
|
||||
}
|
||||
|
||||
util.DefaultTablePrinter(c.Writer, nil).PrintTable(rt, 0)
|
||||
return nil
|
||||
return yaml.WriteOut(c.Writer, phaseList)
|
||||
}
|
||||
|
||||
// TreeCommand plan command
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
"opendev.org/airship/airshipctl/pkg/config"
|
||||
"opendev.org/airship/airshipctl/pkg/log"
|
||||
"opendev.org/airship/airshipctl/pkg/phase"
|
||||
"opendev.org/airship/airshipctl/pkg/phase/ifc"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -34,6 +35,8 @@ const (
|
||||
testNewHelperErr = "missing configuration"
|
||||
testNoBundlePath = "no such file or directory"
|
||||
defaultCurrentContext = "context"
|
||||
testTargetPath = "testdata"
|
||||
testMetadataPath = "metadata.yaml"
|
||||
)
|
||||
|
||||
func TestRunCommand(t *testing.T) {
|
||||
@ -67,7 +70,7 @@ func TestRunCommand(t *testing.T) {
|
||||
conf.Manifests = map[string]*config.Manifest{
|
||||
"manifest": {
|
||||
MetadataPath: "broken_metadata.yaml",
|
||||
TargetPath: "testdata",
|
||||
TargetPath: testTargetPath,
|
||||
PhaseRepositoryName: config.DefaultTestPhaseRepo,
|
||||
Repositories: map[string]*config.Repository{
|
||||
config.DefaultTestPhaseRepo: {
|
||||
@ -107,18 +110,44 @@ func TestRunCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListCommand(t *testing.T) {
|
||||
outputString1 := "NAMESPACE RESOURCE CLUSTER " +
|
||||
"NAME EXECUTOR DOC ENTRYPOINT "
|
||||
outputString2 := " Phase/phase ephemeral" +
|
||||
"-cluster KubernetesApply ephemeral/phase "
|
||||
yamlOutput := `---
|
||||
- apiVersion: airshipit.org/v1alpha1
|
||||
config:
|
||||
documentEntryPoint: ephemeral/phase
|
||||
executorRef:
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: KubernetesApply
|
||||
name: kubernetes-apply
|
||||
kind: Phase
|
||||
metadata:
|
||||
clusterName: ephemeral-cluster
|
||||
creationTimestamp: null
|
||||
name: phase
|
||||
...
|
||||
`
|
||||
tests := []struct {
|
||||
name string
|
||||
errContains string
|
||||
runFlags phase.RunFlags
|
||||
factory config.Factory
|
||||
name string
|
||||
errContains string
|
||||
runFlags phase.RunFlags
|
||||
expectedOut [][]byte
|
||||
expectedYamlOut string
|
||||
factory config.Factory
|
||||
PlanID ifc.ID
|
||||
PhaseID ifc.ID
|
||||
OutputFormat string
|
||||
}{
|
||||
{
|
||||
name: "Error config factory",
|
||||
factory: func() (*config.Config, error) {
|
||||
return nil, fmt.Errorf(testFactoryErr)
|
||||
},
|
||||
errContains: testFactoryErr,
|
||||
errContains: testFactoryErr,
|
||||
expectedOut: [][]byte{{}},
|
||||
OutputFormat: "table",
|
||||
},
|
||||
{
|
||||
name: "Error new helper",
|
||||
@ -128,16 +157,18 @@ func TestListCommand(t *testing.T) {
|
||||
Contexts: make(map[string]*config.Context),
|
||||
}, nil
|
||||
},
|
||||
errContains: testNewHelperErr,
|
||||
errContains: testNewHelperErr,
|
||||
expectedOut: [][]byte{{}},
|
||||
OutputFormat: "table",
|
||||
},
|
||||
{
|
||||
name: "Error phase by id",
|
||||
name: "List phases",
|
||||
factory: func() (*config.Config, error) {
|
||||
conf := config.NewConfig()
|
||||
conf.Manifests = map[string]*config.Manifest{
|
||||
"manifest": {
|
||||
MetadataPath: "broken_metadata.yaml",
|
||||
TargetPath: "testdata",
|
||||
MetadataPath: testMetadataPath,
|
||||
TargetPath: testTargetPath,
|
||||
PhaseRepositoryName: config.DefaultTestPhaseRepo,
|
||||
Repositories: map[string]*config.Repository{
|
||||
config.DefaultTestPhaseRepo: {
|
||||
@ -154,15 +185,55 @@ func TestListCommand(t *testing.T) {
|
||||
}
|
||||
return conf, nil
|
||||
},
|
||||
errContains: testNoBundlePath,
|
||||
expectedOut: [][]byte{
|
||||
[]byte(outputString1),
|
||||
[]byte(outputString2),
|
||||
{},
|
||||
},
|
||||
OutputFormat: "table",
|
||||
},
|
||||
{
|
||||
name: "List phases of a plan",
|
||||
factory: func() (*config.Config, error) {
|
||||
conf := config.NewConfig()
|
||||
manifest := conf.Manifests[config.AirshipDefaultManifest]
|
||||
manifest.TargetPath = testTargetPath
|
||||
manifest.MetadataPath = testMetadataPath
|
||||
manifest.Repositories[config.DefaultTestPhaseRepo].URLString = ""
|
||||
return conf, nil
|
||||
},
|
||||
PlanID: ifc.ID{Name: "phasePlan"},
|
||||
expectedOut: [][]byte{
|
||||
[]byte(outputString1),
|
||||
[]byte(outputString2),
|
||||
{},
|
||||
},
|
||||
OutputFormat: "table",
|
||||
},
|
||||
{
|
||||
name: "List phases yaml format",
|
||||
factory: func() (*config.Config, error) {
|
||||
conf := config.NewConfig()
|
||||
manifest := conf.Manifests[config.AirshipDefaultManifest]
|
||||
manifest.TargetPath = testTargetPath
|
||||
manifest.MetadataPath = testMetadataPath
|
||||
manifest.Repositories[config.DefaultTestPhaseRepo].URLString = ""
|
||||
return conf, nil
|
||||
},
|
||||
OutputFormat: "yaml",
|
||||
expectedYamlOut: yamlOutput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buffer := &bytes.Buffer{}
|
||||
command := phase.ListCommand{
|
||||
Factory: tt.factory,
|
||||
Factory: tt.factory,
|
||||
Writer: buffer,
|
||||
PlanID: tt.PlanID,
|
||||
OutputFormat: tt.OutputFormat,
|
||||
}
|
||||
err := command.RunE()
|
||||
if tt.errContains != "" {
|
||||
@ -171,6 +242,14 @@ func TestListCommand(t *testing.T) {
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
out, err := ioutil.ReadAll(buffer)
|
||||
require.NoError(t, err)
|
||||
if tt.OutputFormat == "yaml" {
|
||||
assert.Equal(t, tt.expectedYamlOut, string(out))
|
||||
} else {
|
||||
b := bytes.Split(out, []byte("\n"))
|
||||
assert.Equal(t, tt.expectedOut, b)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -205,7 +284,7 @@ func TestTreeCommand(t *testing.T) {
|
||||
conf.Manifests = map[string]*config.Manifest{
|
||||
"manifest": {
|
||||
MetadataPath: "broken_metadata.yaml",
|
||||
TargetPath: "testdata",
|
||||
TargetPath: testTargetPath,
|
||||
PhaseRepositoryName: config.DefaultTestPhaseRepo,
|
||||
Repositories: map[string]*config.Repository{
|
||||
config.DefaultTestPhaseRepo: {
|
||||
@ -264,7 +343,7 @@ func TestPlanListCommand(t *testing.T) {
|
||||
conf := config.NewConfig()
|
||||
manifest := conf.Manifests[config.AirshipDefaultManifest]
|
||||
manifest.TargetPath = "testdata"
|
||||
manifest.MetadataPath = "metadata.yaml"
|
||||
manifest.MetadataPath = testMetadataPath
|
||||
manifest.Repositories[config.DefaultTestPhaseRepo].URLString = ""
|
||||
return conf, nil
|
||||
},
|
||||
@ -328,7 +407,7 @@ func TestPlanRunCommand(t *testing.T) {
|
||||
conf := config.NewConfig()
|
||||
conf.Manifests = map[string]*config.Manifest{
|
||||
"manifest": {
|
||||
MetadataPath: "metadata.yaml",
|
||||
MetadataPath: testMetadataPath,
|
||||
TargetPath: "testdata",
|
||||
PhaseRepositoryName: config.DefaultTestPhaseRepo,
|
||||
Repositories: map[string]*config.Repository{
|
||||
|
@ -52,6 +52,15 @@ func (e ErrRenderPhaseNameNotSpecified) Error() string {
|
||||
e.Sources)
|
||||
}
|
||||
|
||||
// ErrInvalidFormat is called when the user provides format other than yaml/json
|
||||
type ErrInvalidFormat struct {
|
||||
RequestedFormat string
|
||||
}
|
||||
|
||||
func (e ErrInvalidFormat) Error() string {
|
||||
return fmt.Sprintf("invalid output format specified %s. Allowed values are table|yaml", e.RequestedFormat)
|
||||
}
|
||||
|
||||
// ErrInvalidPhase is returned if the phase is invalid
|
||||
type ErrInvalidPhase struct {
|
||||
Reason string
|
||||
|
@ -110,11 +110,13 @@ func TestHelperPlan(t *testing.T) {
|
||||
name string
|
||||
errContains string
|
||||
expectedPlan *v1alpha1.PhasePlan
|
||||
planID ifc.ID
|
||||
config func(t *testing.T) *config.Config
|
||||
}{
|
||||
{
|
||||
name: "Valid Phase Plan",
|
||||
config: testConfig,
|
||||
planID: ifc.ID{Name: "phasePlan"},
|
||||
expectedPlan: &airshipv1.PhasePlan{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "PhasePlan",
|
||||
@ -188,7 +190,7 @@ func TestHelperListPhases(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Success phase list",
|
||||
phaseLen: 5,
|
||||
phaseLen: 8,
|
||||
config: testConfig,
|
||||
},
|
||||
{
|
||||
|
88
pkg/phase/printers.go
Normal file
88
pkg/phase/printers.go
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 phase
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
|
||||
"opendev.org/airship/airshipctl/pkg/util"
|
||||
|
||||
"sigs.k8s.io/cli-utils/pkg/print/table"
|
||||
)
|
||||
|
||||
//PrintPhaseListTable prints phase list table
|
||||
func PrintPhaseListTable(w io.Writer, phases []*v1alpha1.Phase) error {
|
||||
rt, err := util.NewResourceTable(phases, util.DefaultStatusFunction())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printer := util.DefaultTablePrinter(w, nil)
|
||||
clusternameCol := table.ColumnDef{
|
||||
ColumnName: "clustername",
|
||||
ColumnHeader: "CLUSTER NAME",
|
||||
ColumnWidth: 20,
|
||||
PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int,
|
||||
error) {
|
||||
phase, err := phaseFromResource(r)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return fmt.Fprintf(w, phase.ClusterName)
|
||||
},
|
||||
}
|
||||
executorrefkindCol := table.ColumnDef{
|
||||
ColumnName: "executorrefkind",
|
||||
ColumnHeader: "EXECUTOR",
|
||||
ColumnWidth: 20,
|
||||
PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int,
|
||||
error) {
|
||||
phase, err := phaseFromResource(r)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return fmt.Fprintf(w, phase.Config.ExecutorRef.Kind)
|
||||
},
|
||||
}
|
||||
docentrypointCol := table.ColumnDef{
|
||||
ColumnName: "docentrypoint",
|
||||
ColumnHeader: "DOC ENTRYPOINT",
|
||||
ColumnWidth: 40,
|
||||
PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int,
|
||||
error) {
|
||||
phase, err := phaseFromResource(r)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return fmt.Fprintf(w, phase.Config.DocumentEntryPoint)
|
||||
},
|
||||
}
|
||||
printer.Columns = append(printer.Columns, clusternameCol, executorrefkindCol, docentrypointCol)
|
||||
printer.PrintTable(rt, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func phaseFromResource(r table.Resource) (*v1alpha1.Phase, error) {
|
||||
rs := r.ResourceStatus()
|
||||
if rs == nil {
|
||||
return nil, errors.New("Resource status is nil")
|
||||
}
|
||||
phase := &v1alpha1.Phase{}
|
||||
return phase, runtime.DefaultUnstructuredConverter.FromUnstructured(rs.Resource.Object, phase)
|
||||
}
|
120
pkg/phase/printers_test.go
Normal file
120
pkg/phase/printers_test.go
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
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 phase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
|
||||
"opendev.org/airship/airshipctl/pkg/util"
|
||||
)
|
||||
|
||||
func TestPrintPhaseListTable(t *testing.T) {
|
||||
phases := []*v1alpha1.Phase{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Phase",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "p1",
|
||||
ClusterName: "cluster",
|
||||
},
|
||||
Config: v1alpha1.PhaseConfig{
|
||||
DocumentEntryPoint: "test",
|
||||
ExecutorRef: &v1.ObjectReference{Kind: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
phases []*v1alpha1.Phase
|
||||
wantPanic bool
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
phases: phases,
|
||||
wantPanic: false,
|
||||
},
|
||||
{
|
||||
name: "phase with no executor ref",
|
||||
phases: []*v1alpha1.Phase{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Pe",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "p1",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPanic: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := &bytes.Buffer{}
|
||||
defer func() {
|
||||
r := recover()
|
||||
if (r != nil) != tt.wantPanic {
|
||||
t.Errorf("should have panic")
|
||||
}
|
||||
}()
|
||||
err := PrintPhaseListTable(w, tt.phases)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonPrintable(t *testing.T) {
|
||||
_, err := util.NewResourceTable("non Printable string", util.DefaultStatusFunction())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
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": map[string]interface{}{
|
||||
"kind": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
printable := &v1alpha1.Phase{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Phase",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "p1",
|
||||
},
|
||||
Config: v1alpha1.PhaseConfig{
|
||||
ExecutorRef: &v1.ObjectReference{Kind: "test"},
|
||||
},
|
||||
}
|
||||
rs := f(printable)
|
||||
assert.Equal(t, expectedObj, rs.Resource.Object)
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
resources:
|
||||
- phases.yaml
|
||||
- phaseplan.yaml
|
||||
- some_phase.yaml
|
||||
- some_exc.yaml
|
||||
|
32
pkg/phase/testdata/valid_site/phases/phases.yaml
vendored
Normal file
32
pkg/phase/testdata/valid_site/phases/phases.yaml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: Phase
|
||||
metadata:
|
||||
name: isogen
|
||||
config:
|
||||
executorRef:
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: Clusterctl
|
||||
name: clusterctl-v1
|
||||
documentEntryPoint: valid_site/phases
|
||||
---
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: Phase
|
||||
metadata:
|
||||
name: remotedirect
|
||||
config:
|
||||
executorRef:
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: Clusterctl
|
||||
name: clusterctl-v1
|
||||
documentEntryPoint: valid_site/phases
|
||||
---
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: Phase
|
||||
metadata:
|
||||
name: initinfra
|
||||
config:
|
||||
executorRef:
|
||||
apiVersion: airshipit.org/v1alpha1
|
||||
kind: Clusterctl
|
||||
name: clusterctl-v1
|
||||
documentEntryPoint: valid_site/phases
|
Loading…
Reference in New Issue
Block a user