diff --git a/internal/pkg/cli/svc_deploy.go b/internal/pkg/cli/svc_deploy.go index 60a8d50e1a1..1287dd973a3 100644 --- a/internal/pkg/cli/svc_deploy.go +++ b/internal/pkg/cli/svc_deploy.go @@ -6,6 +6,7 @@ package cli import ( "errors" "fmt" + "github.com/aws/aws-sdk-go/aws" "path/filepath" "strings" @@ -312,6 +313,8 @@ func buildArgs(name, imageTag, copilotDir string, unmarshaledManifest interface{ Context: *args.Context, Args: args.Args, ImageTag: imageTag, + CacheFrom: args.CacheFrom, + Target: aws.StringValue(args.Target), }, nil } diff --git a/internal/pkg/docker/docker.go b/internal/pkg/docker/docker.go index 09f1d71e21f..66b48af6825 100644 --- a/internal/pkg/docker/docker.go +++ b/internal/pkg/docker/docker.go @@ -35,6 +35,8 @@ type BuildArguments struct { ImageTag string // Required. Tag to pass to `docker build` via -t flag. Usually Git commit short ID. Dockerfile string // Required. Dockerfile to pass to `docker build` via --file flag. Context string // Optional. Build context directory to pass to `docker build` + Target string // Optional. The target build stage to pass to `docker build` + CacheFrom []string // Optional. Images to consider as cache sources to pass to `docker build` Args map[string]string // Optional. Build args to pass via `--build-arg` flags. Equivalent to ARG directives in dockerfile. AdditionalTags []string // Optional. Additional image tags to pass to docker. } @@ -53,6 +55,16 @@ func (r Runner) Build(in *BuildArguments) error { args = append(args, "-t", imageName(in.URI, tag)) } + // Add cache from options + for _, imageFrom := range in.CacheFrom { + args = append(args, "--cache-from", imageFrom) + } + + // Add target option + if in.Target != "" { + args = append(args, "--target", in.Target) + } + // Add the "args:" override section from manifest to the docker build call // Collect the keys in a slice to sort for test stability diff --git a/internal/pkg/docker/docker_test.go b/internal/pkg/docker/docker_test.go index 3f4151c4972..8c41900b122 100644 --- a/internal/pkg/docker/docker_test.go +++ b/internal/pkg/docker/docker_test.go @@ -31,6 +31,8 @@ func TestBuild(t *testing.T) { context string additionalTags []string args map[string]string + target string + cacheFrom []string setupMocks func(controller *gomock.Controller) wantedError error @@ -103,6 +105,20 @@ func TestBuild(t *testing.T) { "mockPath/to", "-f", "mockPath/to/mockDockerfile"}).Return(nil) }, }, + "success with options": { + path: mockPath, + target: "foobar", + cacheFrom: []string{"foo/bar:latest", "foo/bar/baz:1.2.3"}, + setupMocks: func(c *gomock.Controller) { + mockRunner = mocks.NewMockrunner(c) + mockRunner.EXPECT().Run("docker", []string{"build", + "-t", mockURI + ":" + mockTag1, + "--cache-from", "foo/bar:latest", + "--cache-from", "foo/bar/baz:1.2.3", + "--target", "foobar", + "mockPath/to", "-f", "mockPath/to/mockDockerfile"}).Return(nil) + }, + }, } for name, tc := range tests { @@ -119,6 +135,8 @@ func TestBuild(t *testing.T) { ImageTag: mockTag1, AdditionalTags: tc.additionalTags, Args: tc.args, + Target: tc.target, + CacheFrom: tc.cacheFrom, } got := s.Build(&buildInput) diff --git a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml index 08a07957ded..2ea81989b96 100644 --- a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml +++ b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml @@ -8,7 +8,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, target, cache_from. build: ./subscribers/Dockerfile # Number of CPU units for the task. diff --git a/internal/pkg/manifest/testdata/scheduled-job-fully-specified.yml b/internal/pkg/manifest/testdata/scheduled-job-fully-specified.yml index 57812d7f06a..d8f3da0db22 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, target, cache_from. 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 c2abc70647a..891c86bb7b9 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, target, cache_from. build: ./cuteness-aggregator/Dockerfile # Number of CPU units for the task. diff --git a/internal/pkg/manifest/testdata/scheduled-job-no-timeout.yml b/internal/pkg/manifest/testdata/scheduled-job-no-timeout.yml index 9aa71c6e21e..206d95dd5e1 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, target, cache_from. 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 30134038549..52e701cc0c6 100644 --- a/internal/pkg/manifest/workload.go +++ b/internal/pkg/manifest/workload.go @@ -55,31 +55,27 @@ func (i Image) GetLocation() string { func (i *Image) BuildConfig(rootDirectory string) *DockerBuildArgs { df := i.dockerfile() ctx := i.context() + dockerfile := aws.String(filepath.Join(rootDirectory, dockerfileDefaultName)) + context := aws.String(rootDirectory) + if df != "" && ctx != "" { - return &DockerBuildArgs{ - Dockerfile: aws.String(filepath.Join(rootDirectory, df)), - Context: aws.String(filepath.Join(rootDirectory, ctx)), - Args: i.args(), - } + dockerfile = aws.String(filepath.Join(rootDirectory, df)) + context = aws.String(filepath.Join(rootDirectory, ctx)) } if df != "" && ctx == "" { - return &DockerBuildArgs{ - Dockerfile: aws.String(filepath.Join(rootDirectory, df)), - Context: aws.String(filepath.Join(rootDirectory, filepath.Dir(df))), - Args: i.args(), - } + dockerfile = aws.String(filepath.Join(rootDirectory, df)) + context = aws.String(filepath.Join(rootDirectory, filepath.Dir(df))) } if df == "" && ctx != "" { - return &DockerBuildArgs{ - Dockerfile: aws.String(filepath.Join(rootDirectory, ctx, dockerfileDefaultName)), - Context: aws.String(filepath.Join(rootDirectory, ctx)), - Args: i.args(), - } + dockerfile = aws.String(filepath.Join(rootDirectory, ctx, dockerfileDefaultName)) + context = aws.String(filepath.Join(rootDirectory, ctx)) } return &DockerBuildArgs{ - Dockerfile: aws.String(filepath.Join(rootDirectory, dockerfileDefaultName)), - Context: aws.String(rootDirectory), + Dockerfile: dockerfile, + Context: context, Args: i.args(), + Target: i.target(), + CacheFrom: i.cacheFrom(), } } @@ -111,6 +107,17 @@ func (i *Image) args() map[string]string { return i.Build.BuildArgs.Args } +// target returns the build target stage if it exists, otherwise nil. +func (i *Image) target() *string { + return i.Build.BuildArgs.Target +} + +// cacheFrom returns the cache from build section, if it exists. +// Otherwise it returns an empty array. +func (i *Image) cacheFrom() []string { + return i.Build.BuildArgs.CacheFrom +} + // BuildArgsOrString is a custom type which supports unmarshaling yaml which // can either be of type string or type DockerBuildArgs. type BuildArgsOrString struct { @@ -156,10 +163,12 @@ type DockerBuildArgs struct { Context *string `yaml:"context,omitempty"` Dockerfile *string `yaml:"dockerfile,omitempty"` Args map[string]string `yaml:"args,omitempty"` + Target *string `yaml:"target,omitempty"` + CacheFrom []string `yaml:"cache_from,omitempty"` } func (b *DockerBuildArgs) isEmpty() bool { - if b.Context == nil && b.Dockerfile == nil && b.Args == nil { + if b.Context == nil && b.Dockerfile == nil && b.Args == nil && b.Target == nil && b.CacheFrom == nil { return true } return false diff --git a/internal/pkg/manifest/workload_test.go b/internal/pkg/manifest/workload_test.go index e2775f26040..fa1754f74c0 100644 --- a/internal/pkg/manifest/workload_test.go +++ b/internal/pkg/manifest/workload_test.go @@ -56,6 +56,22 @@ func TestBuildArgs_UnmarshalYAML(t *testing.T) { }, }, }, + "Dockerfile with cache from and target build opts": { + inContent: []byte(`build: + cache_from: + - foo/bar:latest + - foo/bar/baz:1.2.3 + target: foobar`), + wantedStruct: BuildArgsOrString{ + BuildArgs: DockerBuildArgs{ + Target: aws.String("foobar"), + CacheFrom: []string{ + "foo/bar:latest", + "foo/bar/baz:1.2.3", + }, + }, + }, + }, "Error if unmarshalable": { inContent: []byte(`build: badfield: OH NOES @@ -76,6 +92,8 @@ func TestBuildArgs_UnmarshalYAML(t *testing.T) { require.Equal(t, tc.wantedStruct.BuildArgs.Context, b.Build.BuildArgs.Context) require.Equal(t, tc.wantedStruct.BuildArgs.Dockerfile, b.Build.BuildArgs.Dockerfile) require.Equal(t, tc.wantedStruct.BuildArgs.Args, b.Build.BuildArgs.Args) + require.Equal(t, tc.wantedStruct.BuildArgs.Target, b.Build.BuildArgs.Target) + require.Equal(t, tc.wantedStruct.BuildArgs.CacheFrom, b.Build.BuildArgs.CacheFrom) } }) } @@ -154,6 +172,26 @@ func TestBuildConfig(t *testing.T) { }, }, }, + "including build options": { + inBuild: BuildArgsOrString{ + BuildArgs: DockerBuildArgs{ + Target: aws.String("foobar"), + CacheFrom: []string{ + "foo/bar:latest", + "foo/bar/baz:1.2.3", + }, + }, + }, + wantedBuild: DockerBuildArgs{ + Dockerfile: aws.String(filepath.Join(mockWsRoot, "Dockerfile")), + Context: aws.String(mockWsRoot), + Target: aws.String("foobar"), + CacheFrom: []string{ + "foo/bar:latest", + "foo/bar/baz:1.2.3", + }, + }, + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { diff --git a/site/content/docs/manifest/backend-service.md b/site/content/docs/manifest/backend-service.md index 3c2d8227e35..e398701456b 100644 --- a/site/content/docs/manifest/backend-service.md +++ b/site/content/docs/manifest/backend-service.md @@ -69,10 +69,13 @@ image: build: dockerfile: path/to/dockerfile context: context/dir + target: build-stage + cache_from: + - image:tag args: key: value ``` -In this case, copilot will use the context directory you specified and convert the key-value pairs under args to --build-arg overrides. The equivalent docker build call will be: `$ docker build --file path/to/dockerfile --build-arg key=value context/dir`. +In this case, copilot will use the context directory you specified and convert the key-value pairs under args to --build-arg overrides. The equivalent docker build call will be: `$ docker build --file path/to/dockerfile --target build-stage --cache-from image:tag --build-arg key=value context/dir`. You can omit fields and Copilot will do its best to understand what you mean. For example, if you specify `context` but not `dockerfile`, Copilot will run Docker in the context directory and assume that your Dockerfile is named "Dockerfile." If you specify `dockerfile` but no `context`, Copilot assumes you want to run Docker in the directory that contains `dockerfile`. diff --git a/site/content/docs/manifest/lb-web-service.md b/site/content/docs/manifest/lb-web-service.md index 6cdbd55b3b1..3f998593888 100644 --- a/site/content/docs/manifest/lb-web-service.md +++ b/site/content/docs/manifest/lb-web-service.md @@ -71,10 +71,13 @@ image: build: dockerfile: path/to/dockerfile context: context/dir + target: build-stage + cache_from: + - image:tag args: key: value ``` -In this case, copilot will use the context directory you specified and convert the key-value pairs under args to --build-arg overrides. The equivalent docker build call will be: `$ docker build --file path/to/dockerfile --build-arg key=value context/dir`. +In this case, copilot will use the context directory you specified and convert the key-value pairs under args to --build-arg overrides. The equivalent docker build call will be: `$ docker build --file path/to/dockerfile --target build-stage --cache-from image:tag --build-arg key=value context/dir`. You can omit fields and Copilot will do its best to understand what you mean. For example, if you specify `context` but not `dockerfile`, Copilot will run Docker in the context directory and assume that your Dockerfile is named "Dockerfile." If you specify `dockerfile` but no `context`, Copilot assumes you want to run Docker in the directory that contains `dockerfile`. diff --git a/templates/cicd/buildspec.yml b/templates/cicd/buildspec.yml index 93f441eb0b2..a1113f6fa80 100644 --- a/templates/cicd/buildspec.yml +++ b/templates/cicd/buildspec.yml @@ -70,7 +70,9 @@ phases: base_dockerfile=$(echo $manifest | jq '.image.build') build_dockerfile=$(echo $manifest| jq 'if .image.build?.dockerfile? then .image.build.dockerfile else "" end' | sed 's/"//g') build_context=$(echo $manifest| jq 'if .image.build?.context? then .image.build.context else "" end' | sed 's/"//g') + build_target=$(echo $manifest| jq 'if .image.build?.target? then .image.build.target else "" end' | sed 's/"//g') dockerfile_args=$(echo $manifest | jq 'if .image.build?.args? then .image.build.args else "" end | to_entries?') + build_cache_from=$(echo $manifest | jq 'if .image.build?.cache_from? then .image.build.cache_from else "" end') df_rel_path=$( echo $base_dockerfile | sed 's/"//g') if [ -n "$build_dockerfile" ]; then df_rel_path=$build_dockerfile @@ -86,6 +88,14 @@ phases: build_args="$build_args--build-arg $arg " done fi + if [ -n "$build_target" ]; then + build_args="$build_args--target $build_target " + fi + if [ -n "$build_cache_from" ]; then + for arg in $(echo $build_cache_from | jq -r '.[]'); do + build_args="$build_args--cache-from $arg " + done + fi echo "Name: $workload" echo "Relative Dockerfile path: $df_rel_path" echo "Docker build context: $df_dir_path" diff --git a/templates/workloads/jobs/scheduled-job/manifest.yml b/templates/workloads/jobs/scheduled-job/manifest.yml index 7c9f0ebd846..26f007f5cd5 100644 --- a/templates/workloads/jobs/scheduled-job/manifest.yml +++ b/templates/workloads/jobs/scheduled-job/manifest.yml @@ -10,7 +10,7 @@ type: {{.Type}} image: {{- if .ImageConfig.Build.BuildArgs.Dockerfile}} - # 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, target, cache_from. build: {{.ImageConfig.Build.BuildArgs.Dockerfile}} {{- end}} {{- if .ImageConfig.Location}} diff --git a/templates/workloads/services/backend/manifest.yml b/templates/workloads/services/backend/manifest.yml index 012a1e62e2b..e2fd3d2dbbb 100644 --- a/templates/workloads/services/backend/manifest.yml +++ b/templates/workloads/services/backend/manifest.yml @@ -13,7 +13,7 @@ type: {{.Type}} image: {{- if .ImageConfig.Build.BuildArgs.Dockerfile}} - # 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, target, cache_from. build: {{.ImageConfig.Build.BuildArgs.Dockerfile}} {{- end}} {{- if .ImageConfig.Image.Location}} diff --git a/templates/workloads/services/lb-web/manifest.yml b/templates/workloads/services/lb-web/manifest.yml index 9e9045c8a0d..1f22632ec11 100644 --- a/templates/workloads/services/lb-web/manifest.yml +++ b/templates/workloads/services/lb-web/manifest.yml @@ -9,7 +9,7 @@ type: {{.Type}} image: {{- if .ImageConfig.Build.BuildArgs.Dockerfile}} - # 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, target, cache_from. build: {{.ImageConfig.Build.BuildArgs.Dockerfile}} {{- end}} {{- if .ImageConfig.Image.Location}}