Add phase run method

Change introduces phase executor interface as well as method to call it.
Each registered executor must implement this interface.

Relates-To: #259
Change-Id: I44665e5318ae59b4549cc77d10526a71bd40b40a
This commit is contained in:
Dmitry Ukov 2020-06-05 16:04:17 +04:00
parent d63cdc6c24
commit 66b6acf565
10 changed files with 363 additions and 0 deletions

31
pkg/phase/errors.go Normal file
View File

@ -0,0 +1,31 @@
/*
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 (
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// ErrExecutorNotFound is returned if phase executor was not found in executor
// registry map
type ErrExecutorNotFound struct {
GVK schema.GroupVersionKind
}
func (e ErrExecutorNotFound) Error() string {
return fmt.Sprintf("executor identified by '%s' is not found", e.GVK)
}

42
pkg/phase/ifc/executor.go Normal file
View File

@ -0,0 +1,42 @@
/*
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 ifc
import (
"io"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/environment"
)
// Executor interface should be implemented by each runner
type Executor interface {
Run(dryrun, debug bool) error
Render(io.Writer) error
Validate() error
Wait() error
}
// ExecutorFactory for executor instantiation
// First argument is document object which represents executor
// configuration.
// Second argument is document bundle used by executor.
// Third argument airship configuration settings since each phase
// has to be aware of execution context and global settings
type ExecutorFactory func(
document.Document,
document.Bundle,
*environment.AirshipCTLSettings,
) (Executor, error)

View File

@ -17,9 +17,13 @@ package phase
import (
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
const (
@ -28,6 +32,11 @@ const (
PhaseDirName = "phases"
)
var (
// ExecutorRegistry contins registered runner factories
ExecutorRegistry = make(map[schema.GroupVersionKind]ifc.ExecutorFactory)
)
// Cmd object to work with phase api
type Cmd struct {
*environment.AirshipCTLSettings
@ -42,6 +51,84 @@ func (p *Cmd) getBundle() (document.Bundle, error) {
return document.NewBundleByPath(filepath.Join(ccm.TargetPath, ccm.SubPath, PhaseDirName))
}
// GetPhase returns particular phase object identified by name
func (p *Cmd) GetPhase(name string) (*airshipv1.Phase, error) {
bundle, err := p.getBundle()
if err != nil {
return nil, err
}
phaseConfig := &airshipv1.Phase{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
selector, err := document.NewSelector().ByObject(phaseConfig, airshipv1.Scheme)
if err != nil {
return nil, err
}
doc, err := bundle.SelectOne(selector)
if err != nil {
return nil, err
}
if err = doc.ToAPIObject(phaseConfig, airshipv1.Scheme); err != nil {
return nil, err
}
return phaseConfig, nil
}
// GetExecutor referenced in a phase configuration
func (p *Cmd) GetExecutor(phase *airshipv1.Phase) (ifc.Executor, error) {
bundle, err := p.getBundle()
if err != nil {
return nil, err
}
phaseConfig := phase.Config
// Searching executor configuration document referenced in
// phase configuration
refGVK := phaseConfig.ExecutorRef.GroupVersionKind()
selector := document.NewSelector().
ByGvk(refGVK.Group, refGVK.Version, refGVK.Kind).
ByName(phaseConfig.ExecutorRef.Name).
ByNamespace(phaseConfig.ExecutorRef.Namespace)
doc, err := bundle.SelectOne(selector)
if err != nil {
return nil, err
}
// Define executor configuration options
targetPath, err := p.Config.CurrentContextTargetPath()
if err != nil {
return nil, err
}
executorDocBundle, err := document.NewBundleByPath(filepath.Join(targetPath, phaseConfig.DocumentEntryPoint))
if err != nil {
return nil, err
}
// Look for executor factory defined in registry
executorFactory, found := ExecutorRegistry[refGVK]
if !found {
return nil, ErrExecutorNotFound{GVK: refGVK}
}
return executorFactory(doc, executorDocBundle, p.AirshipCTLSettings)
}
// Exec particular phase
func (p *Cmd) Exec(name string) error {
phaseConfig, err := p.GetPhase(name)
if err != nil {
return err
}
executor, err := p.GetExecutor(phaseConfig)
if err != nil {
return err
}
return executor.Run(p.DryRun, p.Debug)
}
// Plan shows available phase names
func (p *Cmd) Plan() (map[string][]string, error) {
bundle, err := p.getBundle()

View File

@ -19,13 +19,20 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/phase"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
func TestPhasePlan(t *testing.T) {
@ -52,6 +59,8 @@ func TestPhasePlan(t *testing.T) {
"isogen",
"remotedirect",
"initinfra",
"some_phase",
"capi_init",
},
},
},
@ -89,6 +98,149 @@ func TestPhasePlan(t *testing.T) {
}
}
func TestGetPhase(t *testing.T) {
testCases := []struct {
name string
settings func() *environment.AirshipCTLSettings
phaseName string
expectedPhase *airshipv1.Phase
expectedErr error
}{
{
name: "No context",
settings: func() *environment.AirshipCTLSettings {
s := makeDefaultSettings()
s.Config.CurrentContext = "badCtx"
return s
},
expectedErr: config.ErrMissingConfig{What: "Context with name 'badCtx'"},
},
{
name: "Get existing phase",
settings: makeDefaultSettings,
phaseName: "capi_init",
expectedPhase: &airshipv1.Phase{
TypeMeta: metav1.TypeMeta{
APIVersion: "airshipit.org/v1alpha1",
Kind: "Phase",
},
ObjectMeta: metav1.ObjectMeta{
Name: "capi_init",
},
Config: airshipv1.PhaseConfig{
ExecutorRef: &corev1.ObjectReference{
Kind: "Clusterctl",
APIVersion: "airshipit.org/v1alpha1",
Name: "clusterctl-v1",
},
DocumentEntryPoint: "manifests/site/test-site/auth",
},
},
},
{
name: "Get non-existing phase",
settings: makeDefaultSettings,
phaseName: "some_name",
expectedErr: document.ErrDocNotFound{
Selector: document.Selector{
Selector: types.Selector{
Gvk: resid.Gvk{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "Phase",
},
Name: "some_name",
},
},
},
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
cmd := phase.Cmd{AirshipCTLSettings: tt.settings()}
actualPhase, actualErr := cmd.GetPhase(tt.phaseName)
assert.Equal(t, tt.expectedErr, actualErr)
assert.Equal(t, tt.expectedPhase, actualPhase)
})
}
}
func TestGetExecutor(t *testing.T) {
testCases := []struct {
name string
settings func() *environment.AirshipCTLSettings
phase *airshipv1.Phase
expectedExc ifc.Executor
expectedErr error
}{
{
name: "No context",
settings: func() *environment.AirshipCTLSettings {
s := makeDefaultSettings()
s.Config.CurrentContext = "badCtx"
return s
},
expectedErr: config.ErrMissingConfig{What: "Context with name 'badCtx'"},
},
{
name: "Get non-existing executor",
settings: makeDefaultSettings,
phase: &airshipv1.Phase{
Config: airshipv1.PhaseConfig{
ExecutorRef: &corev1.ObjectReference{
APIVersion: "example.com/v1",
Kind: "SomeKind",
},
},
},
expectedErr: document.ErrDocNotFound{
Selector: document.Selector{
Selector: types.Selector{
Gvk: resid.Gvk{
Group: "example.com",
Version: "v1",
Kind: "SomeKind",
},
},
},
},
},
{
name: "Get unregistered executor",
settings: makeDefaultSettings,
phase: &airshipv1.Phase{
Config: airshipv1.PhaseConfig{
ExecutorRef: &corev1.ObjectReference{
APIVersion: "airshipit.org/v1alpha1",
Kind: "SomeExecutor",
Name: "executor-name",
},
DocumentEntryPoint: "valid_site/phases",
},
},
expectedErr: phase.ErrExecutorNotFound{
GVK: schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "SomeExecutor",
},
},
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
cmd := phase.Cmd{AirshipCTLSettings: tt.settings()}
actualExc, actualErr := cmd.GetExecutor(tt.phase)
assert.Equal(t, tt.expectedErr, actualErr)
assert.Equal(t, tt.expectedExc, actualExc)
})
}
}
func makeDefaultSettings() *environment.AirshipCTLSettings {
testSettings := &environment.AirshipCTLSettings{
AirshipConfigPath: "testdata/airshipconfig.yaml",

View File

@ -0,0 +1,10 @@
apiVersion: airshipit.org/v1alpha1
kind: Phase
metadata:
name: capi_init
config:
executorRef:
apiVersion: airshipit.org/v1alpha1
kind: Clusterctl
name: clusterctl-v1
documentEntryPoint: manifests/site/test-site/auth

View File

@ -0,0 +1,12 @@
apiVersion: airshipit.org/v1alpha1
kind: Clusterctl
metadata:
name: clusterctl-v1
action: init
init-options:
core-provider: "cluster-api:v0.3.3"
providers:
- name: "cluster-api"
type: "CoreProvider"
versions:
v0.3.3: manifests/function/capi/v0.3.3

View File

@ -1,2 +1,6 @@
resources:
- phaseplan.yaml
- some_phase.yaml
- some_exc.yaml
- capi_init.yaml
- clusterctl.yaml

View File

@ -8,3 +8,5 @@ phaseGroups:
- name: isogen
- name: remotedirect
- name: initinfra
- name: some_phase
- name: capi_init

View File

@ -0,0 +1,13 @@
apiVersion: airshipit.org/v1alpha1
kind: SomeExecutor
metadata:
labels:
airshipit.org/deploy-k8s: "false"
name: executor-name
init-options:
core-provider: "cluster-api:v0.3.3"
providers:
- name: "cluster-api"
type: "CoreProvider"
versions:
v0.3.3: manifests/function/capi/v0.3.3

View File

@ -0,0 +1,10 @@
apiVersion: airshipit.org/v1alpha1
kind: Phase
metadata:
name: some_phase
config:
executorRef:
apiVersion: airshipit.org/v1alpha1
kind: SomeExecutor
name: executor-name
documentEntryPoint: manifests/site/test-site/auth