Implement plan run command

Change-Id: Ie627ce670cd2b19d6999dc7c7a7a6dc12b25cace
Closes: #395
This commit is contained in:
Dmitry Ukov 2020-12-09 17:38:40 +04:00
parent 4cba00d98c
commit 178b0eff3e
21 changed files with 360 additions and 157 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,14 +29,37 @@ Run life-cycle phase plan which was defined in document model.
// NewRunCommand creates a command which execute a particular phase plan
func NewRunCommand(cfgFactory config.Factory) *cobra.Command {
listCmd := &cobra.Command{
r := &phase.PlanRunCommand{
Factory: cfgFactory,
Options: phase.PlanRunFlags{},
}
runCmd := &cobra.Command{
Use: "run PLAN_NAME",
Short: "Run plan",
Long: runLong[1:],
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return errors.ErrNotImplemented{What: "airshipctl plan run"}
r.Options.PlanID.Name = args[0]
return r.RunE()
},
}
return listCmd
flags := runCmd.Flags()
flags.BoolVar(
&r.Options.DryRun,
"dry-run",
false,
"simulate phase execution")
flags.DurationVar(
&r.Options.Timeout,
"wait-timeout",
0,
"wait timeout")
flags.StringVar(
&r.Options.Kubeconfig,
"kubeconfig",
"",
"Path to kubeconfig associated with site being managed")
return runCmd
}

View File

@ -4,4 +4,7 @@ Usage:
run PLAN_NAME [flags]
Flags:
-h, --help help for run
--dry-run simulate phase execution
-h, --help help for run
--kubeconfig string Path to kubeconfig associated with site being managed
--wait-timeout duration wait timeout

View File

@ -14,7 +14,10 @@ airshipctl plan run PLAN_NAME [flags]
### Options
```
-h, --help help for run
--dry-run simulate phase execution
-h, --help help for run
--kubeconfig string Path to kubeconfig associated with site being managed
--wait-timeout duration wait timeout
```
### Options inherited from parent commands

View File

