diff --git a/bake/bake.go b/bake/bake.go index 66ef0a1d55b4..a71a339000ca 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -606,9 +606,12 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) override.Append = appendTo override.ArrValue = append(override.ArrValue, parts[1]) } - case "resources": + case "resources", "secret": if len(keys) != 3 { - return nil, errors.Errorf("invalid key %s, resources requires name", parts[0]) + return nil, errors.Errorf("invalid key %s, %s requires name", parts[0], keys[1]) + } + if appendTo { + return nil, errors.Errorf("invalid key %s, %s does not support append", parts[0], keys[1]) } override.Value = parts[1] case "args": @@ -974,6 +977,8 @@ func (t *Target) Merge(t2 *Target) { func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementConf) error { // IMPORTANT: if you add more fields here, do not forget to update // docs/bake-reference.md and https://docs.docker.com/build/bake/overrides/ + secretOverrides := map[string]Override{} + secretEntitlements := map[string]struct{}{} for key, o := range overrides { value := o.Value keys := strings.SplitN(key, ".", 2) @@ -1061,21 +1066,24 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon t.Target = &value case "call": t.Call = &value + case "secret": + if len(keys) != 2 { + return errors.Errorf("invalid format for secret, expecting secret.=") + } + secretOverrides[keys[1]] = o case "secrets": secrets, err := parseArrValue[buildflags.Secret](o.ArrValue) if err != nil { return errors.Wrap(err, "invalid value for outputs") } + for _, s := range secrets { + secretEntitlements[s.ID] = struct{}{} + } if o.Append { t.Secrets = t.Secrets.Merge(secrets) } else { t.Secrets = secrets } - for _, s := range t.Secrets { - if s.FilePath != "" { - ent.FSRead = append(ent.FSRead, s.FilePath) - } - } case "ssh": ssh, err := parseArrValue[buildflags.SSH](o.ArrValue) if err != nil { @@ -1189,9 +1197,46 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon return errors.Errorf("unknown key: %s", keys[0]) } } + for id, o := range secretOverrides { + if err := t.updateSecret(id, o.Value); err != nil { + return err + } + secretEntitlements[id] = struct{}{} + } + for _, s := range t.Secrets { + if _, ok := secretEntitlements[s.ID]; ok && s.FilePath != "" { + ent.FSRead = append(ent.FSRead, s.FilePath) + } + } return nil } +func (t *Target) updateSecret(id, value string) error { + if id == "" { + return errors.Errorf("invalid format for secret, expecting secret.=") + } + + for _, s := range t.Secrets { + if s.ID != id { + continue + } + + var next buildflags.Secret + if err := next.UnmarshalText([]byte(value)); err != nil { + return err + } + if next.ID != "" && next.ID != id { + return errors.Errorf("secret override id %q does not match declared secret %q", next.ID, id) + } + + s.Env = next.Env + s.FilePath = next.FilePath + return nil + } + + return errors.Errorf("secret %q must be declared before it can be overridden", id) +} + func (g *Group) GetEvalContexts(ectx *hcl.EvalContext, block *hcl.Block, loadDeps func(hcl.Expression) hcl.Diagnostics) ([]*hcl.EvalContext, error) { content, _, err := block.Body.PartialContent(&hcl.BodySchema{ Attributes: []hcl.AttributeSchema{{Name: "matrix"}}, diff --git a/bake/bake_test.go b/bake/bake_test.go index de3fb9636e06..1622d2007659 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -237,6 +237,49 @@ target "webapp" { require.Equal(t, "BAR", m["webapp"].Secrets[1].ID) }) + t.Run("SecretSourceOverrideEnv", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.FOO=env=BAR"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Len(t, m["webapp"].Secrets, 1) + require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) + require.Equal(t, "BAR", m["webapp"].Secrets[0].Env) + require.Empty(t, m["webapp"].Secrets[0].FilePath) + }) + + t.Run("SecretSourceOverrideFile", func(t *testing.T) { + ent := &EntitlementConf{} + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.FOO=src=/tmp/foo"}, nil, nil, ent) + require.NoError(t, err) + require.Len(t, m["webapp"].Secrets, 1) + require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) + require.Equal(t, "/tmp/foo", m["webapp"].Secrets[0].FilePath) + require.Empty(t, m["webapp"].Secrets[0].Env) + require.Equal(t, []string{"/tmp/foo"}, ent.FSRead) + }) + + t.Run("SecretSourceOverrideUsesFinalSourceForEntitlements", func(t *testing.T) { + ent := &EntitlementConf{} + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secrets=id=FOO,src=/tmp/foo", "webapp.secret.FOO=env=BAR"}, nil, nil, ent) + require.NoError(t, err) + require.Len(t, m["webapp"].Secrets, 1) + require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) + require.Equal(t, "BAR", m["webapp"].Secrets[0].Env) + require.Empty(t, m["webapp"].Secrets[0].FilePath) + require.Empty(t, ent.FSRead) + }) + + t.Run("SecretSourceOverrideUndeclared", func(t *testing.T) { + _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.BAR=env=BAR"}, nil, nil, &EntitlementConf{}) + require.Error(t, err) + require.Equal(t, `secret "BAR" must be declared before it can be overridden`, err.Error()) + }) + + t.Run("SecretSourceOverrideMismatchedID", func(t *testing.T) { + _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.FOO=id=BAR,env=BAR"}, nil, nil, &EntitlementConf{}) + require.Error(t, err) + require.Equal(t, `secret override id "BAR" does not match declared secret "FOO"`, err.Error()) + }) + t.Run("ShmSizeOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.shm-size=256m"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) diff --git a/docs/bake-reference.md b/docs/bake-reference.md index e473b2d1ba30..0ad14fd468f7 100644 --- a/docs/bake-reference.md +++ b/docs/bake-reference.md @@ -1001,6 +1001,14 @@ RUN --mount=type=secret,id=KUBECONFIG,env=KUBECONFIG \ helm upgrade --install ``` +You can override the source for an existing secret without changing the target's +secret IDs. The secret must already be declared by the target. + +```console +$ docker buildx bake --set default.secret.aws=env=AWS_CREDENTIALS +$ docker buildx bake --set default.secret.KUBECONFIG=src=/path/to/kubeconfig +``` + ### `target.shm-size` Sets the size of the shared memory allocated for build containers when using diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 8c8633e322e8..7f313161129c 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -433,6 +433,7 @@ $ docker buildx bake --set foo*.no-cache # bypass caching only fo $ docker buildx bake --set target.platform+=linux/arm64 # appends 'linux/arm64' to the platform list $ docker buildx bake --set target.contexts.bar=../bar # overrides 'bar' named context $ docker buildx bake --set target.resources.memory=2g # overrides memory resource limit +$ docker buildx bake --set target.secret.aws=env=AWS # overrides source for an existing secret ``` > [!NOTE] @@ -464,6 +465,7 @@ You can override the following fields: * `pull` * `push` * `resources` +* `secret.` * `secrets` * `ssh` * `tags` diff --git a/tests/bake.go b/tests/bake.go index c8a1d1ee4dd4..10bce58aa005 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -41,6 +41,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakePrint, testBakePrintSensitive, testBakePrintOverrideEmpty, + testBakeSecretSourceOverride, testBakePrintKeepEscaped, testBakePrintRemoteContextSubdir, testBakeLocal, @@ -474,6 +475,52 @@ target "default" { }`, stdout.String()) } +func testBakeSecretSourceOverride(t *testing.T, sb integration.Sandbox) { + bakefile := []byte(` +target "build" { + secret = [ + "id=aws,src=aws-default", + "id=token,env=TOKEN", + ] +} +`) + dir := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + fstest.CreateFile("Dockerfile", []byte("FROM scratch\n"), 0600), + fstest.CreateFile("aws-default", []byte("default"), 0600), + fstest.CreateFile("tokenfile", []byte("token"), 0600), + ) + + cmd := buildxCmd(sb, withDir(dir), withArgs( + "bake", "--print", "build", + "--set", "build.secret.aws=env=AWS_CREDENTIALS", + "--set", "build.secret.token=src=tokenfile", + )) + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + cmd.Stdout = &stdout + cmd.Stderr = &stderr + require.NoError(t, cmd.Run(), stdout.String(), stderr.String()) + + var def struct { + Target map[string]*bake.Target `json:"target"` + } + require.NoError(t, json.Unmarshal(stdout.Bytes(), &def)) + require.Contains(t, def.Target, "build") + require.Len(t, def.Target["build"].Secrets, 2) + require.Equal(t, "aws", def.Target["build"].Secrets[0].ID) + require.Equal(t, "AWS_CREDENTIALS", def.Target["build"].Secrets[0].Env) + require.Empty(t, def.Target["build"].Secrets[0].FilePath) + require.Equal(t, "token", def.Target["build"].Secrets[1].ID) + require.Equal(t, "tokenfile", def.Target["build"].Secrets[1].FilePath) + require.Empty(t, def.Target["build"].Secrets[1].Env) + + out, err := bakeCmd(sb, withDir(dir), withArgs("--print", "build", "--set", "build.secret.missing=env=MISSING")) + require.Error(t, err) + require.Contains(t, out, `secret "missing" must be declared before it can be overridden`) +} + func testBakePrintKeepEscaped(t *testing.T, sb integration.Sandbox) { bakefile := []byte(` target "default" {