Bootstrap Ephemeral - Executor methods

This patchset includes the go code for the Executor for the Bootstrap
container/Ephemeral cluster and respective unit test code.

This executor allows to deploy a K8S cluster on a public/private cloud
provider through a dedicated Bootstrap container.

This patchset does not include the manifests for the executor to avoid
too big patchset. It will be included in a different patchset.

Change-Id: I3f9d2ba1bb65b344522e105fd26310af24e3a1b1
This commit is contained in:
Sidney Shiba 2020-12-10 12:09:32 -06:00
parent e0ad83c398
commit 4b31d3b936
6 changed files with 504 additions and 94 deletions

View File

@ -41,7 +41,7 @@ type BootstrapContainer struct {
ContainerRuntime string `json:"containerRuntime,omitempty"` ContainerRuntime string `json:"containerRuntime,omitempty"`
Image string `json:"image,omitempty"` Image string `json:"image,omitempty"`
Volume string `json:"volume,omitempty"` Volume string `json:"volume,omitempty"`
Kubeconfig string `json:"kubeconfig,omitempty"` Kubeconfig string `json:"saveKubeconfigFileName,omitempty"`
} }
// DefaultBootConfiguration can be used to safely unmarshal BootConfiguration object without nil pointers // DefaultBootConfiguration can be used to safely unmarshal BootConfiguration object without nil pointers

View File

