diff --git a/bake/bake.go b/bake/bake.go index ca085c99..d49c7a33 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -45,6 +45,7 @@ type File struct { type Override struct { Value string ArrValue []string + Append bool } func defaultFilenames() []string { @@ -528,9 +529,12 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) m := map[string]map[string]Override{} for _, v := range v { parts := strings.SplitN(v, "=", 2) - keys := strings.SplitN(parts[0], ".", 3) + + skey := strings.TrimSuffix(parts[0], "+") + appendTo := strings.HasSuffix(parts[0], "+") + keys := strings.SplitN(skey, ".", 3) if len(keys) < 2 { - return nil, errors.Errorf("invalid override key %s, expected target.name", parts[0]) + return nil, errors.Errorf("invalid override key %s, expected target.name", skey) } pattern := keys[0] @@ -543,8 +547,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) return nil, err } - kk := strings.SplitN(parts[0], ".", 2) - + okey := strings.Join(keys[1:], ".") for _, name := range names { t, ok := m[name] if !ok { @@ -552,14 +555,15 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) m[name] = t } - o := t[kk[1]] + override := t[okey] // IMPORTANT: if you add more fields here, do not forget to update // docs/reference/buildx_bake.md (--set) and https://docs.docker.com/build/bake/overrides/ switch keys[1] { case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest", "entitlements", "network", "annotations": if len(parts) == 2 { - o.ArrValue = append(o.ArrValue, parts[1]) + override.Append = appendTo + override.ArrValue = append(override.ArrValue, parts[1]) } case "args": if len(keys) != 3 { @@ -570,7 +574,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) if !ok { continue } - o.Value = v + override.Value = v } fallthrough case "contexts": @@ -580,11 +584,11 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) fallthrough default: if len(parts) == 2 { - o.Value = parts[1] + override.Value = parts[1] } } - t[kk[1]] = o + t[okey] = override } } return m, nil @@ -896,13 +900,21 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon } t.Labels[keys[1]] = &value case "tags": - t.Tags = o.ArrValue + if o.Append { + t.Tags = append(t.Tags, o.ArrValue...) + } else { + t.Tags = o.ArrValue + } case "cache-from": cacheFrom, err := buildflags.ParseCacheEntry(o.ArrValue) if err != nil { return err } - t.CacheFrom = cacheFrom + if o.Append { + t.CacheFrom = t.CacheFrom.Merge(cacheFrom) + } else { + t.CacheFrom = cacheFrom + } for _, c := range t.CacheFrom { if c.Type == "local" { if v, ok := c.Attrs["src"]; ok { @@ -915,7 +927,11 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon if err != nil { return err } - t.CacheTo = cacheTo + if o.Append { + t.CacheTo = t.CacheTo.Merge(cacheTo) + } else { + t.CacheTo = cacheTo + } for _, c := range t.CacheTo { if c.Type == "local" { if v, ok := c.Attrs["dest"]; ok { @@ -932,7 +948,11 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon if err != nil { return errors.Wrap(err, "invalid value for outputs") } - t.Secrets = secrets + 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) @@ -943,18 +963,30 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon if err != nil { return errors.Wrap(err, "invalid value for outputs") } - t.SSH = ssh + if o.Append { + t.SSH = t.SSH.Merge(ssh) + } else { + t.SSH = ssh + } for _, s := range t.SSH { ent.FSRead = append(ent.FSRead, s.Paths...) } case "platform": - t.Platforms = o.ArrValue + if o.Append { + t.Platforms = append(t.Platforms, o.ArrValue...) + } else { + t.Platforms = o.ArrValue + } case "output": outputs, err := parseArrValue[buildflags.ExportEntry](o.ArrValue) if err != nil { return errors.Wrap(err, "invalid value for outputs") } - t.Outputs = outputs + if o.Append { + t.Outputs = t.Outputs.Merge(outputs) + } else { + t.Outputs = outputs + } for _, o := range t.Outputs { if o.Destination != "" { ent.FSWrite = append(ent.FSWrite, o.Destination) @@ -984,11 +1016,19 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon } t.NoCache = &noCache case "no-cache-filter": - t.NoCacheFilter = o.ArrValue + if o.Append { + t.NoCacheFilter = append(t.NoCacheFilter, o.ArrValue...) + } else { + t.NoCacheFilter = o.ArrValue + } case "shm-size": t.ShmSize = &value case "ulimits": - t.Ulimits = o.ArrValue + if o.Append { + t.Ulimits = append(t.Ulimits, o.ArrValue...) + } else { + t.Ulimits = o.ArrValue + } case "network": t.NetworkMode = &value case "pull": diff --git a/bake/bake_test.go b/bake/bake_test.go index 651db4bb..d946cb1c 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -37,6 +37,15 @@ target "webapp" { annotations = [ "index,manifest:org.opencontainers.image.authors=dvdksn" ] + attest = [ + "type=provenance,mode=max" + ] + platforms = [ + "linux/amd64" + ] + secret = [ + "id=FOO,env=FOO" + ] inherits = ["webDEP"] }`), } @@ -127,6 +136,22 @@ target "webapp" { require.Equal(t, []string{"webapp"}, g["default"].Targets) }) + t.Run("AttestOverride", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.attest=type=sbom"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Len(t, m["webapp"].Attest, 2) + require.Equal(t, "provenance", m["webapp"].Attest[0].Type) + require.Equal(t, "sbom", m["webapp"].Attest[1].Type) + }) + + t.Run("AttestAppend", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.attest+=type=sbom"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Len(t, m["webapp"].Attest, 2) + require.Equal(t, "provenance", m["webapp"].Attest[0].Type) + require.Equal(t, "sbom", m["webapp"].Attest[1].Type) + }) + t.Run("ContextOverride", func(t *testing.T) { t.Parallel() _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.context"}, nil, &EntitlementConf{}) @@ -148,6 +173,49 @@ target "webapp" { require.Equal(t, []string{"webapp"}, g["default"].Targets) }) + t.Run("PlatformOverride", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform=linux/arm64"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"linux/arm64"}, m["webapp"].Platforms) + }) + + t.Run("PlatformAppend", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform+=linux/arm64"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"linux/amd64", "linux/arm64"}, m["webapp"].Platforms) + }) + + t.Run("PlatformAppendMulti", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform+=linux/arm64", "webapp.platform+=linux/riscv64"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"linux/amd64", "linux/arm64", "linux/riscv64"}, m["webapp"].Platforms) + }) + + t.Run("PlatformAppendMultiLastOverride", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform+=linux/arm64", "webapp.platform=linux/riscv64"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"linux/arm64", "linux/riscv64"}, m["webapp"].Platforms) + }) + + t.Run("SecretsOverride", func(t *testing.T) { + t.Setenv("FOO", "foo") + t.Setenv("BAR", "bar") + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secrets=id=BAR,env=BAR"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Len(t, m["webapp"].Secrets, 1) + require.Equal(t, "BAR", m["webapp"].Secrets[0].ID) + }) + + t.Run("SecretsAppend", func(t *testing.T) { + t.Setenv("FOO", "foo") + t.Setenv("BAR", "bar") + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secrets+=id=BAR,env=BAR"}, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Len(t, m["webapp"].Secrets, 2) + require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) + require.Equal(t, "BAR", m["webapp"].Secrets[1].ID) + }) + t.Run("ShmSizeOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.shm-size=256m"}, nil, &EntitlementConf{}) require.NoError(t, err) diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index d3b422b1..733a3eae 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -347,19 +347,22 @@ is defined in https://golang.org/pkg/path/#Match. ```console $ docker buildx bake --set target.args.mybuildarg=value $ docker buildx bake --set target.platform=linux/arm64 -$ docker buildx bake --set foo*.args.mybuildarg=value # overrides build arg for all targets starting with 'foo' -$ docker buildx bake --set *.platform=linux/arm64 # overrides platform for all targets -$ docker buildx bake --set foo*.no-cache # bypass caching only for targets starting with 'foo' +$ docker buildx bake --set foo*.args.mybuildarg=value # overrides build arg for all targets starting with 'foo' +$ docker buildx bake --set *.platform=linux/arm64 # overrides platform for all targets +$ docker buildx bake --set foo*.no-cache # bypass caching only for targets starting with 'foo' +$ docker buildx bake --set target.platform+=linux/arm64 # appends 'linux/arm64' to the platform list ``` You can override the following fields: * `annotations` +* `attest` * `args` * `cache-from` * `cache-to` * `context` * `dockerfile` +* `entitlements` * `labels` * `load` * `no-cache` @@ -372,3 +375,20 @@ You can override the following fields: * `ssh` * `tags` * `target` + +You can append using `+=` operator for the following fields: + +* `annotations`¹ +* `attest`¹ +* `cache-from` +* `cache-to` +* `entitlements`¹ +* `no-cache-filter` +* `output` +* `platform` +* `secrets` +* `ssh` +* `tags` + +> [!NOTE] +> ¹ These fields already append by default.