diff --git a/build/opt.go b/build/opt.go index 4b5246c75181..a4535e679ac1 100644 --- a/build/opt.go +++ b/build/opt.go @@ -65,6 +65,11 @@ var sendGitQueryAsInput = sync.OnceValue(func() bool { return false }) +const ( + noDefaultAttestationsEnv = "BUILDX_NO_DEFAULT_ATTESTATIONS" + noDefaultOCIArtifactEnv = "BUILDX_NO_DEFAULT_OCI_ARTIFACT" +) + // policyExplicitlyDisabled reports whether the user passed `--policy // disabled=true`, which suppresses both user-defined and builtin default // policies. @@ -355,12 +360,11 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver } if _, ok := opt.Attests["provenance"]; !ok && supportAttestations { - const noAttestEnv = "BUILDX_NO_DEFAULT_ATTESTATIONS" var noProv bool - if v, ok := os.LookupEnv(noAttestEnv); ok { + if v, ok := os.LookupEnv(noDefaultAttestationsEnv); ok { noProv, err = strconv.ParseBool(v) if err != nil { - return nil, nil, errors.Wrap(err, "invalid "+noAttestEnv) + return nil, nil, errors.Wrap(err, "invalid "+noDefaultAttestationsEnv) } } if !noProv { @@ -436,6 +440,14 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver } opt.Exports = exports + var noDefaultOCIArtifact bool + if v, ok := os.LookupEnv(noDefaultOCIArtifactEnv); ok { + noDefaultOCIArtifact, err = strconv.ParseBool(v) + if err != nil { + return nil, nil, errors.Wrap(err, "invalid "+noDefaultOCIArtifactEnv) + } + } + // set up exporters for i, e := range opt.Exports { if e.Type == "oci" && !nodeDriver.Features(ctx)[driver.OCIExporter] { @@ -497,6 +509,14 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver opt.Exports[i].Attrs["buildinfo-attrs"] = v } } + if noDefaultOCIArtifact && supportAttestations { + switch opt.Exports[i].Type { + case client.ExporterImage, client.ExporterOCI, "moby": + if _, ok := opt.Exports[i].Attrs[string(exptypes.OptKeyOCIArtifact)]; !ok { + opt.Exports[i].Attrs[string(exptypes.OptKeyOCIArtifact)] = "false" + } + } + } } so.Exports = opt.Exports diff --git a/tests/bake.go b/tests/bake.go index ae26e64bf4b2..6e206e433acb 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -79,6 +79,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakeMetadataWarningsDedup, testBakeMultiExporters, testBakeLoadPush, + testBakeNoDefaultOCIArtifact, testBakeListTargets, testBakeListVariables, testBakeListTypedVariables, @@ -2232,6 +2233,41 @@ target "default" { // TODO: test metadata file when supported by multi exporters https://github.com/docker/buildx/issues/2181 } +func testBakeNoDefaultOCIArtifact(t *testing.T, sb integration.Sandbox) { + if isMobyWorker(sb) { + t.Skip("attestations are not supported by the docker worker") + } + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildx/bake-no-default-oci-artifact:latest" + + dockerfile := []byte(` +FROM scratch +COPY foo /foo +`) + bakefile := fmt.Appendf(nil, ` +target "default" { + output = ["type=image,name=%s,push=true"] + attest = ["type=provenance"] +} +`, target) + dir := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + fstest.CreateFile("Dockerfile", dockerfile, 0600), + fstest.CreateFile("foo", []byte("foo"), 0600), + ) + + out, err := bakeCmd(sb, withDir(dir), withEnv("BUILDX_NO_DEFAULT_OCI_ARTIFACT=true")) + require.NoError(t, err, string(out)) + + requireLegacyAttestationStorage(t, sb, target) +} + func testBakeLoadPush(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker") diff --git a/tests/build.go b/tests/build.go index 1ed34642e193..63fec7d1fd5b 100644 --- a/tests/build.go +++ b/tests/build.go @@ -66,6 +66,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){ testBuildLocalExportDeleteMode, testBuildRegistryExport, testBuildRegistryExportAttestations, + testBuildRegistryExportNoDefaultOCIArtifact, testBuildTarExport, testBuildMobyFromLocalImage, testBuildDetailsLink, @@ -633,6 +634,60 @@ func testBuildRegistryExportAttestations(t *testing.T, sb integration.Sandbox) { require.Len(t, att.Layers, 1) } +func testBuildRegistryExportNoDefaultOCIArtifact(t *testing.T, sb integration.Sandbox) { + if isMobyWorker(sb) { + t.Skip("attestations are not supported by the docker worker") + } + + dir := createTestProject(t) + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildx/registry-no-default-oci-artifact:latest" + + out, err := buildCmd(sb, + withEnv("BUILDX_NO_DEFAULT_OCI_ARTIFACT=true"), + withArgs(fmt.Sprintf("--output=type=image,name=%s,push=true", target), "--provenance=true", dir), + ) + require.NoError(t, err, string(out)) + + requireLegacyAttestationStorage(t, sb, target) +} + +func requireLegacyAttestationStorage(t *testing.T, sb integration.Sandbox, ref string) { + t.Helper() + + cmd := buildxCmd(sb, withArgs("imagetools", "inspect", ref, "--raw")) + dt, err := cmd.CombinedOutput() + require.NoError(t, err, string(dt)) + + var idx ocispecs.Index + err = json.Unmarshal(dt, &idx) + require.NoError(t, err) + + var attestation ocispecs.Descriptor + for _, desc := range idx.Manifests { + if desc.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { + attestation = desc + break + } + } + require.NotEmpty(t, attestation.Digest) + + cmd = buildxCmd(sb, withArgs("imagetools", "inspect", ref+"@"+attestation.Digest.String(), "--raw")) + dt, err = cmd.CombinedOutput() + require.NoError(t, err, string(dt)) + + var mfst ocispecs.Manifest + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + require.Nil(t, mfst.Subject) + require.NotEmpty(t, mfst.Layers) +} + func testImageIDOutput(t *testing.T, sb integration.Sandbox) { dockerfile := []byte(`FROM busybox:latest`)