diff --git a/bake/bake.go b/bake/bake.go index 8facfb79..23ac031f 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -894,19 +894,17 @@ func (t *Target) AddOverrides(overrides map[string]Override) error { } t.Pull = &pull case "push": - _, err := strconv.ParseBool(value) + 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 - } - } + t.Outputs = setPushOverride(t.Outputs, push) + case "load": + load, err := strconv.ParseBool(value) + if err != nil { + return errors.Errorf("invalid value %s for boolean key load", value) } + t.Outputs = setLoadOverride(t.Outputs, load) default: return errors.Errorf("unknown key: %s", keys[0]) } @@ -1394,23 +1392,90 @@ func removeAttestDupes(s []string) []string { return res } -func parseOutputType(str string) string { +func parseOutput(str string) map[string]string { csvReader := csv.NewReader(strings.NewReader(str)) fields, err := csvReader.Read() if err != nil { - return "" + return nil } + res := map[string]string{} for _, field := range fields { parts := strings.SplitN(field, "=", 2) if len(parts) == 2 { - if parts[0] == "type" { - return parts[1] - } + res[parts[0]] = parts[1] + } + } + return res +} + +func parseOutputType(str string) string { + if out := parseOutput(str); out != nil { + if v, ok := out["type"]; ok { + return v } } return "" } +func setPushOverride(outputs []string, push bool) []string { + var out []string + setPush := true + for _, output := range outputs { + typ := parseOutputType(output) + if typ == "image" || typ == "registry" { + // no need to set push if image or registry types already defined + setPush = false + if typ == "registry" { + if !push { + // don't set registry output if "push" is false + continue + } + // no need to set "push" attribute to true for registry + out = append(out, output) + continue + } + out = append(out, output+",push="+strconv.FormatBool(push)) + } else { + if typ != "docker" { + // if there is any output that is not docker, don't set "push" + setPush = false + } + out = append(out, output) + } + } + if push && setPush { + out = append(out, "type=image,push=true") + } + return out +} + +func setLoadOverride(outputs []string, load bool) []string { + if !load { + return outputs + } + setLoad := true + for _, output := range outputs { + if typ := parseOutputType(output); typ == "docker" { + if v := parseOutput(output); v != nil { + // dest set means we want to output as tar so don't set load + if _, ok := v["dest"]; !ok { + setLoad = false + break + } + } + } else if typ != "image" && typ != "registry" && typ != "oci" { + // if there is any output that is not an image, registry + // or oci, don't set "load" similar to push override + setLoad = false + break + } + } + if setLoad { + outputs = append(outputs, "type=docker") + } + return outputs +} + func validateTargetName(name string) error { if !targetNamePattern.MatchString(name) { return errors.Errorf("only %q are allowed", validTargetNameChars) diff --git a/bake/bake_test.go b/bake/bake_test.go index 80d96d50..fe21d2b5 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -217,67 +217,252 @@ target "webapp" { } func TestPushOverride(t *testing.T) { - t.Parallel() + t.Run("empty output", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + }`), + } + m, _, err := ReadTargets(context.TODO(), []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]) + }) - fp := File{ - Name: "docker-bake.hcl", - Data: []byte( - `target "app" { + t.Run("type image", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + 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) + } + m, _, err := ReadTargets(context.TODO(), []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]) + }) - 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.hcl", - Data: []byte( - `target "app" { + t.Run("type image push false", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + 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) + } + m, _, err := ReadTargets(context.TODO(), []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]) + }) - 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.hcl", - Data: []byte( - `target "app" { + t.Run("type registry", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=registry"] }`), - } - ctx = context.TODO() - m, _, err = ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"*.push=true"}, nil) - require.NoError(t, err) + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=true"}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(m["app"].Outputs)) + require.Equal(t, "type=registry", m["app"].Outputs[0]) + }) - require.Equal(t, 1, len(m["app"].Outputs)) - require.Equal(t, "type=image,push=true", m["app"].Outputs[0]) + t.Run("type registry push false", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=registry"] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=false"}, nil) + require.NoError(t, err) + require.Equal(t, 0, len(m["app"].Outputs)) + }) - fp = File{ - Name: "docker-bake.hcl", - Data: []byte( - `target "foo" { + t.Run("type local and empty target", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "foo" { output = [ "type=local,dest=out" ] } target "bar" { }`), - } - ctx = context.TODO() - m, _, err = ReadTargets(ctx, []File{fp}, []string{"foo", "bar"}, []string{"*.push=true"}, nil) - require.NoError(t, err) + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo", "bar"}, []string{"*.push=true"}, nil) + require.NoError(t, err) + require.Equal(t, 2, len(m)) + require.Equal(t, 1, len(m["foo"].Outputs)) + require.Equal(t, []string{"type=local,dest=out"}, m["foo"].Outputs) + require.Equal(t, 1, len(m["bar"].Outputs)) + require.Equal(t, []string{"type=image,push=true"}, m["bar"].Outputs) + }) +} - require.Equal(t, 2, len(m)) - require.Equal(t, 1, len(m["foo"].Outputs)) - require.Equal(t, []string{"type=local,dest=out"}, m["foo"].Outputs) - require.Equal(t, 1, len(m["bar"].Outputs)) - require.Equal(t, []string{"type=image,push=true"}, m["bar"].Outputs) +func TestLoadOverride(t *testing.T) { + t.Run("empty output", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(m["app"].Outputs)) + require.Equal(t, "type=docker", m["app"].Outputs[0]) + }) + + t.Run("type docker", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=docker"] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(m["app"].Outputs)) + require.Equal(t, []string{"type=docker"}, m["app"].Outputs) + }) + + t.Run("type image", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=image"] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil) + require.NoError(t, err) + require.Equal(t, 2, len(m["app"].Outputs)) + require.Equal(t, []string{"type=image", "type=docker"}, m["app"].Outputs) + }) + + t.Run("type image load false", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=image"] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=false"}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(m["app"].Outputs)) + require.Equal(t, []string{"type=image"}, m["app"].Outputs) + }) + + t.Run("type registry", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=registry"] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil) + require.NoError(t, err) + require.Equal(t, 2, len(m["app"].Outputs)) + require.Equal(t, []string{"type=registry", "type=docker"}, m["app"].Outputs) + }) + + t.Run("type oci", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=oci,dest=out"] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil) + require.NoError(t, err) + require.Equal(t, 2, len(m["app"].Outputs)) + require.Equal(t, []string{"type=oci,dest=out", "type=docker"}, m["app"].Outputs) + }) + + t.Run("type docker with dest", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + output = ["type=docker,dest=out"] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil) + require.NoError(t, err) + require.Equal(t, 2, len(m["app"].Outputs)) + require.Equal(t, []string{"type=docker,dest=out", "type=docker"}, m["app"].Outputs) + }) + + t.Run("type local and empty target", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "foo" { + output = [ "type=local,dest=out" ] + } + target "bar" { + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo", "bar"}, []string{"*.load=true"}, nil) + require.NoError(t, err) + require.Equal(t, 2, len(m)) + require.Equal(t, 1, len(m["foo"].Outputs)) + require.Equal(t, []string{"type=local,dest=out"}, m["foo"].Outputs) + require.Equal(t, 1, len(m["bar"].Outputs)) + require.Equal(t, []string{"type=docker"}, m["bar"].Outputs) + }) +} + +func TestLoadAndPushOverride(t *testing.T) { + t.Run("type local and empty target", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "foo" { + output = [ "type=local,dest=out" ] + } + target "bar" { + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo", "bar"}, []string{"*.load=true", "*.push=true"}, nil) + require.NoError(t, err) + require.Equal(t, 2, len(m)) + + require.Equal(t, 1, len(m["foo"].Outputs)) + sort.Strings(m["foo"].Outputs) + require.Equal(t, []string{"type=local,dest=out"}, m["foo"].Outputs) + + require.Equal(t, 2, len(m["bar"].Outputs)) + sort.Strings(m["bar"].Outputs) + require.Equal(t, []string{"type=docker", "type=image,push=true"}, m["bar"].Outputs) + }) + + t.Run("type registry", func(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "foo" { + output = [ "type=registry" ] + }`), + } + m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo"}, []string{"*.load=true", "*.push=true"}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(m)) + + require.Equal(t, 2, len(m["foo"].Outputs)) + sort.Strings(m["foo"].Outputs) + require.Equal(t, []string{"type=docker", "type=registry"}, m["foo"].Outputs) + }) } func TestReadTargetsCompose(t *testing.T) { diff --git a/commands/bake.go b/commands/bake.go index 14e3d4a3..3747add7 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -75,7 +75,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba overrides = append(overrides, "*.push=true") } if in.exportLoad { - overrides = append(overrides, "*.output=type=docker") + overrides = append(overrides, "*.load=true") } if cFlags.noCache != nil { overrides = append(overrides, fmt.Sprintf("*.no-cache=%t", *cFlags.noCache)) diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 30d7ccfd..21c7571d 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -162,6 +162,7 @@ You can override the following fields: * `context` * `dockerfile` * `labels` +* `load` * `no-cache` * `no-cache-filter` * `output` diff --git a/tests/bake.go b/tests/bake.go index ce1b4c65..0de3c497 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -814,11 +814,11 @@ target "default" { outb, err := cmd.CombinedOutput() require.NoError(t, err, string(outb)) - // TODO: test registry when --load case fixed for bake (currently overrides --push) - //desc, provider, err := contentutil.ProviderFromRef(target) - //require.NoError(t, err) - //_, err = testutil.ReadImages(sb.Context(), provider, desc) - //require.NoError(t, err) + // test registry + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + _, err = testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) // test docker store cmd = dockerCmd(sb, withArgs("image", "inspect", target))