@ -788,15 +788,13 @@ apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
phaseGroups:
- name: group1
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target
```
### Cluster Templates

View File

@ -3,15 +3,13 @@ kind: PhasePlan
metadata:
name: phasePlan
description: "Default phase plan"
phaseGroups:
- name: group1
phases:
- name: initinfra-ephemeral
- name: initinfra-networking-ephemeral
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: initinfra-networking-target
- name: workers-target
- name: workers-classification
- name: workload-target
phases:
- name: initinfra-ephemeral
- name: initinfra-networking-ephemeral
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: initinfra-networking-target
- name: workers-target
- name: workers-classification
- name: workload-target

View File

@ -2,15 +2,13 @@ apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
phaseGroups:
- name: group1
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target
---
apiVersion: airshipit.org/v1alpha1
kind: Clusterctl

View File

@ -2,11 +2,9 @@ apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
phaseGroups:
- name: group1
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target

View File

@ -2,12 +2,10 @@ apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
phaseGroups:
- name: group1
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-networking-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-networking-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target

View File

@ -2,12 +2,10 @@ apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
phaseGroups:
- name: group1
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target
phases:
- name: clusterctl-init-ephemeral
- name: controlplane-ephemeral
- name: initinfra-target
- name: clusterctl-init-target
- name: clusterctl-move
- name: workers-target

View File

@ -24,18 +24,11 @@ import (
type PhasePlan struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Description string `json:"description,omitempty"`
PhaseGroups []PhaseGroup `json:"phaseGroups,omitempty"`
Description string `json:"description,omitempty"`
Phases []PhaseStep `json:"phases,omitempty"`
}
// PhaseGroup represents set of phases (i.e. steps) executed sequentially.
// Phase groups are executed simultaneously
type PhaseGroup struct {
Name string `json:"name,omitempty"`
Phases []PhaseGroupStep `json:"phases,omitempty"`
}
// PhaseGroupStep represents phase (or step) within phase group
type PhaseGroupStep struct {
// PhaseStep represents phase (or step) within a phase plan
type PhaseStep struct {
Name string `json:"name,omitempty"`
}

View File

@ -506,52 +506,15 @@ func (in *PhaseConfig) DeepCopy() *PhaseConfig {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PhaseGroup) DeepCopyInto(out *PhaseGroup) {
*out = *in
if in.Phases != nil {
in, out := &in.Phases, &out.Phases
*out = make([]PhaseGroupStep, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhaseGroup.
func (in *PhaseGroup) DeepCopy() *PhaseGroup {
if in == nil {
return nil
}
out := new(PhaseGroup)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PhaseGroupStep) DeepCopyInto(out *PhaseGroupStep) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhaseGroupStep.
func (in *PhaseGroupStep) DeepCopy() *PhaseGroupStep {
if in == nil {
return nil
}
out := new(PhaseGroupStep)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PhasePlan) DeepCopyInto(out *PhasePlan) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.PhaseGroups != nil {
in, out := &in.PhaseGroups, &out.PhaseGroups
*out = make([]PhaseGroup, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
if in.Phases != nil {
in, out := &in.Phases, &out.Phases
*out = make([]PhaseStep, len(*in))
copy(*out, *in)
}
}
@ -573,6 +536,21 @@ func (in *PhasePlan) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PhaseStep) DeepCopyInto(out *PhaseStep) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhaseStep.
func (in *PhaseStep) DeepCopy() *PhaseStep {
if in == nil {
return nil
}
out := new(PhaseStep)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Provider) DeepCopyInto(out *Provider) {
*out = *in

View File

@ -177,6 +177,34 @@ func (p *phase) Details() (string, error) {
return "", nil
}
var _ ifc.Plan = &plan{}
type plan struct {
helper ifc.Helper
apiObj *v1alpha1.PhasePlan
phaseClient ifc.Client
}
// Validate makes sure that phase plan is properly configured
// TODO implement this
func (p *plan) Validate() error { return nil }
// Run function excutes Run method for each phase
func (p *plan) Run(ro ifc.RunOptions) error {
for _, step := range p.apiObj.Phases {
phaseRunner, err := p.phaseClient.PhaseByID(ifc.ID{Name: step.Name})
if err != nil {
return err
}
log.Printf("executing phase: %s\n", step)
if err = phaseRunner.Run(ro); err != nil {
return err
}
}
return nil
}
var _ ifc.Client = &client{}
type client struct {
@ -245,6 +273,18 @@ func (c *client) PhaseByID(id ifc.ID) (ifc.Phase, error) {
return phase, nil
}
func (c *client) PlanByID(id ifc.ID) (ifc.Plan, error) {
planObj, err := c.Plan(id)
if err != nil {
return nil, err
}
return &plan{
apiObj: planObj,
helper: c.Helper,
phaseClient: c,
}, nil
}
func (c *client) PhaseByAPIObj(phaseObj *v1alpha1.Phase) (ifc.Phase, error) {
phase := &phase{
apiObj: phaseObj,

View File

@ -123,7 +123,7 @@ func TestPhaseRun(t *testing.T) {
client := phase.NewClient(helper, phase.InjectRegistry(tt.registryFunc))
require.NotNil(t, client)
p, err := client.PhaseByID(tt.phaseID)
require.NotNil(t, client)
require.NoError(t, err)
err = p.Run(ifc.RunOptions{DryRun: true})
if tt.errContains != "" {
require.Error(t, err)
@ -223,7 +223,7 @@ func TestBundleFactoryExecutor(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, helper)
fakeRegistry := func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
fakeReg := func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
validBundleFactory := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
@ -245,7 +245,7 @@ func TestBundleFactoryExecutor(t *testing.T) {
invalidBundleFactory: bundleCheckFunc,
}
}
c := phase.NewClient(helper, phase.InjectRegistry(fakeRegistry))
c := phase.NewClient(helper, phase.InjectRegistry(fakeReg))
p, err := c.PhaseByID(ifc.ID{Name: "capi_init"})
require.NoError(t, err)
_, err = p.Executor()
@ -257,6 +257,50 @@ func TestBundleFactoryExecutor(t *testing.T) {
assert.Equal(t, phase.ErrDocumentEntrypointNotDefined{PhaseName: "no_entry_point"}, err)
}
func TestPlanRun(t *testing.T) {
testCases := []struct {
name string
errContains string
planID ifc.ID
configFunc func(t *testing.T) *config.Config
registryFunc phase.ExecutorRegistry
}{
{
name: "Success fake executor",
configFunc: testConfig,
planID: ifc.ID{Name: "init"},
registryFunc: fakeRegistry,
},
{
name: "Error executor doc doesn't exist",
configFunc: testConfig,
planID: ifc.ID{Name: "some_plan"},
registryFunc: fakeRegistry,
errContains: "found no documents",
},
}
for _, tc := range testCases {
tt := tc
t.Run(tt.name, func(t *testing.T) {
conf := tt.configFunc(t)
helper, err := phase.NewHelper(conf)
require.NoError(t, err)
require.NotNil(t, helper)
client := phase.NewClient(helper, phase.InjectRegistry(tt.registryFunc))
require.NotNil(t, client)
p, err := client.PlanByID(tt.planID)
require.NoError(t, err)
err = p.Run(ifc.RunOptions{DryRun: true})
if tt.errContains != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
} else {
require.NoError(t, err)
}
})
}
}
func fakeExecFactory(config ifc.ExecutorConfig) (ifc.Executor, error) {
return fakeExecutor{}, nil
}

View File

@ -32,15 +32,20 @@ import (
"opendev.org/airship/airshipctl/pkg/util"
)
// RunFlags options for phase run command
type RunFlags struct {
// GenericRunFlags generic options for run command
type GenericRunFlags struct {
DryRun bool
Timeout time.Duration
PhaseID ifc.ID
Kubeconfig string
Progress bool
}
// RunFlags options for phase run command
type RunFlags struct {
GenericRunFlags
PhaseID ifc.ID
}
// RunCommand phase run command
type RunCommand struct {
Options RunFlags
@ -202,3 +207,37 @@ func (c *PlanListCommand) RunE() error {
printer.PrintTable(rt, 0)
return nil
}
// PlanRunFlags options for phase run command
type PlanRunFlags struct {
GenericRunFlags
PlanID ifc.ID
}
// PlanRunCommand phase run command
type PlanRunCommand struct {
Options PlanRunFlags
Factory config.Factory
}
// RunE executes phase plan
func (c *PlanRunCommand) RunE() error {
cfg, err := c.Factory()
if err != nil {
return err
}
helper, err := NewHelper(cfg)
if err != nil {
return err
}
kubeconfigOption := InjectKubeconfigPath(c.Options.Kubeconfig)
client := NewClient(helper, kubeconfigOption)
plan, err := client.PlanByID(c.Options.PlanID)
if err != nil {
return err
}
return plan.Run(ifc.RunOptions{DryRun: c.Options.DryRun, Timeout: c.Options.Timeout, Progress: c.Options.Progress})
}

View File

@ -293,3 +293,77 @@ func TestPlanListCommand(t *testing.T) {
})
}
}
func TestPlanRunCommand(t *testing.T) {
testErr := fmt.Errorf(testFactoryErr)
testCases := []struct {
name string
factory config.Factory
expectedErr string
}{
{
name: "Error config factory",
factory: func() (*config.Config, error) {
return nil, testErr
},
expectedErr: testFactoryErr,
},
{
name: "Error new helper",
factory: func() (*config.Config, error) {
return &config.Config{
CurrentContext: "does not exist",
Contexts: make(map[string]*config.Context),
}, nil
},
expectedErr: "Missing configuration: Context with name 'does not exist'",
},
{
name: "Error phase by id",
factory: func() (*config.Config, error) {
conf := config.NewConfig()
conf.Manifests = map[string]*config.Manifest{
"manifest": {
MetadataPath: "metadata.yaml",
TargetPath: "testdata",
PhaseRepositoryName: config.DefaultTestPhaseRepo,
Repositories: map[string]*config.Repository{
config.DefaultTestPhaseRepo: {
URLString: "",
},
},
},
}
conf.CurrentContext = "context"
conf.Contexts = map[string]*config.Context{
"context": {
Manifest: "manifest",
},
}
return conf, nil
},
expectedErr: `Error events received on channel, errors are:
[document filtered by selector [Group="airshipit.org", Version="v1alpha1", Kind="KubeConfig"] found no documents]`,
},
}
for _, tc := range testCases {
tt := tc
t.Run(tt.name, func(t *testing.T) {
cmd := phase.PlanRunCommand{
Options: phase.PlanRunFlags{
GenericRunFlags: phase.GenericRunFlags{
DryRun: true,
},
},
Factory: tt.factory,
}
err := cmd.RunE()
if tt.expectedErr != "" {
require.Error(t, err)
assert.Equal(t, tt.expectedErr, err.Error())
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -90,13 +90,18 @@ func (helper *Helper) Phase(phaseID ifc.ID) (*v1alpha1.Phase, error) {
}
// Plan returns plan associated with a manifest
func (helper *Helper) Plan() (*v1alpha1.PhasePlan, error) {
func (helper *Helper) Plan(planID ifc.ID) (*v1alpha1.PhasePlan, error) {
bundle, err := document.NewBundleByPath(helper.phaseBundleRoot)
if err != nil {
return nil, err
}
plan := &v1alpha1.PhasePlan{}
plan := &v1alpha1.PhasePlan{
ObjectMeta: v1.ObjectMeta{
Name: planID.Name,
Namespace: planID.Namespace,
},
}
selector, err := document.NewSelector().ByObject(plan, v1alpha1.Scheme)
if err != nil {
return nil, err

View File

@ -104,6 +104,7 @@ func TestHelperPhase(t *testing.T) {
}
func TestHelperPlan(t *testing.T) {
testPlanName := "phasePlan"
testCases := []struct {
name string
errContains string
@ -119,28 +120,23 @@ func TestHelperPlan(t *testing.T) {
APIVersion: "airshipit.org/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "phasePlan",
Name: testPlanName,
},
PhaseGroups: []airshipv1.PhaseGroup{
Phases: []airshipv1.PhaseStep{
{
Name: "group1",
Phases: []airshipv1.PhaseGroupStep{
{
Name: "isogen",
},
{
Name: "remotedirect",
},
{
Name: "initinfra",
},
{
Name: "some_phase",
},
{
Name: "capi_init",
},
},
Name: "isogen",
},
{
Name: "remotedirect",
},
{
Name: "initinfra",
},
{
Name: "some_phase",
},
{
Name: "capi_init",
},
},
},
@ -172,7 +168,7 @@ func TestHelperPlan(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, helper)
actualPlan, actualErr := helper.Plan()
actualPlan, actualErr := helper.Plan(ifc.ID{Name: testPlanName})
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
@ -244,7 +240,7 @@ func TestHelperListPlans(t *testing.T) {
}{
{
name: "Success plan list",
expectedLen: 1,
expectedLen: 3,
config: testConfig,
},
{

View File

@ -27,7 +27,7 @@ type Helper interface {
DocEntryPointPrefix() string
WorkDir() (string, error)
Phase(phaseID ID) (*v1alpha1.Phase, error)
Plan() (*v1alpha1.PhasePlan, error)
Plan(planID ID) (*v1alpha1.PhasePlan, error)
ListPhases() ([]*v1alpha1.Phase, error)
ListPlans() ([]*v1alpha1.PhasePlan, error)
ClusterMapAPIobj() (*v1alpha1.ClusterMap, error)

View File

@ -31,6 +31,12 @@ type Phase interface {
Render(io.Writer, bool, RenderOptions) error
}
// Plan provides a way to interact with phase plans
type Plan interface {
Validate() error
Run(RunOptions) error
}
// ID uniquely identifies the phase
type ID struct {
Name string
@ -40,6 +46,7 @@ type ID struct {
// Client is a phase client that can be used by command line or ui packages
type Client interface {
PhaseByID(ID) (Phase, error)
PlanByID(ID) (Plan, error)
PhaseByAPIObj(*v1alpha1.Phase) (Phase, error)
ClusterMap() (clustermap.ClusterMap, error)
}

View File

@ -3,7 +3,5 @@ kind: PhasePlan
metadata:
name: phasePlan
description: "Default phase plan"
phaseGroups:
- name: group1
phases:
- name: phase
phases:
- name: phase

View File

@ -2,11 +2,23 @@ apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: phasePlan
phaseGroups:
- name: group1
phases:
- name: isogen
- name: remotedirect
- name: initinfra
- name: some_phase
- name: capi_init
phases:
- name: isogen
- name: remotedirect
- name: initinfra
- name: some_phase
- name: capi_init
---
apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: init
phases:
- name: capi_init
---
apiVersion: airshipit.org/v1alpha1
kind: PhasePlan
metadata:
name: some_plan
phases:
- name: some_phase