Skip to content

Commit 9ba0e85

Browse files
Support docker build --secret flag (#4731)
* Support docker build --secret flag * split out buildkit test and run only on GCP * Update hack/schemas/main.go Co-authored-by: Brian de Alwis <[email protected]> * Update hack/schemas/main.go Co-authored-by: Brian de Alwis <[email protected]> * add back Destination field and add more tests * buildkit not currently supported in gcb * generate schemas * move secret check into helper method Co-authored-by: Brian de Alwis <[email protected]>
1 parent b2f9ad4 commit 9ba0e85

File tree

10 files changed

+206
-38
lines changed

10 files changed

+206
-38
lines changed

docs/content/en/schemas/v2beta8.json

+37-1
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,11 @@
923923
"x-intellij-html-description": "used to pass in --no-cache to docker build to prevent caching.",
924924
"default": "false"
925925
},
926+
"secret": {
927+
"$ref": "#/definitions/DockerSecret",
928+
"description": "contains information about a local secret passed to `docker build`, along with optional destination information.",
929+
"x-intellij-html-description": "contains information about a local secret passed to <code>docker build</code>, along with optional destination information."
930+
},
926931
"target": {
927932
"type": "string",
928933
"description": "Dockerfile target name to build.",
@@ -935,7 +940,8 @@
935940
"buildArgs",
936941
"network",
937942
"cacheFrom",
938-
"noCache"
943+
"noCache",
944+
"secret"
939945
],
940946
"additionalProperties": false,
941947
"description": "describes an artifact built from a Dockerfile, usually using `docker build`.",
@@ -962,6 +968,36 @@
962968
"description": "contains information about the docker `config.json` to mount.",
963969
"x-intellij-html-description": "contains information about the docker <code>config.json</code> to mount."
964970
},
971+
"DockerSecret": {
972+
"required": [
973+
"id"
974+
],
975+
"properties": {
976+
"dst": {
977+
"type": "string",
978+
"description": "path in the container to mount the secret.",
979+
"x-intellij-html-description": "path in the container to mount the secret."
980+
},
981+
"id": {
982+
"type": "string",
983+
"description": "id of the secret.",
984+
"x-intellij-html-description": "id of the secret."
985+
},
986+
"src": {
987+
"type": "string",
988+
"description": "path to the secret on the host machine.",
989+
"x-intellij-html-description": "path to the secret on the host machine."
990+
}
991+
},
992+
"preferredOrder": [
993+
"id",
994+
"src",
995+
"dst"
996+
],
997+
"additionalProperties": false,
998+
"description": "contains information about a local secret passed to `docker build`, along with optional destination information.",
999+
"x-intellij-html-description": "contains information about a local secret passed to <code>docker build</code>, along with optional destination information."
1000+
},
9651001
"DockerfileDependency": {
9661002
"properties": {
9671003
"buildArgs": {

hack/schemas/main.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,11 @@ func (g *schemaGenerator) newDefinition(name string, t ast.Expr, comment string,
264264
}
265265

266266
if g.strict && name != "" {
267+
if comment == "" {
268+
panic(fmt.Sprintf("field %q needs comment (all public fields require comments)", name))
269+
}
267270
if !strings.HasPrefix(comment, name+" ") {
268-
panic(fmt.Sprintf("comment should start with field name on field %s", name))
271+
panic(fmt.Sprintf("comment %q should start with field name on field %s", comment, name))
269272
}
270273
}
271274

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# syntax=docker/dockerfile:1.0.0-experimental
2+
3+
FROM alpine:3.10
4+
5+
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secret
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: skaffold/v2beta8
2+
kind: Config
3+
build:
4+
local:
5+
useBuildkit: true
6+
push: false
7+
artifacts:
8+
- image: secret
9+
docker:
10+
secret:
11+
id: mysecret
12+
src: mysecret.txt

pkg/skaffold/build/gcb/docker.go

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package gcb
1818

1919
import (
20+
"errors"
2021
"fmt"
2122

2223
cloudbuild "google.golang.org/api/cloudbuild/v1"
@@ -62,6 +63,10 @@ func (b *Builder) cacheFromSteps(artifact *latest.DockerArtifact) []*cloudbuild.
6263

6364
// dockerBuildArgs lists the arguments passed to `docker` to build a given image.
6465
func (b *Builder) dockerBuildArgs(artifact *latest.DockerArtifact, tag string) ([]string, error) {
66+
// TODO(nkubala): remove when buildkit is supported in GCB (#4773)
67+
if artifact.Secret != nil {
68+
return nil, errors.New("docker build secrets not currently supported in GCB builds")
69+
}
6570
buildArgs, err := util.EvaluateEnvTemplateMap(artifact.BuildArgs)
6671
if err != nil {
6772
return nil, fmt.Errorf("unable to evaluate build args: %w", err)

pkg/skaffold/build/gcb/docker_test.go

+62-36
Original file line numberDiff line numberDiff line change
@@ -27,49 +27,75 @@ import (
2727
)
2828

2929
func TestDockerBuildSpec(t *testing.T) {
30-
artifact := &latest.Artifact{
31-
ArtifactType: latest.ArtifactType{
32-
DockerArtifact: &latest.DockerArtifact{
33-
DockerfilePath: "Dockerfile",
34-
BuildArgs: map[string]*string{
35-
"arg1": util.StringPtr("value1"),
36-
"arg2": nil,
30+
tests := []struct {
31+
description string
32+
artifact *latest.Artifact
33+
expected cloudbuild.Build
34+
shouldErr bool
35+
}{
36+
{
37+
description: "normal docker build",
38+
artifact: &latest.Artifact{
39+
ArtifactType: latest.ArtifactType{
40+
DockerArtifact: &latest.DockerArtifact{
41+
DockerfilePath: "Dockerfile",
42+
BuildArgs: map[string]*string{
43+
"arg1": util.StringPtr("value1"),
44+
"arg2": nil,
45+
},
46+
},
3747
},
3848
},
39-
},
40-
}
41-
42-
builder := NewBuilder(&mockConfig{
43-
gcb: latest.GoogleCloudBuild{
44-
DockerImage: "docker/docker",
45-
DiskSizeGb: 100,
46-
MachineType: "n1-standard-1",
47-
Timeout: "10m",
48-
},
49-
})
50-
desc, err := builder.buildSpec(artifact, "nginx", "bucket", "object")
51-
52-
expected := cloudbuild.Build{
53-
LogsBucket: "bucket",
54-
Source: &cloudbuild.Source{
55-
StorageSource: &cloudbuild.StorageSource{
56-
Bucket: "bucket",
57-
Object: "object",
49+
expected: cloudbuild.Build{
50+
LogsBucket: "bucket",
51+
Source: &cloudbuild.Source{
52+
StorageSource: &cloudbuild.StorageSource{
53+
Bucket: "bucket",
54+
Object: "object",
55+
},
56+
},
57+
Steps: []*cloudbuild.BuildStep{{
58+
Name: "docker/docker",
59+
Args: []string{"build", "--tag", "nginx", "-f", "Dockerfile", "--build-arg", "arg1=value1", "--build-arg", "arg2", "."},
60+
}},
61+
Images: []string{"nginx"},
62+
Options: &cloudbuild.BuildOptions{
63+
DiskSizeGb: 100,
64+
MachineType: "n1-standard-1",
65+
},
66+
Timeout: "10m",
5867
},
5968
},
60-
Steps: []*cloudbuild.BuildStep{{
61-
Name: "docker/docker",
62-
Args: []string{"build", "--tag", "nginx", "-f", "Dockerfile", "--build-arg", "arg1=value1", "--build-arg", "arg2", "."},
63-
}},
64-
Images: []string{"nginx"},
65-
Options: &cloudbuild.BuildOptions{
66-
DiskSizeGb: 100,
67-
MachineType: "n1-standard-1",
69+
{
70+
description: "buildkit features not supported in GCB",
71+
artifact: &latest.Artifact{
72+
ArtifactType: latest.ArtifactType{
73+
DockerArtifact: &latest.DockerArtifact{
74+
DockerfilePath: "Dockerfile",
75+
Secret: &latest.DockerSecret{
76+
ID: "secret",
77+
},
78+
},
79+
},
80+
},
81+
shouldErr: true,
6882
},
69-
Timeout: "10m",
7083
}
7184

72-
testutil.CheckErrorAndDeepEqual(t, false, err, expected, desc)
85+
for _, test := range tests {
86+
testutil.Run(t, test.description, func(t *testutil.T) {
87+
builder := NewBuilder(&mockConfig{
88+
gcb: latest.GoogleCloudBuild{
89+
DockerImage: "docker/docker",
90+
DiskSizeGb: 100,
91+
MachineType: "n1-standard-1",
92+
Timeout: "10m",
93+
},
94+
})
95+
desc, err := builder.buildSpec(test.artifact, "nginx", "bucket", "object")
96+
t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, desc)
97+
})
98+
}
7399
}
74100

75101
func TestPullCacheFrom(t *testing.T) {

pkg/skaffold/docker/image.go

+22
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,21 @@ func (l *localDaemon) ConfigFile(ctx context.Context, image string) (*v1.ConfigF
156156
return cfg, nil
157157
}
158158

159+
func (l *localDaemon) CheckCompatible(a *latest.DockerArtifact) error {
160+
if a.Secret != nil {
161+
return fmt.Errorf("docker build secrets require BuildKit - set `useBuildkit: true` in your config, or run with `DOCKER_BUILDKIT=1`")
162+
}
163+
return nil
164+
}
165+
159166
// Build performs a docker build and returns the imageID.
160167
func (l *localDaemon) Build(ctx context.Context, out io.Writer, workspace string, a *latest.DockerArtifact, ref string, mode config.RunMode) (string, error) {
161168
logrus.Debugf("Running docker build: context: %s, dockerfile: %s", workspace, a.DockerfilePath)
162169

170+
if err := l.CheckCompatible(a); err != nil {
171+
return "", err
172+
}
173+
163174
buildArgs, err := EvalBuildArgs(mode, workspace, a)
164175
if err != nil {
165176
return "", fmt.Errorf("unable to evaluate build args: %w", err)
@@ -463,6 +474,17 @@ func ToCLIBuildArgs(a *latest.DockerArtifact, evaluatedArgs map[string]*string)
463474
args = append(args, "--no-cache")
464475
}
465476

477+
if a.Secret != nil {
478+
secretString := fmt.Sprintf("id=%s", a.Secret.ID)
479+
if a.Secret.Source != "" {
480+
secretString += ",src=" + a.Secret.Source
481+
}
482+
if a.Secret.Destination != "" {
483+
secretString += ",dst=" + a.Secret.Destination
484+
}
485+
args = append(args, "--secret", secretString)
486+
}
487+
466488
return args, nil
467489
}
468490

pkg/skaffold/docker/image_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,47 @@ func TestGetBuildArgs(t *testing.T) {
312312
},
313313
want: []string{"--no-cache"},
314314
},
315+
316+
{
317+
description: "secret with no source",
318+
artifact: &latest.DockerArtifact{
319+
Secret: &latest.DockerSecret{
320+
ID: "mysecret",
321+
},
322+
},
323+
want: []string{"--secret", "id=mysecret"},
324+
},
325+
{
326+
description: "secret with source",
327+
artifact: &latest.DockerArtifact{
328+
Secret: &latest.DockerSecret{
329+
ID: "mysecret",
330+
Source: "foo.src",
331+
},
332+
},
333+
want: []string{"--secret", "id=mysecret,src=foo.src"},
334+
},
335+
{
336+
description: "secret with destination",
337+
artifact: &latest.DockerArtifact{
338+
Secret: &latest.DockerSecret{
339+
ID: "mysecret",
340+
Destination: "foo.dst",
341+
},
342+
},
343+
want: []string{"--secret", "id=mysecret,dst=foo.dst"},
344+
},
345+
{
346+
description: "secret with source and destination",
347+
artifact: &latest.DockerArtifact{
348+
Secret: &latest.DockerSecret{
349+
ID: "mysecret",
350+
Source: "foo.src",
351+
Destination: "foo.dst",
352+
},
353+
},
354+
want: []string{"--secret", "id=mysecret,src=foo.src,dst=foo.dst"},
355+
},
315356
{
316357
description: "all",
317358
artifact: &latest.DockerArtifact{

pkg/skaffold/schema/latest/config.go

+17
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,23 @@ type DockerArtifact struct {
10301030

10311031
// NoCache used to pass in --no-cache to docker build to prevent caching.
10321032
NoCache bool `yaml:"noCache,omitempty"`
1033+
1034+
// Secret contains information about a local secret passed to `docker build`,
1035+
// along with optional destination information.
1036+
Secret *DockerSecret `yaml:"secret,omitempty"`
1037+
}
1038+
1039+
// DockerSecret contains information about a local secret passed to `docker build`,
1040+
// along with optional destination information.
1041+
type DockerSecret struct {
1042+
// ID is the id of the secret.
1043+
ID string `yaml:"id,omitempty" yamltags:"required"`
1044+
1045+
// Source is the path to the secret on the host machine.
1046+
Source string `yaml:"src,omitempty"`
1047+
1048+
// Destination is the path in the container to mount the secret.
1049+
Destination string `yaml:"dst,omitempty"`
10331050
}
10341051

10351052
// BazelArtifact describes an artifact built with [Bazel](https://bazel.build/).

0 commit comments

Comments
 (0)