// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cli import ( "bytes" "context" "errors" "fmt" "path/filepath" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/copilot-cli/internal/pkg/deploy" "github.com/aws/copilot-cli/internal/pkg/docker/dockerengine" "github.com/aws/copilot-cli/internal/pkg/ecs" ecsMocks "github.com/aws/copilot-cli/internal/pkg/ecs/mocks" "github.com/aws/copilot-cli/internal/pkg/cli/mocks" "github.com/aws/copilot-cli/internal/pkg/task" "github.com/aws/copilot-cli/internal/pkg/config" "github.com/golang/mock/gomock" "github.com/spf13/afero" "github.com/stretchr/testify/require" ) type basicOpts struct { inCount int inCPU int inMemory int } var defaultOpts = basicOpts{ inCount: 1, inCPU: 256, inMemory: 512, } type spinnerTestDouble struct { startFn func(string) stopFn func(string) } // Assert that spinnerTestDouble implements the [progress] interface. var _ progress = (*spinnerTestDouble)(nil) func (s *spinnerTestDouble) Start(label string) { if s.startFn != nil { s.startFn(label) } } func (s *spinnerTestDouble) Stop(label string) { if s.stopFn != nil { s.stopFn(label) } } func TestTaskRunOpts_Validate(t *testing.T) { testCases := map[string]struct { basicOpts inName string inImage string inDockerfilePath string inDockerfileContextPath string inTaskRole string inEnv string inCluster string inSubnets []string inSecurityGroups []string inEnvVars map[string]string inEnvFile string inSecrets map[string]string inCommand string inEntryPoint string inOS string inArch string inDefault bool inGenerateCommandTarget string appName string isDockerfileSet bool mockStore func(m *mocks.Mockstore) mockFileSystem func(mockFS afero.Fs) wantedError error }{ "valid with no flag": { basicOpts: defaultOpts, wantedError: nil, }, "valid with flags image and env": { basicOpts: defaultOpts, inName: "my-task", inImage: "113459295.dkr.ecr.ap-northeast-1.amazonaws.com/my-app", inTaskRole: "exec-role", inEnv: "dev", inEnvVars: map[string]string{ "NAME": "my-app", "ENV": "dev", }, inSecrets: map[string]string{ "quiet": "barky doggo", }, inCommand: "echo hello world", inEntryPoint: "exec 'enter here'", appName: "my-app", mockStore: func(m *mocks.Mockstore) { m.EXPECT().GetApplication("my-app").Return(&config.Application{ Name: "my-app", }, nil) m.EXPECT().GetEnvironment("my-app", "dev").Return(&config.Environment{ App: "my-app", Name: "dev", }, nil) }, wantedError: nil, }, "valid without flags image and env": { basicOpts: defaultOpts, inName: "my-task", inDockerfilePath: "hello/world/Dockerfile", inTaskRole: "exec-role", inSubnets: []string{"subnet-10d938jds"}, inSecurityGroups: []string{"sg-0d9sjdk", "sg-d33kds99"}, inEnvVars: map[string]string{ "NAME": "pj", "ENV": "dev", }, inCommand: "echo hello world", mockFileSystem: func(mockFS afero.Fs) { mockFS.MkdirAll("hello/world", 0755) afero.WriteFile(mockFS, "hello/world/Dockerfile", []byte("FROM nginx"), 0644) }, wantedError: nil, }, "invalid with os but not arch": { basicOpts: defaultOpts, inOS: "WINDOWS_SERVER_2019_CORE", wantedError: errors.New("must specify either both `--platform-os` and `--platform-arch` or neither"), }, "invalid with arch but not os": { basicOpts: defaultOpts, inArch: "X86_64", wantedError: errors.New("must specify either both `--platform-os` and `--platform-arch` or neither"), }, "invalid platform": { basicOpts: defaultOpts, inOS: "OStrich", inArch: "MAD666", wantedError: errors.New("platform OSTRICH/MAD666 is invalid; valid platforms are: WINDOWS_SERVER_2019_CORE/X86_64, WINDOWS_SERVER_2019_FULL/X86_64, WINDOWS_SERVER_2022_CORE/X86_64, WINDOWS_SERVER_2022_FULL/X86_64, LINUX/X86_64 and LINUX/ARM64"), }, "uppercase any lowercase before validating": { basicOpts: basicOpts{ inCount: 1, inCPU: 1024, inMemory: 2048, }, inOS: "windows_server_2019_core", inArch: "x86_64", wantedError: nil, }, "invalid number of tasks": { basicOpts: basicOpts{ inCount: -1, inCPU: 256, inMemory: 512, }, wantedError: errNumNotPositive, }, "invalid number of CPU units": { basicOpts: basicOpts{ inCount: 1, inCPU: -15, inMemory: 512, }, wantedError: errCPUNotPositive, }, "invalid number of CPU units for Windows task": { basicOpts: basicOpts{ inCount: 1, inCPU: 260, inMemory: 512, }, inOS: "WINDOWS_SERVER_2019_CORE", inArch: "X86_64", wantedError: errors.New("CPU is 260, but it must be at least 1024 for a Windows-based task"), }, "invalid memory": { basicOpts: basicOpts{ inCount: 1, inCPU: 256, inMemory: -1024, }, wantedError: errMemNotPositive, }, "invalid memory for Windows task": { basicOpts: basicOpts{ inCount: 1, inCPU: 1024, inMemory: 2000, }, inOS: "WINDOWS_SERVER_2019_CORE", inArch: "X86_64", wantedError: errors.New("memory is 2000, but it must be at least 2048 for a Windows-based task"), }, "both build context and image name specified": { basicOpts: defaultOpts, inImage: "113459295.dkr.ecr.ap-northeast-1.amazonaws.com/my-app", inDockerfileContextPath: "../../other", wantedError: errors.New("cannot specify both `--image` and `--build-context`"), }, "both dockerfile and image name specified": { basicOpts: defaultOpts, inImage: "113459295.dkr.ecr.ap-northeast-1.amazonaws.com/my-app", isDockerfileSet: true, wantedError: errors.New("cannot specify both `--image` and `--dockerfile`"), }, "invalid dockerfile path": { basicOpts: defaultOpts, inDockerfilePath: "world/hello/Dockerfile", isDockerfileSet: true, wantedError: fmt.Errorf("invalid `--dockerfile` path: open %s: file does not exist", filepath.FromSlash("world/hello/Dockerfile")), }, "invalid build context path": { basicOpts: defaultOpts, inDockerfileContextPath: "world/hello/Dockerfile", wantedError: fmt.Errorf("invalid `--build-context` path: open %s: file does not exist", filepath.FromSlash("world/hello/Dockerfile")), }, "specified app exists": { basicOpts: defaultOpts, appName: "my-app", mockStore: func(m *mocks.Mockstore) { m.EXPECT().GetApplication("my-app").Return(&config.Application{ Name: "my-app", }, nil) }, wantedError: nil, }, "unknown app": { basicOpts: defaultOpts, appName: "my-app", mockStore: func(m *mocks.Mockstore) { m.EXPECT().GetApplication("my-app").Return(nil, &config.ErrNoSuchApplication{ ApplicationName: "my-app", AccountID: "115", Region: "us-east-1", }) }, wantedError: errors.New("get application: couldn't find an application named my-app in account 115 and region us-east-1"), }, "env exists in app": { basicOpts: defaultOpts, appName: "my-app", inEnv: "dev", mockStore: func(m *mocks.Mockstore) { m.EXPECT().GetEnvironment("my-app", "dev").Return(&config.Environment{ App: "my-app", Name: "dev", }, nil) m.EXPECT().GetApplication("my-app").Return(&config.Application{ Name: "my-app", }, nil) }, wantedError: nil, }, "unknown env in app": { basicOpts: defaultOpts, appName: "my-app", inEnv: "dev", mockStore: func(m *mocks.Mockstore) { m.EXPECT().GetEnvironment("my-app", "dev").Return(nil, &config.ErrNoSuchEnvironment{ ApplicationName: "my-app", EnvironmentName: "dev", }) m.EXPECT().GetApplication("my-app").Return(&config.Application{ Name: "my-app", }, nil) }, wantedError: errors.New("get environment dev config: couldn't find environment dev in the application my-app"), }, "no workspace": { basicOpts: defaultOpts, inEnv: "test", wantedError: errNoAppInWorkspace, }, "both environment and subnets specified": { basicOpts: defaultOpts, inEnv: "test", inSubnets: []string{"subnet id"}, wantedError: errors.New("cannot specify both `--subnets` and `--env`"), }, "both application and subnets specified": { basicOpts: defaultOpts, appName: "my-app", inSubnets: []string{"subnet id"}, wantedError: errors.New("cannot specify both `--subnets` and `--app`"), }, "both default and subnets specified": { basicOpts: defaultOpts, inDefault: true, inSubnets: []string{"subnet id"}, wantedError: errors.New("cannot specify both `--subnets` and `--default`"), }, "both cluster and default specified": { basicOpts: defaultOpts, inDefault: true, inCluster: "special-cluster", wantedError: errors.New("cannot specify both `--default` and `--cluster`"), }, "both cluster and application specified": { basicOpts: defaultOpts, inCluster: "special-cluster", appName: "my-app", wantedError: errors.New("cannot specify both `--app` and `--cluster`"), }, "both cluster and environment specified": { basicOpts: defaultOpts, inCluster: "special-cluster", inEnv: "my-env", wantedError: errors.New("cannot specify both `--env` and `--cluster`"), }, "generate-cmd specified with another flag": { basicOpts: defaultOpts, inGenerateCommandTarget: "cluster/service", // nFlag is set to 2. wantedError: errors.New("cannot specify `--generate-cmd` with any other flag"), }, "invalid env file extension": { basicOpts: defaultOpts, inEnvFile: "test.efdnv", wantedError: errors.New("environment file test.efdnv specified in --env-file must have a .env file extension"), }, "valid env file extension": { basicOpts: defaultOpts, inEnvFile: "test.env", wantedError: nil, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := mocks.NewMockstore(ctrl) opts := runTaskOpts{ runTaskVars: runTaskVars{ appName: tc.appName, count: tc.inCount, cpu: tc.inCPU, memory: tc.inMemory, groupName: tc.inName, image: tc.inImage, env: tc.inEnv, taskRole: tc.inTaskRole, cluster: tc.inCluster, subnets: tc.inSubnets, securityGroups: tc.inSecurityGroups, dockerfilePath: tc.inDockerfilePath, dockerfileContextPath: tc.inDockerfileContextPath, envVars: tc.inEnvVars, envFile: tc.inEnvFile, secrets: tc.inSecrets, command: tc.inCommand, entrypoint: tc.inEntryPoint, useDefaultSubnetsAndCluster: tc.inDefault, generateCommandTarget: tc.inGenerateCommandTarget, os: tc.inOS, arch: tc.inArch, }, isDockerfileSet: tc.isDockerfileSet, nFlag: 2, fs: &afero.Afero{Fs: afero.NewMemMapFs()}, store: mockStore, } if tc.mockFileSystem != nil { tc.mockFileSystem(opts.fs) } if tc.mockStore != nil { tc.mockStore(mockStore) } err := opts.Validate() if tc.wantedError != nil { require.EqualError(t, err, tc.wantedError.Error()) } else { require.NoError(t, err) } }) } } func TestTaskRunOpts_Ask(t *testing.T) { testCases := map[string]struct { inName string inCluster string inSubnets []string inSecurityGroups []string inDefault bool inEnv string appName string inSecrets map[string]string inSsmParamSecrets map[string]string inSecretsManagerSecrets map[string]string inAcknowledgeSecretsAccess bool inExecutionRole string mockSel func(m *mocks.MockappEnvSelector) mockPrompt func(m *mocks.Mockprompter) wantedError error wantedApp string wantedEnv string wantedName string }{ "selected an existing application": { mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), appEnvOptionNone).Return("app", nil) m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), gomock.Any()).Times(1) }, wantedApp: "app", }, "selected None app": { mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), appEnvOptionNone).Return(appEnvOptionNone, nil) }, wantedApp: "", }, "don't prompt for app when under a workspace or app flag is specified": { appName: "my-app", inDefault: true, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).Times(0) m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).AnyTimes() }, wantedApp: "my-app", }, "don't prompt for env if env is provided": { inEnv: "test", appName: "my-app", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Environment(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) }, wantedEnv: "test", wantedApp: "my-app", }, "don't prompt for env if no workspace and selected None app": { mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(gomock.Any(), gomock.Any(), appEnvOptionNone).Return(appEnvOptionNone, nil) m.EXPECT().Environment(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) }, wantedEnv: "", }, "don't prompt for app if using default": { inDefault: true, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).Times(0) m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).AnyTimes() }, wantedApp: "", }, "don't prompt for env if using default": { inDefault: true, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(gomock.Any(), gomock.Any(), appEnvOptionNone).AnyTimes() m.EXPECT().Environment(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) }, wantedEnv: "", }, "don't prompt for app if subnets are specified": { inSubnets: []string{"subnet-1"}, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).Times(0) m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).AnyTimes() }, }, "don't prompt for app if cluster is specified": { inCluster: "cluster-1", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).AnyTimes() m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).Times(0) }, }, "don't prompt for env if subnets are specified": { inSubnets: []string{"subnet-1"}, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).AnyTimes() m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).Times(0) }, }, "don't prompt for env if cluster is specified": { inCluster: "cluster-1", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).AnyTimes() m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).Times(0) }, }, "don't prompt for app if security groups are specified": { inSecurityGroups: []string{"sg-1"}, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).Times(0) m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).AnyTimes() }, }, "don't prompt for env if security groups are specified": { inSecurityGroups: []string{"sg-1"}, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Application(taskRunAppPrompt, gomock.Any(), gomock.Any()).AnyTimes() m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone).Times(0) }, }, "selected an existing environment": { appName: "my-app", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), "my-app", appEnvOptionNone).Return("test", nil) }, wantedEnv: "test", wantedApp: "my-app", }, "selected None env": { appName: "my-app", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), "my-app", appEnvOptionNone).Return(appEnvOptionNone, nil) }, wantedEnv: "", wantedApp: "my-app", }, "error selecting environment": { appName: "my-app", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() }, mockSel: func(m *mocks.MockappEnvSelector) { m.EXPECT().Environment(taskRunEnvPrompt, gomock.Any(), gomock.Any(), appEnvOptionNone). Return("", fmt.Errorf("error selecting environment")) }, wantedError: errors.New("ask for environment: error selecting environment"), }, "When secrets are provided without app and env leads to a secret access permission prompt": { inSecrets: map[string]string{ "quiet": "shh", }, inSsmParamSecrets: map[string]string{ "quiet": "shh", }, inSecretsManagerSecrets: map[string]string{ "quiet": "shh", }, inCluster: "cluster-1", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Confirm(taskSecretsPermissionPrompt, taskSecretsPermissionPromptHelp).Return(true, nil) }, }, "secret access permission prompt is skipped when acknowledge-secret-access flag is provided": { inSecrets: map[string]string{ "quiet": "shh", }, inCluster: "cluster-1", inAcknowledgeSecretsAccess: true, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Confirm(taskSecretsPermissionPrompt, taskSecretsPermissionPromptHelp).Times(0) }, }, "secret access permission prompt is skipped when execution-role is provided": { inSecrets: map[string]string{ "quiet": "shh", }, inCluster: "cluster-1", inExecutionRole: "test-role", mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Confirm(taskSecretsPermissionPrompt, taskSecretsPermissionPromptHelp).Times(0) }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockSel := mocks.NewMockappEnvSelector(ctrl) mockPrompter := mocks.NewMockprompter(ctrl) if tc.mockSel != nil { tc.mockSel(mockSel) } if tc.mockPrompt != nil { tc.mockPrompt(mockPrompter) } opts := runTaskOpts{ runTaskVars: runTaskVars{ appName: tc.appName, groupName: tc.inName, env: tc.inEnv, useDefaultSubnetsAndCluster: tc.inDefault, subnets: tc.inSubnets, securityGroups: tc.inSecurityGroups, cluster: tc.inCluster, acknowledgeSecretsAccess: tc.inAcknowledgeSecretsAccess, secrets: tc.inSecrets, executionRole: tc.inExecutionRole, }, sel: mockSel, prompt: mockPrompter, secretsManagerSecrets: tc.inSecretsManagerSecrets, ssmParamSecrets: tc.inSsmParamSecrets, } err := opts.Ask() if tc.wantedError == nil { require.NoError(t, err) require.Equal(t, tc.wantedEnv, opts.env) require.Equal(t, tc.wantedApp, opts.appName) if tc.wantedName != "" { require.Equal(t, tc.wantedName, opts.groupName) } } else { require.EqualError(t, tc.wantedError, err.Error()) } }) } } type runTaskMocks struct { deployer *mocks.MocktaskDeployer repository *mocks.MockrepositoryService runner *mocks.MocktaskRunner store *mocks.Mockstore eventsWriter *mocks.MockeventsWriter defaultClusterGetter *mocks.MockdefaultClusterGetter publicIPGetter *mocks.MockpublicIPGetter provider *mocks.MocksessionProvider uploader *mocks.Mockuploader } func mockHasDefaultCluster(m runTaskMocks) { m.defaultClusterGetter.EXPECT().HasDefaultCluster().Return(true, nil).AnyTimes() } func mockRepositoryAnytime(m runTaskMocks) { m.repository.EXPECT().Login().AnyTimes() m.repository.EXPECT().BuildAndPush(context.Background(), gomock.Any(), gomock.Any()).AnyTimes() } func TestTaskRunOpts_Execute(t *testing.T) { const ( inGroupName = "my-task" mockRepoURI = "uri/repo" tag = "tag" ) ctx := context.Background() defaultBuildArguments := dockerengine.BuildArguments{ URI: mockRepoURI, Context: filepath.Dir(defaultDockerfilePath), Tags: []string{imageTagLatest}, } testCases := map[string]struct { inSecrets map[string]string inImage string inTag string inDockerCtx string inFollow bool inCommand string inEntryPoint string inEnvFile string inApp string inEnv string setupFs func(fs *afero.Afero) setupMocks func(m runTaskMocks) wantedError error }{ "check if default cluster exists if deploying to default cluster": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.defaultClusterGetter.EXPECT().HasDefaultCluster().Return(true, nil) m.deployer.EXPECT().DeployTask(gomock.Any()).Return(nil).AnyTimes() mockRepositoryAnytime(m) m.runner.EXPECT().Run().AnyTimes() }, }, "do not check for default cluster if deploying to environment": { inEnv: "test", setupMocks: func(m runTaskMocks) { m.defaultClusterGetter.EXPECT().HasDefaultCluster().Times(0) m.store.EXPECT(). GetEnvironment(gomock.Any(), "test"). Return(&config.Environment{ ExecutionRoleARN: "env execution role", }, nil) m.provider.EXPECT().FromRole(gomock.Any(), gomock.Any()) m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() mockRepositoryAnytime(m) m.runner.EXPECT().Run().AnyTimes() }, }, "error deploying resources": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.deployer.EXPECT().DeployTask(&deploy.CreateTaskResourcesInput{ Name: inGroupName, Image: "", Command: []string{}, EntryPoint: []string{}, }).Return(errors.New("error deploying")) mockHasDefaultCluster(m) }, wantedError: errors.New("provision resources for task my-task: error deploying"), }, "error performing docker login": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.deployer.EXPECT().DeployTask(&deploy.CreateTaskResourcesInput{ Name: inGroupName, Image: "", Command: []string{}, EntryPoint: []string{}, }).Return(nil) m.repository.EXPECT().Login().Return(mockRepoURI, errors.New("some error")) mockHasDefaultCluster(m) }, wantedError: errors.New("login to docker: some error"), }, "error updating resources": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.deployer.EXPECT().DeployTask(&deploy.CreateTaskResourcesInput{ Name: inGroupName, Image: "", Command: []string{}, EntryPoint: []string{}, }).Return(nil) m.repository.EXPECT().Login().Return(mockRepoURI, nil) m.repository.EXPECT().BuildAndPush(ctx, gomock.Any(), gomock.Any()) m.deployer.EXPECT().DeployTask(&deploy.CreateTaskResourcesInput{ Name: inGroupName, Image: "uri/repo:latest", Command: []string{}, EntryPoint: []string{}, }).Times(1).Return(errors.New("error updating")) mockHasDefaultCluster(m) }, wantedError: errors.New("update resources for task my-task: error updating"), }, "error running tasks": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.deployer.EXPECT().DeployTask(gomock.Any()).Return(nil).Times(2) mockRepositoryAnytime(m) m.runner.EXPECT().Run().Return(nil, errors.New("error running")) mockHasDefaultCluster(m) }, wantedError: errors.New("run task my-task: error running"), }, "deploy with execution role option if env is not empty": { inEnv: "test", setupMocks: func(m runTaskMocks) { m.store.EXPECT().GetEnvironment(gomock.Any(), "test"). Return(&config.Environment{ ExecutionRoleARN: "env execution role", }, nil) m.provider.EXPECT().FromRole(gomock.Any(), gomock.Any()) m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Len(1)).AnyTimes() // NOTE: matching length because gomock is unable to match function arguments. mockRepositoryAnytime(m) m.runner.EXPECT().Run().AnyTimes() m.defaultClusterGetter.EXPECT().HasDefaultCluster().Times(0) }, }, "deploy without execution role option if env is empty": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).Times(0) m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Len(0)).AnyTimes() // NOTE: matching length because gomock is unable to match function arguments. mockRepositoryAnytime(m) m.runner.EXPECT().Run().AnyTimes() mockHasDefaultCluster(m) }, }, "append 'latest' to image tag": { inTag: tag, setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.deployer.EXPECT().DeployTask(gomock.Any()).AnyTimes() m.repository.EXPECT().Login().Return(mockRepoURI, nil) m.repository.EXPECT().BuildAndPush(ctx, gomock.Eq( &dockerengine.BuildArguments{ URI: mockRepoURI, Context: filepath.Dir(defaultDockerfilePath), Tags: []string{imageTagLatest, tag}, }), gomock.Any(), ) m.runner.EXPECT().Run().AnyTimes() mockHasDefaultCluster(m) }, }, "should use provided docker build context instead of dockerfile path": { inDockerCtx: "../../other", setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.deployer.EXPECT().DeployTask(gomock.Any()).AnyTimes() m.repository.EXPECT().Login().Return(mockRepoURI, nil) m.repository.EXPECT().BuildAndPush(ctx, gomock.Eq( &dockerengine.BuildArguments{ URI: mockRepoURI, Context: "../../other", Tags: []string{imageTagLatest}, }), gomock.Any(), ) m.runner.EXPECT().Run().AnyTimes() mockHasDefaultCluster(m) }, }, "update image to task resource if image is not provided": { inCommand: `/bin/sh -c "curl $ECS_CONTAINER_METADATA_URI_V4"`, inEntryPoint: `exec "some command"`, setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.store.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).AnyTimes() m.deployer.EXPECT().DeployTask(&deploy.CreateTaskResourcesInput{ Name: inGroupName, Image: "", Command: []string{"/bin/sh", "-c", "curl $ECS_CONTAINER_METADATA_URI_V4"}, EntryPoint: []string{"exec", "some command"}, }).Times(1).Return(nil) m.repository.EXPECT().Login().Return(mockRepoURI, nil) m.repository.EXPECT().BuildAndPush(ctx, gomock.Eq(&defaultBuildArguments), gomock.Any()) m.deployer.EXPECT().DeployTask(&deploy.CreateTaskResourcesInput{ Name: inGroupName, Image: "uri/repo:latest", Command: []string{"/bin/sh", "-c", "curl $ECS_CONTAINER_METADATA_URI_V4"}, EntryPoint: []string{"exec", "some command"}, }).Times(1).Return(nil) m.runner.EXPECT().Run().AnyTimes() mockHasDefaultCluster(m) }, }, "fail to get ENI information for some tasks": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.deployer.EXPECT().DeployTask(gomock.Any()).AnyTimes() m.runner.EXPECT().Run().Return([]*task.Task{ { TaskARN: "task-1", ENI: "eni-1", }, { TaskARN: "task-2", }, { TaskARN: "task-3", }, }, nil) m.publicIPGetter.EXPECT().PublicIP("eni-1").Return("1.2.3", nil) mockHasDefaultCluster(m) mockRepositoryAnytime(m) }, }, "fail to get public ips": { setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.deployer.EXPECT().DeployTask(gomock.Any()).AnyTimes() m.runner.EXPECT().Run().Return([]*task.Task{ { TaskARN: "task-1", ENI: "eni-1", }, }, nil) m.publicIPGetter.EXPECT().PublicIP("eni-1").Return("", errors.New("some error")) mockHasDefaultCluster(m) mockRepositoryAnytime(m) }, // wantedError is nil because we will just not show the IP address if we can't instead of erroring out. }, "fail to write events": { inFollow: true, inImage: "image", setupMocks: func(m runTaskMocks) { m.provider.EXPECT().Default().Return(&session.Session{}, nil) m.deployer.EXPECT().DeployTask(gomock.Any()).AnyTimes() m.runner.EXPECT().Run().Return([]*task.Task{ { TaskARN: "task-1", ENI: "eni-1", }, }, nil) m.publicIPGetter.EXPECT().PublicIP("eni-1").Return("1.2.3", nil) m.eventsWriter.EXPECT().WriteEventsUntilStopped().Times(1). Return(errors.New("error writing events")) mockHasDefaultCluster(m) }, wantedError: errors.New("write events: error writing events"), }, "error getting app config (to look for permissions boundary policy)": { inApp: "my-app", inEnv: "test", setupMocks: func(m runTaskMocks) { m.store.EXPECT().GetEnvironment(gomock.Any(), "test"). Return(&config.Environment{ ExecutionRoleARN: "env execution role", }, nil) m.provider.EXPECT().FromRole(gomock.Any(), gomock.Any()) m.store.EXPECT().GetApplication("my-app").Return(nil, errors.New("some error")) m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Len(1)).AnyTimes() // NOTE: matching length because gomock is unable to match function arguments. mockRepositoryAnytime(m) m.runner.EXPECT().Run().AnyTimes() m.defaultClusterGetter.EXPECT().HasDefaultCluster().Times(0) }, wantedError: fmt.Errorf("provision resources for task %s: get application: some error", "my-task"), }, "env file happy path": { inEnvFile: "testdir/../magic.env", inApp: "my-app", inImage: "some-image", setupFs: func(fs *afero.Afero) { afero.Fs.Mkdir(fs, "testdir", 0755) afero.WriteFile(fs, "magic.env", []byte("SOMETHING=VALUE"), 0644) }, setupMocks: func(m runTaskMocks) { region := "us-east-35" m.provider.EXPECT().Default().Return(&session.Session{ Config: &aws.Config{ // uh oh, new leaked region Region: aws.String(region), }, }, nil) m.defaultClusterGetter.EXPECT().HasDefaultCluster().Return(true, nil) info := deploy.TaskStackInfo{BucketName: "arn:aws:s3:::bigbucket"} m.store.EXPECT().GetApplication("my-app").Return(&config.Application{Name: "my-app"}, nil).Times(2) m.deployer.EXPECT().GetTaskStack(inGroupName).Return(&info, nil) key := "manual/env-files/magic.env/4963d64294508aa3fa103ccac5ad1537944c577d469608ddccad09b6f79b6406.env" url := "https://bigbucket.s3-us-west-2.amazonaws.com/" + key m.uploader.EXPECT().Upload("arn:aws:s3:::bigbucket", key, bytes.NewReader([]byte("SOMETHING=VALUE"))).Return(url, nil) m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() mockRepositoryAnytime(m) m.runner.EXPECT().Run().AnyTimes() }, }, "env file not found": { inEnvFile: "sadness.env", inApp: "my-app", setupMocks: func(m runTaskMocks) { region := "us-east-35" m.provider.EXPECT().Default().Return(&session.Session{ Config: &aws.Config{ // uh oh, new leaked region Region: aws.String(region), }, }, nil) m.defaultClusterGetter.EXPECT().HasDefaultCluster().Return(true, nil) info := deploy.TaskStackInfo{BucketName: "arn:aws:s3:::bigbucket"} m.store.EXPECT().GetApplication("my-app").Return(&config.Application{Name: "my-app"}, nil) m.deployer.EXPECT().GetTaskStack(inGroupName).Return(&info, nil) m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Any()).Return(nil) }, wantedError: errors.New("deploy env file sadness.env: read env file sadness.env: open sadness.env: file does not exist"), }, "env file pipeline resource add fail": { inEnvFile: "testdir/../magic.env", inApp: "my-app", setupFs: func(fs *afero.Afero) { afero.Fs.Mkdir(fs, "testdir", 0755) afero.WriteFile(fs, "magic.env", []byte("SOMETHING=VALUE"), 0644) }, setupMocks: func(m runTaskMocks) { region := "us-east-35" m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Any()).Return(nil) m.provider.EXPECT().Default().Return(&session.Session{ Config: &aws.Config{ // uh oh, new leaked region Region: aws.String(region), }, }, nil) m.defaultClusterGetter.EXPECT().HasDefaultCluster().Return(true, nil) m.store.EXPECT().GetApplication("my-app").Return(&config.Application{Name: "my-app"}, nil) m.deployer.EXPECT().GetTaskStack(inGroupName).Return(nil, errors.New("hull breach in sector 3")) }, wantedError: errors.New("deploy env file testdir/../magic.env: deploy env file: hull breach in sector 3"), }, "env file s3 upload failure": { inEnvFile: "testdir/../magic.env", inApp: "my-app", setupFs: func(fs *afero.Afero) { afero.Fs.Mkdir(fs, "testdir", 0755) afero.WriteFile(fs, "magic.env", []byte("SOMETHING=VALUE"), 0644) }, setupMocks: func(m runTaskMocks) { region := "us-east-35" m.deployer.EXPECT().DeployTask(gomock.Any(), gomock.Any()).Return(nil) m.provider.EXPECT().Default().Return(&session.Session{ Config: &aws.Config{ // uh oh, new leaked region Region: aws.String(region), }, }, nil) m.defaultClusterGetter.EXPECT().HasDefaultCluster().Return(true, nil) info := deploy.TaskStackInfo{BucketName: "arn:aws:s3:::bigbucket"} m.store.EXPECT().GetApplication("my-app").Return(&config.Application{Name: "my-app"}, nil) m.deployer.EXPECT().GetTaskStack(inGroupName).Return(&info, nil) key := "manual/env-files/magic.env/4963d64294508aa3fa103ccac5ad1537944c577d469608ddccad09b6f79b6406.env" arn := "arn:aws:s3:::bigbucket/" + key m.uploader.EXPECT().Upload("arn:aws:s3:::bigbucket", key, bytes.NewReader([]byte("SOMETHING=VALUE"))).Return(arn, errors.New("out of floppy disks")) }, wantedError: errors.New("deploy env file testdir/../magic.env: put env file testdir/../magic.env artifact to bucket arn:aws:s3:::bigbucket: out of floppy disks"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() fs := &afero.Afero{Fs: afero.NewMemMapFs()} if tc.setupFs != nil { tc.setupFs(fs) } mocks := runTaskMocks{ deployer: mocks.NewMocktaskDeployer(ctrl), repository: mocks.NewMockrepositoryService(ctrl), runner: mocks.NewMocktaskRunner(ctrl), store: mocks.NewMockstore(ctrl), eventsWriter: mocks.NewMockeventsWriter(ctrl), defaultClusterGetter: mocks.NewMockdefaultClusterGetter(ctrl), publicIPGetter: mocks.NewMockpublicIPGetter(ctrl), provider: mocks.NewMocksessionProvider(ctrl), uploader: mocks.NewMockuploader(ctrl), } tc.setupMocks(mocks) opts := &runTaskOpts{ runTaskVars: runTaskVars{ groupName: inGroupName, image: tc.inImage, imageTag: tc.inTag, dockerfileContextPath: tc.inDockerCtx, appName: tc.inApp, env: tc.inEnv, follow: tc.inFollow, secrets: tc.inSecrets, command: tc.inCommand, entrypoint: tc.inEntryPoint, envFile: tc.inEnvFile, }, spinner: &spinnerTestDouble{}, store: mocks.store, provider: mocks.provider, fs: fs.Fs, } opts.configureRuntimeOpts = func() error { opts.runner = mocks.runner opts.deployer = mocks.deployer opts.defaultClusterGetter = mocks.defaultClusterGetter opts.publicIPGetter = mocks.publicIPGetter return nil } opts.configureRepository = func() error { opts.repository = mocks.repository return nil } opts.configureEventsWriter = func(tasks []*task.Task) { opts.eventsWriter = mocks.eventsWriter } opts.configureUploader = func(session *session.Session) uploader { return mocks.uploader } err := opts.Execute() if tc.wantedError != nil { require.EqualError(t, err, tc.wantedError.Error()) } else { require.NoError(t, err) } }) } } type mockRunTaskRequester struct { mockRunTaskRequestFromECSService func(client ecs.ECSServiceDescriber, cluster string, service string) (*ecs.RunTaskRequest, error) mockRunTaskRequestFromService func(client ecs.ServiceDescriber, app, env, svc string) (*ecs.RunTaskRequest, error) mockRunTaskRequestFromJob func(client ecs.JobDescriber, app, env, job string) (*ecs.RunTaskRequest, error) } type taskRunMocks struct { store *mocks.Mockstore provider *mocks.MocksessionProvider envCompatibilityChecker *mocks.MockversionCompatibilityChecker } func TestTaskRunOpts_runTaskCommand(t *testing.T) { wantedCommand := ecs.RunTaskRequest{} testCases := map[string]struct { inGenerateCommandTarget string setUpMocks func(m *taskRunMocks) mockRunTaskRequester mockRunTaskRequester wantedCommand *ecs.RunTaskRequest wantedError error }{ "should generate a command given an service ARN": { inGenerateCommandTarget: "arn:aws:ecs:us-east-1:123456789012:service/crowded-cluster/good-service", setUpMocks: func(m *taskRunMocks) { m.provider.EXPECT().Default() }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromECSService: func(client ecs.ECSServiceDescriber, cluster string, service string) (*ecs.RunTaskRequest, error) { return &wantedCommand, nil }, }, wantedCommand: &wantedCommand, }, "fail to generate a command given a service ARN": { inGenerateCommandTarget: "arn:aws:ecs:us-east-1:123456789012:service/crowded-cluster/good-service", setUpMocks: func(m *taskRunMocks) { m.provider.EXPECT().Default() }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromECSService: func(client ecs.ECSServiceDescriber, cluster string, service string) (*ecs.RunTaskRequest, error) { return nil, errors.New("some error") }, }, wantedError: fmt.Errorf("generate task run command from ECS service crowded-cluster/good-service: some error"), }, "should generate a command given a cluster/service target": { inGenerateCommandTarget: "crowded-cluster/good-service", setUpMocks: func(m *taskRunMocks) { m.provider.EXPECT().Default() }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromECSService: func(client ecs.ECSServiceDescriber, cluster string, service string) (*ecs.RunTaskRequest, error) { return &wantedCommand, nil }, }, wantedCommand: &wantedCommand, }, "fail to generate a command given a cluster/service target": { inGenerateCommandTarget: "crowded-cluster/good-service", setUpMocks: func(m *taskRunMocks) { m.provider.EXPECT().Default() }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromECSService: func(client ecs.ECSServiceDescriber, cluster string, service string) (*ecs.RunTaskRequest, error) { return nil, errors.New("some error") }, }, wantedError: fmt.Errorf("generate task run command from ECS service crowded-cluster/good-service: some error"), }, "should generate a command given an app/env/svc target": { inGenerateCommandTarget: "good-app/good-env/good-service", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "good-service").Return(nil, &config.ErrNoSuchJob{}) m.store.EXPECT().GetService("good-app", "good-service").Return(&config.Workload{}, nil) }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromService: func(client ecs.ServiceDescriber, app, env, svc string) (*ecs.RunTaskRequest, error) { return &wantedCommand, nil }, }, wantedCommand: &wantedCommand, }, "fail to generate a command given an app/env/svc target": { inGenerateCommandTarget: "good-app/good-env/good-service", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "good-service").Return(nil, &config.ErrNoSuchJob{}) m.store.EXPECT().GetService("good-app", "good-service").Return(&config.Workload{}, nil) }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromService: func(client ecs.ServiceDescriber, app, env, svc string) (*ecs.RunTaskRequest, error) { return nil, errors.New("some error") }, }, wantedError: fmt.Errorf("generate task run command from service good-service of application good-app deployed in environment good-env: some error"), }, "should generate a command given an app/env/job target": { inGenerateCommandTarget: "good-app/good-env/good-job", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "good-job").Return(&config.Workload{}, nil) m.envCompatibilityChecker.EXPECT().Version().Return("v1.12.2", nil) }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromJob: func(client ecs.JobDescriber, app, env, svc string) (*ecs.RunTaskRequest, error) { return &wantedCommand, nil }, }, wantedCommand: &wantedCommand, }, "fail to generate a command given an app/env/job target": { inGenerateCommandTarget: "good-app/good-env/good-job", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "good-job").Return(&config.Workload{}, nil) m.envCompatibilityChecker.EXPECT().Version().Return("v1.12.2", nil) }, mockRunTaskRequester: mockRunTaskRequester{ mockRunTaskRequestFromJob: func(client ecs.JobDescriber, app, env, svc string) (*ecs.RunTaskRequest, error) { return nil, errors.New("some error") }, }, wantedError: fmt.Errorf("generate task run command from job good-job of application good-app deployed in environment good-env: some error"), }, "error out if fail to get env version when target is job": { inGenerateCommandTarget: "good-app/good-env/good-job", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "good-job").Return(&config.Workload{}, nil) m.envCompatibilityChecker.EXPECT().Version().Return("", errors.New("some error")) }, wantedError: fmt.Errorf(`retrieve version of environment stack "good-env" in application "good-app": some error`), }, "error out if env version doesn't support `--generate-cmd` for jobs": { inGenerateCommandTarget: "good-app/good-env/good-job", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "good-job").Return(&config.Workload{}, nil) m.envCompatibilityChecker.EXPECT().Version().Return("v1.9.0", nil) }, wantedError: fmt.Errorf(`environment "good-env" is on version "v1.9.0" which does not support the "task run --generate-cmd" feature`), }, "fail to determine if the workload is a job given an app/env/workload target": { inGenerateCommandTarget: "good-app/good-env/bad-workload", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "bad-workload").Return(nil, errors.New("some error")) }, wantedError: fmt.Errorf("determine whether workload bad-workload is a job: some error"), }, "fail to determine if the workload is a service given an app/env/workload target": { inGenerateCommandTarget: "good-app/good-env/bad-workload", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "bad-workload").Return(nil, &config.ErrNoSuchJob{}) m.store.EXPECT().GetService("good-app", "bad-workload").Return(nil, errors.New("some error")) }, wantedError: fmt.Errorf("determine whether workload bad-workload is a service: some error"), }, "workload is neither a job nor a service": { inGenerateCommandTarget: "good-app/good-env/bad-workload", setUpMocks: func(m *taskRunMocks) { m.store.EXPECT().GetEnvironment("good-app", "good-env").Return(&config.Environment{ ManagerRoleARN: "mock-role", Region: "mock-region", }, nil) m.provider.EXPECT().FromRole("mock-role", "mock-region") m.store.EXPECT().GetJob("good-app", "bad-workload").Return(nil, &config.ErrNoSuchJob{}) m.store.EXPECT().GetService("good-app", "bad-workload").Return(nil, &config.ErrNoSuchService{}) }, wantedError: fmt.Errorf("workload bad-workload is neither a service nor a job"), }, "invalid input": { inGenerateCommandTarget: "invalid/illegal/not-good/input/is/bad", setUpMocks: func(m *taskRunMocks) {}, wantedError: errors.New("invalid input to --generate-cmd: must be of format <cluster>/<service> or <app>/<env>/<workload>"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() m := &taskRunMocks{ store: mocks.NewMockstore(ctrl), provider: mocks.NewMocksessionProvider(ctrl), envCompatibilityChecker: mocks.NewMockversionCompatibilityChecker(ctrl), } if tc.setUpMocks != nil { tc.setUpMocks(m) } opts := &runTaskOpts{ runTaskVars: runTaskVars{ generateCommandTarget: tc.inGenerateCommandTarget, }, store: m.store, provider: m.provider, configureECSServiceDescriber: func(session *session.Session) ecs.ECSServiceDescriber { return ecsMocks.NewMockECSServiceDescriber(ctrl) }, configureJobDescriber: func(session *session.Session) ecs.JobDescriber { return ecsMocks.NewMockJobDescriber(ctrl) }, configureServiceDescriber: func(session *session.Session) ecs.ServiceDescriber { return ecsMocks.NewMockServiceDescriber(ctrl) }, runTaskRequestFromECSService: tc.mockRunTaskRequester.mockRunTaskRequestFromECSService, runTaskRequestFromService: tc.mockRunTaskRequester.mockRunTaskRequestFromService, runTaskRequestFromJob: tc.mockRunTaskRequester.mockRunTaskRequestFromJob, envCompatibilityChecker: func(app, env string) (versionCompatibilityChecker, error) { return m.envCompatibilityChecker, nil }, } got, err := opts.runTaskCommand() if tc.wantedError != nil { require.EqualError(t, err, tc.wantedError.Error()) } else { require.NoError(t, err) require.Equal(t, tc.wantedCommand, got) } }) } }