// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cli import ( "errors" "fmt" "path/filepath" "testing" "time" "github.com/aws/copilot-cli/internal/pkg/config" "github.com/aws/copilot-cli/internal/pkg/docker/dockerengine" "github.com/aws/copilot-cli/internal/pkg/docker/dockerfile" "github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo" "github.com/aws/copilot-cli/internal/pkg/term/color" "github.com/aws/copilot-cli/internal/pkg/workspace" "github.com/aws/copilot-cli/internal/pkg/cli/mocks" "github.com/aws/copilot-cli/internal/pkg/initialize" "github.com/aws/copilot-cli/internal/pkg/manifest" "github.com/golang/mock/gomock" "github.com/spf13/afero" "github.com/stretchr/testify/require" ) type initJobMocks struct { mockPrompt *mocks.Mockprompter mockDockerEngine *mocks.MockdockerEngine mockMftReader *mocks.MockmanifestReader mockStore *mocks.Mockstore mockDockerfileSel *mocks.MockdockerfileSelector mockScheduleSel *mocks.MockscheduleSelector } func TestJobInitOpts_Validate(t *testing.T) { testCases := map[string]struct { inAppName string inJobName string inDockerfilePath string inImage string inTimeout string inRetries int setupMocks func(mocks initJobMocks) mockFileSystem func(mockFS afero.Fs) wantedErr error }{ "fail if using different app name with the workspace": { inAppName: "demo", wantedErr: fmt.Errorf("cannot specify app demo because the workspace is already registered with app phonetool"), }, "fail if cannot validate application": { inAppName: "phonetool", inDockerfilePath: "mockDockerfile", inImage: "mockImage", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(nil, errors.New("some error")) }, wantedErr: fmt.Errorf("get application phonetool configuration: some error"), }, "invalid dockerfile directory path": { inAppName: "phonetool", inDockerfilePath: "./hello/Dockerfile", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{}, nil) }, wantedErr: fmt.Errorf("open %s: file does not exist", filepath.FromSlash("hello/Dockerfile")), }, "invalid timeout duration; incorrect format": { inAppName: "phonetool", inTimeout: "30 minutes", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{}, nil) }, wantedErr: fmt.Errorf("timeout value 30 minutes is invalid: %s", errDurationInvalid), }, "invalid timeout duration; subseconds": { inAppName: "phonetool", inTimeout: "30m45.5s", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{}, nil) }, wantedErr: fmt.Errorf("timeout value 30m45.5s is invalid: %s", errDurationBadUnits), }, "invalid timeout duration; milliseconds": { inAppName: "phonetool", inTimeout: "3ms", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{}, nil) }, wantedErr: fmt.Errorf("timeout value 3ms is invalid: %s", errDurationBadUnits), }, "invalid timeout; too short": { inAppName: "phonetool", inTimeout: "0s", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{}, nil) }, wantedErr: errors.New("timeout value 0s is invalid: duration must be 1s or greater"), }, "invalid number of times to retry": { inAppName: "phonetool", inRetries: -3, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{}, nil) }, wantedErr: errors.New("number of retries must be non-negative"), }, "fail if both image and dockerfile are set": { inAppName: "phonetool", inDockerfilePath: "mockDockerfile", inImage: "mockImage", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{}, nil) }, wantedErr: fmt.Errorf("--dockerfile and --image cannot be specified together"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockstore := mocks.NewMockstore(ctrl) mocks := initJobMocks{ mockStore: mockstore, } if tc.setupMocks != nil { tc.setupMocks(mocks) } opts := initJobOpts{ initJobVars: initJobVars{ initWkldVars: initWkldVars{ appName: tc.inAppName, name: tc.inJobName, image: tc.inImage, dockerfilePath: tc.inDockerfilePath, }, timeout: tc.inTimeout, retries: tc.inRetries, }, store: mockstore, fs: &afero.Afero{Fs: afero.NewMemMapFs()}, wsAppName: "phonetool", } if tc.mockFileSystem != nil { tc.mockFileSystem(opts.fs) } // WHEN err := opts.Validate() // THEN if tc.wantedErr != nil { require.EqualError(t, err, tc.wantedErr.Error()) } else { require.NoError(t, err) } }) } } func TestJobInitOpts_Ask(t *testing.T) { const ( mockAppName = "phonetool" wantedJobType = manifestinfo.ScheduledJobType wantedJobName = "cuteness-aggregator" wantedDockerfilePath = "cuteness-aggregator/Dockerfile" wantedImage = "mockImage" wantedCronSchedule = "0 9-17 * * MON-FRI" ) mockError := errors.New("mock error") testCases := map[string]struct { inJobType string inJobName string inImage string inDockerfilePath string inJobSchedule string setupMocks func(mocks initJobMocks) wantedErr error wantedSchedule string }{ "invalid job type": { inJobType: "TestJobType", wantedErr: errors.New(`invalid job type TestJobType: must be one of "Scheduled Job"`), }, "invalid job name": { inJobType: wantedJobType, inJobName: "1234", wantedErr: fmt.Errorf("job name 1234 is invalid: %s", errBasicNameRegexNotMatched), }, "error if fail to get job name": { inJobType: wantedJobType, inJobName: "", inDockerfilePath: wantedDockerfilePath, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockPrompt.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return("", mockError) }, wantedErr: fmt.Errorf("get job name: mock error"), }, "returns an error if job already exists": { inJobType: wantedJobType, inJobName: "", inDockerfilePath: wantedDockerfilePath, setupMocks: func(m initJobMocks) { m.mockPrompt.EXPECT().Get(gomock.Eq("What do you want to name this job?"), gomock.Any(), gomock.Any(), gomock.Any()). Return(wantedJobName, nil) m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(&config.Workload{}, nil) }, wantedErr: fmt.Errorf("job cuteness-aggregator already exists"), }, "returns an error if fail to validate service existence": { inJobType: wantedJobType, inJobName: "", inDockerfilePath: wantedDockerfilePath, setupMocks: func(m initJobMocks) { m.mockPrompt.EXPECT().Get(gomock.Eq("What do you want to name this job?"), gomock.Any(), gomock.Any(), gomock.Any()). Return(wantedJobName, nil) m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, mockError) }, wantedErr: fmt.Errorf("validate if job exists: mock error"), }, "prompt for job name": { inJobType: wantedJobType, inJobName: "", inDockerfilePath: wantedDockerfilePath, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockPrompt.EXPECT().Get(gomock.Eq( "What do you want to name this job?"), gomock.Any(), gomock.Any(), gomock.Any(), ).Return(wantedJobName, nil) m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) }, wantedSchedule: wantedCronSchedule, }, "error if fail to get local manifest": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: wantedDockerfilePath, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, mockError) }, wantedErr: fmt.Errorf("read manifest file for job cuteness-aggregator: mock error"), }, "error if manifest type doesn't match": { inJobType: "Scheduled Job", inJobName: wantedJobName, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return([]byte(` type: Backend Service`), nil) }, wantedErr: fmt.Errorf("manifest file for job cuteness-aggregator exists with a different type Backend Service"), }, "skip asking questions if local manifest file exists": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: wantedDockerfilePath, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return([]byte(`name: cuteness-aggregator type: Scheduled Job`), nil) }, wantedSchedule: wantedCronSchedule, }, "skip selecting Dockerfile if image flag is set": { inJobType: wantedJobType, inJobName: wantedJobName, inImage: "mockImage", inDockerfilePath: "", inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) }, wantedSchedule: wantedCronSchedule, }, "return error if fail to check if docker engine is running": { inJobType: wantedJobType, inJobName: wantedJobName, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockDockerEngine.EXPECT().CheckDockerEngineRunning().Return(errors.New("some error")) }, wantedErr: fmt.Errorf("check if docker engine is running: some error"), }, "skip selecting Dockerfile if docker command is not found": { inJobType: wantedJobType, inJobName: wantedJobName, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockPrompt.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, gomock.Any(), gomock.Any()). Return("mockImage", nil) m.mockDockerEngine.EXPECT().CheckDockerEngineRunning().Return(dockerengine.ErrDockerCommandNotFound) }, wantedSchedule: wantedCronSchedule, }, "skip selecting Dockerfile if docker engine is not responsive": { inJobType: wantedJobType, inJobName: wantedJobName, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockPrompt.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, gomock.Any(), gomock.Any()). Return("mockImage", nil) m.mockDockerEngine.EXPECT().CheckDockerEngineRunning().Return(&dockerengine.ErrDockerDaemonNotResponsive{}) }, wantedSchedule: wantedCronSchedule, }, "returns an error if fail to get image location": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: "", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockPrompt.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, gomock.Any(), gomock.Any()). Return("", mockError) m.mockDockerfileSel.EXPECT().Dockerfile( gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePrompt, wantedJobName)), gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, wantedJobName)), gomock.Eq(wkldInitDockerfileHelpPrompt), gomock.Eq(wkldInitDockerfilePathHelpPrompt), gomock.Any(), ).Return("Use an existing image instead", nil) m.mockDockerEngine.EXPECT().CheckDockerEngineRunning().Return(nil) }, wantedErr: fmt.Errorf("get image location: mock error"), }, "using existing image": { inJobType: wantedJobType, inJobName: wantedJobName, inJobSchedule: wantedCronSchedule, inDockerfilePath: "", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockPrompt.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, gomock.Any(), gomock.Any()). Return("mockImage", nil) m.mockDockerfileSel.EXPECT().Dockerfile( gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePrompt, wantedJobName)), gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, wantedJobName)), gomock.Eq(wkldInitDockerfileHelpPrompt), gomock.Eq(wkldInitDockerfilePathHelpPrompt), gomock.Any(), ).Return("Use an existing image instead", nil) m.mockDockerEngine.EXPECT().CheckDockerEngineRunning().Return(nil) }, wantedSchedule: wantedCronSchedule, }, "prompt for existing dockerfile": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: "", inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockDockerfileSel.EXPECT().Dockerfile( gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePrompt, color.HighlightUserInput(wantedJobName))), gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, color.HighlightUserInput(wantedJobName))), gomock.Any(), gomock.Any(), gomock.Any(), ).Return("cuteness-aggregator/Dockerfile", nil) m.mockDockerEngine.EXPECT().CheckDockerEngineRunning().Return(nil) }, wantedSchedule: wantedCronSchedule, }, "error if fail to get dockerfile": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: "", inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockDockerfileSel.EXPECT().Dockerfile( gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePrompt, color.HighlightUserInput(wantedJobName))), gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, color.HighlightUserInput(wantedJobName))), gomock.Any(), gomock.Any(), gomock.Any(), ).Return("", errors.New("some error")) m.mockDockerEngine.EXPECT().CheckDockerEngineRunning().Return(nil) }, wantedErr: fmt.Errorf("select Dockerfile: some error"), }, "asks for schedule": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: wantedDockerfilePath, inJobSchedule: "", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{}) m.mockScheduleSel.EXPECT().Schedule( gomock.Eq(jobInitSchedulePrompt), gomock.Eq(jobInitScheduleHelp), gomock.Any(), gomock.Any(), ).Return(wantedCronSchedule, nil) }, wantedSchedule: wantedCronSchedule, }, "error getting schedule": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: wantedDockerfilePath, inJobSchedule: "", setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) m.mockScheduleSel.EXPECT().Schedule( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), ).Return("", fmt.Errorf("some error")) }, wantedErr: fmt.Errorf("get schedule: some error"), }, "valid schedule": { inJobType: wantedJobType, inJobName: wantedJobName, inDockerfilePath: wantedDockerfilePath, inJobSchedule: wantedCronSchedule, setupMocks: func(m initJobMocks) { m.mockStore.EXPECT().GetJob(mockAppName, wantedJobName).Return(nil, &config.ErrNoSuchJob{}) m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedJobName).Return(nil, &workspace.ErrFileNotExists{FileName: wantedJobName}) }, wantedSchedule: wantedCronSchedule, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { // GIVEN ctrl := gomock.NewController(t) defer ctrl.Finish() m := initJobMocks{ mockPrompt: mocks.NewMockprompter(ctrl), mockDockerfileSel: mocks.NewMockdockerfileSelector(ctrl), mockScheduleSel: mocks.NewMockscheduleSelector(ctrl), mockDockerEngine: mocks.NewMockdockerEngine(ctrl), mockMftReader: mocks.NewMockmanifestReader(ctrl), mockStore: mocks.NewMockstore(ctrl), } if tc.setupMocks != nil { tc.setupMocks(m) } opts := &initJobOpts{ initJobVars: initJobVars{ initWkldVars: initWkldVars{ wkldType: tc.inJobType, name: tc.inJobName, image: tc.inImage, dockerfilePath: tc.inDockerfilePath, appName: mockAppName, }, schedule: tc.inJobSchedule, }, dockerfileSel: m.mockDockerfileSel, scheduleSelector: m.mockScheduleSel, store: m.mockStore, dockerEngine: m.mockDockerEngine, mftReader: m.mockMftReader, prompt: m.mockPrompt, } // WHEN err := opts.Ask() // THEN if tc.wantedErr != nil { require.EqualError(t, err, tc.wantedErr.Error()) return } require.NoError(t, err) require.Equal(t, wantedJobType, opts.wkldType) require.Equal(t, wantedJobName, opts.name) if opts.dockerfilePath != "" { require.Equal(t, wantedDockerfilePath, opts.dockerfilePath) } if opts.image != "" { require.Equal(t, wantedImage, opts.image) } require.Equal(t, tc.wantedSchedule, opts.schedule) }) } } func TestJobInitOpts_Execute(t *testing.T) { mockEnvironmentManifest := []byte(`name: test type: Environment network: vpc: id: 'vpc-mockid' subnets: private: - id: 'subnet-1' - id: 'subnet-2' - id: 'subnet-3' - id: 'subnet-4'`) second := time.Second zero := 0 testCases := map[string]struct { mockJobInit func(m *mocks.MockjobInitializer) mockDockerfile func(m *mocks.MockdockerfileParser) mockDockerEngine func(m *mocks.MockdockerEngine) mockStore func(m *mocks.Mockstore) mockEnvDescriber func(m *mocks.MockenvDescriber) inApp string inName string inType string inDf string inSchedule string inManifestExists bool wantedErr error wantedManifestPath string }{ "success on typical job props": { inApp: "sample", inName: "mailer", inType: manifestinfo.ScheduledJobType, inDf: "./Dockerfile", inSchedule: "@hourly", wantedManifestPath: "manifest/path", mockDockerfile: func(m *mocks.MockdockerfileParser) { m.EXPECT().GetHealthCheck().Return(&dockerfile.HealthCheck{ Cmd: []string{"mockCommand"}, Interval: second, Timeout: second, StartPeriod: second, Retries: zero, }, nil) }, mockDockerEngine: func(m *mocks.MockdockerEngine) { m.EXPECT().CheckDockerEngineRunning().Return(nil) m.EXPECT().GetPlatform().Return("linux", "amd64", nil) }, mockJobInit: func(m *mocks.MockjobInitializer) { m.EXPECT().Job(&initialize.JobProps{ WorkloadProps: initialize.WorkloadProps{ App: "sample", Name: "mailer", Type: "Scheduled Job", DockerfilePath: "./Dockerfile", Platform: manifest.PlatformArgsOrString{}, }, Schedule: "@hourly", HealthCheck: manifest.ContainerHealthCheck{ Command: []string{"mockCommand"}, Interval: &second, Retries: &zero, Timeout: &second, StartPeriod: &second, }, }).Return("manifest/path", nil) }, mockStore: func(m *mocks.Mockstore) { m.EXPECT().ListEnvironments("sample").Return(nil, nil) }, }, "fail to init job": { mockDockerEngine: func(m *mocks.MockdockerEngine) { m.EXPECT().CheckDockerEngineRunning().Return(nil) m.EXPECT().GetPlatform().Return("linux", "amd64", nil) }, mockStore: func(m *mocks.Mockstore) { m.EXPECT().ListEnvironments("").Return(nil, nil) }, mockJobInit: func(m *mocks.MockjobInitializer) { m.EXPECT().Job(gomock.Any()).Return("", errors.New("some error")) }, wantedErr: errors.New("some error"), }, "doesn't attempt to detect and populate the platform if manifest already exists": { inApp: "sample", inName: "mailer", inType: manifestinfo.ScheduledJobType, inDf: "./Dockerfile", inSchedule: "@hourly", inManifestExists: true, wantedManifestPath: "manifest/path", mockDockerfile: func(m *mocks.MockdockerfileParser) { m.EXPECT().GetHealthCheck().Return(&dockerfile.HealthCheck{ Cmd: []string{"mockCommand"}, Interval: second, Timeout: second, StartPeriod: second, Retries: zero, }, nil) }, mockDockerEngine: func(m *mocks.MockdockerEngine) { m.EXPECT().CheckDockerEngineRunning().Times(0) m.EXPECT().GetPlatform().Times(0) }, mockJobInit: func(m *mocks.MockjobInitializer) { m.EXPECT().Job(&initialize.JobProps{ WorkloadProps: initialize.WorkloadProps{ App: "sample", Name: "mailer", Type: "Scheduled Job", DockerfilePath: "./Dockerfile", Platform: manifest.PlatformArgsOrString{}, }, Schedule: "@hourly", HealthCheck: manifest.ContainerHealthCheck{ Command: []string{"mockCommand"}, Interval: &second, Retries: &zero, Timeout: &second, StartPeriod: &second, }, }).Return("manifest/path", nil) }, mockStore: func(m *mocks.Mockstore) { m.EXPECT().ListEnvironments("sample").Return(nil, nil) }, }, "doesn't complain if docker is unavailable": { inApp: "sample", inName: "mailer", inType: manifestinfo.ScheduledJobType, inDf: "./Dockerfile", inSchedule: "@hourly", wantedManifestPath: "manifest/path", mockDockerfile: func(m *mocks.MockdockerfileParser) { m.EXPECT().GetHealthCheck().Return(&dockerfile.HealthCheck{ Cmd: []string{"mockCommand"}, Interval: second, Timeout: second, StartPeriod: second, Retries: zero, }, nil) }, mockDockerEngine: func(m *mocks.MockdockerEngine) { m.EXPECT().CheckDockerEngineRunning().Return(dockerengine.ErrDockerCommandNotFound) m.EXPECT().GetPlatform().Times(0) }, mockJobInit: func(m *mocks.MockjobInitializer) { m.EXPECT().Job(&initialize.JobProps{ WorkloadProps: initialize.WorkloadProps{ App: "sample", Name: "mailer", Type: "Scheduled Job", DockerfilePath: "./Dockerfile", Platform: manifest.PlatformArgsOrString{}, }, Schedule: "@hourly", HealthCheck: manifest.ContainerHealthCheck{ Command: []string{"mockCommand"}, Interval: &second, Retries: &zero, Timeout: &second, StartPeriod: &second, }, }).Return("manifest/path", nil) }, mockStore: func(m *mocks.Mockstore) { m.EXPECT().ListEnvironments("sample").Return(nil, nil) }, }, "return error if platform detection fails": { mockDockerEngine: func(m *mocks.MockdockerEngine) { m.EXPECT().CheckDockerEngineRunning().Return(nil) m.EXPECT().GetPlatform().Return("", "", errors.New("some error")) }, wantedErr: errors.New("get docker engine platform: some error"), }, "initialize a job in environments with only private subnets": { inApp: "sample", inName: "mailer", inType: manifestinfo.ScheduledJobType, inDf: "./Dockerfile", inSchedule: "@hourly", wantedManifestPath: "manifest/path", mockDockerfile: func(m *mocks.MockdockerfileParser) { m.EXPECT().GetHealthCheck().Return(&dockerfile.HealthCheck{ Cmd: []string{"mockCommand"}, Interval: second, Timeout: second, StartPeriod: second, Retries: zero, }, nil) }, mockDockerEngine: func(m *mocks.MockdockerEngine) { m.EXPECT().CheckDockerEngineRunning().Return(nil) m.EXPECT().GetPlatform().Return("linux", "amd64", nil) }, mockJobInit: func(m *mocks.MockjobInitializer) { m.EXPECT().Job(&initialize.JobProps{ WorkloadProps: initialize.WorkloadProps{ App: "sample", Name: "mailer", Type: "Scheduled Job", DockerfilePath: "./Dockerfile", Platform: manifest.PlatformArgsOrString{}, PrivateOnlyEnvironments: []string{"test"}, }, Schedule: "@hourly", HealthCheck: manifest.ContainerHealthCheck{ Command: []string{"mockCommand"}, Interval: &second, Retries: &zero, Timeout: &second, StartPeriod: &second, }, }).Return("manifest/path", nil) }, mockStore: func(m *mocks.Mockstore) { m.EXPECT().ListEnvironments("sample").Return([]*config.Environment{ { App: "sample", Name: "test", }, }, nil) }, mockEnvDescriber: func(m *mocks.MockenvDescriber) { m.EXPECT().Manifest().Return(mockEnvironmentManifest, nil) }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { // GIVEN ctrl := gomock.NewController(t) defer ctrl.Finish() mockJobInitializer := mocks.NewMockjobInitializer(ctrl) mockDockerfile := mocks.NewMockdockerfileParser(ctrl) mockDockerEngine := mocks.NewMockdockerEngine(ctrl) mockStore := mocks.NewMockstore(ctrl) mockEnvDescriber := mocks.NewMockenvDescriber(ctrl) if tc.mockJobInit != nil { tc.mockJobInit(mockJobInitializer) } if tc.mockDockerfile != nil { tc.mockDockerfile(mockDockerfile) } if tc.mockDockerEngine != nil { tc.mockDockerEngine(mockDockerEngine) } if tc.mockStore != nil { tc.mockStore(mockStore) } if tc.mockEnvDescriber != nil { tc.mockEnvDescriber(mockEnvDescriber) } opts := initJobOpts{ initJobVars: initJobVars{ initWkldVars: initWkldVars{ appName: tc.inApp, name: tc.inName, wkldType: tc.inType, dockerfilePath: tc.inDf, allowAppDowngrade: true, }, schedule: tc.inSchedule, }, init: mockJobInitializer, initParser: func(s string) dockerfileParser { return mockDockerfile }, dockerEngine: mockDockerEngine, manifestExists: tc.inManifestExists, store: mockStore, initEnvDescriber: func(string, string) (envDescriber, error) { return mockEnvDescriber, nil }, } // WHEN err := opts.Execute() // THEN if tc.wantedErr == nil { require.NoError(t, err) require.Equal(t, tc.wantedManifestPath, opts.manifestPath) } else { require.EqualError(t, err, tc.wantedErr.Error()) } }) } } func Test_ValidateSchedule(t *testing.T) { testCases := map[string]struct { inSchedule string wantedErr error }{ "invalid schedule; not cron": { inSchedule: "every 56 minutes", wantedErr: fmt.Errorf("schedule every 56 minutes is invalid: %s", errScheduleInvalid), }, "invalid schedule; cron interval in subseconds": { inSchedule: "@every 75.9s", wantedErr: fmt.Errorf("interval @every 75.9s is invalid: %s", errDurationBadUnits), }, "invalid schedule; cron interval in milliseconds": { inSchedule: "@every 3ms", wantedErr: fmt.Errorf("interval @every 3ms is invalid: %s", errDurationBadUnits), }, "invalid schedule; cron interval too frequent": { inSchedule: "@every 30s", wantedErr: errors.New("interval @every 30s is invalid: duration must be 1m0s or greater"), }, "invalid schedule; cron interval is zero": { inSchedule: "@every 0s", wantedErr: errors.New("interval @every 0s is invalid: duration must be 1m0s or greater"), }, "invalid schedule; cron interval duration improperly formed": { inSchedule: "@every 5min", wantedErr: errors.New("interval @every 5min must include a valid Go duration string (example: @every 1h30m)"), }, "valid schedule; crontab": { inSchedule: "* * * * *", wantedErr: nil, }, "valid schedule; predefined schedule": { inSchedule: "@daily", wantedErr: nil, }, "valid schedule; interval": { inSchedule: "@every 5m", wantedErr: nil, }, "valid schedule; interval with 0 for some units": { inSchedule: "@every 1h0m0s", wantedErr: nil, }, "valid schedule; interval with carryover value for some units": { inSchedule: "@every 0h60m60s", wantedErr: nil, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { // WHEN err := validateSchedule(tc.inSchedule) // THEN if tc.wantedErr != nil { require.EqualError(t, err, tc.wantedErr.Error()) } else { require.NoError(t, err) } }) } }