@ -104,6 +104,14 @@ func (options *BootstrapContainerOptions) VerifyInputs() error {
return nil return nil
} }
// VerifyArtifacts verifies the artifacts
func (options *BootstrapContainerOptions) VerifyArtifacts() error {
hostVol := strings.Split(options.Cfg.BootstrapContainer.Volume, ":")[0]
configPath := filepath.Join(hostVol, options.Cfg.EphemeralCluster.ConfigFilename)
_, err := os.Stat(configPath)
return err
}
// GetContainerStatus returns the Bootstrap Container state // GetContainerStatus returns the Bootstrap Container state
func (options *BootstrapContainerOptions) GetContainerStatus() (container.Status, error) { func (options *BootstrapContainerOptions) GetContainerStatus() (container.Status, error) {
// Check status of the container, e.g., "running" // Check status of the container, e.g., "running"

View File

@ -26,46 +26,9 @@ import (
"opendev.org/airship/airshipctl/pkg/bootstrap/ephemeral" "opendev.org/airship/airshipctl/pkg/bootstrap/ephemeral"
"opendev.org/airship/airshipctl/pkg/container" "opendev.org/airship/airshipctl/pkg/container"
"opendev.org/airship/airshipctl/pkg/log" "opendev.org/airship/airshipctl/pkg/log"
testcontainer "opendev.org/airship/airshipctl/testutil/container"
) )
type mockContainer struct {
imagePull func() error
runCommand func() error
getContainerLogs func() (io.ReadCloser, error)
rmContainer func() error
getID func() string
waitUntilFinished func() error
inspectContainer func() (container.State, error)
}
func (mc *mockContainer) ImagePull() error {
return mc.imagePull()
}
func (mc *mockContainer) RunCommand([]string, io.Reader, []string, []string) error {
return mc.runCommand()
}
func (mc *mockContainer) GetContainerLogs() (io.ReadCloser, error) {
return mc.getContainerLogs()
}
func (mc *mockContainer) RmContainer() error {
return mc.rmContainer()
}
func (mc *mockContainer) GetID() string {
return mc.getID()
}
func (mc *mockContainer) WaitUntilFinished() error {
return mc.waitUntilFinished()
}
func (mc *mockContainer) InspectContainer() (container.State, error) {
return mc.inspectContainer()
}
// Unit test for the method getContainerStatus() // Unit test for the method getContainerStatus()
func TestGetContainerStatus(t *testing.T) { func TestGetContainerStatus(t *testing.T) {
testCfg := &api.BootConfiguration{ testCfg := &api.BootConfiguration{
@ -82,16 +45,49 @@ func TestGetContainerStatus(t *testing.T) {
} }
tests := []struct { tests := []struct {
container *mockContainer container *testcontainer.MockContainer
cfg *api.BootConfiguration cfg *api.BootConfiguration
debug bool debug bool
expectedStatus container.Status expectedStatus container.Status
expectedErr error expectedErr error
}{ }{
{
// options.Container.InspectContainer() returning error
container: &testcontainer.MockContainer{
MockInspectContainer: func() (container.State, error) {
state := container.State{}
state.Status = ephemeral.BootNullString
state.ExitCode = 0
return state, ephemeral.ErrInvalidBootstrapCommand{}
},
},
cfg: testCfg,
debug: false,
expectedStatus: ephemeral.BootNullString,
expectedErr: ephemeral.ErrInvalidBootstrapCommand{},
},
{
// options.Container.GetContainerLogs() returns error
container: &testcontainer.MockContainer{
MockInspectContainer: func() (container.State, error) {
state := container.State{}
state.Status = container.RunningContainerStatus
state.ExitCode = 1
return state, nil
},
MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, ephemeral.ErrInvalidBootstrapCommand{}
},
},
cfg: testCfg,
debug: false,
expectedStatus: ephemeral.BootNullString,
expectedErr: ephemeral.ErrInvalidBootstrapCommand{},
},
{ {
// Container running and no errors // Container running and no errors
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.RunningContainerStatus state.Status = container.RunningContainerStatus
state.ExitCode = 0 state.ExitCode = 0
@ -105,14 +101,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerLoadEphemeralConfigError // Container running and with ContainerLoadEphemeralConfigError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 1 state.ExitCode = 1
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -126,14 +122,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerValidationEphemeralConfigError // Container running and with ContainerValidationEphemeralConfigError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 2 state.ExitCode = 2
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -147,14 +143,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerSetEnvVarsError // Container running and with ContainerSetEnvVarsError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 3 state.ExitCode = 3
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -168,14 +164,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerUnknownCommandError // Container running and with ContainerUnknownCommandError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 4 state.ExitCode = 4
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -189,14 +185,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerCreationEphemeralFailedError // Container running and with ContainerCreationEphemeralFailedError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 5 state.ExitCode = 5
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -210,14 +206,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerDeletionEphemeralFailedError // Container running and with ContainerDeletionEphemeralFailedError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 6 state.ExitCode = 6
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -231,14 +227,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerHelpCommandFailedError // Container running and with ContainerHelpCommandFailedError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 7 state.ExitCode = 7
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -252,14 +248,14 @@ func TestGetContainerStatus(t *testing.T) {
}, },
{ {
// Container running and with ContainerUnknownError // Container running and with ContainerUnknownError
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 8 state.ExitCode = 8
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -304,7 +300,7 @@ func TestWaitUntilContainerExitsOrTimesout(t *testing.T) {
} }
tests := []struct { tests := []struct {
container *mockContainer container *testcontainer.MockContainer
cfg *api.BootConfiguration cfg *api.BootConfiguration
debug bool debug bool
maxRetries int maxRetries int
@ -312,14 +308,14 @@ func TestWaitUntilContainerExitsOrTimesout(t *testing.T) {
}{ }{
{ {
// Test: container exits normally // Test: container exits normally
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 0 state.ExitCode = 0
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -330,14 +326,14 @@ func TestWaitUntilContainerExitsOrTimesout(t *testing.T) {
}, },
{ {
// Test: container times out // Test: container times out
container: &mockContainer{ container: &testcontainer.MockContainer{
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.RunningContainerStatus state.Status = container.RunningContainerStatus
state.ExitCode = 0 state.ExitCode = 0
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -346,6 +342,21 @@ func TestWaitUntilContainerExitsOrTimesout(t *testing.T) {
maxRetries: 1, maxRetries: 1,
expectedErr: ephemeral.ErrNumberOfRetriesExceeded{}, expectedErr: ephemeral.ErrNumberOfRetriesExceeded{},
}, },
{
// Test: options.GetContainerStatus() returns error
container: &testcontainer.MockContainer{
MockInspectContainer: func() (container.State, error) {
state := container.State{}
state.Status = ephemeral.BootNullString
state.ExitCode = 0
return state, ephemeral.ErrInvalidBootstrapCommand{}
},
},
cfg: testCfg,
debug: false,
maxRetries: 10,
expectedErr: ephemeral.ErrInvalidBootstrapCommand{},
},
} }
for _, tt := range tests { for _, tt := range tests {
outBuf := &bytes.Buffer{} outBuf := &bytes.Buffer{}
@ -377,7 +388,7 @@ func TestCreateBootstrapContainer(t *testing.T) {
} }
tests := []struct { tests := []struct {
container *mockContainer container *testcontainer.MockContainer
cfg *api.BootConfiguration cfg *api.BootConfiguration
debug bool debug bool
bootstrapCommand string bootstrapCommand string
@ -385,16 +396,16 @@ func TestCreateBootstrapContainer(t *testing.T) {
}{ }{
{ {
// Test: Create Ephemeral Cluster successful // Test: Create Ephemeral Cluster successful
container: &mockContainer{ container: &testcontainer.MockContainer{
runCommand: func() error { return nil }, MockRunCommand: func() error { return nil },
rmContainer: func() error { return nil }, MockRmContainer: func() error { return nil },
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 0 state.ExitCode = 0
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -405,16 +416,16 @@ func TestCreateBootstrapContainer(t *testing.T) {
}, },
{ {
// Test: Delete Ephemeral Cluster successful // Test: Delete Ephemeral Cluster successful
container: &mockContainer{ container: &testcontainer.MockContainer{
runCommand: func() error { return nil }, MockRunCommand: func() error { return nil },
rmContainer: func() error { return nil }, MockRmContainer: func() error { return nil },
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 0 state.ExitCode = 0
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -425,16 +436,16 @@ func TestCreateBootstrapContainer(t *testing.T) {
}, },
{ {
// Test: Create Ephemeral Cluster exit with error // Test: Create Ephemeral Cluster exit with error
container: &mockContainer{ container: &testcontainer.MockContainer{
runCommand: func() error { return nil }, MockRunCommand: func() error { return nil },
rmContainer: func() error { return nil }, MockRmContainer: func() error { return nil },
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 1 state.ExitCode = 1
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -448,16 +459,16 @@ func TestCreateBootstrapContainer(t *testing.T) {
}, },
{ {
// Test: Delete Ephemeral Cluster exit with error // Test: Delete Ephemeral Cluster exit with error
container: &mockContainer{ container: &testcontainer.MockContainer{
runCommand: func() error { return nil }, MockRunCommand: func() error { return nil },
rmContainer: func() error { return nil }, MockRmContainer: func() error { return nil },
inspectContainer: func() (container.State, error) { MockInspectContainer: func() (container.State, error) {
state := container.State{} state := container.State{}
state.Status = container.ExitedContainerStatus state.Status = container.ExitedContainerStatus
state.ExitCode = 1 state.ExitCode = 1
return state, nil return state, nil
}, },
getContainerLogs: func() (io.ReadCloser, error) { MockGetContainerLogs: func() (io.ReadCloser, error) {
return nil, nil return nil, nil
}, },
}, },
@ -469,6 +480,16 @@ func TestCreateBootstrapContainer(t *testing.T) {
ErrMsg: ephemeral.ContainerLoadEphemeralConfigError, ErrMsg: ephemeral.ContainerLoadEphemeralConfigError,
}, },
}, },
{
// Test: options.Container.RunCommand() returns error
container: &testcontainer.MockContainer{
MockRunCommand: func() error { return ephemeral.ErrBootstrapContainerRun{} },
},
cfg: testCfg,
bootstrapCommand: ephemeral.BootCmdCreate,
debug: false,
expectedErr: ephemeral.ErrBootstrapContainerRun{},
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@ -49,6 +49,9 @@ func DefaultExecutorRegistry() map[schema.GroupVersionKind]ifc.ExecutorFactory {
if err := executors.RegisterContainerExecutor(execMap); err != nil { if err := executors.RegisterContainerExecutor(execMap); err != nil {
log.Fatal(ErrExecutorRegistration{ExecutorName: "generic-container", Err: err}) log.Fatal(ErrExecutorRegistration{ExecutorName: "generic-container", Err: err})
} }
if err := executors.RegisterEphemeralExecutor(execMap); err != nil {
log.Fatal(ErrExecutorRegistration{ExecutorName: "ephemeral", Err: err})
}
return execMap return execMap
} }

View File

@ -0,0 +1,164 @@
/*
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 executors
import (
"context"
"io"
"time"
"k8s.io/apimachinery/pkg/runtime/schema"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/bootstrap/ephemeral"
"opendev.org/airship/airshipctl/pkg/container"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/errors"
"opendev.org/airship/airshipctl/pkg/events"
"opendev.org/airship/airshipctl/pkg/log"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
var _ ifc.Executor = &EphemeralExecutor{}
// EphemeralExecutor contains resources for ephemeral executor
type EphemeralExecutor struct {
ExecutorBundle document.Bundle
ExecutorDocument document.Document
BootConf *v1alpha1.BootConfiguration
Container container.Container
}
// RegisterEphemeralExecutor adds executor to phase executor registry
func RegisterEphemeralExecutor(registry map[schema.GroupVersionKind]ifc.ExecutorFactory) error {
obj := v1alpha1.DefaultBootConfiguration()
gvks, _, err := v1alpha1.Scheme.ObjectKinds(obj)
if err != nil {
return err
}
registry[gvks[0]] = NewEphemeralExecutor
return nil
}
// NewEphemeralExecutor creates instance of phase executor
func NewEphemeralExecutor(cfg ifc.ExecutorConfig) (ifc.Executor, error) {
apiObj := &v1alpha1.BootConfiguration{}
err := cfg.ExecutorDocument.ToAPIObject(apiObj, v1alpha1.Scheme)
if err != nil {
return nil, err
}
return &EphemeralExecutor{
ExecutorDocument: cfg.ExecutorDocument,
BootConf: apiObj,
}, nil
}
// Run ephemeral as a phase runner
func (c *EphemeralExecutor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
defer close(evtCh)
evtCh <- events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapStart,
Message: "Processing Ephemeral cluster operation ...",
})
if opts.DryRun {
log.Print("Dryrun: bootstrap container command will be skipped")
evtCh <- events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapDryRun,
})
return
}
if c.Container == nil {
ctx := context.Background()
builder, err := container.NewContainer(
ctx,
c.BootConf.BootstrapContainer.ContainerRuntime,
c.BootConf.BootstrapContainer.Image)
if err != nil {
handleEphemeralError(evtCh, err)
return
}
c.Container = builder
}
bootstrapOpts := ephemeral.BootstrapContainerOptions{
Container: c.Container,
Cfg: c.BootConf,
Sleep: time.Sleep,
}
evtCh <- events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapValidation,
Message: "Verifying executor manifest document ...",
})
err := bootstrapOpts.VerifyInputs()
if err != nil {
handleEphemeralError(evtCh, err)
return
}
evtCh <- events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapRun,
Message: "Creating and starting the Bootstrap Container ...",
})
err = bootstrapOpts.CreateBootstrapContainer()
if err != nil {
handleEphemeralError(evtCh, err)
return
}
evtCh <- events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapValidation,
Message: "Verifying generation of kubeconfig file ...",
})
err = bootstrapOpts.VerifyArtifacts()
if err != nil {
handleEphemeralError(evtCh, err)
return
}
evtCh <- events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapEnd,
Message: "Ephemeral cluster operation has completed successfully and artifacts verified",
})
}
// Validate executor configuration and documents
func (c *EphemeralExecutor) Validate() error {
return errors.ErrNotImplemented{}
}
// Render executor documents
func (c *EphemeralExecutor) Render(w io.Writer, _ ifc.RenderOptions) error {
log.Print("Ephemeral Executor Render() will be implemented later.")
return nil
}
func handleEphemeralError(ch chan<- events.Event, err error) {
ch <- events.Event{
Type: events.ErrorType,
ErrorEvent: events.ErrorEvent{
Error: err,
},
}
}

View File

@ -0,0 +1,214 @@
/*
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 executors_test
import (
"bytes"
"os"
"path/filepath"
"testing"
"time"
"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/bootstrap/ephemeral"
"opendev.org/airship/airshipctl/pkg/container"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/errors"
"opendev.org/airship/airshipctl/pkg/events"
"opendev.org/airship/airshipctl/pkg/phase/executors"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
"opendev.org/airship/airshipctl/testutil"
testcontainer "opendev.org/airship/airshipctl/testutil/container"
)
var (
executorEphemeralDoc = `
apiVersion: airshipit.org/v1alpha1
kind: BootConfiguration
metadata:
name: ephemeral-az-genesis
labels:
airshipit.org/deploy-k8s: "false"
ephemeralCluster:
bootstrapCommand: create
configFilename: azure-config.yaml
bootstrapContainer:
containerRuntime: docker
image: quay.io/sshiba/capz-bootstrap:latest
volume: /home/esidshi/.airship:/kube
saveKubeconfigFileName: capz.kubeconfig
`
)
// TestRegisterEphemeralExecutor - Unit testing function RegisterEphemeralExecutor()
func TestRegisterEphemeralExecutor(t *testing.T) {
registry := make(map[schema.GroupVersionKind]ifc.ExecutorFactory)
expectedGVK := schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "BootConfiguration",
}
err := executors.RegisterEphemeralExecutor(registry)
require.NoError(t, err)
_, found := registry[expectedGVK]
assert.True(t, found)
}
// TestNewEphemeralExecutor - Unit testing function NewExecutor()
func TestNewEphemeralExecutor(t *testing.T) {
execDoc, err := document.NewDocumentFromBytes([]byte(executorEphemeralDoc))
require.NoError(t, err)
_, err = executors.NewEphemeralExecutor(ifc.ExecutorConfig{ExecutorDocument: execDoc})
require.NoError(t, err)
}
// TestExecutorEphemeralRun - Unit testing method (c *Executor) Run()
func TestExecutorEphemeralRun(t *testing.T) {
tempVol, cleanup := testutil.TempDir(t, "bootstrap-test")
defer cleanup(t)
volBind := tempVol + ":/dst"
configFilename := "dummy-config.yaml"
configPath := filepath.Join(tempVol, configFilename)
assert.NoError(t, testConfigFile(configPath),
ephemeral.ErrBootstrapContainerRun{},
"Failed to create dummy config file")
testCfg := &v1alpha1.BootConfiguration{
BootstrapContainer: v1alpha1.BootstrapContainer{
Volume: volBind,
ContainerRuntime: "docker",
Image: "quay.io/sshiba/capz-bootstrap:latest",
Kubeconfig: "dummy.kubeconfig",
},
EphemeralCluster: v1alpha1.EphemeralCluster{
BootstrapCommand: "create",
ConfigFilename: configFilename,
},
}
testCases := []struct {
name string
container *testcontainer.MockContainer
expectedEvt []events.Event
}{
{
name: "Run bootstrap container successfully",
container: &testcontainer.MockContainer{
MockRunCommand: func() error { return nil },
MockRmContainer: func() error { return nil },
MockInspectContainer: func() (container.State, error) {
state := container.State{}
state.Status = "exited"
state.ExitCode = 0
return state, nil
},
},
expectedEvt: []events.Event{
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapStart,
}),
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapValidation,
}),
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapRun,
}),
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapValidation,
}),
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapEnd,
}),
},
},
{
name: "Run bootstrap container with Unknow Error",
container: &testcontainer.MockContainer{
MockRunCommand: func() error { return ephemeral.ErrBootstrapContainerRun{} },
MockRmContainer: func() error { return nil },
},
expectedEvt: []events.Event{
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapStart,
}),
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapValidation,
}),
events.NewEvent().WithBootstrapEvent(events.BootstrapEvent{
Operation: events.BootstrapRun,
}),
events.NewEvent().WithErrorEvent(events.ErrorEvent{
Error: ephemeral.ErrBootstrapContainerRun{},
}),
},
},
}
for _, test := range testCases {
tt := test
t.Run(tt.name, func(t *testing.T) {
executor := &executors.EphemeralExecutor{
BootConf: testCfg,
Container: tt.container,
}
ch := make(chan events.Event)
go executor.Run(ch, ifc.RunOptions{})
var actualEvt []events.Event
for evt := range ch {
if evt.Type == events.BootstrapType {
// Set message to empty string, so it's not compared
evt.BootstrapEvent.Message = ""
}
actualEvt = append(actualEvt, evt)
}
for i := range tt.expectedEvt {
// Fix timestamps for comparison
timeStamp := time.Time{}
tt.expectedEvt[i].Timestamp = timeStamp
actualEvt[i].Timestamp = timeStamp
}
assert.Equal(t, tt.expectedEvt, actualEvt)
})
}
}
func testConfigFile(path string) error {
_, err := os.Create(path)
return err
}
// TestValidate - Unit testing function Validate()
func TestEphemeralValidate(t *testing.T) {
executor := &executors.EphemeralExecutor{}
actualErr := executor.Validate()
assert.Equal(t, errors.ErrNotImplemented{}, actualErr)
}
// TestEphemeralRender - Unit testing function Render()
func TestEphemeralRender(t *testing.T) {
executor := &executors.EphemeralExecutor{}
writerReader := bytes.NewBuffer([]byte{})
actualErr := executor.Render(writerReader, ifc.RenderOptions{})
assert.Equal(t, nil, actualErr)
}