From b45b71eec05e3469546004b7b73d22eb53f4ad80 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:00:08 +0200 Subject: [PATCH] bake: use singular keys for --set overrides Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/bake.go | 105 ++++++++++++++++++++++++------- bake/bake_test.go | 114 +++++++++++++++++++++++++++++++++- docs/reference/buildx_bake.md | 85 ++++++++++++++++--------- 3 files changed, 247 insertions(+), 57 deletions(-) diff --git a/bake/bake.go b/bake/bake.go index d166f3b384ef..b626186712ba 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -657,8 +657,35 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) return nil, errors.Errorf("invalid override key %s, expected target.name", skey) } + // Normalize aliases so they share replacement and append semantics. + switch keys[1] { + case "annotations": + keys[1] = "annotation" + case "args": + keys[1] = "arg" + case "entitlements": + keys[1] = "entitlement" + case "extra-hosts": + keys[1] = "extra-host" + case "labels": + keys[1] = "label" + case "platforms": + keys[1] = "platform" + case "resources": + keys[1] = "resource" + case "tags": + keys[1] = "tag" + case "ulimits": + keys[1] = "ulimit" + case "secrets": + if len(keys) != 2 { + return nil, errors.Errorf("invalid key %s, secret does not support subkeys", parts[0]) + } + keys[1] = "secret" + } + pattern := keys[0] - if len(parts) != 2 && keys[1] != "args" { + if len(parts) != 2 && keys[1] != "arg" { return nil, errors.Errorf("invalid override %s, expected target.name=value", v) } @@ -680,12 +707,27 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) // 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", "annotations", "policy", "ulimits", "no-cache-filter": + case "output", "cache-to", "cache-from", "tag", "platform", "ssh", "attest", "entitlement", "annotation", "policy", "ulimit", "no-cache-filter": + if len(keys) != 2 { + return nil, errors.Errorf("invalid key %s, %s does not support subkeys", parts[0], keys[1]) + } if len(parts) == 2 { override.Append = appendTo override.ArrValue = append(override.ArrValue, parts[1]) } - case "resources", "secret": + case "secret": + if len(keys) == 2 { + if len(parts) == 2 { + override.Append = appendTo + override.ArrValue = append(override.ArrValue, parts[1]) + } + break + } + if appendTo { + return nil, errors.Errorf("invalid key %s, secret does not support append", parts[0]) + } + override.Value = parts[1] + case "resource": if len(keys) != 3 { return nil, errors.Errorf("invalid key %s, %s requires name", parts[0], keys[1]) } @@ -693,9 +735,9 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) return nil, errors.Errorf("invalid key %s, %s does not support append", parts[0], keys[1]) } override.Value = parts[1] - case "args": + case "arg": if len(keys) != 3 { - return nil, errors.Errorf("invalid key %s, args requires name", parts[0]) + return nil, errors.Errorf("invalid key %s, arg requires name", parts[0]) } if len(parts) < 2 && envLookupAllowed() { v, ok := os.LookupEnv(keys[2]) @@ -704,12 +746,23 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error) } override.Value = v } - fallthrough - case "contexts": + if len(parts) == 2 { + override.Value = parts[1] + } + case "contexts", "label", "extra-host": if len(keys) != 3 { - return nil, errors.Errorf("invalid key %s, contexts requires name", parts[0]) + return nil, errors.Errorf("invalid key %s, %s requires name", parts[0], keys[1]) + } + if len(parts) == 2 { + override.Value = parts[1] + } + case "context", "dockerfile", "target", "call", "no-cache", "shm-size", "network", "pull", "push", "load": + if len(keys) != 2 { + return nil, errors.Errorf("invalid key %s, %s does not support subkeys", parts[0], keys[1]) + } + if len(parts) == 2 { + override.Value = parts[1] } - fallthrough default: if len(parts) == 2 { override.Value = parts[1] @@ -1165,9 +1218,9 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon t.Context = &value case "dockerfile": t.Dockerfile = &value - case "args": + case "arg", "args": if len(keys) != 2 { - return errors.Errorf("invalid format for args, expecting args.=") + return errors.Errorf("invalid format for arg, expecting arg.=") } if t.Args == nil { t.Args = map[string]*string{} @@ -1181,15 +1234,15 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon t.Contexts = map[string]string{} } t.Contexts[keys[1]] = value - case "labels": + case "label", "labels": if len(keys) != 2 { - return errors.Errorf("invalid format for labels, expecting labels.=") + return errors.Errorf("invalid format for label, expecting label.=") } if t.Labels == nil { t.Labels = map[string]*string{} } t.Labels[keys[1]] = &value - case "tags": + case "tag", "tags": if o.Append { t.Tags = append(t.Tags, o.ArrValue...) } else { @@ -1245,11 +1298,15 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon case "call": t.Call = &value case "secret": - if len(keys) != 2 { - return errors.Errorf("invalid format for secret, expecting secret.=") + if len(keys) == 2 { + secretOverrides[keys[1]] = o + break } - secretOverrides[keys[1]] = o + fallthrough case "secrets": + if len(keys) != 1 { + return errors.Errorf("invalid format for secret, expecting secret.=") + } secrets, err := parseArrValue[buildflags.Secret](o.ArrValue) if err != nil { return errors.Wrap(err, "invalid value for outputs") @@ -1296,7 +1353,7 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon ent.FSWrite = append(ent.FSWrite, o.Destination) } } - case "entitlements": + case "entitlement", "entitlements": t.Entitlements = append(t.Entitlements, o.ArrValue...) for _, v := range o.ArrValue { if v == string(EntitlementKeyNetworkHost) { @@ -1305,7 +1362,7 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon ent.SecurityInsecure = true } } - case "annotations": + case "annotation", "annotations": t.Annotations = append(t.Annotations, o.ArrValue...) case "attest": attest, err := parseArrValue[buildflags.Attest](o.ArrValue) @@ -1327,15 +1384,15 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon } case "shm-size": t.ShmSize = &value - case "ulimits": + case "ulimit", "ulimits": if o.Append { t.Ulimits = append(t.Ulimits, o.ArrValue...) } else { t.Ulimits = o.ArrValue } - case "resources": + case "resource", "resources": if len(keys) != 2 { - return errors.Errorf("invalid format for resources, expecting resources.=") + return errors.Errorf("invalid format for resource, expecting resource.=") } if t.Resources == nil { t.Resources = &buildflags.ResourcesConfig{} @@ -1363,9 +1420,9 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon return errors.Errorf("invalid value %s for boolean key load", value) } t.Outputs = setLoadOverride(t.Outputs, load) - case "extra-hosts": + case "extra-host", "extra-hosts": if len(keys) != 2 { - return errors.Errorf("invalid format for extra-hosts, expecting extra-hosts.=") + return errors.Errorf("invalid format for extra-host, expecting extra-host.=") } if t.ExtraHosts == nil { t.ExtraHosts = map[string]*string{} diff --git a/bake/bake_test.go b/bake/bake_test.go index 97c2c0b2b8af..3d2b710d4bf2 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -157,6 +157,51 @@ target "webapp" { require.Equal(t, []string{"webapp"}, g["default"].Targets) }) + t.Run("AnnotationAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.annotation=index,manifest:org.opencontainers.image.vendor=docker"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"index,manifest:org.opencontainers.image.authors=dvdksn", "index,manifest:org.opencontainers.image.vendor=docker"}, m["webapp"].Annotations) + }) + + t.Run("ArgAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.arg.MY_VAR=value"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, ptrstr("value"), m["webapp"].Args["MY_VAR"]) + }) + + t.Run("ArgAliasFromEnv", func(t *testing.T) { + key := "VAR_FROM_ENV" + t.Name() + t.Setenv(key, "value") + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.arg." + key}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, ptrstr("value"), m["webapp"].Args[key]) + }) + + t.Run("ArgNameRequired", func(t *testing.T) { + _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.arg=value"}, nil, nil, &EntitlementConf{}) + require.EqualError(t, err, "invalid key webapp.arg, arg requires name") + }) + + t.Run("EntitlementAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.entitlements=security.insecure", "webapp.entitlement=network.host"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"security.insecure", "network.host"}, m["webapp"].Entitlements) + }) + + t.Run("ExtraHostAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.extra-hosts.legacy.example.com=127.0.0.2", "webapp.extra-host.example.com=127.0.0.1"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, ptrstr("127.0.0.2"), m["webapp"].ExtraHosts["legacy.example.com"]) + require.Equal(t, ptrstr("127.0.0.1"), m["webapp"].ExtraHosts["example.com"]) + }) + + t.Run("LabelAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.labels.com.example.legacy=old", "webapp.label.com.example.foo=bar"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, ptrstr("old"), m["webapp"].Labels["com.example.legacy"]) + require.Equal(t, ptrstr("bar"), m["webapp"].Labels["com.example.foo"]) + }) + t.Run("AttestOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.attest=type=sbom"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) @@ -185,6 +230,16 @@ target "webapp" { require.Equal(t, []string{"webapp"}, g["default"].Targets) }) + t.Run("ContextSubkeyInvalid", func(t *testing.T) { + _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.context.foo=bar"}, nil, nil, &EntitlementConf{}) + require.EqualError(t, err, "invalid key webapp.context.foo, context does not support subkeys") + }) + + t.Run("UnknownSubkey", func(t *testing.T) { + _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.bogus.foo=bar"}, nil, nil, &EntitlementConf{}) + require.EqualError(t, err, "unknown key: bogus") + }) + t.Run("NoCacheOverride", func(t *testing.T) { t.Parallel() m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.no-cache=false"}, nil, nil, &EntitlementConf{}) @@ -224,6 +279,23 @@ target "webapp" { require.Equal(t, []string{"linux/arm64", "linux/riscv64"}, m["webapp"].Platforms) }) + t.Run("PlatformsAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platforms=linux/arm64", "webapp.platform=linux/riscv64"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"linux/arm64", "linux/riscv64"}, m["webapp"].Platforms) + }) + + t.Run("TagAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.tags=example.com/foo", "webapp.tag+=example.com/bar"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, []string{"example.com/foo", "example.com/bar"}, m["webapp"].Tags) + }) + + t.Run("TagSubkeyInvalid", func(t *testing.T) { + _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.tag.foo=bar"}, nil, nil, &EntitlementConf{}) + require.EqualError(t, err, "invalid key webapp.tag.foo, tag does not support subkeys") + }) + t.Run("SecretsOverride", func(t *testing.T) { t.Setenv("FOO", "foo") t.Setenv("BAR", "bar") @@ -243,6 +315,36 @@ target "webapp" { require.Equal(t, "BAR", m["webapp"].Secrets[1].ID) }) + t.Run("SecretAliasOverride", func(t *testing.T) { + t.Setenv("BAR", "bar") + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret=id=BAR,env=BAR"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Len(t, m["webapp"].Secrets, 1) + require.Equal(t, "BAR", m["webapp"].Secrets[0].ID) + }) + + t.Run("SecretAliasAppend", func(t *testing.T) { + t.Setenv("BAR", "bar") + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret+=id=BAR,env=BAR"}, nil, 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("SecretAliasWithSourceOverride", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret=id=BAR,env=BAR", "webapp.secret.BAR=env=BAZ"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Len(t, m["webapp"].Secrets, 1) + require.Equal(t, "BAR", m["webapp"].Secrets[0].ID) + require.Equal(t, "BAZ", m["webapp"].Secrets[0].Env) + }) + + t.Run("SecretsSubkeyInvalid", func(t *testing.T) { + _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secrets.FOO=env=BAR"}, nil, nil, &EntitlementConf{}) + require.EqualError(t, err, "invalid key webapp.secrets.FOO, secret does not support subkeys") + }) + 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) @@ -298,10 +400,10 @@ target "webapp" { require.Equal(t, "host", *m["webapp"].NetworkMode) }) - t.Run("UlimitsOverride", func(t *testing.T) { - m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.ulimits=nofile=2048:2048"}, nil, nil, &EntitlementConf{}) + t.Run("UlimitAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.ulimits=nofile=2048:2048", "webapp.ulimit=nproc=1024:1024"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) - require.Equal(t, []string{"nofile=2048:2048"}, m["webapp"].Ulimits) + require.Equal(t, []string{"nofile=2048:2048", "nproc=1024:1024"}, m["webapp"].Ulimits) }) t.Run("ResourceLimitsOverride", func(t *testing.T) { @@ -319,6 +421,12 @@ target "webapp" { require.Error(t, err) }) + t.Run("ResourceAlias", func(t *testing.T) { + m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.resource.memory=512m"}, nil, nil, &EntitlementConf{}) + require.NoError(t, err) + require.Equal(t, ptrstr("512m"), m["webapp"].Resources.Memory) + }) + t.Run("PullOverride", func(t *testing.T) { t.Parallel() m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.pull=false"}, nil, nil, &EntitlementConf{}) diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index c272ed3c05fd..07b1ec60815e 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -428,38 +428,38 @@ Override target configurations from command line. The pattern matching syntax is defined in https://golang.org/pkg/path/#Match. ```console -$ docker buildx bake --set target.args.mybuildarg=value +$ docker buildx bake --set target.arg.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*.arg.mybuildarg=value # overrides build arg for all targets starting with 'foo' +$ docker buildx bake --set *.platform=linux/arm64 # overrides platforms 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 $ 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.resource.memory=2g # overrides memory resource limit $ docker buildx bake --set target.secret.aws=env=AWS # overrides source for an existing secret ``` > [!NOTE] > -> `--set` is a repeatable flag. For array fields such as `tags`, repeat `--set` +> `--set` is a repeatable flag. For array fields such as `tag`, repeat `--set` > to provide multiple values or use the `+=` operator to append without -> replacing. Array literal syntax like `--set target.tags=[a,b]` is not +> replacing. Array literal syntax like `--set target.tag=[a,b]` is not > supported. You can override the following fields: -* `annotations` +* `annotation` * `attest` -* `args` +* `arg.` * `cache-from` * `cache-to` * `call` * `context` -* `contexts` +* `contexts.` * `dockerfile` -* `entitlements` -* `extra-hosts` -* `labels` +* `entitlement` +* `extra-host.` +* `label.` * `load` * `no-cache` * `no-cache-filter` @@ -469,45 +469,70 @@ You can override the following fields: * `policy` * `pull` * `push` -* `resources` +* `resource.` +* `secret` * `secret.` -* `secrets` * `shm-size` * `ssh` -* `tags` +* `tag` * `target` -* `ulimits` +* `ulimit` You can append using `+=` operator for the following fields: -* `annotations`¹ +* `annotation`¹ * `attest`¹ * `cache-from` * `cache-to` -* `entitlements`¹ +* `entitlement`¹ * `no-cache-filter` * `output` * `platform` * `policy` -* `secrets` +* `secret` * `ssh` -* `tags` -* `ulimits` +* `tag` +* `ulimit` > [!NOTE] > ¹ These fields already append by default. +Override keys use singular names. Existing plural Bake field names and +historical `--set` spellings remain supported as aliases: + +| Alias | Canonical key | +|----------------|---------------| +| `annotations` | `annotation` | +| `args` | `arg` | +| `entitlements` | `entitlement` | +| `extra-hosts` | `extra-host` | +| `labels` | `label` | +| `platforms` | `platform` | +| `resources` | `resource` | +| `secrets` | `secret` | +| `tags` | `tag` | +| `ulimits` | `ulimit` | + +Aliases are normalized before overrides are applied, so they can be mixed +with canonical keys in repeated `--set` options. `context` and `contexts` are +not aliases: `context` sets the build context, while `contexts.` sets a +named context. `secret.` also has separate semantics: it changes the +source of a secret that is already declared by the target. The plural alias +does not support this form; `secrets.` is invalid. + #### Inline values for composable attributes -Some fields, such as `ssh`, `secret`, `output`, `cache-to`, `cache-from`, -`attest`, and `annotations`, are composable attributes that accept a list of -object values in a Bake file. When you override these fields with `--set`, you -provide each value using the same inline string syntax as the corresponding -build flag, not the HCL object form. The `--set` override replaces or appends -to the list as a whole; it doesn't address individual sub-fields with a -sub-selector. Only the map-valued fields `args`, `contexts`, `labels`, and -`extra-hosts` support targeting a specific entry with a sub-key (for example -`--set target.args.MYARG=value`). +Composable fields such as `ssh`, `secret`, `output`, `cache-to`, `cache-from`, +`attest`, and `annotation` accept a list of object values in a Bake file. When +you override these fields with `--set`, provide each value using the same +inline string syntax as the corresponding build flag, not the HCL object +form. The override replaces or appends to the list as a whole and does not +address individual object fields with a subkey. + +Only `arg`, `contexts`, `extra-host`, `label`, `resource`, and `secret` support +entry-addressed subkeys. For example, use `--set target.arg.MYARG=value` to +override one build argument. As described above, `secret.` changes the +source of an existing secret rather than an object field. For example, to set the SSH agent socket or key for a target, use the same `id=path` form accepted by [`build --ssh`](buildx_build.md#ssh):