Merge "[#358] Introduce Phase List command to output phase list(pkg module)."

This commit is contained in:
Zuul 2021-03-30 00:37:04 +00:00 committed by Gerrit Code Review
commit adacadda4d
9 changed files with 364 additions and 30 deletions

View File

@ -31,4 +31,5 @@ type PhasePlan struct {
// PhaseStep represents phase (or step) within a phase plan // PhaseStep represents phase (or step) within a phase plan
type PhaseStep struct { type PhaseStep struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
} }

View File

@ -28,8 +28,10 @@ import (
"opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document" "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/phase/ifc"
"opendev.org/airship/airshipctl/pkg/util" "opendev.org/airship/airshipctl/pkg/util"
"opendev.org/airship/airshipctl/pkg/util/yaml"
) )
// GenericRunFlags generic options for run command // GenericRunFlags generic options for run command
@ -77,10 +79,14 @@ type ListCommand struct {
Writer io.Writer Writer io.Writer
ClusterName string ClusterName string
PlanID ifc.ID PlanID ifc.ID
OutputFormat string
} }
// RunE runs a phase plan command // RunE runs a phase list command
func (c *ListCommand) RunE() error { func (c *ListCommand) RunE() error {
if c.OutputFormat != "table" && c.OutputFormat != "yaml" {
return phaseerrors.ErrInvalidFormat{RequestedFormat: c.OutputFormat}
}
cfg, err := c.Factory() cfg, err := c.Factory()
if err != nil { if err != nil {
return err return err
@ -92,18 +98,14 @@ func (c *ListCommand) RunE() error {
} }
o := ifc.ListPhaseOptions{ClusterName: c.ClusterName, PlanID: c.PlanID} o := ifc.ListPhaseOptions{ClusterName: c.ClusterName, PlanID: c.PlanID}
phases, err := helper.ListPhases(o) phaseList, err := helper.ListPhases(o)
if err != nil { if err != nil {
return err return err
} }
if c.OutputFormat == "table" {
rt, err := util.NewResourceTable(phases, util.DefaultStatusFunction()) return PrintPhaseListTable(c.Writer, phaseList)
if err != nil {
return err
} }
return yaml.WriteOut(c.Writer, phaseList)
util.DefaultTablePrinter(c.Writer, nil).PrintTable(rt, 0)
return nil
} }
// TreeCommand plan command // TreeCommand plan command

View File

@ -27,6 +27,7 @@ import (
"opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/log" "opendev.org/airship/airshipctl/pkg/log"
"opendev.org/airship/airshipctl/pkg/phase" "opendev.org/airship/airshipctl/pkg/phase"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
) )
const ( const (
@ -34,6 +35,8 @@ const (
testNewHelperErr = "missing configuration" testNewHelperErr = "missing configuration"
testNoBundlePath = "no such file or directory" testNoBundlePath = "no such file or directory"
defaultCurrentContext = "context" defaultCurrentContext = "context"
testTargetPath = "testdata"
testMetadataPath = "metadata.yaml"
) )
func TestRunCommand(t *testing.T) { func TestRunCommand(t *testing.T) {
@ -67,7 +70,7 @@ func TestRunCommand(t *testing.T) {
conf.Manifests = map[string]*config.Manifest{ conf.Manifests = map[string]*config.Manifest{
"manifest": { "manifest": {
MetadataPath: "broken_metadata.yaml", MetadataPath: "broken_metadata.yaml",
TargetPath: "testdata", TargetPath: testTargetPath,
PhaseRepositoryName: config.DefaultTestPhaseRepo, PhaseRepositoryName: config.DefaultTestPhaseRepo,
Repositories: map[string]*config.Repository{ Repositories: map[string]*config.Repository{
config.DefaultTestPhaseRepo: { config.DefaultTestPhaseRepo: {
@ -107,11 +110,35 @@ func TestRunCommand(t *testing.T) {
} }
func TestListCommand(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 { tests := []struct {
name string name string
errContains string errContains string
runFlags phase.RunFlags runFlags phase.RunFlags
expectedOut [][]byte
expectedYamlOut string
factory config.Factory factory config.Factory
PlanID ifc.ID
PhaseID ifc.ID
OutputFormat string
}{ }{
{ {
name: "Error config factory", name: "Error config factory",
@ -119,6 +146,8 @@ func TestListCommand(t *testing.T) {
return nil, fmt.Errorf(testFactoryErr) return nil, fmt.Errorf(testFactoryErr)
}, },
errContains: testFactoryErr, errContains: testFactoryErr,
expectedOut: [][]byte{{}},
OutputFormat: "table",
}, },
{ {
name: "Error new helper", name: "Error new helper",
@ -129,15 +158,17 @@ func TestListCommand(t *testing.T) {
}, nil }, nil
}, },
errContains: testNewHelperErr, errContains: testNewHelperErr,
expectedOut: [][]byte{{}},
OutputFormat: "table",
}, },
{ {
name: "Error phase by id", name: "List phases",
factory: func() (*config.Config, error) { factory: func() (*config.Config, error) {
conf := config.NewConfig() conf := config.NewConfig()
conf.Manifests = map[string]*config.Manifest{ conf.Manifests = map[string]*config.Manifest{
"manifest": { "manifest": {
MetadataPath: "broken_metadata.yaml", MetadataPath: testMetadataPath,
TargetPath: "testdata", TargetPath: testTargetPath,
PhaseRepositoryName: config.DefaultTestPhaseRepo, PhaseRepositoryName: config.DefaultTestPhaseRepo,
Repositories: map[string]*config.Repository{ Repositories: map[string]*config.Repository{
config.DefaultTestPhaseRepo: { config.DefaultTestPhaseRepo: {
@ -154,15 +185,55 @@ func TestListCommand(t *testing.T) {
} }
return conf, nil 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 { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
buffer := &bytes.Buffer{}
command := phase.ListCommand{ command := phase.ListCommand{
Factory: tt.factory, Factory: tt.factory,
Writer: buffer,
PlanID: tt.PlanID,
OutputFormat: tt.OutputFormat,
} }
err := command.RunE() err := command.RunE()
if tt.errContains != "" { if tt.errContains != "" {
@ -171,6 +242,14 @@ func TestListCommand(t *testing.T) {
} else { } else {
assert.NoError(t, err) 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{ conf.Manifests = map[string]*config.Manifest{
"manifest": { "manifest": {
MetadataPath: "broken_metadata.yaml", MetadataPath: "broken_metadata.yaml",
TargetPath: "testdata", TargetPath: testTargetPath,
PhaseRepositoryName: config.DefaultTestPhaseRepo, PhaseRepositoryName: config.DefaultTestPhaseRepo,
Repositories: map[string]*config.Repository{ Repositories: map[string]*config.Repository{
config.DefaultTestPhaseRepo: { config.DefaultTestPhaseRepo: {
@ -264,7 +343,7 @@ func TestPlanListCommand(t *testing.T) {
conf := config.NewConfig() conf := config.NewConfig()
manifest := conf.Manifests[config.AirshipDefaultManifest] manifest := conf.Manifests[config.AirshipDefaultManifest]
manifest.TargetPath = "testdata" manifest.TargetPath = "testdata"
manifest.MetadataPath = "metadata.yaml" manifest.MetadataPath = testMetadataPath
manifest.Repositories[config.DefaultTestPhaseRepo].URLString = "" manifest.Repositories[config.DefaultTestPhaseRepo].URLString = ""
return conf, nil return conf, nil
}, },
@ -328,7 +407,7 @@ func TestPlanRunCommand(t *testing.T) {
conf := config.NewConfig() conf := config.NewConfig()
conf.Manifests = map[string]*config.Manifest{ conf.Manifests = map[string]*config.Manifest{
"manifest": { "manifest": {
MetadataPath: "metadata.yaml", MetadataPath: testMetadataPath,
TargetPath: "testdata", TargetPath: "testdata",
PhaseRepositoryName: config.DefaultTestPhaseRepo, PhaseRepositoryName: config.DefaultTestPhaseRepo,
Repositories: map[string]*config.Repository{ Repositories: map[string]*config.Repository{

View File

@ -52,6 +52,15 @@ func (e ErrRenderPhaseNameNotSpecified) Error() string {
e.Sources) 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 // ErrInvalidPhase is returned if the phase is invalid
type ErrInvalidPhase struct { type ErrInvalidPhase struct {
Reason string Reason string

View File

@ -110,11 +110,13 @@ func TestHelperPlan(t *testing.T) {
name string name string
errContains string errContains string
expectedPlan *v1alpha1.PhasePlan expectedPlan *v1alpha1.PhasePlan
planID ifc.ID
config func(t *testing.T) *config.Config config func(t *testing.T) *config.Config
}{ }{
{ {
name: "Valid Phase Plan", name: "Valid Phase Plan",
config: testConfig, config: testConfig,
planID: ifc.ID{Name: "phasePlan"},
expectedPlan: &airshipv1.PhasePlan{ expectedPlan: &airshipv1.PhasePlan{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "PhasePlan", Kind: "PhasePlan",
@ -188,7 +190,7 @@ func TestHelperListPhases(t *testing.T) {
}{ }{
{ {
name: "Success phase list", name: "Success phase list",
phaseLen: 5, phaseLen: 8,
config: testConfig, config: testConfig,
}, },
{ {

88
pkg/phase/printers.go Normal file
View 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
View 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)
}

View File

@ -1,4 +1,5 @@
resources: resources:
- phases.yaml
- phaseplan.yaml - phaseplan.yaml
- some_phase.yaml - some_phase.yaml
- some_exc.yaml - some_exc.yaml

View 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