Implement phase and phase.Client interface

Relates-To: #342

Change-Id: I8882204c0e9eae05fca30c093fcd3bad58e308e1
This commit is contained in:
Kostiantyn Kalynovskyi 2020-09-08 23:54:57 -05:00
parent 76b1ffd722
commit d79c71e94d
7 changed files with 382 additions and 5 deletions

197
pkg/phase/client.go Normal file
View File

@ -0,0 +1,197 @@
/*
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 (
"path/filepath"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/events"
"opendev.org/airship/airshipctl/pkg/k8s/kubeconfig"
"opendev.org/airship/airshipctl/pkg/k8s/utils"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
var _ ifc.Phase = &phase{}
// Phase implements phase interface
type phase struct {
helper ifc.Helper
apiObj *v1alpha1.Phase
registry ExecutorRegistry
processor events.EventProcessor
}
// Executor returns executor interface associated with the phase
func (p *phase) Executor() (ifc.Executor, error) {
executorDoc, err := p.helper.ExecutorDoc(ifc.ID{Name: p.apiObj.Name, Namespace: p.apiObj.Namespace})
if err != nil {
return nil, err
}
var bundle document.Bundle
// just pass nil bundle if DocumentRoot is empty, executors should be ready for that
if docRoot := p.DocumentRoot(); docRoot != "" {
bundle, err = document.NewBundleByPath(docRoot)
if err != nil {
return nil, err
}
}
refGVK := p.apiObj.Config.ExecutorRef.GroupVersionKind()
// Look for executor factory defined in registry
executorFactory, found := p.registry()[refGVK]
if !found {
return nil, ErrExecutorNotFound{GVK: refGVK}
}
cMap, err := p.helper.ClusterMap()
if err != nil {
return nil, err
}
wd, err := p.helper.WorkDir()
if err != nil {
return nil, err
}
kubeconf := kubeconfig.NewBuilder().
WithBundle(p.helper.PhaseRoot()).
WithClusterMap(cMap).
WithClusterName(p.apiObj.ClusterName).
WithTempRoot(wd).
Build()
return executorFactory(
ifc.ExecutorConfig{
ClusterMap: cMap,
ExecutorBundle: bundle,
PhaseName: p.apiObj.Name,
KubeConfig: kubeconf,
ExecutorDocument: executorDoc,
ClusterName: p.apiObj.ClusterName,
Helper: p.helper,
})
}
// Run runs the phase via executor
func (p *phase) Run(ro ifc.RunOptions) error {
executor, err := p.Executor()
if err != nil {
return err
}
ch := make(chan events.Event)
go func() {
executor.Run(ch, ro)
}()
return p.processor.Process(ch)
}
// Validate makes sure that phase is properly configured
// TODO implement this
func (p *phase) Validate() error {
return nil
}
// DocumentRoot root that holds all the documents associated with the phase
func (p *phase) DocumentRoot() string {
if p.apiObj.Config.DocumentEntryPoint == "" {
return ""
}
targetPath := p.helper.TargetPath()
return filepath.Join(targetPath, p.apiObj.Config.DocumentEntryPoint)
}
// Details returns description of the phase
// TODO implement this: add details field to api.Phase and method to executor and combine them here
// to give a clear understanding to user of what this phase is about
func (p *phase) Details() (string, error) {
return "", nil
}
var _ ifc.Client = &client{}
type client struct {
ifc.Helper
registry ExecutorRegistry
processorFunc ProcessorFunc
}
// ProcessorFunc that returns processor interface
type ProcessorFunc func() events.EventProcessor
// Option allows to add various options to a phase
type Option func(*client)
// InjectProcessor is an option that allows to inject event processor into phase client
func InjectProcessor(procFunc ProcessorFunc) Option {
return func(c *client) {
c.processorFunc = procFunc
}
}
// InjectRegistry is an option that allows to inject executor registry into phase client
func InjectRegistry(registry ExecutorRegistry) Option {
return func(c *client) {
c.registry = registry
}
}
// NewClient returns implementation of phase Client interface
func NewClient(helper ifc.Helper, opts ...Option) ifc.Client {
c := &client{Helper: helper}
for _, opt := range opts {
opt(c)
}
if c.registry == nil {
c.registry = DefaultExecutorRegistry
}
if c.processorFunc == nil {
c.processorFunc = defaultProcessor
}
return c
}
func (c *client) PhaseByID(id ifc.ID) (ifc.Phase, error) {
phaseObj, err := c.Phase(id)
if err != nil {
return nil, err
}
phase := &phase{
apiObj: phaseObj,
helper: c.Helper,
processor: c.processorFunc(),
registry: c.registry,
}
return phase, nil
}
func (c *client) PhaseByAPIObj(phaseObj *v1alpha1.Phase) (ifc.Phase, error) {
phase := &phase{
apiObj: phaseObj,
helper: c.Helper,
processor: c.processorFunc(),
registry: c.registry,
}
return phase, nil
}
func defaultProcessor() events.EventProcessor {
return events.NewDefaultProcessor(utils.Streams())
}

180
pkg/phase/client_test.go Normal file
View File

@ -0,0 +1,180 @@
/*
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_test
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime/schema"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/events"
"opendev.org/airship/airshipctl/pkg/phase"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
func TestClientPhaseExecutor(t *testing.T) {
tests := []struct {
name string
errContains string
expectedExecutor ifc.Executor
phaseID ifc.ID
configFunc func(t *testing.T) *config.Config
registryFunc phase.ExecutorRegistry
}{
{
name: "Success fake executor",
expectedExecutor: fakeExecutor{},
configFunc: testConfig,
phaseID: ifc.ID{Name: "capi_init"},
registryFunc: fakeRegistry,
},
{
name: "Error executor doc doesn't exist",
expectedExecutor: fakeExecutor{},
configFunc: testConfig,
phaseID: ifc.ID{Name: "some_phase"},
registryFunc: fakeRegistry,
errContains: "found no documents",
},
{
name: "Error executor doc not registered",
expectedExecutor: fakeExecutor{},
configFunc: testConfig,
phaseID: ifc.ID{Name: "capi_init"},
registryFunc: func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
return make(map[schema.GroupVersionKind]ifc.ExecutorFactory)
},
errContains: "executor identified by 'airshipit.org/v1alpha1, Kind=Clusterctl' is not found",
},
}
for _, tt := range tests {
tt := tt
t.Run("", 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.PhaseByID(tt.phaseID)
require.NotNil(t, client)
executor, err := p.Executor()
if tt.errContains != "" {
require.Error(t, err)
assert.Nil(t, executor)
assert.Contains(t, err.Error(), tt.errContains)
} else {
require.NoError(t, err)
require.NotNil(t, executor)
assertEqualExecutor(t, tt.expectedExecutor, executor)
}
})
}
}
func TestPhaseRun(t *testing.T) {
tests := []struct {
name string
errContains string
phaseID ifc.ID
configFunc func(t *testing.T) *config.Config
registryFunc phase.ExecutorRegistry
}{
{
name: "Success fake executor",
configFunc: testConfig,
phaseID: ifc.ID{Name: "capi_init"},
registryFunc: fakeRegistry,
},
{
name: "Error executor doc doesn't exist",
configFunc: testConfig,
phaseID: ifc.ID{Name: "some_phase"},
registryFunc: fakeRegistry,
errContains: "found no documents",
},
}
for _, tt := range tests {
tt := tt
t.Run("", 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.PhaseByID(tt.phaseID)
require.NotNil(t, client)
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)
}
})
}
}
// TODO develop tests, when we add phase object validation
func TestClientByAPIObj(t *testing.T) {
helper, err := phase.NewHelper(testConfig(t))
require.NoError(t, err)
require.NotNil(t, helper)
client := phase.NewClient(helper)
require.NotNil(t, client)
p, err := client.PhaseByAPIObj(&v1alpha1.Phase{})
require.NoError(t, err)
require.NotNil(t, p)
}
func fakeRegistry() map[schema.GroupVersionKind]ifc.ExecutorFactory {
gvk := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "Clusterctl",
}
return map[schema.GroupVersionKind]ifc.ExecutorFactory{
gvk: fakeExecFactory,
}
}
func fakeExecFactory(config ifc.ExecutorConfig) (ifc.Executor, error) {
return fakeExecutor{}, nil
}
var _ ifc.Executor = fakeExecutor{}
type fakeExecutor struct {
}
func (e fakeExecutor) Render(w io.Writer, ro ifc.RenderOptions) error {
return nil
}
func (e fakeExecutor) Run(ch chan events.Event, ro ifc.RunOptions) {
defer close(ch)
}
func (e fakeExecutor) Validate() error {
return nil
}

View File

@ -64,7 +64,7 @@ func TestHelperPhase(t *testing.T) {
APIVersion: "airshipit.org/v1alpha1",
Name: "clusterctl-v1",
},
DocumentEntryPoint: "manifests/site/test-site/auth",
DocumentEntryPoint: "valid_site/phases",
},
},
},

View File

@ -60,5 +60,6 @@ type ExecutorConfig struct {
ExecutorDocument document.Document
ExecutorBundle document.Bundle
AirshipConfig *config.Config
Helper Helper
KubeConfig kubeconfig.Interface
}

View File

@ -134,7 +134,7 @@ func TestGetPhase(t *testing.T) {
APIVersion: "airshipit.org/v1alpha1",
Name: "clusterctl-v1",
},
DocumentEntryPoint: "manifests/site/test-site/auth",
DocumentEntryPoint: "valid_site/phases",
},
},
},

View File

@ -7,4 +7,4 @@ config:
apiVersion: airshipit.org/v1alpha1
kind: Clusterctl
name: clusterctl-v1
documentEntryPoint: manifests/site/test-site/auth
documentEntryPoint: valid_site/phases

View File

@ -5,6 +5,5 @@ metadata:
config:
executorRef:
apiVersion: airshipit.org/v1alpha1
kind: SomeExecutor
kind: Does not exist
name: executor-name
documentEntryPoint: manifests/site/test-site/auth