Implement helper interface

Change-Id: If65d7d8cd3fa082facc077753bb06275da2a6643
Relates-To: #342
This commit is contained in:
Kostiantyn Kalynovskyi 2020-09-08 23:42:16 -05:00 committed by Kostyantyn Kalynovskyi
parent 99d37b3907
commit 76b1ffd722
3 changed files with 694 additions and 1 deletions

View File

@ -1137,7 +1137,11 @@ func (c *Config) CurrentContextManifestMetadata() (*Metadata, error) {
if err != nil {
return nil, err
}
meta := &Metadata{}
meta := &Metadata{
// Populate with empty values to avoid nil pointers
Inventory: &InventoryMeta{},
PhaseMeta: &PhaseMeta{},
}
err = util.ReadYAMLFile(filepath.Join(manifest.TargetPath, manifest.MetadataPath), meta)
if err != nil {
return nil, err

231
pkg/phase/helper.go Normal file
View File

@ -0,0 +1,231 @@
/*
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"
"io"
"path/filepath"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
"opendev.org/airship/airshipctl/pkg/util"
)
// Helper provides functions built around phase bundle to filter and build documents
type Helper struct {
phaseRoot string
inventoryRoot string
targetPath string
metadata *config.Metadata
}
// NewHelper constructs metadata interface based on config
func NewHelper(cfg *config.Config) (ifc.Helper, error) {
helper := &Helper{}
var err error
helper.targetPath, err = cfg.CurrentContextTargetPath()
if err != nil {
return nil, err
}
helper.metadata, err = cfg.CurrentContextManifestMetadata()
if err != nil {
return nil, err
}
helper.phaseRoot = filepath.Join(helper.targetPath, helper.metadata.PhaseMeta.Path)
helper.inventoryRoot = filepath.Join(helper.targetPath, helper.metadata.Inventory.Path)
return helper, nil
}
// Phase returns a phase APIObject based on phase selector
func (helper *Helper) Phase(phaseID ifc.ID) (*v1alpha1.Phase, error) {
bundle, err := document.NewBundleByPath(helper.phaseRoot)
if err != nil {
return nil, err
}
phase := &v1alpha1.Phase{
ObjectMeta: v1.ObjectMeta{
Name: phaseID.Name,
Namespace: phaseID.Namespace,
},
}
selector, err := document.NewSelector().ByObject(phase, v1alpha1.Scheme)
if err != nil {
return nil, err
}
doc, err := bundle.SelectOne(selector)
if err != nil {
return nil, err
}
if err = doc.ToAPIObject(phase, v1alpha1.Scheme); err != nil {
return nil, err
}
return phase, nil
}
// Plan returns plan associated with a manifest
func (helper *Helper) Plan() (*v1alpha1.PhasePlan, error) {
bundle, err := document.NewBundleByPath(helper.phaseRoot)
if err != nil {
return nil, err
}
plan := &v1alpha1.PhasePlan{}
selector, err := document.NewSelector().ByObject(plan, v1alpha1.Scheme)
if err != nil {
return nil, err
}
doc, err := bundle.SelectOne(selector)
if err != nil {
return nil, err
}
if err := doc.ToAPIObject(plan, v1alpha1.Scheme); err != nil {
return nil, err
}
return plan, nil
}
// ListPhases returns all phases associated with manifest
func (helper *Helper) ListPhases() ([]*v1alpha1.Phase, error) {
bundle, err := document.NewBundleByPath(helper.phaseRoot)
if err != nil {
return nil, err
}
phase := &v1alpha1.Phase{}
selector, err := document.NewSelector().ByObject(phase, v1alpha1.Scheme)
if err != nil {
return nil, err
}
docs, err := bundle.Select(selector)
if err != nil {
return nil, err
}
phases := []*v1alpha1.Phase{}
for _, doc := range docs {
p := &v1alpha1.Phase{}
if err = doc.ToAPIObject(p, v1alpha1.Scheme); err != nil {
return nil, err
}
phases = append(phases, phase)
}
return phases, nil
}
// ClusterMapAPIobj associated with the the manifest
func (helper *Helper) ClusterMapAPIobj() (*v1alpha1.ClusterMap, error) {
bundle, err := document.NewBundleByPath(helper.phaseRoot)
if err != nil {
return nil, err
}
cMap := &v1alpha1.ClusterMap{}
selector, err := document.NewSelector().ByObject(cMap, v1alpha1.Scheme)
if err != nil {
return nil, err
}
doc, err := bundle.SelectOne(selector)
if err != nil {
return nil, err
}
if err = doc.ToAPIObject(cMap, v1alpha1.Scheme); err != nil {
return nil, err
}
return cMap, nil
}
// ClusterMap associated with the the manifest
func (helper *Helper) ClusterMap() (clustermap.ClusterMap, error) {
cMap, err := helper.ClusterMapAPIobj()
if err != nil {
return nil, err
}
return clustermap.NewClusterMap(cMap), nil
}
// ExecutorDoc returns executor document associated with phase
func (helper *Helper) ExecutorDoc(phaseID ifc.ID) (document.Document, error) {
bundle, err := document.NewBundleByPath(helper.phaseRoot)
if err != nil {
return nil, err
}
phaseObj, err := helper.Phase(phaseID)
if err != nil {
return nil, err
}
phaseConfig := phaseObj.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)
return bundle.SelectOne(selector)
}
// TargetPath returns manifest root
func (helper *Helper) TargetPath() string {
return helper.targetPath
}
// PhaseRoot returns path to document root with phase documents
func (helper *Helper) PhaseRoot() string {
return helper.phaseRoot
}
// WorkDir return manifest root
// TODO add creation of WorkDir if it doesn't exist
func (helper *Helper) WorkDir() (string, error) {
return filepath.Join(util.UserHomeDir(), config.AirshipConfigDir), nil
}
// PrintPlan prints plan
// TODO make this more readable in the future, and move to client
func PrintPlan(plan *v1alpha1.PhasePlan, w io.Writer) error {
result := make(map[string][]string)
for _, phaseGroup := range plan.PhaseGroups {
phases := make([]string, len(phaseGroup.Phases))
for i, phase := range phaseGroup.Phases {
phases[i] = phase.Name
}
result[phaseGroup.Name] = phases
}
tw := util.NewTabWriter(w)
defer tw.Flush()
fmt.Fprintf(tw, "GROUP\tPHASE\n")
for group, phaseList := range result {
fmt.Fprintf(tw, "%s\t\n", group)
for _, phase := range phaseList {
fmt.Fprintf(tw, "\t%s\n", phase)
}
}
return nil
}

458
pkg/phase/helper_test.go Normal file
View File

@ -0,0 +1,458 @@
/*
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 (
"bytes"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/phase"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
const (
brokenMetaPath = "broken_metadata.yaml"
noPlanMetaPath = "no_plan_site/metadata.yaml"
)
func TestHelperPhase(t *testing.T) {
testCases := []struct {
name string
errContains string
phaseID ifc.ID
config func(t *testing.T) *config.Config
expectedPhase *airshipv1.Phase
}{
{
name: "Success Get existing phase",
config: testConfig,
phaseID: ifc.ID{Name: "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: "Error get non-existing phase",
config: testConfig,
phaseID: ifc.ID{Name: "some_name"},
errContains: "found no documents",
},
{
name: "Error bundle path doesn't exist",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = brokenMetaPath
return conf
},
errContains: "no such file or directory",
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
helper, err := phase.NewHelper(tt.config(t))
require.NoError(t, err)
require.NotNil(t, helper)
actualPhase, actualErr := helper.Phase(tt.phaseID)
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
assert.Equal(t, tt.expectedPhase, actualPhase)
}
})
}
}
func TestHelperPlan(t *testing.T) {
testCases := []struct {
name string
errContains string
expectedPlan *v1alpha1.PhasePlan
config func(t *testing.T) *config.Config
}{
{
name: "Valid Phase Plan",
config: testConfig,
expectedPlan: &airshipv1.PhasePlan{
TypeMeta: metav1.TypeMeta{
Kind: "PhasePlan",
APIVersion: "airshipit.org/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "phasePlan",
},
PhaseGroups: []airshipv1.PhaseGroup{
{
Name: "group1",
Phases: []airshipv1.PhaseGroupStep{
{
Name: "isogen",
},
{
Name: "remotedirect",
},
{
Name: "initinfra",
},
{
Name: "some_phase",
},
{
Name: "capi_init",
},
},
},
},
},
},
{
name: "No Phase Plan",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = noPlanMetaPath
return conf
},
errContains: "found no documents",
},
{
name: "Error bundle path doesn't exist",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = brokenMetaPath
return conf
},
errContains: "no such file or directory",
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
helper, err := phase.NewHelper(tt.config(t))
require.NoError(t, err)
require.NotNil(t, helper)
actualPlan, actualErr := helper.Plan()
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
assert.Equal(t, tt.expectedPlan, actualPlan)
}
})
}
}
func TestHelperListPhases(t *testing.T) {
testCases := []struct {
name string
errContains string
phaseLen int
config func(t *testing.T) *config.Config
}{
{
name: "Success phase list",
phaseLen: 2,
config: testConfig,
},
{
name: "Error bundle path doesn't exist",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = brokenMetaPath
return conf
},
errContains: "no such file or directory",
},
{
name: "Success 0 phases",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = noPlanMetaPath
return conf
},
phaseLen: 0,
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
helper, err := phase.NewHelper(tt.config(t))
require.NoError(t, err)
require.NotNil(t, helper)
actualList, actualErr := helper.ListPhases()
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
assert.Len(t, actualList, tt.phaseLen)
}
})
}
}
func TestHelperClusterMapAPI(t *testing.T) {
testCases := []struct {
name string
errContains string
expectedCMap *v1alpha1.ClusterMap
config func(t *testing.T) *config.Config
}{
{
name: "Success cluster map",
expectedCMap: &airshipv1.ClusterMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "airshipit.org/v1alpha1",
Kind: "ClusterMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: "clusterctl-v1",
},
Map: map[string]*airshipv1.Cluster{
"target": {
Parent: "ephemeral",
DynamicKubeConfig: false,
},
"ephemeral": {},
},
},
config: testConfig,
},
{
name: "Error bundle path doesn't exist",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = brokenMetaPath
return conf
},
errContains: "no such file or directory",
},
{
name: "Error no cluster map",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = noPlanMetaPath
return conf
},
errContains: "found no documents",
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
helper, err := phase.NewHelper(tt.config(t))
require.NoError(t, err)
require.NotNil(t, helper)
actualCMap, actualErr := helper.ClusterMapAPIobj()
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
assert.Equal(t, tt.expectedCMap, actualCMap)
}
})
}
}
func TestHelperClusterMap(t *testing.T) {
testCases := []struct {
name string
errContains string
config func(t *testing.T) *config.Config
}{
{
name: "Success phase list",
config: testConfig,
},
{
name: "Error no cluster map",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = noPlanMetaPath
return conf
},
errContains: "found no documents",
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
helper, err := phase.NewHelper(tt.config(t))
require.NoError(t, err)
require.NotNil(t, helper)
actualCMap, actualErr := helper.ClusterMap()
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
assert.NotNil(t, actualCMap)
}
})
}
}
func TestHelperExecutorDoc(t *testing.T) {
testCases := []struct {
name string
errContains string
expectedExecutor string
phaseID ifc.ID
config func(t *testing.T) *config.Config
}{
{
name: "Success Get existing phase",
config: testConfig,
phaseID: ifc.ID{Name: "capi_init"},
expectedExecutor: "clusterctl-v1",
},
{
name: "Error get non-existing phase",
config: testConfig,
phaseID: ifc.ID{Name: "some_name"},
errContains: "found no documents",
},
{
name: "Error bundle path doesn't exist",
config: func(t *testing.T) *config.Config {
conf := testConfig(t)
conf.Manifests["dummy_manifest"].MetadataPath = brokenMetaPath
return conf
},
errContains: "no such file or directory",
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
helper, err := phase.NewHelper(tt.config(t))
require.NoError(t, err)
actualDoc, actualErr := helper.ExecutorDoc(tt.phaseID)
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
require.NotNil(t, actualDoc)
assert.Equal(t, tt.expectedExecutor, actualDoc.GetName())
}
})
}
}
func TestHelperPrintPlan(t *testing.T) {
helper, err := phase.NewHelper(testConfig(t))
require.NoError(t, err)
require.NotNil(t, helper)
plan, err := helper.Plan()
require.NoError(t, err)
require.NotNil(t, plan)
buf := bytes.NewBuffer([]byte{})
err = phase.PrintPlan(plan, buf)
require.NoError(t, err)
// easy check to make sure printed plan contains all phases in plan
assert.Contains(t, buf.String(), "remotedirect")
assert.Contains(t, buf.String(), "isogen")
assert.Contains(t, buf.String(), "initinfra")
assert.Contains(t, buf.String(), "some_phase")
assert.Contains(t, buf.String(), "capi_init")
}
func TestHelperTargetPath(t *testing.T) {
helper, err := phase.NewHelper(testConfig(t))
require.NoError(t, err)
require.NotNil(t, helper)
assert.Equal(t, "testdata", helper.TargetPath())
}
func TestHelperPhaseRoot(t *testing.T) {
helper, err := phase.NewHelper(testConfig(t))
require.NoError(t, err)
require.NotNil(t, helper)
expectedPhaseRoot := filepath.Join("testdata", "valid_site", "phases")
assert.Equal(t, expectedPhaseRoot, helper.PhaseRoot())
}
func TestHelperWorkdir(t *testing.T) {
helper, err := phase.NewHelper(testConfig(t))
require.NoError(t, err)
require.NotNil(t, helper)
workDir, err := helper.WorkDir()
assert.NoError(t, err)
assert.Greater(t, len(workDir), 0)
}
func testConfig(t *testing.T) *config.Config {
t.Helper()
confString := `apiVersion: airshipit.org/v1alpha1
contexts:
dummy_cluster:
contextKubeconf: dummy_cluster
manifest: dummy_manifest
currentContext: dummy_cluster
kind: Config
manifests:
dummy_manifest:
primaryRepositoryName: primary
targetPath: testdata
metadataPath: valid_site/metadata.yaml
subPath: valid_site`
conf := &config.Config{}
err := yaml.Unmarshal([]byte(confString), conf)
require.NoError(t, err)
return conf
}