airshipctl/pkg/phase/client_test.go
Ruslan Aliev bc9f97ff2e Embed validation cfg into phase and plan definition
* validation config is now part of airshipctl api
 * additional CRD locations can be only kustomize entrypoints
 * changed mechanism to call document-validation executor to allow
   to pass validation config from phase or plan
 * kubeval version pinned to the latest 0.16.1
 * default k8s version to validate against uplifted to 1.18.6
 * default URL with k8s schemas changed to more updated and reliable

Change-Id: Ifb24be224d5f0860d323a671b94e28a86debc65b
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Closes: #563
2021-06-11 15:37:38 +00:00

531 lines
15 KiB
Go

/*
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 (
"fmt"
"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/errors"
"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(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.PhaseByID(tt.phaseID)
require.NotNil(t, p)
require.NoError(t, err)
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.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 TestPhaseValidate(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: func(t *testing.T) *config.Config {
cfg := testConfig(t)
cfg.Manifests["dummy_manifest"].MetadataPath = "valid_validation_site/metadata.yaml"
return cfg
},
phaseID: ifc.ID{Name: "kube_apply"},
registryFunc: func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
gvk := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "KubernetesApply",
}
return map[schema.GroupVersionKind]ifc.ExecutorFactory{
gvk: fakeExecFactory,
}
},
},
{
name: "invalid CRD entrypoint - location not exists",
configFunc: func(t *testing.T) *config.Config {
cfg := testConfig(t)
cfg.Manifests["dummy_manifest"].MetadataPath = "valid_validation_site/metadata.yaml"
return cfg
},
phaseID: ifc.ID{Name: "kube_apply_noentrypoint"},
registryFunc: func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
gvk := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "KubernetesApply",
}
return map[schema.GroupVersionKind]ifc.ExecutorFactory{
gvk: fakeExecFactory,
}
},
errContains: "evalsymlink failure",
},
{
name: "no CRD documents found",
configFunc: func(t *testing.T) *config.Config {
cfg := testConfig(t)
cfg.Manifests["dummy_manifest"].MetadataPath = "valid_validation_site/metadata.yaml"
return cfg
},
phaseID: ifc.ID{Name: "kube_apply"},
registryFunc: func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
gvk := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "KubernetesApply",
}
return map[schema.GroupVersionKind]ifc.ExecutorFactory{
gvk: fakeExecFactory,
}
},
errContains: "",
},
{
name: "validator executor not found",
configFunc: func(t *testing.T) *config.Config {
cfg := testConfig(t)
cfg.Manifests["dummy_manifest"].MetadataPath = "invalid_validation_site/metadata.yaml"
return cfg
},
phaseID: ifc.ID{Name: "kube_apply"},
registryFunc: func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
gvk := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "KubernetesApply",
}
return map[schema.GroupVersionKind]ifc.ExecutorFactory{
gvk: fakeExecFactory,
}
},
errContains: `document filtered by selector [Group="airshipit.org", Version="v1alpha1", ` +
`Kind="GenericContainer", Name="document-validation"] found no documents`,
},
{
name: "Error no document entry point",
configFunc: testConfig,
phaseID: ifc.ID{Name: "no_entry_point"},
registryFunc: fakeRegistry,
errContains: "executor identified by 'airshipit.org/v1alpha1, Kind=SomeExecutor' is not found",
},
{
name: "Error no executor",
configFunc: testConfig,
phaseID: ifc.ID{Name: "no_executor_phase"},
registryFunc: fakeRegistry,
errContains: "Phase name 'no_executor_phase', namespace '' must have executorRef field defined in config",
},
{
name: "Error executor validate",
configFunc: testConfig,
phaseID: ifc.ID{Name: "kube_apply"},
registryFunc: func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
gvk := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "KubernetesApply",
}
return map[schema.GroupVersionKind]ifc.ExecutorFactory{
gvk: func(config ifc.ExecutorConfig) (ifc.Executor, error) {
return fakeExecutor{
validate: fmt.Errorf("validation error"),
}, nil
},
}
},
errContains: "validation error",
},
}
for _, tt := range tests {
tt := tt
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.PhaseByID(tt.phaseID)
require.NoError(t, err)
err = p.Validate()
if tt.errContains != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
} else {
require.NoError(t, err)
}
})
}
}
func (e fakeExecutor) Status() (ifc.ExecutorStatus, error) {
return ifc.ExecutorStatus{}, nil
}
// 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)
}
// assertEqualExecutor allows to compare executor interfaces
// check if we expect nil, and if so actual interface must be nil also otherwise compare types
func assertEqualExecutor(t *testing.T, expected, actual ifc.Executor) {
if expected == nil {
assert.Nil(t, actual)
return
}
assert.IsType(t, expected, actual)
}
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 TestDocumentRoot(t *testing.T) {
tests := []struct {
name string
expectedRoot string
phaseID ifc.ID
expectedErr error
metadataPath string
}{
{
name: "Success entrypoint exists",
expectedRoot: "testdata/valid_site/phases",
phaseID: ifc.ID{Name: "capi_init"},
},
{
name: "Error entrypoint does not exists",
phaseID: ifc.ID{Name: "some_phase"},
expectedErr: errors.ErrDocumentEntrypointNotDefined{PhaseName: "some_phase"},
},
{
name: "Success entrypoint with doc prefix path",
expectedRoot: "testdata/valid_site_with_doc_prefix/phases/entrypoint",
phaseID: ifc.ID{Name: "sample"},
metadataPath: "valid_site_with_doc_prefix/metadata.yaml",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
cfg := testConfig(t)
if tt.metadataPath != "" {
cfg.Manifests["dummy_manifest"].MetadataPath = tt.metadataPath
}
helper, err := phase.NewHelper(cfg)
require.NoError(t, err)
require.NotNil(t, helper)
c := phase.NewClient(helper)
p, err := c.PhaseByID(ifc.ID{Name: tt.phaseID.Name, Namespace: tt.phaseID.Namespace})
require.NoError(t, err)
require.NotNil(t, p)
root, err := p.DocumentRoot()
assert.Equal(t, tt.expectedErr, err)
assert.Equal(t, tt.expectedRoot, root)
})
}
}
func TestBundleFactoryExecutor(t *testing.T) {
cfg := testConfig(t)
helper, err := phase.NewHelper(cfg)
require.NoError(t, err)
require.NotNil(t, helper)
fakeReg := func() map[schema.GroupVersionKind]ifc.ExecutorFactory {
validBundleFactory := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "Clusterctl",
}
invalidBundleFactory := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "SomeExecutor",
}
bundleCheckFunc := func(config ifc.ExecutorConfig) (ifc.Executor, error) {
_, bundleErr := config.BundleFactory()
return nil, bundleErr
}
return map[schema.GroupVersionKind]ifc.ExecutorFactory{
// validBundleFactory has DocumentEntryPoint defined
validBundleFactory: bundleCheckFunc,
// invalidBundleFactory does not have DocumentEntryPoint defined, error should be returned
invalidBundleFactory: bundleCheckFunc,
}
}
c := phase.NewClient(helper, phase.InjectRegistry(fakeReg))
p, err := c.PhaseByID(ifc.ID{Name: "capi_init"})
require.NoError(t, err)
_, err = p.Executor()
assert.NoError(t, err)
p, err = c.PhaseByID(ifc.ID{Name: "no_entry_point"})
require.NoError(t, err)
_, err = p.Executor()
assert.Error(t, err)
assert.Equal(t, errors.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 TestPlanValidate(t *testing.T) {
testCases := []struct {
name string
errContains string
planID ifc.ID
configFunc func(t *testing.T) *config.Config
registryFunc phase.ExecutorRegistry
}{
{
name: "Valid fake executor",
configFunc: testConfig,
planID: ifc.ID{Name: "init"},
registryFunc: fakeRegistry,
errContains: `document filtered by selector [Group="airshipit.org", Version="v1alpha1", ` +
`Kind="GenericContainer", Name="document-validation"] found no documents`,
},
{
name: "Invalid fake executor",
configFunc: testConfig,
planID: ifc.ID{Name: "plan_invalid_phase"},
registryFunc: fakeRegistry,
errContains: "executor identified by 'airshipit.org/v1alpha1, Kind=SomeExecutor' is not found",
},
{
name: "Phase does not exist",
configFunc: testConfig,
planID: ifc.ID{Name: "phase_not_exist"},
registryFunc: fakeRegistry,
errContains: `document filtered by selector [Group="airshipit.org", Version="v1alpha1", ` +
`Kind="Phase", Name="non_existent_name"] 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.Validate()
if tt.errContains != "" {
require.Error(t, err)
assert.Equal(t, tt.errContains, err.Error())
} else {
require.NoError(t, err)
}
})
}
}
func fakeExecFactory(_ ifc.ExecutorConfig) (ifc.Executor, error) {
return fakeExecutor{}, nil
}
var _ ifc.Executor = fakeExecutor{}
type fakeExecutor struct {
validate error
}
func (e fakeExecutor) Render(_ io.Writer, _ ifc.RenderOptions) error {
return nil
}
func (e fakeExecutor) Run(ch chan events.Event, _ ifc.RunOptions) {
defer close(ch)
}
func (e fakeExecutor) Validate() error {
return e.validate
}