diff --git a/pkg/bootstrap/ephemeral/command.go b/pkg/bootstrap/ephemeral/command.go new file mode 100644 index 000000000..0366e2db0 --- /dev/null +++ b/pkg/bootstrap/ephemeral/command.go @@ -0,0 +1,246 @@ +/* + 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 ephemeral + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/container" + "opendev.org/airship/airshipctl/pkg/log" +) + +const ( + // BootCmdCreate is the string for the command "create" Ephemeral cluster + BootCmdCreate = "create" + // BootCmdDelete is the string for the command "delete" Ephemeral cluster + BootCmdDelete = "delete" + // BootCmdHelp is the string for the command "help" for the Ephemeral cluster + BootCmdHelp = "help" + // BootVolumeSeparator is the string Container volume mount + BootVolumeSeparator = ":" + // BootNullString represents an empty string + BootNullString = "" + // BootHelpFilename is the help filename + BootHelpFilename = "help.txt" + // Bootstrap Environment Variables + envBootstrapCommand = "BOOTSTRAP_COMMAND" + envBootstrapConfig = "BOOTSTRAP_CONFIG" + envBootstrapVolume = "BOOTSTRAP_VOLUME" +) + +var exitCodeMap = map[int]string{ + 1: ContainerLoadEphemeralConfigError, + 2: ContainerValidationEphemeralConfigError, + 3: ContainerSetEnvVarsError, + 4: ContainerUnknownCommandError, + 5: ContainerCreationEphemeralFailedError, + 6: ContainerDeletionEphemeralFailedError, + 7: ContainerHelpCommandFailedError, + 8: ContainerUnknownError, +} + +// BootstrapContainerOptions structure used by the executor +type BootstrapContainerOptions struct { + Container container.Container + Cfg *v1alpha1.BootConfiguration + Sleep func(d time.Duration) + + // optional fields for verbose output + Debug bool +} + +// VerifyInputs verify if all input data to the container is correct +func (options *BootstrapContainerOptions) VerifyInputs() error { + if options.Cfg.BootstrapContainer.Volume == "" { + return ErrInvalidInput{ + What: MissingVolumeError, + } + } + + if options.Cfg.BootstrapContainer.Image == "" { + return ErrInvalidInput{ + What: MissingContainerImageError, + } + } + + if options.Cfg.BootstrapContainer.ContainerRuntime == "" { + return ErrInvalidInput{ + What: MissingContainerRuntimeError, + } + } + + if options.Cfg.EphemeralCluster.ConfigFilename == "" { + return ErrInvalidInput{ + What: MissingConfigError, + } + } + + vols := strings.Split(options.Cfg.BootstrapContainer.Volume, ":") + switch { + case len(vols) == 1: + options.Cfg.BootstrapContainer.Volume = fmt.Sprintf("%s:%s", vols[0], vols[0]) + case len(vols) > 2: + return ErrVolumeMalFormed{} + } + return nil +} + +// GetContainerStatus returns the Bootstrap Container state +func (options *BootstrapContainerOptions) GetContainerStatus() (container.Status, error) { + // Check status of the container, e.g., "running" + state, err := options.Container.InspectContainer() + if err != nil { + return BootNullString, err + } + + var exitCode int + exitCode = state.ExitCode + if exitCode > 0 { + reader, err := options.Container.GetContainerLogs() + if err != nil { + log.Printf("Error while trying to retrieve the container logs") + return BootNullString, err + } + + containerError := ErrBootstrapContainerRun{} + containerError.ExitCode = exitCode + containerError.ErrMsg = exitCodeMap[exitCode] + + if reader != nil { + logs := new(bytes.Buffer) + _, err = logs.ReadFrom(reader) + if err != nil { + return BootNullString, err + } + reader.Close() + containerError.StdErr = logs.String() + } + return state.Status, containerError + } + + return state.Status, nil +} + +// WaitUntilContainerExitsOrTimesout waits for the container to exit or time out +func (options *BootstrapContainerOptions) WaitUntilContainerExitsOrTimesout( + maxRetries int, + configFilename string, + bootstrapCommand string) error { + // Give 2 seconds before checking if container is still running + // This period should be enough to detect some initial errors thrown by the container + options.Sleep(2 * time.Second) + + // Wait until container finished executing bootstrap of ephemeral cluster + status, err := options.GetContainerStatus() + if err != nil { + return err + } + if status == container.ExitedContainerStatus { + // bootstrap container command execution completed + return nil + } + for attempt := 1; attempt <= maxRetries; attempt++ { + log.Printf("Waiting for bootstrap container using %s config file to %s Ephemeral cluster (%d/%d)", + configFilename, bootstrapCommand, attempt, maxRetries) + // Wait for 15 seconds and check again bootstrap container state + options.Sleep(15 * time.Second) + status, err = options.GetContainerStatus() + if err != nil { + return err + } + if status == container.ExitedContainerStatus { + // bootstrap container command execution completed + return nil + } + } + return ErrNumberOfRetriesExceeded{} +} + +// CreateBootstrapContainer creates a Bootstrap Container +func (options *BootstrapContainerOptions) CreateBootstrapContainer() error { + containerVolMount := options.Cfg.BootstrapContainer.Volume + vols := []string{containerVolMount} + log.Printf("Running default container command. Mounted dir: %s", vols) + + bootstrapCommand := options.Cfg.EphemeralCluster.BootstrapCommand + configFilename := options.Cfg.EphemeralCluster.ConfigFilename + envVars := []string{ + fmt.Sprintf("%s=%s", envBootstrapCommand, bootstrapCommand), + fmt.Sprintf("%s=%s", envBootstrapConfig, configFilename), + fmt.Sprintf("%s=%s", envBootstrapVolume, containerVolMount), + } + + err := options.Container.RunCommand([]string{}, nil, vols, envVars) + if err != nil { + return err + } + + maxRetries := 50 + switch bootstrapCommand { + case BootCmdCreate: + // Wait until container finished executing bootstrap of ephemeral cluster + err = options.WaitUntilContainerExitsOrTimesout(maxRetries, configFilename, bootstrapCommand) + if err != nil { + log.Printf("Failed to create Ephemeral cluster using %s config file", configFilename) + return err + } + log.Printf("Ephemeral cluster created successfully using %s config file", configFilename) + case BootCmdDelete: + // Wait until container finished executing bootstrap of ephemeral cluster + err = options.WaitUntilContainerExitsOrTimesout(maxRetries, configFilename, bootstrapCommand) + if err != nil { + log.Printf("Failed to delete Ephemeral cluster using %s config file", configFilename) + return err + } + log.Printf("Ephemeral cluster deleted successfully using %s config file", configFilename) + case BootCmdHelp: + // Display Ephemeral Config file format for help + sepPos := strings.Index(containerVolMount, BootVolumeSeparator) + helpPath := filepath.Join(containerVolMount[:sepPos], BootHelpFilename) + + // Display help.txt on stdout + data, err := ioutil.ReadFile(helpPath) + if err != nil { + log.Printf("File reading %s error: %s", helpPath, err) + return err + } + // Printing the help.txt content to stdout + fmt.Println(string(data)) + + // Delete help.txt file + err = os.Remove(helpPath) + if err != nil { + log.Printf("Could not delete %s", helpPath) + return err + } + default: + return ErrInvalidBootstrapCommand{} + } + + log.Printf("Ephemeral cluster %s command completed successfully.", bootstrapCommand) + if !options.Debug { + log.Print("Removing bootstrap container.") + return options.Container.RmContainer() + } + + return nil +} diff --git a/pkg/bootstrap/ephemeral/command_test.go b/pkg/bootstrap/ephemeral/command_test.go new file mode 100644 index 000000000..fe7e2d5e9 --- /dev/null +++ b/pkg/bootstrap/ephemeral/command_test.go @@ -0,0 +1,625 @@ +/* + 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 ephemeral_test + +import ( + "bytes" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + api "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/log" +) + +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() +func TestGetContainerStatus(t *testing.T) { + testCfg := &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + } + + tests := []struct { + container *mockContainer + cfg *api.BootConfiguration + debug bool + expectedStatus container.Status + expectedErr error + }{ + { + // Container running and no errors + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.RunningContainerStatus + state.ExitCode = 0 + return state, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.RunningContainerStatus, + expectedErr: nil, + }, + { + // Container running and with ContainerLoadEphemeralConfigError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 1 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 1, + ErrMsg: ephemeral.ContainerLoadEphemeralConfigError, + }, + }, + { + // Container running and with ContainerValidationEphemeralConfigError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 2 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 2, + ErrMsg: ephemeral.ContainerValidationEphemeralConfigError, + }, + }, + { + // Container running and with ContainerSetEnvVarsError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 3 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 3, + ErrMsg: ephemeral.ContainerSetEnvVarsError, + }, + }, + { + // Container running and with ContainerUnknownCommandError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 4 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 4, + ErrMsg: ephemeral.ContainerUnknownCommandError, + }, + }, + { + // Container running and with ContainerCreationEphemeralFailedError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 5 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 5, + ErrMsg: ephemeral.ContainerCreationEphemeralFailedError, + }, + }, + { + // Container running and with ContainerDeletionEphemeralFailedError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 6 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 6, + ErrMsg: ephemeral.ContainerDeletionEphemeralFailedError, + }, + }, + { + // Container running and with ContainerHelpCommandFailedError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 7 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 7, + ErrMsg: ephemeral.ContainerHelpCommandFailedError, + }, + }, + { + // Container running and with ContainerUnknownError + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 8 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + expectedStatus: container.ExitedContainerStatus, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 8, + ErrMsg: ephemeral.ContainerUnknownError, + }, + }, + } + + for _, tt := range tests { + outBuf := &bytes.Buffer{} + log.Init(tt.debug, outBuf) + bootstrapOpts := ephemeral.BootstrapContainerOptions{ + Container: tt.container, + Cfg: tt.cfg, + Sleep: func(_ time.Duration) {}, + Debug: tt.debug, + } + actualStatus, actualErr := bootstrapOpts.GetContainerStatus() + assert.Equal(t, tt.expectedStatus, actualStatus) + assert.Equal(t, tt.expectedErr, actualErr) + } +} + +// Unit test for the method waitUntilContainerExitsOrTimesout() +func TestWaitUntilContainerExitsOrTimesout(t *testing.T) { + testCfg := &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + } + + tests := []struct { + container *mockContainer + cfg *api.BootConfiguration + debug bool + maxRetries int + expectedErr error + }{ + { + // Test: container exits normally + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 0 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + maxRetries: 10, + expectedErr: nil, + }, + { + // Test: container times out + container: &mockContainer{ + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.RunningContainerStatus + state.ExitCode = 0 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + debug: false, + maxRetries: 1, + expectedErr: ephemeral.ErrNumberOfRetriesExceeded{}, + }, + } + for _, tt := range tests { + outBuf := &bytes.Buffer{} + log.Init(tt.debug, outBuf) + bootstrapOpts := ephemeral.BootstrapContainerOptions{ + Container: tt.container, + Cfg: tt.cfg, + Sleep: func(_ time.Duration) {}, + Debug: tt.debug, + } + actualErr := bootstrapOpts.WaitUntilContainerExitsOrTimesout(tt.maxRetries, "dummy-config.yaml", "dummy") + assert.Equal(t, tt.expectedErr, actualErr) + } +} + +// Unit test for the method createBootstrapContainer() +func TestCreateBootstrapContainer(t *testing.T) { + testCfg := &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + } + + tests := []struct { + container *mockContainer + cfg *api.BootConfiguration + debug bool + bootstrapCommand string + expectedErr error + }{ + { + // Test: Create Ephemeral Cluster successful + container: &mockContainer{ + runCommand: func() error { return nil }, + rmContainer: func() error { return nil }, + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 0 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + bootstrapCommand: ephemeral.BootCmdCreate, + debug: false, + expectedErr: nil, + }, + { + // Test: Delete Ephemeral Cluster successful + container: &mockContainer{ + runCommand: func() error { return nil }, + rmContainer: func() error { return nil }, + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 0 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + bootstrapCommand: ephemeral.BootCmdDelete, + debug: false, + expectedErr: nil, + }, + { + // Test: Create Ephemeral Cluster exit with error + container: &mockContainer{ + runCommand: func() error { return nil }, + rmContainer: func() error { return nil }, + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 1 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + bootstrapCommand: ephemeral.BootCmdCreate, + debug: false, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 1, + ErrMsg: ephemeral.ContainerLoadEphemeralConfigError, + }, + }, + { + // Test: Delete Ephemeral Cluster exit with error + container: &mockContainer{ + runCommand: func() error { return nil }, + rmContainer: func() error { return nil }, + inspectContainer: func() (container.State, error) { + state := container.State{} + state.Status = container.ExitedContainerStatus + state.ExitCode = 1 + return state, nil + }, + getContainerLogs: func() (io.ReadCloser, error) { + return nil, nil + }, + }, + cfg: testCfg, + bootstrapCommand: ephemeral.BootCmdDelete, + debug: false, + expectedErr: ephemeral.ErrBootstrapContainerRun{ + ExitCode: 1, + ErrMsg: ephemeral.ContainerLoadEphemeralConfigError, + }, + }, + } + + for _, tt := range tests { + tt.cfg.EphemeralCluster.BootstrapCommand = tt.bootstrapCommand + bootstrapOpts := ephemeral.BootstrapContainerOptions{ + Container: tt.container, + Cfg: tt.cfg, + Sleep: func(_ time.Duration) {}, + Debug: tt.debug, + } + actualErr := bootstrapOpts.CreateBootstrapContainer() + assert.Equal(t, tt.expectedErr, actualErr) + } +} + +func TestVerifyInputs(t *testing.T) { + tests := []struct { + name string + cfg *api.BootConfiguration + expectedErr error + }{ + { + name: "Verify successful", + cfg: &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + }, + expectedErr: nil, + }, + { + name: "Missing Ephemeral cluster config file", + cfg: &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "", + }, + }, + expectedErr: ephemeral.ErrInvalidInput{ + What: ephemeral.MissingConfigError, + }, + }, + { + name: "Missing Volume mount for the Bootstrap container", + cfg: &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + }, + expectedErr: ephemeral.ErrInvalidInput{ + What: ephemeral.MissingVolumeError, + }, + }, + { + name: "Bootstrap container Volume mount mal formed 1", + cfg: &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + }, + expectedErr: nil, + }, + { + name: "Bootstrap container Volume mount mal formed 2", + cfg: &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy:/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + }, + expectedErr: ephemeral.ErrVolumeMalFormed{}, + }, + { + name: "Bootstrap container image missing", + cfg: &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "docker", + Image: "", + Volume: "/dummy:/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + }, + expectedErr: ephemeral.ErrInvalidInput{ + What: ephemeral.MissingContainerImageError, + }, + }, + { + name: "Bootstrap container runtime missing", + cfg: &api.BootConfiguration{ + BootstrapContainer: api.BootstrapContainer{ + ContainerRuntime: "", + Image: "quay.io/dummy/dummy:latest", + Volume: "/dummy:/dummy:/dummy", + Kubeconfig: "dummy.kubeconfig", + }, + EphemeralCluster: api.EphemeralCluster{ + BootstrapCommand: "dummy", + ConfigFilename: "dummy.yaml", + }, + }, + expectedErr: ephemeral.ErrInvalidInput{ + What: ephemeral.MissingContainerRuntimeError, + }, + }, + } + + for _, tt := range tests { + tt := tt + bootstrapOpts := ephemeral.BootstrapContainerOptions{ + Cfg: tt.cfg, + } + t.Run(tt.name, func(subTest *testing.T) { + actualErr := bootstrapOpts.VerifyInputs() + assert.Equal(subTest, tt.expectedErr, actualErr) + }) + } +} diff --git a/pkg/bootstrap/ephemeral/errors.go b/pkg/bootstrap/ephemeral/errors.go new file mode 100644 index 000000000..325e7b2d8 --- /dev/null +++ b/pkg/bootstrap/ephemeral/errors.go @@ -0,0 +1,95 @@ +/* + 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 ephemeral + +import "fmt" + +const ( + // ContainerLoadEphemeralConfigError ... + ContainerLoadEphemeralConfigError = "Container error: Failed to load Ephemeral cluster config file" + // ContainerValidationEphemeralConfigError ... + ContainerValidationEphemeralConfigError = "Container error: Ephemeral cluster configuration file validation failed" + // ContainerSetEnvVarsError ... + ContainerSetEnvVarsError = "Container error: Failed to set environment variables" + // ContainerUnknownCommandError ... + ContainerUnknownCommandError = "Container error: Container unknown command" + // ContainerCreationEphemeralFailedError ... + ContainerCreationEphemeralFailedError = "Container error: Creation of Ephemeral cluster failed" + // ContainerDeletionEphemeralFailedError ... + ContainerDeletionEphemeralFailedError = "Container error: Deletion of Ephemeral cluster failed" + // ContainerHelpCommandFailedError ... + ContainerHelpCommandFailedError = "Container error: Help command failed" + // ContainerUnknownError ... + ContainerUnknownError = "Container error: Unknown error code" + // NumberOfRetriesExceededError ... + NumberOfRetriesExceededError = "number of retries exceeded" + // MissingConfigError ... + MissingConfigError = "Missing Ephemeral Cluster ConfigFilename" + // MissingVolumeError ... + MissingVolumeError = "Must specify volume bind for Bootstrap builder container" + // VolumeMalFormedError ... + VolumeMalFormedError = "Bootstrap Container volume mount is mal formed" + // MissingContainerImageError ... + MissingContainerImageError = "Missing Bootstrap Container image" + // MissingContainerRuntimeError ... + MissingContainerRuntimeError = "Missing Container runtime information" + // InvalidBootstrapCommandError ... + InvalidBootstrapCommandError = "Invalid Bootstrap Container command" +) + +// ErrBootstrapContainerRun is returned when there is an error executing the container +type ErrBootstrapContainerRun struct { + ExitCode int + StdErr string + ErrMsg string +} + +func (e ErrBootstrapContainerRun) Error() string { + return fmt.Sprintf("Error from boostrap container: %s exit Code: %d\nContainer Logs: %s", + e.ErrMsg, e.ExitCode, e.StdErr) +} + +// ErrNumberOfRetriesExceeded is returned when number of retries have exceeded a limit +type ErrNumberOfRetriesExceeded struct { +} + +func (e ErrNumberOfRetriesExceeded) Error() string { + return NumberOfRetriesExceededError +} + +// ErrInvalidInput is returned when invalid values were passed to the Bootstrap Container +type ErrInvalidInput struct { + What string +} + +func (e ErrInvalidInput) Error() string { + return fmt.Sprintf("Invalid Bootstrap Container input: %s", e.What) +} + +// ErrVolumeMalFormed is returned when error volume mount is mal formed +type ErrVolumeMalFormed struct { +} + +func (e ErrVolumeMalFormed) Error() string { + return VolumeMalFormedError +} + +// ErrInvalidBootstrapCommand is returned when invalid command is invoked +type ErrInvalidBootstrapCommand struct { +} + +func (e ErrInvalidBootstrapCommand) Error() string { + return InvalidBootstrapCommandError +}