diff --git a/bake/bake.go b/bake/bake.go index 37d9ec4b..44c961f0 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -2,6 +2,7 @@ package bake import ( "context" + "encoding/csv" "fmt" "io/ioutil" "os" @@ -29,6 +30,11 @@ type File struct { Data []byte } +type Override struct { + Key string + Value string +} + func defaultFilenames() []string { return []string{ "docker-compose.yml", // support app @@ -240,8 +246,8 @@ func (c Config) expandTargets(pattern string) ([]string, error) { return names, nil } -func (c Config) newOverrides(v []string) (map[string]*Target, error) { - m := map[string]*Target{} +func (c Config) newOverrides(v []string) (map[string][]Override, error) { + m := map[string][]Override{} for _, v := range v { parts := strings.SplitN(v, "=", 2) @@ -260,72 +266,34 @@ func (c Config) newOverrides(v []string) (map[string]*Target, error) { return nil, err } + kk := strings.SplitN(parts[0], ".", 2) + for _, name := range names { - t, ok := m[name] - if !ok { - t = &Target{} + t := m[name] + + o := Override{ + Key: kk[1], + } + if len(parts) == 2 { + o.Value = parts[1] } 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 + if !ok { + continue } - } 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]) - case "no-cache": - noCache, err := strconv.ParseBool(parts[1]) - if err != nil { - return nil, errors.Errorf("invalid value %s for boolean key no-cache", parts[1]) - } - t.NoCache = &noCache - case "pull": - pull, err := strconv.ParseBool(parts[1]) - if err != nil { - return nil, errors.Errorf("invalid value %s for boolean key pull", parts[1]) + o.Value = v } - t.Pull = &pull - default: - return nil, errors.Errorf("unknown key: %s", keys[1]) } + + t = append(t, o) + m[name] = t } } @@ -358,7 +326,7 @@ func (c Config) group(name string, visited map[string]struct{}) []string { return targets } -func (c Config) ResolveTarget(name string, overrides map[string]*Target) (*Target, error) { +func (c Config) ResolveTarget(name string, overrides map[string][]Override) (*Target, error) { t, err := c.target(name, map[string]struct{}{}, overrides) if err != nil { return nil, err @@ -374,7 +342,7 @@ func (c Config) ResolveTarget(name string, overrides map[string]*Target) (*Targe return t, nil } -func (c Config) target(name string, visited map[string]struct{}, overrides map[string]*Target) (*Target, error) { +func (c Config) target(name string, visited map[string]struct{}, overrides map[string][]Override) (*Target, error) { if _, ok := visited[name]; ok { return nil, nil } @@ -404,9 +372,10 @@ func (c Config) target(name string, visited map[string]struct{}, overrides map[s m.Merge(tt) m.Merge(t) tt = m - if override, ok := overrides[name]; ok { - tt.Merge(override) + if err := tt.AddOverrides(overrides[name]); err != nil { + return nil, err } + tt.normalize() return tt, nil } @@ -507,6 +476,81 @@ func (t *Target) Merge(t2 *Target) { t.Inherits = append(t.Inherits, t2.Inherits...) } +func (t *Target) AddOverrides(overrides []Override) error { + for _, o := range overrides { + value := o.Value + keys := strings.SplitN(o.Key, ".", 2) + switch keys[0] { + case "context": + t.Context = &value + case "dockerfile": + t.Dockerfile = &value + case "args": + if len(keys) != 2 { + return errors.Errorf("args require name") + } + if t.Args == nil { + t.Args = map[string]string{} + } + t.Args[keys[1]] = value + + case "labels": + if len(keys) != 2 { + return errors.Errorf("labels require name") + } + if t.Labels == nil { + t.Labels = map[string]string{} + } + t.Labels[keys[1]] = value + case "tags": + t.Tags = append(t.Tags, value) + case "cache-from": + t.CacheFrom = append(t.CacheFrom, value) + case "cache-to": + t.CacheTo = append(t.CacheTo, value) + case "target": + t.Target = &value + case "secrets": + t.Secrets = append(t.Secrets, value) + case "ssh": + t.SSH = append(t.SSH, value) + case "platform": + t.Platforms = append(t.Platforms, value) + case "output": + t.Outputs = append(t.Outputs, value) + case "no-cache": + noCache, err := strconv.ParseBool(value) + if err != nil { + return errors.Errorf("invalid value %s for boolean key no-cache", value) + } + t.NoCache = &noCache + case "pull": + pull, err := strconv.ParseBool(value) + if err != nil { + return errors.Errorf("invalid value %s for boolean key pull", value) + } + t.Pull = &pull + case "push": + _, err := strconv.ParseBool(value) + if err != nil { + return errors.Errorf("invalid value %s for boolean key push", value) + } + if len(t.Outputs) == 0 { + t.Outputs = append(t.Outputs, "type=image,push=true") + } else { + for i, output := range t.Outputs { + if typ := parseOutputType(output); typ == "image" || typ == "registry" { + t.Outputs[i] = t.Outputs[i] + ",push=" + value + } + } + } + default: + return errors.Errorf("unknown key: %s", keys[0]) + } + } + return nil +} + func TargetsToBuildOpt(m map[string]*Target, inp *Input) (map[string]build.Options, error) { m2 := make(map[string]build.Options, len(m)) for k, v := range m { @@ -666,3 +710,20 @@ func removeDupes(s []string) []string { func isRemoteResource(str string) bool { return urlutil.IsGitURL(str) || urlutil.IsURL(str) } + +func parseOutputType(str string) string { + csvReader := csv.NewReader(strings.NewReader(str)) + fields, err := csvReader.Read() + if err != nil { + return "" + } + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + if len(parts) == 2 { + if parts[0] == "type" { + return parts[1] + } + } + } + return "" +} diff --git a/bake/bake_test.go b/bake/bake_test.go index 9542eecd..ebce5dd2 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -179,6 +179,51 @@ target "webapp" { }) } +func TestPushOverride(t *testing.T) { + t.Parallel() + + fp := File{ + Name: "docker-bake.hc", + Data: []byte( + `target "app" { + output = ["type=image,compression=zstd"] + }`), + } + ctx := context.TODO() + m, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"*.push=true"}, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m["app"].Outputs)) + require.Equal(t, "type=image,compression=zstd,push=true", m["app"].Outputs[0]) + + fp = File{ + Name: "docker-bake.hc", + Data: []byte( + `target "app" { + output = ["type=image,compression=zstd"] + }`), + } + ctx = context.TODO() + m, _, err = ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"*.push=false"}, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m["app"].Outputs)) + require.Equal(t, "type=image,compression=zstd,push=false", m["app"].Outputs[0]) + + fp = File{ + Name: "docker-bake.hc", + Data: []byte( + `target "app" { + }`), + } + ctx = context.TODO() + m, _, err = ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"*.push=true"}, nil) + require.NoError(t, err) + + require.Equal(t, 1, len(m["app"].Outputs)) + require.Equal(t, "type=image,push=true", m["app"].Outputs[0]) +} + func TestReadTargetsCompose(t *testing.T) { t.Parallel() @@ -228,7 +273,6 @@ services: } func TestHCLCwdPrefix(t *testing.T) { - fp := File{ Name: "docker-bake.hc", Data: []byte( diff --git a/commands/bake.go b/commands/bake.go index 48e9684b..0ab0f7c4 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -64,7 +64,7 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions) (err error if in.exportLoad { return errors.Errorf("push and load may not be set together at the moment") } - overrides = append(overrides, "*.output=type=registry") + overrides = append(overrides, "*.push=true") } else if in.exportLoad { overrides = append(overrides, "*.output=type=docker") } diff --git a/util/buildflags/output.go b/util/buildflags/output.go index b55be289..0382d8f1 100644 --- a/util/buildflags/output.go +++ b/util/buildflags/output.go @@ -100,7 +100,9 @@ func ParseOutputs(inp []string) ([]client.ExportEntry, error) { delete(out.Attrs, "dest") case "registry": out.Type = client.ExporterImage - out.Attrs["push"] = "true" + if _, ok := out.Attrs["push"]; !ok { + out.Attrs["push"] = "true" + } } outs = append(outs, out)