diff --git a/internal/pkg/cli/flag.go b/internal/pkg/cli/flag.go index 85f4e69e03e..8afff26cb11 100644 --- a/internal/pkg/cli/flag.go +++ b/internal/pkg/cli/flag.go @@ -90,7 +90,7 @@ const ( ) // Short flag names. -// A short flag only exists if the flag is mandatory by the command. +// A short flag only exists if the flag or flag set is mandatory by the command. const ( nameFlagShort = "n" appFlagShort = "a" @@ -100,6 +100,7 @@ const ( jobTypeFlagShort = "t" dockerFileFlagShort = "d" + imageFlagShort = "i" githubURLFlagShort = "u" githubAccessTokenFlagShort = "t" gitBranchFlagShort = "b" @@ -112,6 +113,10 @@ const ( var ( svcTypeFlagDescription = fmt.Sprintf(`Type of service to create. Must be one of: %s`, strings.Join(template.QuoteSliceFunc(manifest.ServiceTypes), ", ")) + imageFlagDescription = fmt.Sprintf(`The location of an existing Docker image. +Mutually exclusive with -%s, --%s`, dockerFileFlagShort, dockerFileFlag) + dockerFileFlagDescription = fmt.Sprintf(`Path to the Dockerfile. +Mutually exclusive with -%s, --%s`, imageFlagShort, imageFlag) storageTypeFlagDescription = fmt.Sprintf(`Type of storage to add. Must be one of: %s`, strings.Join(template.QuoteSliceFunc(storageTypes), ", ")) jobTypeFlagDescription = fmt.Sprintf(`Type of job to create. Must be one of: @@ -139,7 +144,6 @@ const ( yesFlagDescription = "Skips confirmation prompt." jsonFlagDescription = "Optional. Outputs in JSON format." - dockerFileFlagDescription = "Path to the Dockerfile." imageTagFlagDescription = `Optional. The container image tag.` resourceTagsFlagDescription = `Optional. Labels with a key and value separated with commas. Allows you to categorize resources.` @@ -185,7 +189,6 @@ Must be of the format ':'.` countFlagDescription = "Optional. The number of tasks to set up." cpuFlagDescription = "Optional. The number of CPU units to reserve for each task." memoryFlagDescription = "Optional. The amount of memory to reserve in MiB for each task." - imageFlagDescription = "Optional. The image to run instead of building a Dockerfile." taskRoleFlagDescription = "Optional. The ARN of the role for the task to use." executionRoleFlagDescription = "Optional. The ARN of the role that grants the container agent permission to make AWS API calls." envVarsFlagDescription = "Optional. Environment variables specified by key=value separated with commas." diff --git a/internal/pkg/cli/job_init.go b/internal/pkg/cli/job_init.go index 4e220914bb0..da60c229866 100644 --- a/internal/pkg/cli/job_init.go +++ b/internal/pkg/cli/job_init.go @@ -44,6 +44,7 @@ type initJobVars struct { appName string name string dockerfilePath string + image string timeout string retries int schedule string @@ -109,6 +110,9 @@ func (o *initJobOpts) Validate() error { return err } } + if o.dockerfilePath != "" && o.image != "" { + return fmt.Errorf("--%s and --%s cannot be specified together", dockerFileFlag, imageFlag) + } if o.dockerfilePath != "" { if _, err := o.fs.Stat(o.dockerfilePath); err != nil { return err @@ -138,9 +142,15 @@ func (o *initJobOpts) Ask() error { if err := o.askJobName(); err != nil { return err } - if err := o.askDockerfile(); err != nil { + dfSelected, err := o.askDockerfile() + if err != nil { return err } + if !dfSelected { + if err := o.askImage(); err != nil { + return err + } + } if err := o.askSchedule(); err != nil { return err } @@ -209,14 +219,19 @@ func (o *initJobOpts) createManifest() (string, error) { } func (o *initJobOpts) newJobManifest() (*manifest.ScheduledJob, error) { - dfPath, err := relativeDockerfilePath(o.ws, o.dockerfilePath) - if err != nil { - return nil, err + var dfPath string + if o.dockerfilePath != "" { + path, err := relativeDockerfilePath(o.ws, o.dockerfilePath) + if err != nil { + return nil, err + } + dfPath = path } return manifest.NewScheduledJob(&manifest.ScheduledJobProps{ WorkloadProps: &manifest.WorkloadProps{ Name: o.name, Dockerfile: dfPath, + Image: o.image, }, Schedule: o.schedule, Timeout: o.timeout, @@ -251,10 +266,24 @@ func (o *initJobOpts) askJobName() error { return nil } -func (o *initJobOpts) askDockerfile() error { - if o.dockerfilePath != "" { +func (o *initJobOpts) askImage() error { + if o.image != "" { return nil } + image, err := o.prompt.Get(wkldInitImagePrompt, wkldInitImagePromptHelp, nil, + prompt.WithFinalMessage("Image:")) + if err != nil { + return fmt.Errorf("get image location: %w", err) + } + o.image = image + return nil +} + +// isDfSelected indicates if any Dockerfile is in use. +func (o *initJobOpts) askDockerfile() (isDfSelected bool, err error) { + if o.dockerfilePath != "" || o.image != "" { + return true, nil + } df, err := o.sel.Dockerfile( fmt.Sprintf(fmtWkldInitDockerfilePrompt, color.HighlightUserInput(o.name)), fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, color.HighlightUserInput(o.name)), @@ -265,10 +294,13 @@ func (o *initJobOpts) askDockerfile() error { }, ) if err != nil { - return fmt.Errorf("select Dockerfile: %w", err) + return false, fmt.Errorf("select Dockerfile: %w", err) + } + if df == selector.DockerfilePromptUseImage { + return false, nil } o.dockerfilePath = df - return nil + return true, nil } func (o *initJobOpts) askSchedule() error { @@ -339,6 +371,7 @@ func buildJobInitCmd() *cobra.Command { cmd.Flags().StringVarP(&vars.schedule, scheduleFlag, scheduleFlagShort, "", scheduleFlagDescription) cmd.Flags().StringVar(&vars.timeout, timeoutFlag, "", timeoutFlagDescription) cmd.Flags().IntVar(&vars.retries, retriesFlag, 0, retriesFlagDescription) + cmd.Flags().StringVarP(&vars.image, imageFlag, imageFlagShort, "", imageFlagDescription) cmd.Annotations = map[string]string{ "group": group.Develop, diff --git a/internal/pkg/cli/job_init_test.go b/internal/pkg/cli/job_init_test.go index 3f65dc58551..eb2b5b40139 100644 --- a/internal/pkg/cli/job_init_test.go +++ b/internal/pkg/cli/job_init_test.go @@ -23,6 +23,7 @@ func TestJobInitOpts_Validate(t *testing.T) { testCases := map[string]struct { inJobName string inDockerfilePath string + inImage string inTimeout string inRetries int inSchedule string @@ -102,6 +103,11 @@ func TestJobInitOpts_Validate(t *testing.T) { inRetries: -3, wantedErr: errors.New("number of retries must be non-negative"), }, + "fail if both image and dockerfile are set": { + inDockerfilePath: "mockDockerfile", + inImage: "mockImage", + wantedErr: fmt.Errorf("--dockerfile and --image cannot be specified together"), + }, } for name, tc := range testCases { @@ -109,6 +115,7 @@ func TestJobInitOpts_Validate(t *testing.T) { opts := initJobOpts{ initJobVars: initJobVars{ name: tc.inJobName, + image: tc.inImage, dockerfilePath: tc.inDockerfilePath, timeout: tc.inTimeout, retries: tc.inRetries, @@ -137,11 +144,13 @@ func TestJobInitOpts_Ask(t *testing.T) { wantedJobType = manifest.ScheduledJobType wantedJobName = "cuteness-aggregator" wantedDockerfilePath = "cuteness-aggregator/Dockerfile" + wantedImage = "mockImage" wantedCronSchedule = "0 9-17 * * MON-FRI" ) testCases := map[string]struct { inJobType string inJobName string + inImage string inDockerfilePath string inJobSchedule string @@ -185,6 +194,62 @@ func TestJobInitOpts_Ask(t *testing.T) { mockSel: func(m *mocks.MockinitJobSelector) {}, wantedErr: fmt.Errorf("get job name: some error"), }, + "skip selecting Dockerfile if image flag is set": { + inJobType: wantedJobType, + inJobName: wantedJobName, + inImage: "mockImage", + inDockerfilePath: "", + inJobSchedule: wantedCronSchedule, + + mockPrompt: func(m *mocks.Mockprompter) {}, + mockSel: func(m *mocks.MockinitJobSelector) {}, + mockFileSystem: func(mockFS afero.Fs) {}, + wantedErr: nil, + wantedSchedule: wantedCronSchedule, + }, + "returns an error if fail to get image location": { + inJobType: wantedJobType, + inJobName: wantedJobName, + inDockerfilePath: "", + + mockPrompt: func(m *mocks.Mockprompter) { + m.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, nil, gomock.Any()). + Return("", mockError) + }, + mockSel: func(m *mocks.MockinitJobSelector) { + m.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) + }, + mockFileSystem: func(mockFS afero.Fs) {}, + wantedErr: fmt.Errorf("get image location: mock error"), + }, + "using existing image": { + inJobType: wantedJobType, + inJobName: wantedJobName, + inJobSchedule: wantedCronSchedule, + inDockerfilePath: "", + + mockPrompt: func(m *mocks.Mockprompter) { + m.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, nil, gomock.Any()). + Return("mockImage", nil) + }, + mockSel: func(m *mocks.MockinitJobSelector) { + m.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) + }, + mockFileSystem: func(mockFS afero.Fs) {}, + wantedSchedule: wantedCronSchedule, + }, "prompt for existing dockerfile": { inJobType: wantedJobType, inJobName: wantedJobName, @@ -274,6 +339,7 @@ func TestJobInitOpts_Ask(t *testing.T) { initJobVars: initJobVars{ jobType: tc.inJobType, name: tc.inJobName, + image: tc.inImage, dockerfilePath: tc.inDockerfilePath, schedule: tc.inJobSchedule, }, @@ -297,9 +363,13 @@ func TestJobInitOpts_Ask(t *testing.T) { require.NoError(t, err) require.Equal(t, wantedJobType, opts.jobType) require.Equal(t, wantedJobName, opts.name) - require.Equal(t, wantedDockerfilePath, opts.dockerfilePath) + 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) - }) } } @@ -309,6 +379,7 @@ func TestJobInitOpts_Execute(t *testing.T) { inJobType string inJobName string inDockerfilePath string + inImage string inAppName string mockWriter func(m *mocks.MockjobDirManifestWriter) mockstore func(m *mocks.Mockstore) @@ -354,6 +425,44 @@ func TestJobInitOpts_Execute(t *testing.T) { m.EXPECT().Stop(log.Ssuccessf(fmtAddJobToAppComplete, "resizer")) }, }, + "using existing image": { + inJobType: manifest.ScheduledJobType, + inAppName: "app", + inJobName: "resizer", + inImage: "mockImage", + + mockWriter: func(m *mocks.MockjobDirManifestWriter) { + m.EXPECT().WriteJobManifest(gomock.Any(), "resizer").Do(func(m *manifest.ScheduledJob, _ string) { + require.Equal(t, *m.Workload.Type, manifest.ScheduledJobType) + require.Equal(t, *m.ImageConfig.Location, "mockImage") + }).Return("/resizer/manifest.yml", nil) + }, + mockstore: func(m *mocks.Mockstore) { + m.EXPECT().CreateJob(gomock.Any()). + Do(func(app *config.Workload) { + require.Equal(t, &config.Workload{ + Name: "resizer", + App: "app", + Type: manifest.ScheduledJobType, + }, app) + }). + Return(nil) + m.EXPECT().GetApplication("app").Return(&config.Application{ + Name: "app", + AccountID: "1234", + }, nil) + }, + mockappDeployer: func(m *mocks.MockappDeployer) { + m.EXPECT().AddJobToApp(&config.Application{ + Name: "app", + AccountID: "1234", + }, "resizer") + }, + mockProg: func(m *mocks.Mockprogress) { + m.EXPECT().Start(fmt.Sprintf(fmtAddJobToAppStart, "resizer")) + m.EXPECT().Stop(log.Ssuccessf(fmtAddJobToAppComplete, "resizer")) + }, + }, "write manifest error": { inJobType: manifest.ScheduledJobType, inAppName: "app", @@ -461,6 +570,7 @@ func TestJobInitOpts_Execute(t *testing.T) { appName: tc.inAppName, name: tc.inJobName, dockerfilePath: tc.inDockerfilePath, + image: tc.inImage, jobType: tc.inJobType, }, ws: mockWriter, diff --git a/internal/pkg/cli/svc_init.go b/internal/pkg/cli/svc_init.go index 2d4e9507c6c..320f319679a 100644 --- a/internal/pkg/cli/svc_init.go +++ b/internal/pkg/cli/svc_init.go @@ -51,6 +51,10 @@ const ( fmtAddSvcToAppStart = "Creating ECR repositories for service %s." fmtAddSvcToAppFailed = "Failed to create ECR repositories for service %s.\n" fmtAddSvcToAppComplete = "Created ECR repositories for service %s.\n" + + wkldInitImagePrompt = `What's the location of the image to use?` + wkldInitImagePromptHelp = `The name of an existing Docker image. Images in the Docker Hub registry are available by default. +Other repositories are specified with either repository-url/image:tag or repository-url/image@digest` ) const ( @@ -63,6 +67,7 @@ type initSvcVars struct { serviceType string name string dockerfilePath string + image string port uint16 } @@ -136,6 +141,9 @@ func (o *initSvcOpts) Validate() error { return err } } + if o.dockerfilePath != "" && o.image != "" { + return fmt.Errorf("--%s and --%s cannot be specified together", dockerFileFlag, imageFlag) + } if o.dockerfilePath != "" { if _, err := o.fs.Stat(o.dockerfilePath); err != nil { return err @@ -157,9 +165,15 @@ func (o *initSvcOpts) Ask() error { if err := o.askSvcName(); err != nil { return err } - if err := o.askDockerfile(); err != nil { + dfSelected, err := o.askDockerfile() + if err != nil { return err } + if !dfSelected { + if err := o.askImage(); err != nil { + return err + } + } if err := o.askSvcPort(); err != nil { return err } @@ -229,25 +243,30 @@ func (o *initSvcOpts) createManifest() (string, error) { } func (o *initSvcOpts) newManifest() (encoding.BinaryMarshaler, error) { + var dfPath string + if o.dockerfilePath != "" { + path, err := relativeDockerfilePath(o.ws, o.dockerfilePath) + if err != nil { + return nil, err + } + dfPath = path + } switch o.serviceType { case manifest.LoadBalancedWebServiceType: - return o.newLoadBalancedWebServiceManifest() + return o.newLoadBalancedWebServiceManifest(dfPath) case manifest.BackendServiceType: - return o.newBackendServiceManifest() + return o.newBackendServiceManifest(dfPath) default: return nil, fmt.Errorf("service type %s doesn't have a manifest", o.serviceType) } } -func (o *initSvcOpts) newLoadBalancedWebServiceManifest() (*manifest.LoadBalancedWebService, error) { - dfPath, err := relativeDockerfilePath(o.ws, o.dockerfilePath) - if err != nil { - return nil, err - } +func (o *initSvcOpts) newLoadBalancedWebServiceManifest(dockerfilePath string) (*manifest.LoadBalancedWebService, error) { props := &manifest.LoadBalancedWebServiceProps{ WorkloadProps: &manifest.WorkloadProps{ Name: o.name, - Dockerfile: dfPath, + Dockerfile: dockerfilePath, + Image: o.image, }, Port: o.port, Path: "/", @@ -267,11 +286,7 @@ func (o *initSvcOpts) newLoadBalancedWebServiceManifest() (*manifest.LoadBalance return manifest.NewLoadBalancedWebService(props), nil } -func (o *initSvcOpts) newBackendServiceManifest() (*manifest.BackendService, error) { - dfPath, err := relativeDockerfilePath(o.ws, o.dockerfilePath) - if err != nil { - return nil, err - } +func (o *initSvcOpts) newBackendServiceManifest(dockerfilePath string) (*manifest.BackendService, error) { hc, err := o.parseHealthCheck() if err != nil { return nil, err @@ -279,7 +294,8 @@ func (o *initSvcOpts) newBackendServiceManifest() (*manifest.BackendService, err return manifest.NewBackendService(manifest.BackendServiceProps{ WorkloadProps: manifest.WorkloadProps{ Name: o.name, - Dockerfile: dfPath, + Dockerfile: dockerfilePath, + Image: o.image, }, Port: o.port, HealthCheck: hc, @@ -321,11 +337,24 @@ func (o *initSvcOpts) askSvcName() error { return nil } -// askDockerfile prompts for the Dockerfile by looking at sub-directories with a Dockerfile. -func (o *initSvcOpts) askDockerfile() error { - if o.dockerfilePath != "" { +func (o *initSvcOpts) askImage() error { + if o.image != "" { return nil } + image, err := o.prompt.Get(wkldInitImagePrompt, wkldInitImagePromptHelp, nil, + prompt.WithFinalMessage("Image:")) + if err != nil { + return fmt.Errorf("get image location: %w", err) + } + o.image = image + return nil +} + +// isDfSelected indicates if any Dockerfile is in use. +func (o *initSvcOpts) askDockerfile() (isDfSelected bool, err error) { + if o.dockerfilePath != "" || o.image != "" { + return true, nil + } df, err := o.sel.Dockerfile( fmt.Sprintf(fmtWkldInitDockerfilePrompt, color.HighlightUserInput(o.name)), fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, color.HighlightUserInput(o.name)), @@ -336,10 +365,13 @@ func (o *initSvcOpts) askDockerfile() error { }, ) if err != nil { - return err + return false, fmt.Errorf("select Dockerfile: %w", err) + } + if df == selector.DockerfilePromptUseImage { + return false, nil } o.dockerfilePath = df - return nil + return true, nil } func (o *initSvcOpts) askSvcPort() error { @@ -348,22 +380,23 @@ func (o *initSvcOpts) askSvcPort() error { return nil } - o.setupParser(o) - ports, err := o.df.GetExposedPorts() - // Ignore any errors in dockerfile parsing--we'll use the default instead. - if err != nil { - log.Debugln(err.Error()) - } - var defaultPort string - switch len(ports) { - case 0: - // There were no ports detected, keep the default port prompt. - defaultPort = defaultSvcPortString - case 1: - o.port = ports[0] - return nil - default: - defaultPort = strconv.Itoa(int(ports[0])) + defaultPort := defaultSvcPortString + if o.dockerfilePath != "" { + o.setupParser(o) + ports, err := o.df.GetExposedPorts() + // Ignore any errors in dockerfile parsing--we'll use the default instead. + if err != nil { + log.Debugln(err.Error()) + } + switch len(ports) { + case 0: + // There were no ports detected, keep the default port prompt. + case 1: + o.port = ports[0] + return nil + default: + defaultPort = strconv.Itoa(int(ports[0])) + } } port, err := o.prompt.Get( @@ -388,6 +421,9 @@ func (o *initSvcOpts) askSvcPort() error { } func (o *initSvcOpts) parseHealthCheck() (*manifest.ContainerHealthCheck, error) { + if o.dockerfilePath == "" { + return nil, nil + } o.setupParser(o) hc, err := o.df.GetHealthCheck() if err != nil { @@ -455,6 +491,8 @@ This command is also run as part of "copilot init".`, cmd.Flags().StringVarP(&vars.name, nameFlag, nameFlagShort, "", svcFlagDescription) cmd.Flags().StringVarP(&vars.serviceType, svcTypeFlag, svcTypeFlagShort, "", svcTypeFlagDescription) cmd.Flags().StringVarP(&vars.dockerfilePath, dockerFileFlag, dockerFileFlagShort, "", dockerFileFlagDescription) + cmd.Flags().StringVarP(&vars.image, imageFlag, imageFlagShort, "", imageFlagDescription) + cmd.Flags().Uint16Var(&vars.port, svcPortFlag, 0, svcPortFlagDescription) // Bucket flags by service type. @@ -462,6 +500,7 @@ This command is also run as part of "copilot init".`, requiredFlags.AddFlag(cmd.Flags().Lookup(nameFlag)) requiredFlags.AddFlag(cmd.Flags().Lookup(svcTypeFlag)) requiredFlags.AddFlag(cmd.Flags().Lookup(dockerFileFlag)) + requiredFlags.AddFlag(cmd.Flags().Lookup(imageFlag)) lbWebSvcFlags := pflag.NewFlagSet(manifest.LoadBalancedWebServiceType, pflag.ContinueOnError) lbWebSvcFlags.AddFlag(cmd.Flags().Lookup(svcPortFlag)) diff --git a/internal/pkg/cli/svc_init_test.go b/internal/pkg/cli/svc_init_test.go index 93b43bed086..71a2a322b9a 100644 --- a/internal/pkg/cli/svc_init_test.go +++ b/internal/pkg/cli/svc_init_test.go @@ -25,6 +25,7 @@ func TestSvcInitOpts_Validate(t *testing.T) { inSvcType string inSvcName string inDockerfilePath string + inImage string inAppName string inSvcPort uint16 @@ -41,6 +42,12 @@ func TestSvcInitOpts_Validate(t *testing.T) { inSvcName: "1234", wantedErr: fmt.Errorf("service name 1234 is invalid: %s", errValueBadFormat), }, + "fail if both image and dockerfile are set": { + inAppName: "phonetool", + inDockerfilePath: "mockDockerfile", + inImage: "mockImage", + wantedErr: fmt.Errorf("--dockerfile and --image cannot be specified together"), + }, "invalid dockerfile directory path": { inAppName: "phonetool", inDockerfilePath: "./hello/Dockerfile", @@ -72,6 +79,7 @@ func TestSvcInitOpts_Validate(t *testing.T) { name: tc.inSvcName, dockerfilePath: tc.inDockerfilePath, port: tc.inSvcPort, + image: tc.inImage, appName: tc.inAppName, }, fs: &afero.Afero{Fs: afero.NewMemMapFs()}, @@ -98,11 +106,13 @@ func TestSvcInitOpts_Ask(t *testing.T) { wantedSvcName = "frontend" wantedDockerfilePath = "frontend/Dockerfile" wantedSvcPort = 80 + wantedImage = "mockImage" ) testCases := map[string]struct { inSvcType string inSvcName string inDockerfilePath string + inImage string inSvcPort uint16 mockPrompt func(m *mocks.Mockprompter) @@ -167,6 +177,62 @@ func TestSvcInitOpts_Ask(t *testing.T) { mockSel: func(m *mocks.MockdockerfileSelector) {}, wantedErr: fmt.Errorf("get service name: some error"), }, + "skip selecting Dockerfile if image flag is set": { + inSvcType: wantedSvcType, + inSvcName: wantedSvcName, + inSvcPort: wantedSvcPort, + inImage: "mockImage", + inDockerfilePath: "", + + mockPrompt: func(m *mocks.Mockprompter) {}, + mockSel: func(m *mocks.MockdockerfileSelector) {}, + mockDockerfile: func(m *mocks.MockdockerfileParser) {}, + wantedErr: nil, + }, + "returns an error if fail to get image location": { + inSvcType: wantedSvcType, + inSvcName: wantedSvcName, + inSvcPort: wantedSvcPort, + inDockerfilePath: "", + + mockPrompt: func(m *mocks.Mockprompter) { + m.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, nil, gomock.Any()). + Return("", mockError) + }, + mockSel: func(m *mocks.MockdockerfileSelector) { + m.EXPECT().Dockerfile( + gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePrompt, wantedSvcName)), + gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, wantedSvcName)), + gomock.Eq(wkldInitDockerfileHelpPrompt), + gomock.Eq(wkldInitDockerfilePathHelpPrompt), + gomock.Any(), + ).Return("Use an existing image instead", nil) + }, + mockDockerfile: func(m *mocks.MockdockerfileParser) {}, + wantedErr: fmt.Errorf("get image location: mock error"), + }, + "using existing image": { + inSvcType: wantedSvcType, + inSvcName: wantedSvcName, + inDockerfilePath: "", + + mockPrompt: func(m *mocks.Mockprompter) { + m.EXPECT().Get(wkldInitImagePrompt, wkldInitImagePromptHelp, nil, gomock.Any()). + Return("mockImage", nil) + m.EXPECT().Get(gomock.Eq(fmt.Sprintf(svcInitSvcPortPrompt, "port")), gomock.Any(), gomock.Any(), gomock.Any()). + Return(defaultSvcPortString, nil) + }, + mockSel: func(m *mocks.MockdockerfileSelector) { + m.EXPECT().Dockerfile( + gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePrompt, wantedSvcName)), + gomock.Eq(fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, wantedSvcName)), + gomock.Eq(wkldInitDockerfileHelpPrompt), + gomock.Eq(wkldInitDockerfilePathHelpPrompt), + gomock.Any(), + ).Return("Use an existing image instead", nil) + }, + mockDockerfile: func(m *mocks.MockdockerfileParser) {}, + }, "select Dockerfile": { inSvcType: wantedSvcType, inSvcName: wantedSvcName, @@ -199,7 +265,7 @@ func TestSvcInitOpts_Ask(t *testing.T) { }, mockPrompt: func(m *mocks.Mockprompter) {}, mockDockerfile: func(m *mocks.MockdockerfileParser) {}, - wantedErr: fmt.Errorf("some error"), + wantedErr: fmt.Errorf("select Dockerfile: some error"), }, "asks for port if not specified": { inSvcType: wantedSvcType, @@ -289,6 +355,7 @@ func TestSvcInitOpts_Ask(t *testing.T) { serviceType: tc.inSvcType, name: tc.inSvcName, port: tc.inSvcPort, + image: tc.inImage, dockerfilePath: tc.inDockerfilePath, }, fs: &afero.Afero{Fs: afero.NewMemMapFs()}, @@ -311,7 +378,12 @@ func TestSvcInitOpts_Ask(t *testing.T) { require.NoError(t, err) require.Equal(t, wantedSvcType, opts.serviceType) require.Equal(t, wantedSvcName, opts.name) - require.Equal(t, wantedDockerfilePath, opts.dockerfilePath) + if opts.dockerfilePath != "" { + require.Equal(t, wantedDockerfilePath, opts.dockerfilePath) + } + if opts.image != "" { + require.Equal(t, wantedImage, opts.image) + } } }) } @@ -330,6 +402,7 @@ func TestAppInitOpts_Execute(t *testing.T) { inSvcName string inDockerfilePath string inAppName string + inImage string mockWriter func(m *mocks.MocksvcDirManifestWriter) mockstore func(m *mocks.Mockstore) mockappDeployer func(m *mocks.MockappDeployer) @@ -460,6 +533,49 @@ func TestAppInitOpts_Execute(t *testing.T) { }, wantedErr: fmt.Errorf("saving service frontend: oops"), }, + "using existing image": { + inSvcType: manifest.BackendServiceType, + inAppName: "app", + inSvcName: "backend", + inImage: "mockImage", + inSvcPort: 80, + + mockWriter: func(m *mocks.MocksvcDirManifestWriter) { + m.EXPECT().WriteServiceManifest(gomock.Any(), "backend"). + Do(func(m *manifest.BackendService, _ string) { + require.Equal(t, *m.Workload.Type, manifest.BackendServiceType) + require.Equal(t, *m.ImageConfig.Location, "mockImage") + require.Nil(t, m.ImageConfig.HealthCheck) + }).Return("/backend/manifest.yml", nil) + }, + mockstore: func(m *mocks.Mockstore) { + m.EXPECT().CreateService(gomock.Any()). + Do(func(app *config.Workload) { + require.Equal(t, &config.Workload{ + Name: "backend", + App: "app", + Type: manifest.BackendServiceType, + }, app) + }). + Return(nil) + + m.EXPECT().GetApplication("app").Return(&config.Application{ + Name: "app", + AccountID: "1234", + }, nil) + }, + mockappDeployer: func(m *mocks.MockappDeployer) { + m.EXPECT().AddServiceToApp(&config.Application{ + Name: "app", + AccountID: "1234", + }, "backend") + }, + mockProg: func(m *mocks.Mockprogress) { + m.EXPECT().Start(fmt.Sprintf(fmtAddSvcToAppStart, "backend")) + m.EXPECT().Stop(log.Ssuccessf(fmtAddSvcToAppComplete, "backend")) + }, + mockDf: func(m *mocks.MockdockerfileParser) {}, + }, "no healthcheck options": { inSvcType: manifest.BackendServiceType, inAppName: "app", @@ -595,6 +711,7 @@ func TestAppInitOpts_Execute(t *testing.T) { name: tc.inSvcName, port: tc.inSvcPort, dockerfilePath: tc.inDockerfilePath, + image: tc.inImage, appName: tc.inAppName, }, setupParser: func(o *initSvcOpts) {}, @@ -624,8 +741,6 @@ func TestAppInitOpts_createLoadBalancedAppManifest(t *testing.T) { inSvcName string inDockerfilePath string inAppName string - setupMocks func(controller *gomock.Controller) - mockWriter func(m *mocks.MocksvcDirManifestWriter) mockstore func(m *mocks.Mockstore) wantedErr error @@ -637,9 +752,6 @@ func TestAppInitOpts_createLoadBalancedAppManifest(t *testing.T) { inSvcPort: 80, inDockerfilePath: "/Dockerfile", - mockWriter: func(m *mocks.MocksvcDirManifestWriter) { - m.EXPECT().CopilotDirPath().Return("/copilot", nil) - }, mockstore: func(m *mocks.Mockstore) { m.EXPECT().ListServices("app").Return([]*config.Workload{}, nil) }, @@ -652,9 +764,6 @@ func TestAppInitOpts_createLoadBalancedAppManifest(t *testing.T) { inSvcPort: 80, inDockerfilePath: "/Dockerfile", - mockWriter: func(m *mocks.MocksvcDirManifestWriter) { - m.EXPECT().CopilotDirPath().Return("/copilot", nil) - }, mockstore: func(m *mocks.Mockstore) { m.EXPECT().ListServices("app").Return([]*config.Workload{ { @@ -672,9 +781,6 @@ func TestAppInitOpts_createLoadBalancedAppManifest(t *testing.T) { inSvcPort: 80, inDockerfilePath: "/Dockerfile", - mockWriter: func(m *mocks.MocksvcDirManifestWriter) { - m.EXPECT().CopilotDirPath().Return("/copilot", nil) - }, mockstore: func(m *mocks.Mockstore) { m.EXPECT().ListServices("app").Return([]*config.Workload{ { @@ -692,9 +798,6 @@ func TestAppInitOpts_createLoadBalancedAppManifest(t *testing.T) { inSvcPort: 80, inDockerfilePath: "/Dockerfile", - mockWriter: func(m *mocks.MocksvcDirManifestWriter) { - m.EXPECT().CopilotDirPath().Return("/copilot", nil) - }, mockstore: func(m *mocks.Mockstore) { m.EXPECT().ListServices("app").Return([]*config.Workload{ { @@ -714,28 +817,22 @@ func TestAppInitOpts_createLoadBalancedAppManifest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockWriter := mocks.NewMocksvcDirManifestWriter(ctrl) mockstore := mocks.NewMockstore(ctrl) - if tc.mockWriter != nil { - tc.mockWriter(mockWriter) - } if tc.mockstore != nil { tc.mockstore(mockstore) } opts := initSvcOpts{ initSvcVars: initSvcVars{ - serviceType: manifest.LoadBalancedWebServiceType, - name: tc.inSvcName, - port: tc.inSvcPort, - dockerfilePath: tc.inDockerfilePath, - appName: tc.inAppName, + serviceType: manifest.LoadBalancedWebServiceType, + name: tc.inSvcName, + port: tc.inSvcPort, + appName: tc.inAppName, }, - ws: mockWriter, store: mockstore, } // WHEN - manifest, err := opts.newLoadBalancedWebServiceManifest() + manifest, err := opts.newLoadBalancedWebServiceManifest(tc.inDockerfilePath) // THEN if tc.wantedErr == nil { diff --git a/internal/pkg/cli/task_run.go b/internal/pkg/cli/task_run.go index 7ca733e1c54..dc23e3703dc 100644 --- a/internal/pkg/cli/task_run.go +++ b/internal/pkg/cli/task_run.go @@ -627,7 +627,7 @@ Run a task with a command. cmd.Flags().StringVarP(&vars.groupName, taskGroupNameFlag, nameFlagShort, "", taskGroupFlagDescription) - cmd.Flags().StringVar(&vars.image, imageFlag, "", imageFlagDescription) + cmd.Flags().StringVarP(&vars.image, imageFlag, imageFlagShort, "", imageFlagDescription) cmd.Flags().StringVar(&vars.dockerfilePath, dockerFileFlag, defaultDockerfilePath, dockerFileFlagDescription) cmd.Flags().StringVar(&vars.imageTag, imageTagFlag, "", taskImageTagFlagDescription) diff --git a/internal/pkg/manifest/backend_svc.go b/internal/pkg/manifest/backend_svc.go index 55cfaa9aa23..663b4609c3e 100644 --- a/internal/pkg/manifest/backend_svc.go +++ b/internal/pkg/manifest/backend_svc.go @@ -76,7 +76,8 @@ func NewBackendService(props BackendServiceProps) *BackendService { } // Apply overrides. svc.Name = aws.String(props.Name) - svc.BackendServiceConfig.ImageConfig.Build.BuildArgs.Dockerfile = aws.String(props.Dockerfile) + svc.BackendServiceConfig.ImageConfig.Image.Location = stringP(props.Image) + svc.BackendServiceConfig.ImageConfig.Build.BuildArgs.Dockerfile = stringP(props.Dockerfile) svc.BackendServiceConfig.ImageConfig.Port = aws.Uint16(props.Port) svc.BackendServiceConfig.ImageConfig.HealthCheck = healthCheck svc.parser = template.New() diff --git a/internal/pkg/manifest/backend_svc_test.go b/internal/pkg/manifest/backend_svc_test.go index 17ec956c46a..bbebc977b8b 100644 --- a/internal/pkg/manifest/backend_svc_test.go +++ b/internal/pkg/manifest/backend_svc_test.go @@ -59,8 +59,8 @@ func TestNewBackendSvc(t *testing.T) { "with custom healthcheck command": { inProps: BackendServiceProps{ WorkloadProps: WorkloadProps{ - Name: "subscribers", - Dockerfile: "./subscribers/Dockerfile", + Name: "subscribers", + Image: "mockImage", }, HealthCheck: &ContainerHealthCheck{ Command: []string{"CMD", "curl -f http://localhost:8080 || exit 1"}, @@ -76,11 +76,7 @@ func TestNewBackendSvc(t *testing.T) { ImageConfig: imageWithPortAndHealthcheck{ ServiceImageWithPort: ServiceImageWithPort{ Image: Image{ - Build: BuildArgsOrString{ - BuildArgs: DockerBuildArgs{ - Dockerfile: aws.String("./subscribers/Dockerfile"), - }, - }, + Location: aws.String("mockImage"), }, Port: aws.Uint16(8080), }, @@ -138,8 +134,8 @@ func TestBackendSvc_MarshalBinary(t *testing.T) { "with custom healthcheck command": { inProps: BackendServiceProps{ WorkloadProps: WorkloadProps{ - Name: "subscribers", - Dockerfile: "./subscribers/Dockerfile", + Name: "subscribers", + Image: "flask-sample", }, HealthCheck: &ContainerHealthCheck{ Command: []string{"CMD-SHELL", "curl -f http://localhost:8080 || exit 1"}, diff --git a/internal/pkg/manifest/job.go b/internal/pkg/manifest/job.go index 9a00f44ce0b..0a0229dc871 100644 --- a/internal/pkg/manifest/job.go +++ b/internal/pkg/manifest/job.go @@ -90,7 +90,8 @@ func NewScheduledJob(props *ScheduledJobProps) *ScheduledJob { job := newDefaultScheduledJob() // Apply overrides. job.Name = aws.String(props.Name) - job.ScheduledJobConfig.ImageConfig.Build.BuildArgs.Dockerfile = aws.String(props.Dockerfile) + job.ScheduledJobConfig.ImageConfig.Build.BuildArgs.Dockerfile = stringP(props.Dockerfile) + job.ScheduledJobConfig.ImageConfig.Location = stringP(props.Image) job.Schedule = props.Schedule job.Retries = props.Retries job.Timeout = props.Timeout diff --git a/internal/pkg/manifest/job_test.go b/internal/pkg/manifest/job_test.go index 8fd5f5084f1..efeef0c6cbc 100644 --- a/internal/pkg/manifest/job_test.go +++ b/internal/pkg/manifest/job_test.go @@ -21,8 +21,8 @@ func TestScheduledJob_MarshalBinary(t *testing.T) { "without timeout or retries": { inProps: ScheduledJobProps{ WorkloadProps: &WorkloadProps{ - Name: "cuteness-aggregator", - Dockerfile: "./cuteness-aggregator/Dockerfile", + Name: "cuteness-aggregator", + Image: "copilot/cuteness-aggregator", }, Schedule: "@weekly", }, diff --git a/internal/pkg/manifest/lb_web_svc.go b/internal/pkg/manifest/lb_web_svc.go index 1af312be182..cd4381dee2e 100644 --- a/internal/pkg/manifest/lb_web_svc.go +++ b/internal/pkg/manifest/lb_web_svc.go @@ -68,7 +68,8 @@ func NewLoadBalancedWebService(props *LoadBalancedWebServiceProps) *LoadBalanced svc := newDefaultLoadBalancedWebService() // Apply overrides. svc.Name = aws.String(props.Name) - svc.LoadBalancedWebServiceConfig.ImageConfig.Build.BuildArgs.Dockerfile = aws.String(props.Dockerfile) + svc.LoadBalancedWebServiceConfig.ImageConfig.Image.Location = stringP(props.Image) + svc.LoadBalancedWebServiceConfig.ImageConfig.Build.BuildArgs.Dockerfile = stringP(props.Dockerfile) svc.LoadBalancedWebServiceConfig.ImageConfig.Port = aws.Uint16(props.Port) svc.RoutingRule.Path = aws.String(props.Path) svc.parser = template.New() diff --git a/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml b/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml index 24901128f81..15ca294940d 100644 --- a/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml +++ b/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml @@ -9,8 +9,8 @@ name: subscribers type: Backend Service image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args - build: ./subscribers/Dockerfile + # The name of the Docker image. + location: flask-sample # Port exposed through your container to route traffic to it. port: 8080 healthcheck: diff --git a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml index 373e72687fc..9a9d874d1dc 100644 --- a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml +++ b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml @@ -9,7 +9,7 @@ name: subscribers type: Backend Service image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args + # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args. build: ./subscribers/Dockerfile # Port exposed through your container to route traffic to it. port: 8080 diff --git a/internal/pkg/manifest/testdata/scheduled-job-fully-specified.yml b/internal/pkg/manifest/testdata/scheduled-job-fully-specified.yml index f2a7d71e438..9d0f5fd9af0 100644 --- a/internal/pkg/manifest/testdata/scheduled-job-fully-specified.yml +++ b/internal/pkg/manifest/testdata/scheduled-job-fully-specified.yml @@ -9,7 +9,7 @@ name: cuteness-aggregator type: Scheduled Job image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args + # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args. build: ./cuteness-aggregator/Dockerfile # Number of CPU units for the task. diff --git a/internal/pkg/manifest/testdata/scheduled-job-no-retries.yml b/internal/pkg/manifest/testdata/scheduled-job-no-retries.yml index 41f7236db5f..28ec0bf709a 100644 --- a/internal/pkg/manifest/testdata/scheduled-job-no-retries.yml +++ b/internal/pkg/manifest/testdata/scheduled-job-no-retries.yml @@ -9,7 +9,7 @@ name: cuteness-aggregator type: Scheduled Job image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args + # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args. build: ./cuteness-aggregator/Dockerfile # Number of CPU units for the task. diff --git a/internal/pkg/manifest/testdata/scheduled-job-no-timeout-or-retries.yml b/internal/pkg/manifest/testdata/scheduled-job-no-timeout-or-retries.yml index 359faf2fe69..87c4cefa8d9 100644 --- a/internal/pkg/manifest/testdata/scheduled-job-no-timeout-or-retries.yml +++ b/internal/pkg/manifest/testdata/scheduled-job-no-timeout-or-retries.yml @@ -9,8 +9,8 @@ name: cuteness-aggregator type: Scheduled Job image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args - build: ./cuteness-aggregator/Dockerfile + # The name of the Docker image. + location: copilot/cuteness-aggregator # Number of CPU units for the task. cpu: 256 diff --git a/internal/pkg/manifest/testdata/scheduled-job-no-timeout.yml b/internal/pkg/manifest/testdata/scheduled-job-no-timeout.yml index d4c585bd97e..04413913597 100644 --- a/internal/pkg/manifest/testdata/scheduled-job-no-timeout.yml +++ b/internal/pkg/manifest/testdata/scheduled-job-no-timeout.yml @@ -9,7 +9,7 @@ name: cuteness-aggregator type: Scheduled Job image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args + # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args. build: ./cuteness-aggregator/Dockerfile # Number of CPU units for the task. diff --git a/internal/pkg/manifest/workload.go b/internal/pkg/manifest/workload.go index 451d120d049..eb2b4013c09 100644 --- a/internal/pkg/manifest/workload.go +++ b/internal/pkg/manifest/workload.go @@ -263,6 +263,7 @@ type TaskConfig struct { type WorkloadProps struct { Name string Dockerfile string + Image string } // UnmarshalWorkload deserializes the YAML input stream into a workload manifest object. @@ -304,8 +305,9 @@ func UnmarshalWorkload(in []byte) (interface{}, error) { } func requiresBuild(image Image) (bool, error) { - hasBuild, hasURL := image.Build.isEmpty(), image.Location == nil - if hasBuild == hasURL { + noBuild, noURL := image.Build.isEmpty(), image.Location == nil + // Error if both of them are specified or neither is specified. + if noBuild == noURL { return false, fmt.Errorf(`either "image.build" or "image.location" needs to be specified in the manifest`) } if image.Location == nil { @@ -328,3 +330,10 @@ func dockerfileBuildRequired(workloadType string, svc interface{}) (bool, error) } return required, nil } + +func stringP(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/internal/pkg/term/selector/selector.go b/internal/pkg/term/selector/selector.go index 0fe530f5140..9e9995a0d61 100644 --- a/internal/pkg/term/selector/selector.go +++ b/internal/pkg/term/selector/selector.go @@ -31,21 +31,10 @@ const ( yearly = "Yearly" ) -var scheduleTypes = []string{ - rate, - fixedSchedule, -} - -var presetSchedules = []string{ - custom, - hourly, - daily, - weekly, - monthly, - yearly, -} +const ( + // DockerfilePromptUseImage is the option for using existing image instead of Dockerfile. + DockerfilePromptUseImage = "Use an existing image instead" -var ( ratePrompt = "How long would you like to wait between executions?" rateHelp = `You can specify the time as a duration string. (For example, 2m, 1h30m, 24h)` @@ -62,6 +51,20 @@ For example: 0 17 ? * MON-FRI (5 pm on weekdays) (Y)es will continue execution. (N)o will allow you to input a different schedule.` ) +var scheduleTypes = []string{ + rate, + fixedSchedule, +} + +var presetSchedules = []string{ + custom, + hourly, + daily, + weekly, + monthly, + yearly, +} + // Prompter wraps the methods to ask for inputs from the terminal. type Prompter interface { Get(message, help string, validator prompt.ValidatorFunc, promptOpts ...prompt.Option) (string, error) @@ -436,6 +439,7 @@ func (s *WorkspaceSelect) Dockerfile(selPrompt, notFoundPrompt, selHelp, notFoun // If Dockerfiles are found in the current directory or subdirectory one level down, ask the user to select one. var sel string if err == nil { + dockerfiles = append(dockerfiles, DockerfilePromptUseImage) sel, err = s.prompt.SelectOne( selPrompt, selHelp, diff --git a/internal/pkg/term/selector/selector_test.go b/internal/pkg/term/selector/selector_test.go index 9702ce3c366..4850848bdb6 100644 --- a/internal/pkg/term/selector/selector_test.go +++ b/internal/pkg/term/selector/selector_test.go @@ -884,7 +884,12 @@ func TestWorkspaceSelect_Dockerfile(t *testing.T) { mockPrompt: func(m *mocks.MockPrompter) { m.EXPECT().SelectOne( gomock.Any(), gomock.Any(), - gomock.Eq(dockerfiles), + gomock.Eq([]string{ + "./Dockerfile", + "backend/Dockerfile", + "frontend/Dockerfile", + "Use an existing image instead", + }), gomock.Any(), ).Return("frontend/Dockerfile", nil) }, diff --git a/templates/workloads/jobs/scheduled-job/manifest.yml b/templates/workloads/jobs/scheduled-job/manifest.yml index 78d6dc84d60..72a87d780c7 100644 --- a/templates/workloads/jobs/scheduled-job/manifest.yml +++ b/templates/workloads/jobs/scheduled-job/manifest.yml @@ -9,8 +9,14 @@ name: {{.Name}} type: {{.Type}} image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args +{{- if .ImageConfig.Build.BuildArgs.Dockerfile}} + # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args. build: {{.ImageConfig.Build.BuildArgs.Dockerfile}} +{{- end}} +{{- if .ImageConfig.Location}} + # The name of the Docker image. + location: {{.ImageConfig.Location}} +{{- end}} # Number of CPU units for the task. cpu: {{.CPU}} diff --git a/templates/workloads/services/backend/manifest.yml b/templates/workloads/services/backend/manifest.yml index cdb46ac707b..dfb650b0873 100644 --- a/templates/workloads/services/backend/manifest.yml +++ b/templates/workloads/services/backend/manifest.yml @@ -9,17 +9,25 @@ name: {{.Name}} type: {{.Type}} image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args +{{- if .ImageConfig.Build.BuildArgs.Dockerfile}} + # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args. build: {{.ImageConfig.Build.BuildArgs.Dockerfile}} +{{- end}} +{{- if .ImageConfig.Image.Location}} + # The name of the Docker image. + location: {{.ImageConfig.Image.Location}} +{{- end}} # Port exposed through your container to route traffic to it. - port: {{.ImageConfig.Port}}{{if .ImageConfig.HealthCheck}} + port: {{.ImageConfig.Port}} +{{- if .ImageConfig.HealthCheck}} healthcheck: # The command the container runs to determine if it's healthy. command: {{fmtSlice (quoteSlice .ImageConfig.HealthCheck.Command)}} interval: {{.ImageConfig.HealthCheck.Interval}} # Time period between healthchecks. Default is 10s. retries: {{.ImageConfig.HealthCheck.Retries}} # Number of times to retry before container is deemed unhealthy. Default is 2. timeout: {{.ImageConfig.HealthCheck.Timeout}} # How long to wait before considering the healthcheck failed. Default is 5s. - start_period: {{.ImageConfig.HealthCheck.StartPeriod}} # Grace period within which to provide containers time to bootstrap before failed health checks count towards the maximum number of retries. Default is 0s.{{end}} + start_period: {{.ImageConfig.HealthCheck.StartPeriod}} # Grace period within which to provide containers time to bootstrap before failed health checks count towards the maximum number of retries. Default is 0s. +{{- end}} # Number of CPU units for the task. cpu: {{.CPU}} diff --git a/templates/workloads/services/lb-web/manifest.yml b/templates/workloads/services/lb-web/manifest.yml index cdbb25451ac..bda8a9bef16 100644 --- a/templates/workloads/services/lb-web/manifest.yml +++ b/templates/workloads/services/lb-web/manifest.yml @@ -8,8 +8,14 @@ name: {{.Name}} type: {{.Type}} image: - # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args +{{- if .ImageConfig.Build.BuildArgs.Dockerfile}} + # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args. build: {{.ImageConfig.Build.BuildArgs.Dockerfile}} +{{- end}} +{{- if .ImageConfig.Image.Location}} + # The name of the Docker image. + location: {{.ImageConfig.Image.Location}} +{{- end}} # Port exposed through your container to route traffic to it. port: {{.ImageConfig.Port}}