package tests import ( "bytes" "context" "encoding/json" "os" "os/exec" "path" "path/filepath" "strings" "testing" "github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/remotes/docker" "github.com/containerd/continuity/fs/fstest" "github.com/containerd/errdefs" "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/docker/buildx/util/resolver" "github.com/moby/buildkit/util/testutil/integration" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/stretchr/testify/require" ) var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsCopyManifest, testImagetoolsCopyIndex, testImagetoolsInspectAndFilter, testImagetoolsCreatePlatformFilter, testImagetoolsOCILayoutInspect, testImagetoolsOCILayoutCreateSourceAndTarget, testImagetoolsOCILayoutReferrers, testImagetoolsOCILayoutExistingContent, testImagetoolsOCILayoutMergeSources, testImagetoolsOCILayoutTargetDigest, testImagetoolsAppend, testImagetoolsFile, testImagetoolsAnnotation, testImagetoolsMergeSources, testImagetoolsMergeSourcesWithAttestations, testImagetoolsMergeSourcesWithFallbackAttestations, testImagetoolsCopyAttestationWithSignature, } // testImagetoolsCopyManifest verifies create/inspect behavior for a single-platform image. func testImagetoolsCopyManifest(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfile(t) registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) target := registry + "/buildx/imtools-manifest:latest" out, err := buildCmd(sb, withArgs("-t", target, "--push", "--platform=linux/amd64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", target, "--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.Equal(t, defaultManifestMediaType(t, sb), mfst.MediaType) registry2, err := sb.NewRegistry() require.NoError(t, err) target2 := registry2 + "/buildx/imtools2-manifest:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "--metadata-file", path.Join(dir, "md.json"), "-t", target2, target)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) mddt, err := os.ReadFile(filepath.Join(dir, "md.json")) require.NoError(t, err) type mdT struct { ImageDescriptor ocispecs.Descriptor `json:"containerimage.descriptor"` ImageName string `json:"image.name"` } var md mdT err = json.Unmarshal(mddt, &md) require.NoError(t, err) require.NotEmpty(t, md.ImageDescriptor) require.Equal(t, registry2+"/buildx/imtools2-manifest", md.ImageName) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx2 ocispecs.Index err = json.Unmarshal(dt, &idx2) require.NoError(t, err) require.Equal(t, defaultIndexMediaType(t, sb), idx2.MediaType) require.Equal(t, 1, len(idx2.Manifests)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2+"@"+string(idx2.Manifests[0].Digest), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var mfst2 ocispecs.Manifest err = json.Unmarshal(dt, &mfst2) require.NoError(t, err) require.Equal(t, defaultManifestMediaType(t, sb), mfst2.MediaType) require.Equal(t, mfst.Config.Digest, mfst2.Config.Digest) require.Equal(t, len(mfst.Layers), len(mfst2.Layers)) for i := range mfst.Layers { require.Equal(t, mfst.Layers[i].Digest, mfst2.Layers[i].Digest) } cmd = buildxCmd(sb, withArgs("imagetools", "create", "--prefer-index=false", "-t", target2+"-not-index", target)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2+"-not-index", "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var mfst3 ocispecs.Manifest err = json.Unmarshal(dt, &mfst3) require.NoError(t, err) require.Equal(t, defaultManifestMediaType(t, sb), mfst3.MediaType) } // testImagetoolsCopyIndex verifies create/inspect behavior for a multi-platform index. func testImagetoolsCopyIndex(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfile(t) registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) target := registry + "/buildx/imtools:latest" out, err := buildCmd(sb, withArgs("-t", target, "--push", "--platform=linux/amd64,linux/arm64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) sourceDigest := digest.FromBytes(dt) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Equal(t, defaultIndexMediaType(t, sb), idx.MediaType) require.Equal(t, 2, len(idx.Manifests)) registry2, err := sb.NewRegistry() require.NoError(t, err) target2 := registry2 + "/buildx/imtools2:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "--metadata-file", path.Join(dir, "md.json"), "-t", target2, target)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) mddt, err := os.ReadFile(filepath.Join(dir, "md.json")) require.NoError(t, err) type mdT struct { ImageDescriptor ocispecs.Descriptor `json:"containerimage.descriptor"` ImageName string `json:"image.name"` } var md mdT err = json.Unmarshal(mddt, &md) require.NoError(t, err) require.NotEmpty(t, md.ImageDescriptor) require.Equal(t, registry2+"/buildx/imtools2", md.ImageName) require.Equal(t, sourceDigest, md.ImageDescriptor.Digest) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) require.Equal(t, sourceDigest, digest.FromBytes(dt)) var idx2 ocispecs.Index err = json.Unmarshal(dt, &idx2) require.NoError(t, err) require.Equal(t, defaultIndexMediaType(t, sb), idx2.MediaType) require.Equal(t, len(idx.Manifests), len(idx2.Manifests)) for i := range idx.Manifests { require.Equal(t, idx.Manifests[i].Digest, idx2.Manifests[i].Digest) } cmd = buildxCmd(sb, withArgs("imagetools", "create", "--prefer-index=false", "-t", target2+"-still-index", target)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2+"-still-index", "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx3 ocispecs.Index err = json.Unmarshal(dt, &idx3) require.NoError(t, err) require.Equal(t, defaultIndexMediaType(t, sb), idx3.MediaType) require.Equal(t, len(idx.Manifests), len(idx3.Manifests)) for i := range idx.Manifests { require.Equal(t, idx.Manifests[i].Digest, idx3.Manifests[i].Digest) } } // testImagetoolsInspectAndFilter verifies inspect output and digest-based platform selection. func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfile(t) registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) target := registry + "/buildx/imtools:latest" out, err := buildCmd(sb, withArgs("-t", target, "--push", "--platform=linux/amd64,linux/arm64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Equal(t, 2, len(idx.Manifests)) mfst := idx.Manifests[0] require.Equal(t, "linux/amd64", platforms.Format(*mfst.Platform)) mfst = idx.Manifests[1] require.Equal(t, "linux/arm64", platforms.Format(*mfst.Platform)) // create arm64 image only cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target+"-arm64", target+"@"+string(idx.Manifests[1].Digest))) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"-arm64", "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx2 ocispecs.Index err = json.Unmarshal(dt, &idx2) require.NoError(t, err) require.Equal(t, 1, len(idx2.Manifests)) require.Equal(t, idx.Manifests[1].Digest, idx2.Manifests[0].Digest) require.Equal(t, platforms.Format(*idx.Manifests[1].Platform), platforms.Format(*idx2.Manifests[0].Platform)) } // testImagetoolsCreatePlatformFilter verifies create --platform keeps only the // selected platform and its matching attestation descriptor. func testImagetoolsCreatePlatformFilter(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) source := registry + "/buildx/imtools-platform-filter-src:latest" out, err := buildCmd(sb, withArgs( "--output", "type=image,name="+source+",push=true,oci-mediatypes=true,oci-artifact=true", "--platform=linux/amd64,linux/arm64", "--provenance=mode=min", dir, )) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var srcIdx ocispecs.Index err = json.Unmarshal(dt, &srcIdx) require.NoError(t, err) require.Len(t, srcIdx.Manifests, 4) var arm64Manifest ocispecs.Descriptor for _, desc := range srcIdx.Manifests { if desc.Platform != nil && platforms.Format(*desc.Platform) == "linux/arm64" { arm64Manifest = desc break } } require.NotEmpty(t, arm64Manifest.Digest) cmd = buildxCmd(sb, withArgs("imagetools", "create", "--dry-run", "--platform=linux/arm64", source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var dryRunIdx ocispecs.Index err = json.Unmarshal(dt, &dryRunIdx) require.NoError(t, err) require.Len(t, dryRunIdx.Manifests, 2) platformCount := 0 attestationCount := 0 for _, desc := range dryRunIdx.Manifests { if desc.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { attestationCount++ require.Equal(t, arm64Manifest.Digest.String(), desc.Annotations["vnd.docker.reference.digest"]) continue } platformCount++ require.NotNil(t, desc.Platform) require.Equal(t, "linux/arm64", platforms.Format(*desc.Platform)) require.Equal(t, arm64Manifest.Digest, desc.Digest) } require.Equal(t, 1, platformCount) require.Equal(t, 1, attestationCount) target := registry + "/buildx/imtools-platform-filter-dst:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "--platform=linux/arm64", "-t", target, source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var filteredIdx ocispecs.Index err = json.Unmarshal(dt, &filteredIdx) require.NoError(t, err) require.Len(t, filteredIdx.Manifests, 2) platformCount = 0 attestationCount = 0 for _, desc := range filteredIdx.Manifests { if desc.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { attestationCount++ require.Equal(t, arm64Manifest.Digest.String(), desc.Annotations["vnd.docker.reference.digest"]) continue } platformCount++ require.NotNil(t, desc.Platform) require.Equal(t, "linux/arm64", platforms.Format(*desc.Platform)) require.Equal(t, arm64Manifest.Digest, desc.Digest) } require.Equal(t, 1, platformCount) require.Equal(t, 1, attestationCount) } // testImagetoolsOCILayoutInspect verifies inspect works against local OCI layout sources. func testImagetoolsOCILayoutInspect(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) source := registry + "/buildx/imtools-oci-layout-inspect-src:latest" out, err := buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/amd64,linux/arm64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) sourceDigest := digest.FromBytes(dt) layoutPath := filepath.Join(dir, "layout-inspect") layoutRef := "oci-layout://" + layoutPath + ":latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", layoutRef, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) require.Equal(t, sourceDigest, digest.FromBytes(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", "oci-layout://"+layoutPath+"@"+sourceDigest.String(), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) require.Equal(t, sourceDigest, digest.FromBytes(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", layoutRef, "--format", "{{.Manifest.Digest}}")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) require.Equal(t, sourceDigest.String(), strings.TrimSpace(string(dt))) } // testImagetoolsOCILayoutCreateSourceAndTarget verifies create can round-trip // between registry and OCI layout sources and targets. func testImagetoolsOCILayoutCreateSourceAndTarget(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registry1, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) registry2, err := sb.NewRegistry() require.NoError(t, err) source := registry1 + "/buildx/imtools-oci-layout-roundtrip-src:latest" out, err := buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/amd64,linux/arm64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) sourceDigest := digest.FromBytes(dt) layoutPath := filepath.Join(dir, "layout-roundtrip") layoutRef := "oci-layout://" + layoutPath + ":v1" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) target := registry2 + "/buildx/imtools-oci-layout-roundtrip-dst:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target, layoutRef)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) require.Equal(t, sourceDigest, digest.FromBytes(dt)) } // testImagetoolsOCILayoutReferrers verifies standalone referrers are recorded // directly in OCI layout index.json with a subject annotation, while reachable // attestation manifests are not duplicated there. func testImagetoolsOCILayoutReferrers(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registry1, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) registry2, err := sb.NewRegistry() require.NoError(t, err) source := registry1 + "/buildx/imtools-oci-layout-referrers-src:latest" out, err := buildCmd(sb, withArgs( "--output", "type=image,name="+source+",push=true,oci-mediatypes=true,oci-artifact=true", "--platform=linux/amd64,linux/arm64", "--provenance=mode=min", dir, )) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var srcIdx ocispecs.Index err = json.Unmarshal(dt, &srcIdx) require.NoError(t, err) var attestations []ocispecs.Descriptor for _, mfst := range srcIdx.Manifests { if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { attestations = append(attestations, mfst) } } require.Len(t, attestations, 2) signatures := make([]ocispecs.Descriptor, 0, len(attestations)) for _, attestation := range attestations { signatures = append(signatures, pushFakeSignatureReferrer(t, source, attestation)) } layoutPath := filepath.Join(dir, "layout-referrers") layoutRef := "oci-layout://" + layoutPath + ":latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) idxBytes, err := os.ReadFile(filepath.Join(layoutPath, "index.json")) require.NoError(t, err) var layoutIdx ocispecs.Index err = json.Unmarshal(idxBytes, &layoutIdx) require.NoError(t, err) directReferrers := map[digest.Digest]ocispecs.Descriptor{} directReferrerCount := 0 for _, desc := range layoutIdx.Manifests { if desc.Annotations["io.containerd.manifest.subject"] != "" { directReferrerCount++ directReferrers[desc.Digest] = desc } } require.Len(t, directReferrers, directReferrerCount) require.Len(t, directReferrers, len(signatures)) for i, sig := range signatures { desc, ok := directReferrers[sig.Digest] require.True(t, ok) require.Equal(t, attestations[i].Digest.String(), desc.Annotations["io.containerd.manifest.subject"]) } for _, attestation := range attestations { _, ok := directReferrers[attestation.Digest] require.False(t, ok) } target := registry2 + "/buildx/imtools-oci-layout-referrers-dst:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target, layoutRef)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var dstIdx ocispecs.Index err = json.Unmarshal(dt, &dstIdx) require.NoError(t, err) copiedAttestations := map[digest.Digest]struct{}{} for _, mfst := range dstIdx.Manifests { if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { copiedAttestations[mfst.Digest] = struct{}{} } } require.Len(t, copiedAttestations, len(attestations)) for _, sig := range signatures { cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+sig.Digest.String(), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var sigManifest ocispecs.Manifest err = json.Unmarshal(dt, &sigManifest) require.NoError(t, err) require.NotNil(t, sigManifest.Subject) _, ok := copiedAttestations[sigManifest.Subject.Digest] require.True(t, ok) } } // testImagetoolsOCILayoutExistingContent verifies importing into an existing OCI // layout still updates index.json state when the top-level descriptor blob is // already present. func testImagetoolsOCILayoutExistingContent(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) source := registry + "/buildx/imtools-oci-layout-existing-content-src:latest" out, err := buildCmd(sb, withArgs( "--output", "type=image,name="+source+",push=true,oci-mediatypes=true,oci-artifact=true", "--platform=linux/amd64,linux/arm64", "--provenance=mode=min", dir, )) require.NoError(t, err, string(out)) layoutPath := filepath.Join(dir, "layout-existing-content") initialRef := "oci-layout://" + layoutPath + ":latest" cmd := buildxCmd(sb, withArgs("imagetools", "create", "-t", initialRef, source)) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var srcIdx ocispecs.Index err = json.Unmarshal(dt, &srcIdx) require.NoError(t, err) var attestations []ocispecs.Descriptor for _, mfst := range srcIdx.Manifests { if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { attestations = append(attestations, mfst) } } require.Len(t, attestations, 2) signatures := make([]ocispecs.Descriptor, 0, len(attestations)) for _, attestation := range attestations { signatures = append(signatures, pushFakeSignatureReferrer(t, source, attestation)) } secondRef := "oci-layout://" + layoutPath + ":second" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", secondRef, source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", secondRef, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) idxBytes, err := os.ReadFile(filepath.Join(layoutPath, "index.json")) require.NoError(t, err) var layoutIdx ocispecs.Index err = json.Unmarshal(idxBytes, &layoutIdx) require.NoError(t, err) directReferrers := map[digest.Digest]ocispecs.Descriptor{} directReferrerCount := 0 for _, desc := range layoutIdx.Manifests { if desc.Annotations["io.containerd.manifest.subject"] != "" { directReferrerCount++ directReferrers[desc.Digest] = desc } } require.Len(t, directReferrers, directReferrerCount) require.Len(t, directReferrers, len(signatures)) for i, sig := range signatures { desc, ok := directReferrers[sig.Digest] require.True(t, ok) require.Equal(t, attestations[i].Digest.String(), desc.Annotations["io.containerd.manifest.subject"]) } } // testImagetoolsOCILayoutMergeSources verifies create merges registry and local OCI layout sources. func testImagetoolsOCILayoutMergeSources(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registry1, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) registry2, err := sb.NewRegistry() require.NoError(t, err) registry3, err := sb.NewRegistry() require.NoError(t, err) srcLocalSeed := registry1 + "/buildx/imtools-oci-layout-merge-local:latest" out, err := buildCmd(sb, withArgs("-t", srcLocalSeed, "--push", "--platform=linux/amd64", "--provenance=false", dir)) require.NoError(t, err, string(out)) layoutPath := filepath.Join(dir, "layout-merge") layoutRef := "oci-layout://" + layoutPath + ":latest" cmd := buildxCmd(sb, withArgs("imagetools", "create", "-t", layoutRef, srcLocalSeed)) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) srcRegistry := registry2 + "/buildx/imtools-oci-layout-merge-registry:latest" out, err = buildCmd(sb, withArgs("-t", srcRegistry, "--push", "--platform=linux/arm64", "--provenance=false", dir)) require.NoError(t, err, string(out)) target := registry3 + "/buildx/imtools-oci-layout-merge-target:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target, layoutRef, srcRegistry)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Len(t, idx.Manifests, 2) } // testImagetoolsOCILayoutTargetDigest verifies exact digest targets work for // OCI layouts and mismatched digests fail. func testImagetoolsOCILayoutTargetDigest(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfile(t) registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) source := registry + "/buildx/imtools-oci-layout-digest-src:latest" out, err := buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/amd64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) sourceDigest := digest.FromBytes(dt) layoutPath := filepath.Join(dir, "layout-digest") exactRef := "oci-layout://" + layoutPath + "@" + sourceDigest.String() cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", exactRef, "--prefer-index=false", source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", exactRef, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) require.Equal(t, sourceDigest, digest.FromBytes(dt)) wrongRef := "oci-layout://" + filepath.Join(dir, "layout-digest-wrong") + "@sha256:1111111111111111111111111111111111111111111111111111111111111111" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", wrongRef, "--prefer-index=false", source)) dt, err = cmd.CombinedOutput() require.Error(t, err, string(dt)) require.Contains(t, string(dt), "requested digest") } // testImagetoolsAppend verifies create --append adds a new source onto an // existing target image and rewrites it as a combined index. func testImagetoolsAppend(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) target := registry + "/buildx/imtools-append-target:latest" out, err := buildCmd(sb, withArgs("-t", target, "--push", "--platform=linux/amd64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var amd64Manifest ocispecs.Manifest err = json.Unmarshal(dt, &amd64Manifest) require.NoError(t, err) require.Equal(t, defaultManifestMediaType(t, sb), amd64Manifest.MediaType) amd64Digest := digest.FromBytes(dt) source := registry + "/buildx/imtools-append-source:latest" out, err = buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/arm64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var arm64Manifest ocispecs.Manifest err = json.Unmarshal(dt, &arm64Manifest) require.NoError(t, err) require.Equal(t, defaultManifestMediaType(t, sb), arm64Manifest.MediaType) arm64Digest := digest.FromBytes(dt) cmd = buildxCmd(sb, withArgs("imagetools", "create", "--append", "-t", target, source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Equal(t, defaultIndexMediaType(t, sb), idx.MediaType) require.Len(t, idx.Manifests, 2) platformsByDigest := map[digest.Digest]string{} for _, desc := range idx.Manifests { require.NotNil(t, desc.Platform) platformsByDigest[desc.Digest] = platforms.Format(*desc.Platform) } require.Equal(t, "linux/amd64", platformsByDigest[amd64Digest]) require.Equal(t, "linux/arm64", platformsByDigest[arm64Digest]) } // testImagetoolsFile verifies create --file reads a source descriptor from disk // and resolves it against the target repository. func testImagetoolsFile(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfile(t) registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) source := registry + "/buildx/imtools-file:latest" out, err := buildCmd(sb, withArgs("-t", source, "--push", "--platform=linux/amd64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var sourceManifest ocispecs.Manifest err = json.Unmarshal(dt, &sourceManifest) require.NoError(t, err) sourceDesc := ocispecs.Descriptor{ MediaType: sourceManifest.MediaType, Digest: digest.FromBytes(dt), Size: int64(len(dt)), } descJSON, err := json.Marshal(sourceDesc) require.NoError(t, err) descPath := filepath.Join(dir, "source-descriptor.json") err = os.WriteFile(descPath, descJSON, 0o644) require.NoError(t, err) target := registry + "/buildx/imtools-file:from-file" cmd = buildxCmd(sb, withArgs("imagetools", "create", "--file", descPath, "-t", target)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Equal(t, defaultIndexMediaType(t, sb), idx.MediaType) require.Len(t, idx.Manifests, 1) require.Equal(t, sourceDesc.Digest, idx.Manifests[0].Digest) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+string(idx.Manifests[0].Digest), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var copiedManifest ocispecs.Manifest err = json.Unmarshal(dt, &copiedManifest) require.NoError(t, err) require.Equal(t, sourceManifest.Config.Digest, copiedManifest.Config.Digest) require.Equal(t, len(sourceManifest.Layers), len(copiedManifest.Layers)) } // testImagetoolsAnnotation verifies index and manifest annotations added by imagetools create. func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfile(t) registry, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) target := registry + "/buildx/imtools:latest" out, err := buildCmd(sb, withArgs("--output", "type=registry,oci-mediatypes=true,name="+target, "--platform=linux/amd64,linux/arm64", "--provenance=false", dir)) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Empty(t, idx.Annotations) imagetoolsCmd := func(source []string) *exec.Cmd { args := []string{"imagetools", "create", "-t", target, "--annotation", "index:foo=bar", "--annotation", "index:bar=baz", "--annotation", "manifest-descriptor:foo=bar", "--annotation", "manifest-descriptor[linux/amd64]:bar=baz"} args = append(args, source...) return buildxCmd(sb, withArgs(args...)) } sources := [][]string{ { target, }, { target + "@" + string(idx.Manifests[0].Digest), target + "@" + string(idx.Manifests[1].Digest), }, } for _, source := range sources { cmd = imagetoolsCmd(source) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) newTarget := registry + "/buildx/imtools:annotations" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", newTarget, target)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", newTarget, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Len(t, idx.Annotations, 2) require.Equal(t, "bar", idx.Annotations["foo"]) require.Equal(t, "baz", idx.Annotations["bar"]) require.Len(t, idx.Manifests, 2) for _, mfst := range idx.Manifests { require.Equal(t, "bar", mfst.Annotations["foo"]) if platforms.Format(*mfst.Platform) == "linux/amd64" { require.Equal(t, "baz", mfst.Annotations["bar"]) } else { require.Empty(t, mfst.Annotations["bar"]) } } } } // testImagetoolsMergeSources verifies create merges manifests from distinct source registries. func testImagetoolsMergeSources(t *testing.T, sb integration.Sandbox) { testImagetoolsMergeSourcesWithMode(t, sb, imagetoolsMergeNoAttestations) } // testImagetoolsMergeSourcesWithAttestations verifies merged sources retain attestation manifests. func testImagetoolsMergeSourcesWithAttestations(t *testing.T, sb integration.Sandbox) { testImagetoolsMergeSourcesWithMode(t, sb, imagetoolsMergeInlineAttestations) } // testImagetoolsMergeSourcesWithFallbackAttestations verifies merged sources // pull a copied single-platform attestation via the referrers fallback tag. func testImagetoolsMergeSourcesWithFallbackAttestations(t *testing.T, sb integration.Sandbox) { testImagetoolsMergeSourcesWithMode(t, sb, imagetoolsMergeFallbackAttestations) } // testImagetoolsCopyAttestationWithSignature verifies copying an attested image // also copies a fake sigstore bundle attached to the attestation manifest. func testImagetoolsCopyAttestationWithSignature(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64") registrySource, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) registryTarget, err := sb.NewRegistry() require.NoError(t, err) source := registrySource + "/buildx/imtools-signature-src:latest" out, err := buildCmd(sb, withArgs( "--output", "type=image,name="+source+",push=true,oci-mediatypes=true,oci-artifact=true", "--platform=linux/amd64,linux/arm64", "--provenance=mode=min", dir, )) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", source, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) require.Len(t, idx.Manifests, 4) platformManifests := make(map[digest.Digest]ocispecs.Descriptor, 2) attestations := make([]ocispecs.Descriptor, 0, 2) for _, desc := range idx.Manifests { if desc.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { attestations = append(attestations, desc) continue } platformManifests[desc.Digest] = desc } require.Len(t, platformManifests, 2) require.Len(t, attestations, 2) // Negative controls: signatures on image manifests, and unsupported // artifact types, should not be copied by imagetools create. platformSignatures := make(map[digest.Digest]ocispecs.Descriptor, len(platformManifests)) platformUnsupportedReferrers := make(map[digest.Digest]ocispecs.Descriptor, len(platformManifests)) for platformDigest, platformDesc := range platformManifests { platformSignatures[platformDigest] = pushFakeSignatureReferrer(t, source, platformDesc) platformUnsupportedReferrers[platformDigest] = pushFakeReferrer( t, source, platformDesc, "application/vnd.example.attachment.v1+json", map[string]string{"example.type": "unsupported"}, ) } signatures := make(map[digest.Digest]ocispecs.Descriptor, len(attestations)) for _, attestationDesc := range attestations { cmd = buildxCmd(sb, withArgs("imagetools", "inspect", source+"@"+string(attestationDesc.Digest), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var attestationManifest ocispecs.Manifest err = json.Unmarshal(dt, &attestationManifest) require.NoError(t, err) require.NotNil(t, attestationManifest.Subject) _, ok := platformManifests[attestationManifest.Subject.Digest] require.True(t, ok) signatures[attestationDesc.Digest] = pushFakeSignatureReferrer(t, source, attestationDesc) } target := registryTarget + "/buildx/imtools-signature-target:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target, source)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var copiedIdx ocispecs.Index err = json.Unmarshal(dt, &copiedIdx) require.NoError(t, err) require.Len(t, copiedIdx.Manifests, 4) for _, attestationDesc := range attestations { require.Contains(t, copiedIdx.Manifests, attestationDesc) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+string(attestationDesc.Digest), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var attestationManifest ocispecs.Manifest err = json.Unmarshal(dt, &attestationManifest) require.NoError(t, err) require.NotNil(t, attestationManifest.Subject) _, ok := platformManifests[attestationManifest.Subject.Digest] require.True(t, ok) signatureDesc := signatures[attestationDesc.Digest] cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+string(signatureDesc.Digest), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var signatureManifest ocispecs.Manifest err = json.Unmarshal(dt, &signatureManifest) require.NoError(t, err) require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", signatureManifest.ArtifactType) require.NotNil(t, signatureManifest.Subject) require.Equal(t, attestationDesc.Digest, signatureManifest.Subject.Digest) require.Equal(t, "dsse-envelope", signatureManifest.Annotations["dev.sigstore.bundle.content"]) } // Only attestation signatures should be present after the copy. The // negative-control referrers attached to image manifests must not exist. for _, platformDesc := range platformManifests { signatureDesc := platformSignatures[platformDesc.Digest] cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+string(signatureDesc.Digest), "--raw")) dt, err = cmd.CombinedOutput() require.Error(t, err, string(dt)) unsupportedDesc := platformUnsupportedReferrers[platformDesc.Digest] cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target+"@"+string(unsupportedDesc.Digest), "--raw")) dt, err = cmd.CombinedOutput() require.Error(t, err, string(dt)) } } type imagetoolsMergeMode int const ( imagetoolsMergeNoAttestations imagetoolsMergeMode = iota imagetoolsMergeInlineAttestations imagetoolsMergeFallbackAttestations ) func prepareSinglePlatformFallbackAsset(t *testing.T, sb integration.Sandbox, dir, registryTarget string) string { t.Helper() registrySource, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) singleSource := registrySource + "/buildx/imtools-merge-single-src:latest" out, err := buildCmd(sb, withArgs( "--output", "type=image,name="+singleSource+",push=true,oci-mediatypes=true,oci-artifact=true", "--platform=linux/arm", "--provenance=mode=min", dir, )) require.NoError(t, err, string(out)) cmd := buildxCmd(sb, withArgs("imagetools", "inspect", singleSource, "--raw")) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) singleSourceIndexDigest := digest.FromBytes(dt) var singleSourceIdx ocispecs.Index err = json.Unmarshal(dt, &singleSourceIdx) require.NoError(t, err) require.Len(t, singleSourceIdx.Manifests, 2) var singleManifest, singleAttestation ocispecs.Descriptor for _, mfst := range singleSourceIdx.Manifests { if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { singleAttestation = mfst continue } require.NotNil(t, mfst.Platform) if mfst.Platform.OS == "linux" && mfst.Platform.Architecture == "arm" { singleManifest = mfst } } require.NotEmpty(t, singleManifest.Digest) require.NotEmpty(t, singleAttestation.Digest) copiedSingle := registryTarget + "/buildx/imtools-merge-single:latest" cmd = buildxCmd(sb, withArgs("imagetools", "create", "--prefer-index=false", "-t", copiedSingle, singleSource+"@"+string(singleManifest.Digest))) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", copiedSingle, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var copiedManifest ocispecs.Manifest err = json.Unmarshal(dt, &copiedManifest) require.NoError(t, err) require.Contains(t, []string{images.MediaTypeDockerSchema2Manifest, ocispecs.MediaTypeImageManifest}, copiedManifest.MediaType) // no index was copied cmd = buildxCmd(sb, withArgs("imagetools", "inspect", copiedSingle+"@"+string(singleSourceIndexDigest), "--raw")) dt, err = cmd.CombinedOutput() require.Error(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", singleSource+"@"+string(singleAttestation.Digest), "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var attestationManifest ocispecs.Manifest err = json.Unmarshal(dt, &attestationManifest) require.NoError(t, err) require.NotNil(t, attestationManifest.Subject) require.Equal(t, singleManifest.Digest, attestationManifest.Subject.Digest) copiedSingleAttestation := registryTarget + "/buildx/imtools-merge-single:attestation-copy" cmd = buildxCmd(sb, withArgs("imagetools", "create", "--prefer-index=false", "-t", copiedSingleAttestation, singleSource+"@"+string(singleAttestation.Digest))) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) fallbackDescriptor := singleAttestation fallbackDescriptor.ArtifactType = "application/vnd.docker.attestation.manifest.v1+json" fallbackDescriptorJSON, err := json.Marshal(fallbackDescriptor) require.NoError(t, err) fallbackRef := registryTarget + "/buildx/imtools-merge-single:sha256-" + singleManifest.Digest.Encoded() cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", fallbackRef, string(fallbackDescriptorJSON))) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", fallbackRef, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var fallbackIdx ocispecs.Index err = json.Unmarshal(dt, &fallbackIdx) require.NoError(t, err) require.Len(t, fallbackIdx.Manifests, 1) require.Equal(t, singleAttestation.Digest, fallbackIdx.Manifests[0].Digest) require.Equal(t, "application/vnd.docker.attestation.manifest.v1+json", fallbackIdx.Manifests[0].ArtifactType) return copiedSingle } func pushFakeSignatureReferrer(t *testing.T, sourceRef string, subject ocispecs.Descriptor) ocispecs.Descriptor { return pushFakeReferrer(t, sourceRef, subject, "application/vnd.dev.sigstore.bundle.v0.3+json", map[string]string{ "dev.sigstore.bundle.content": "dsse-envelope", "dev.sigstore.bundle.predicateType": "https://sigstore.dev/cosign/sign/v1", "org.opencontainers.image.created": "2025-12-05T10:16:57Z", }) } func pushFakeReferrer(t *testing.T, sourceRef string, subject ocispecs.Descriptor, artifactType string, annotations map[string]string) ocispecs.Descriptor { t.Helper() repoName := mustRepoName(t, sourceRef) configBytes := []byte("{}") configDesc := ocispecs.Descriptor{ MediaType: "application/vnd.oci.empty.v1+json", ArtifactType: artifactType, Digest: digest.FromBytes(configBytes), Size: int64(len(configBytes)), } layerBytes := []byte(`{"kind":"fake-referrer"}`) layerDesc := ocispecs.Descriptor{ MediaType: artifactType, Digest: digest.FromBytes(layerBytes), Size: int64(len(layerBytes)), } signatureManifest := ocispecs.Manifest{ Versioned: specsVersioned(), MediaType: ocispecs.MediaTypeImageManifest, ArtifactType: artifactType, Config: configDesc, Layers: []ocispecs.Descriptor{layerDesc}, Subject: &subject, Annotations: annotations, } signatureBytes, err := json.Marshal(signatureManifest) require.NoError(t, err) signatureDesc := ocispecs.Descriptor{ MediaType: ocispecs.MediaTypeImageManifest, ArtifactType: artifactType, Digest: digest.FromBytes(signatureBytes), Size: int64(len(signatureBytes)), Annotations: annotations, } pushRegistryObject(t, repoName, configDesc, configBytes) pushRegistryObject(t, repoName, layerDesc, layerBytes) pushRegistryObject(t, repoName, signatureDesc, signatureBytes) fallbackIndex := ocispecs.Index{ Versioned: specsVersioned(), MediaType: ocispecs.MediaTypeImageIndex, Manifests: []ocispecs.Descriptor{signatureDesc}, } fallbackBytes, err := json.Marshal(fallbackIndex) require.NoError(t, err) fallbackDesc := ocispecs.Descriptor{ MediaType: ocispecs.MediaTypeImageIndex, Digest: digest.FromBytes(fallbackBytes), Size: int64(len(fallbackBytes)), } pushRegistryObject(t, repoName+":sha256-"+subject.Digest.Encoded(), fallbackDesc, fallbackBytes) return signatureDesc } func pushRegistryObject(t *testing.T, ref string, desc ocispecs.Descriptor, dt []byte) { t.Helper() ctx := context.TODO() r := docker.NewResolver(docker.ResolverOptions{ Hosts: resolver.NewRegistryConfig(map[string]resolver.RegistryConfig{}), }) p, err := r.Pusher(ctx, ref) require.NoError(t, err) cw, err := p.Push(ctx, desc) if errdefs.IsAlreadyExists(err) { return } require.NoError(t, err) err = content.Copy(ctx, cw, bytes.NewReader(dt), desc.Size, desc.Digest) if errdefs.IsAlreadyExists(err) { return } require.NoError(t, err) } func mustRepoName(t *testing.T, ref string) string { t.Helper() named, err := reference.ParseNormalizedNamed(ref) require.NoError(t, err) return named.Name() } func specsVersioned() specs.Versioned { return specs.Versioned{SchemaVersion: 2} } func testImagetoolsMergeSourcesWithMode(t *testing.T, sb integration.Sandbox, mode imagetoolsMergeMode) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") } dir := createDockerfileWithArches(t, "amd64", "arm64", "riscv64", "ppc64le", "arm") registry1, err := sb.NewRegistry() if errors.Is(err, integration.ErrRequirements) { t.Skip(err.Error()) } require.NoError(t, err) registry2, err := sb.NewRegistry() require.NoError(t, err) registry3, err := sb.NewRegistry() require.NoError(t, err) registryMerged, err := sb.NewRegistry() require.NoError(t, err) registrySingleCopied, err := sb.NewRegistry() require.NoError(t, err) multiPlatformProvenanceFlag := "--provenance=false" if mode != imagetoolsMergeNoAttestations { multiPlatformProvenanceFlag = "--provenance=true" } src1 := registry1 + "/buildx/imtools-merge-1:latest" out, err := buildCmd(sb, withArgs("-t", src1, "--push", "--platform=linux/amd64,linux/arm64", multiPlatformProvenanceFlag, dir)) require.NoError(t, err, string(out)) src2 := registry2 + "/buildx/imtools-merge-2:latest" out, err = buildCmd(sb, withArgs("-t", src2, "--push", "--platform=linux/riscv64,linux/ppc64le", multiPlatformProvenanceFlag, dir)) require.NoError(t, err, string(out)) var src3 string switch mode { case imagetoolsMergeFallbackAttestations: src3 = prepareSinglePlatformFallbackAsset(t, sb, dir, registrySingleCopied) default: src3 = registry3 + "/buildx/imtools-merge-3:latest" out, err = buildCmd(sb, withArgs("-t", src3, "--push", "--platform=linux/arm", "--provenance=false", dir)) require.NoError(t, err, string(out)) } merged := registryMerged + "/buildx/imtools-merge:latest" cmd := buildxCmd(sb, withArgs("imagetools", "create", "--dry-run", src1, src2, src3)) dt, err := cmd.CombinedOutput() require.NoError(t, err, string(dt)) var dryRunIdx ocispecs.Index err = json.Unmarshal(dt, &dryRunIdx) require.NoError(t, err) expectedManifestCount := 5 expectedAttestationCount := 0 switch mode { case imagetoolsMergeInlineAttestations: expectedManifestCount = 9 expectedAttestationCount = 4 case imagetoolsMergeFallbackAttestations: expectedManifestCount = 10 expectedAttestationCount = 5 } assertMergedIndex(t, dryRunIdx, expectedManifestCount, expectedAttestationCount) cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", merged, src1, src2, src3)) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) cmd = buildxCmd(sb, withArgs("imagetools", "inspect", merged, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) var idx ocispecs.Index err = json.Unmarshal(dt, &idx) require.NoError(t, err) assertMergedIndex(t, idx, expectedManifestCount, expectedAttestationCount) } func assertMergedIndex(t *testing.T, idx ocispecs.Index, expectedManifestCount, expectedAttestationCount int) { t.Helper() require.Len(t, idx.Manifests, expectedManifestCount) platformsFound := map[string]struct{}{} platformDigests := map[digest.Digest]string{} attestations := make([]ocispecs.Descriptor, 0, expectedAttestationCount) for _, mfst := range idx.Manifests { if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { attestations = append(attestations, mfst) continue } require.NotNil(t, mfst.Platform) platform := mfst.Platform.OS + "/" + mfst.Platform.Architecture platformsFound[platform] = struct{}{} platformDigests[mfst.Digest] = platform } require.Len(t, attestations, expectedAttestationCount) for _, mfst := range attestations { refDigest, ok := mfst.Annotations["vnd.docker.reference.digest"] require.True(t, ok) refPlatform, ok := platformDigests[digest.Digest(refDigest)] require.True(t, ok, "attestation %s references unknown manifest %s", mfst.Digest, refDigest) require.NotEmpty(t, refPlatform) } require.Len(t, platformsFound, 5) for _, p := range []string{"linux/amd64", "linux/arm64", "linux/riscv64", "linux/ppc64le", "linux/arm"} { _, ok := platformsFound[p] require.True(t, ok, "missing merged platform %s", p) } } func createDockerfile(t *testing.T) string { return createDockerfileWithArches(t, "amd64", "arm64") } func createDockerfileWithArches(t *testing.T, archs ...string) string { dockerfile := []byte(` FROM scratch ARG TARGETARCH COPY foo-${TARGETARCH} /foo `) appliers := []fstest.Applier{ fstest.CreateFile("Dockerfile", dockerfile, 0600), } for _, arch := range archs { appliers = append(appliers, fstest.CreateFile("foo-"+arch, []byte("foo-"+arch), 0600)) } dir := tmpdir(t, appliers...) return dir }