diff --git a/README.md b/README.md index c4d7736d..175d7d69 100644 --- a/README.md +++ b/README.md @@ -507,11 +507,13 @@ Options: | Flag | Description | | --- | --- | | -f, --file stringArray | Build definition file +| --load | Shorthand for --set=*.output=type=docker | --no-cache | Do not use cache when building the image | --print | Print the options without building | --progress string | Set type of progress output (auto, plain, tty). Use plain to show container output (default "auto") | --pull | Always attempt to pull a newer version of the image -| --set stringArray | Override target value (eg: target.key=value) +| --push | Shorthand for --set=*.output=type=registry +| --set stringArray | Override target value (eg: targetpattern.key=value) #### `-f, --file FILE` @@ -554,14 +556,16 @@ Same as `build --progress`. Set type of progress output (auto, plain, tty). Use Same as `build --pull`. -#### `--set target.key[.subkey]=value` +#### `--set targetpattern.key[.subkey]=value` -Override target configurations from command line. +Override target configurations from command line. The pattern matching syntax is defined in https://golang.org/pkg/path/#Match. Example: ``` 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 ``` #### File definition diff --git a/bake/bake.go b/bake/bake.go index 6ec277f9..fbabd5db 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -106,6 +106,26 @@ func mergeConfig(c1, c2 Config) Config { return c1 } +func (c Config) expandTargets(pattern string) ([]string, error) { + if _, ok := c.Target[pattern]; ok { + return []string{pattern}, nil + } + var names []string + for name := range c.Target { + ok, err := path.Match(pattern, name) + if err != nil { + return nil, errors.Wrapf(err, "could not match targets with '%s'", pattern) + } + if ok { + names = append(names, name) + } + } + if len(names) == 0 { + return nil, errors.Errorf("could not find any target matching '%s'", pattern) + } + return names, nil +} + func (c Config) newOverrides(v []string) (map[string]Target, error) { m := map[string]Target{} for _, v := range v { @@ -116,65 +136,69 @@ func (c Config) newOverrides(v []string) (map[string]Target, error) { return nil, errors.Errorf("invalid override key %s, expected target.name", parts[0]) } - name := keys[0] + pattern := keys[0] if len(parts) != 2 && keys[1] != "args" { return nil, errors.Errorf("invalid override %s, expected target.name=value", v) } - t := m[name] - if _, ok := c.Target[name]; !ok { - return nil, errors.Errorf("unknown target %s", name) + names, err := c.expandTargets(pattern) + if err != nil { + return nil, err } - switch keys[1] { - case "context": - t.Context = &parts[1] - case "dockerfile": - t.Dockerfile = &parts[1] - case "args": - if len(keys) != 3 { - return nil, errors.Errorf("invalid key %s, args requires name", parts[0]) - } - if t.Args == nil { - t.Args = map[string]string{} - } - if len(parts) < 2 { - v, ok := os.LookupEnv(keys[2]) - if ok { - t.Args[keys[2]] = v + for _, name := range names { + t := m[name] + + switch keys[1] { + case "context": + t.Context = &parts[1] + case "dockerfile": + t.Dockerfile = &parts[1] + case "args": + if len(keys) != 3 { + return nil, errors.Errorf("invalid key %s, args requires name", parts[0]) } - } else { - t.Args[keys[2]] = parts[1] - } - case "labels": - if len(keys) != 3 { - return nil, errors.Errorf("invalid key %s, lanels requires name", parts[0]) - } - if t.Labels == nil { - t.Labels = map[string]string{} + if t.Args == nil { + t.Args = map[string]string{} + } + if len(parts) < 2 { + v, ok := os.LookupEnv(keys[2]) + if ok { + t.Args[keys[2]] = v + } + } else { + t.Args[keys[2]] = parts[1] + } + case "labels": + if len(keys) != 3 { + return nil, errors.Errorf("invalid key %s, lanels requires name", parts[0]) + } + if t.Labels == nil { + t.Labels = map[string]string{} + } + t.Labels[keys[2]] = parts[1] + case "tags": + t.Tags = append(t.Tags, parts[1]) + case "cache-from": + t.CacheFrom = append(t.CacheFrom, parts[1]) + case "cache-to": + t.CacheTo = append(t.CacheTo, parts[1]) + case "target": + s := parts[1] + t.Target = &s + case "secrets": + t.Secrets = append(t.Secrets, parts[1]) + case "ssh": + t.SSH = append(t.SSH, parts[1]) + case "platform": + t.Platforms = append(t.Platforms, parts[1]) + case "output": + t.Outputs = append(t.Outputs, parts[1]) + default: + return nil, errors.Errorf("unknown key: %s", keys[1]) } - t.Labels[keys[2]] = parts[1] - case "tags": - t.Tags = append(t.Tags, parts[1]) - case "cache-from": - t.CacheFrom = append(t.CacheFrom, parts[1]) - case "cache-to": - t.CacheTo = append(t.CacheTo, parts[1]) - case "target": - s := parts[1] - t.Target = &s - case "secrets": - t.Secrets = append(t.Secrets, parts[1]) - case "ssh": - t.SSH = append(t.SSH, parts[1]) - case "platform": - t.Platforms = append(t.Platforms, parts[1]) - case "output": - t.Outputs = append(t.Outputs, parts[1]) - default: - return nil, errors.Errorf("unknown key: %s", keys[1]) + m[name] = t } - m[name] = t } return m, nil } diff --git a/bake/bake_test.go b/bake/bake_test.go index cc004d0b..6c741caa 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -18,10 +18,10 @@ func TestReadTargets(t *testing.T) { fp := filepath.Join(tmpdir, "config.hcl") err = ioutil.WriteFile(fp, []byte(` -target "dep" { +target "webDEP" { args { - VAR_INHERITED = "dep" - VAR_BOTH = "dep" + VAR_INHERITED = "webDEP" + VAR_BOTH = "webDEP" } } @@ -30,7 +30,7 @@ target "webapp" { args { VAR_BOTH = "webapp" } - inherits = ["dep"] + inherits = ["webDEP"] }`), 0600) require.NoError(t, err) @@ -43,13 +43,13 @@ target "webapp" { require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) require.Equal(t, ".", *m["webapp"].Context) - require.Equal(t, "dep", m["webapp"].Args["VAR_INHERITED"]) + require.Equal(t, "webDEP", m["webapp"].Args["VAR_INHERITED"]) }) t.Run("InvalidTargetOverrides", func(t *testing.T) { _, err := ReadTargets(ctx, []string{fp}, []string{"webapp"}, []string{"nosuchtarget.context=foo"}) require.NotNil(t, err) - require.Equal(t, err.Error(), "unknown target nosuchtarget") + require.Equal(t, err.Error(), "could not find any target matching 'nosuchtarget'") }) t.Run("ArgsOverrides", func(t *testing.T) { @@ -87,8 +87,8 @@ target "webapp" { // building leaf but overriding parent fields t.Run("parent", func(t *testing.T) { m, err := ReadTargets(ctx, []string{fp}, []string{"webapp"}, []string{ - "dep.args.VAR_INHERITED=override", - "dep.args.VAR_BOTH=override", + "webDEP.args.VAR_INHERITED=override", + "webDEP.args.VAR_BOTH=override", }) require.NoError(t, err) require.Equal(t, m["webapp"].Args["VAR_INHERITED"], "override") @@ -106,6 +106,66 @@ target "webapp" { require.Equal(t, "foo", *m["webapp"].Context) }) + t.Run("PatternOverride", func(t *testing.T) { + // same check for two cases + multiTargetCheck := func(t *testing.T, m map[string]Target, err error) { + require.NoError(t, err) + require.Equal(t, 2, len(m)) + require.Equal(t, "foo", *m["webapp"].Dockerfile) + require.Equal(t, "webDEP", m["webapp"].Args["VAR_INHERITED"]) + require.Equal(t, "foo", *m["webDEP"].Dockerfile) + require.Equal(t, "webDEP", m["webDEP"].Args["VAR_INHERITED"]) + } + + cases := []struct { + name string + targets []string + overrides []string + check func(*testing.T, map[string]Target, error) + }{ + { + name: "multi target single pattern", + targets: []string{"webapp", "webDEP"}, + overrides: []string{"web*.dockerfile=foo"}, + check: multiTargetCheck, + }, + { + name: "multi target multi pattern", + targets: []string{"webapp", "webDEP"}, + overrides: []string{"web*.dockerfile=foo", "*.args.VAR_BOTH=bar"}, + check: multiTargetCheck, + }, + { + name: "single target", + targets: []string{"webapp"}, + overrides: []string{"web*.dockerfile=foo"}, + check: func(t *testing.T, m map[string]Target, err error) { + require.NoError(t, err) + require.Equal(t, 1, len(m)) + require.Equal(t, "foo", *m["webapp"].Dockerfile) + require.Equal(t, "webDEP", m["webapp"].Args["VAR_INHERITED"]) + }, + }, + { + name: "nomatch", + targets: []string{"webapp"}, + overrides: []string{"nomatch*.dockerfile=foo"}, + check: func(t *testing.T, m map[string]Target, err error) { + // NOTE: I am unsure whether failing to match should always error out + // instead of simply skipping that override. + // Let's enforce the error and we can relax it later if users complain. + require.NotNil(t, err) + require.Equal(t, err.Error(), "could not find any target matching 'nomatch*'") + }, + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + m, err := ReadTargets(ctx, []string{fp}, test.targets, test.overrides) + test.check(t, m, err) + }) + } + }) } func TestReadTargetsCompose(t *testing.T) { diff --git a/commands/bake.go b/commands/bake.go index edc93d23..fa1daf35 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -37,7 +37,17 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions) error { targets = []string{"default"} } - m, err := bake.ReadTargets(ctx, in.files, targets, in.overrides) + overrides := in.overrides + if in.exportPush { + if in.exportLoad { + return errors.Errorf("push and load may not be set together at the moment") + } + overrides = append(overrides, "*.output=type=registry") + } else if in.exportLoad { + overrides = append(overrides, "*.output=type=docker") + } + + m, err := bake.ReadTargets(ctx, in.files, targets, overrides) if err != nil { return err } @@ -99,7 +109,9 @@ func bakeCmd(dockerCli command.Cli) *cobra.Command { flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Build definition file") flags.BoolVar(&options.printOnly, "print", false, "Print the options without building") - flags.StringArrayVar(&options.overrides, "set", nil, "Override target value (eg: target.key=value)") + flags.StringArrayVar(&options.overrides, "set", nil, "Override target value (eg: targetpattern.key=value)") + flags.BoolVar(&options.exportPush, "push", false, "Shorthand for --set=*.output=type=registry") + flags.BoolVar(&options.exportLoad, "load", false, "Shorthand for --set=*.output=type=docker") commonFlags(&options.commonOptions, flags) diff --git a/commands/build.go b/commands/build.go index 1e865de3..86274df0 100644 --- a/commands/build.go +++ b/commands/build.go @@ -38,9 +38,6 @@ type buildOptions struct { extraHosts []string networkMode string - exportPush bool - exportLoad bool - // unimplemented squash bool quiet bool @@ -65,9 +62,11 @@ type buildOptions struct { } type commonOptions struct { - noCache bool - progress string - pull bool + noCache bool + progress string + pull bool + exportPush bool + exportLoad bool } func runBuild(dockerCli command.Cli, in buildOptions) error {