From 2682d345fa96c023317c078abf11e870d85cff99 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 19:24:59 -0700 Subject: [PATCH 1/8] tests: tighten imagetools copy assertions Verify copied index content keeps the same digest and descriptor metadata. Add function comments to clarify each imagetools integration test case. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/imagetools.go b/tests/imagetools.go index e947244fe..aa43dec27 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -12,6 +12,7 @@ import ( "github.com/containerd/continuity/fs/fstest" "github.com/containerd/platforms" "github.com/moby/buildkit/util/testutil/integration" + "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -24,6 +25,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsAnnotation, } +// 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") @@ -109,6 +111,7 @@ func testImagetoolsCopyManifest(t *testing.T, sb integration.Sandbox) { require.Equal(t, images.MediaTypeDockerSchema2Manifest, idx3.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") @@ -128,6 +131,7 @@ func testImagetoolsCopyIndex(t *testing.T, sb integration.Sandbox) { 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) @@ -155,10 +159,12 @@ func testImagetoolsCopyIndex(t *testing.T, sb integration.Sandbox) { 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) @@ -189,6 +195,7 @@ func testImagetoolsCopyIndex(t *testing.T, sb integration.Sandbox) { } } +// 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") @@ -221,7 +228,7 @@ func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) { mfst = idx.Manifests[1] require.Equal(t, "linux/arm64", platforms.Format(*mfst.Platform)) - // create amd64 only image + // 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)) @@ -240,6 +247,7 @@ func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) { require.Equal(t, platforms.Format(*idx.Manifests[1].Platform), platforms.Format(*idx2.Manifests[0].Platform)) } +// 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") From b43520b4caa3d2a08463f5bb52e91a2ee87dc8be Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 20:25:57 -0700 Subject: [PATCH 2/8] tests: add imagetools test for merged create With and without attestations. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 111 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 5 deletions(-) diff --git a/tests/imagetools.go b/tests/imagetools.go index aa43dec27..bbcdcfef6 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -23,6 +23,8 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsCopyIndex, testImagetoolsInspectAndFilter, testImagetoolsAnnotation, + testImagetoolsMergeSources, + testImagetoolsMergeSourcesWithAttestations, } // testImagetoolsCopyManifest verifies create/inspect behavior for a single-platform image. @@ -319,17 +321,116 @@ func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { } } +// testImagetoolsMergeSources verifies create merges manifests from distinct source registries. +func testImagetoolsMergeSources(t *testing.T, sb integration.Sandbox) { + testImagetoolsMergeSourcesWithOptions(t, sb, false) +} + +// testImagetoolsMergeSourcesWithAttestations verifies merged sources retain attestation manifests. +func testImagetoolsMergeSourcesWithAttestations(t *testing.T, sb integration.Sandbox) { + testImagetoolsMergeSourcesWithOptions(t, sb, true) +} + +func testImagetoolsMergeSourcesWithOptions(t *testing.T, sb integration.Sandbox, withAttestations bool) { + 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) + + multiPlatformProvenanceFlag := "--provenance=false" + if withAttestations { + 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)) + + 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", "-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) + + expectedManifestCount := 5 + expectedAttestationCount := 0 + if withAttestations { + expectedManifestCount = 9 + expectedAttestationCount = 4 + } + require.Len(t, idx.Manifests, expectedManifestCount) + + platformsFound := map[string]struct{}{} + platformDigests := map[digest.Digest]string{} + attestationCount := 0 + for _, mfst := range idx.Manifests { + if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { + 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) + attestationCount++ + continue + } + require.NotNil(t, mfst.Platform) + platform := mfst.Platform.OS + "/" + mfst.Platform.Architecture + platformsFound[platform] = struct{}{} + platformDigests[mfst.Digest] = platform + } + + require.Equal(t, expectedAttestationCount, attestationCount) + 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 `) - dir := tmpdir( - t, + appliers := []fstest.Applier{ fstest.CreateFile("Dockerfile", dockerfile, 0600), - fstest.CreateFile("foo-amd64", []byte("foo-amd64"), 0600), - fstest.CreateFile("foo-arm64", []byte("foo-arm64"), 0600), - ) + } + for _, arch := range archs { + appliers = append(appliers, fstest.CreateFile("foo-"+arch, []byte("foo-"+arch), 0600)) + } + dir := tmpdir(t, appliers...) return dir } From 5b2a4ccd2d6617e216c5a67d12f41d88e6e19d48 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 20:29:22 -0700 Subject: [PATCH 3/8] tests: add single arch attestation fallback case to test Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 168 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 152 insertions(+), 16 deletions(-) diff --git a/tests/imagetools.go b/tests/imagetools.go index bbcdcfef6..8067e8b61 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -25,6 +25,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsAnnotation, testImagetoolsMergeSources, testImagetoolsMergeSourcesWithAttestations, + testImagetoolsMergeSourcesWithFallbackAttestations, } // testImagetoolsCopyManifest verifies create/inspect behavior for a single-platform image. @@ -323,15 +324,129 @@ func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { // testImagetoolsMergeSources verifies create merges manifests from distinct source registries. func testImagetoolsMergeSources(t *testing.T, sb integration.Sandbox) { - testImagetoolsMergeSourcesWithOptions(t, sb, false) + testImagetoolsMergeSourcesWithMode(t, sb, imagetoolsMergeNoAttestations) } // testImagetoolsMergeSourcesWithAttestations verifies merged sources retain attestation manifests. func testImagetoolsMergeSourcesWithAttestations(t *testing.T, sb integration.Sandbox) { - testImagetoolsMergeSourcesWithOptions(t, sb, true) + testImagetoolsMergeSourcesWithMode(t, sb, imagetoolsMergeInlineAttestations) } -func testImagetoolsMergeSourcesWithOptions(t *testing.T, sb integration.Sandbox, withAttestations bool) { +// 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) +} + +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 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") } @@ -349,9 +464,11 @@ func testImagetoolsMergeSourcesWithOptions(t *testing.T, sb integration.Sandbox, require.NoError(t, err) registryMerged, err := sb.NewRegistry() require.NoError(t, err) + registrySingleCopied, err := sb.NewRegistry() + require.NoError(t, err) multiPlatformProvenanceFlag := "--provenance=false" - if withAttestations { + if mode != imagetoolsMergeNoAttestations { multiPlatformProvenanceFlag = "--provenance=true" } @@ -363,9 +480,15 @@ func testImagetoolsMergeSourcesWithOptions(t *testing.T, sb integration.Sandbox, out, err = buildCmd(sb, withArgs("-t", src2, "--push", "--platform=linux/riscv64,linux/ppc64le", multiPlatformProvenanceFlag, dir)) require.NoError(t, err, string(out)) - 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)) + 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", "-t", merged, src1, src2, src3)) @@ -382,23 +505,28 @@ func testImagetoolsMergeSourcesWithOptions(t *testing.T, sb integration.Sandbox, expectedManifestCount := 5 expectedAttestationCount := 0 - if withAttestations { + switch mode { + case imagetoolsMergeInlineAttestations: expectedManifestCount = 9 expectedAttestationCount = 4 + case imagetoolsMergeFallbackAttestations: + expectedManifestCount = 10 + expectedAttestationCount = 5 } + 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{} - attestationCount := 0 + attestations := make([]ocispecs.Descriptor, 0, expectedAttestationCount) for _, mfst := range idx.Manifests { if mfst.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { - 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) - attestationCount++ + attestations = append(attestations, mfst) continue } require.NotNil(t, mfst.Platform) @@ -407,7 +535,15 @@ func testImagetoolsMergeSourcesWithOptions(t *testing.T, sb integration.Sandbox, platformDigests[mfst.Digest] = platform } - require.Equal(t, expectedAttestationCount, attestationCount) + 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] From 0a5fccb7beba4226de42054b37ce4acfdd3404dc Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 20:43:35 -0700 Subject: [PATCH 4/8] tests: cover imagetools signature referrers Add integration coverage for copying attestation referrers with fake sigstore signature manifests on a multi-platform source image. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 213 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/tests/imagetools.go b/tests/imagetools.go index 8067e8b61..05ecd17a3 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -1,6 +1,8 @@ package tests import ( + "bytes" + "context" "encoding/json" "os" "os/exec" @@ -8,11 +10,17 @@ import ( "path/filepath" "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" @@ -26,6 +34,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsMergeSources, testImagetoolsMergeSourcesWithAttestations, testImagetoolsMergeSourcesWithFallbackAttestations, + testImagetoolsCopyAttestationWithSignature, } // testImagetoolsCopyManifest verifies create/inspect behavior for a single-platform image. @@ -338,6 +347,111 @@ func testImagetoolsMergeSourcesWithFallbackAttestations(t *testing.T, sb integra 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) + + 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"]) + } +} + type imagetoolsMergeMode int const ( @@ -446,6 +560,105 @@ func prepareSinglePlatformFallbackAsset(t *testing.T, sb integration.Sandbox, di return copiedSingle } +func pushFakeSignatureReferrer(t *testing.T, sourceRef string, subject ocispecs.Descriptor) ocispecs.Descriptor { + t.Helper() + + repoName := mustRepoName(t, sourceRef) + + configBytes := []byte("{}") + configDesc := ocispecs.Descriptor{ + MediaType: "application/vnd.oci.empty.v1+json", + ArtifactType: "application/vnd.dev.sigstore.bundle.v0.3+json", + Digest: digest.FromBytes(configBytes), + Size: int64(len(configBytes)), + } + + layerBytes := []byte(`{"kind":"fake-sigstore-bundle"}`) + layerDesc := ocispecs.Descriptor{ + MediaType: "application/vnd.dev.sigstore.bundle.v0.3+json", + Digest: digest.FromBytes(layerBytes), + Size: int64(len(layerBytes)), + } + + annotations := 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", + } + signatureManifest := ocispecs.Manifest{ + Versioned: specsVersioned(), + MediaType: ocispecs.MediaTypeImageManifest, + ArtifactType: "application/vnd.dev.sigstore.bundle.v0.3+json", + 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: "application/vnd.dev.sigstore.bundle.v0.3+json", + 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") From 0d82d2b11518de1f672d93c6d80aafadb2f04bf9 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 20:53:04 -0700 Subject: [PATCH 5/8] tests: cover imagetools platform filtering Add integration coverage for imagetools create --platform on an attested multi-platform image and verify only the matching attestation remains. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/imagetools.go b/tests/imagetools.go index 05ecd17a3..0691b909c 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -30,6 +30,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsCopyManifest, testImagetoolsCopyIndex, testImagetoolsInspectAndFilter, + testImagetoolsCreatePlatformFilter, testImagetoolsAnnotation, testImagetoolsMergeSources, testImagetoolsMergeSourcesWithAttestations, @@ -259,6 +260,78 @@ func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) { 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) + + 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) +} + // testImagetoolsAnnotation verifies index and manifest annotations added by imagetools create. func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { From b90d59a2e9f200b8c18b1f942476d0f95e13db7d Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 21:01:48 -0700 Subject: [PATCH 6/8] tests: cover imagetools append Add integration coverage for imagetools create --append and verify the target is rewritten as a two-platform manifest list. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 66 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/imagetools.go b/tests/imagetools.go index 0691b909c..e5a4b81d1 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -31,6 +31,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsCopyIndex, testImagetoolsInspectAndFilter, testImagetoolsCreatePlatformFilter, + testImagetoolsAppend, testImagetoolsAnnotation, testImagetoolsMergeSources, testImagetoolsMergeSourcesWithAttestations, @@ -332,6 +333,71 @@ func testImagetoolsCreatePlatformFilter(t *testing.T, sb integration.Sandbox) { require.Equal(t, 1, attestationCount) } +// 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, images.MediaTypeDockerSchema2Manifest, 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, images.MediaTypeDockerSchema2Manifest, 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, images.MediaTypeDockerSchema2ManifestList, 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]) +} + // testImagetoolsAnnotation verifies index and manifest annotations added by imagetools create. func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { From d84544176df39d832c3a68b3216f58214131fee6 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 21:05:57 -0700 Subject: [PATCH 7/8] tests: cover imagetools file input Add integration coverage for imagetools create --file and verify a descriptor read from disk resolves to the expected copied manifest. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/imagetools.go b/tests/imagetools.go index e5a4b81d1..f8b0de171 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -32,6 +32,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsInspectAndFilter, testImagetoolsCreatePlatformFilter, testImagetoolsAppend, + testImagetoolsFile, testImagetoolsAnnotation, testImagetoolsMergeSources, testImagetoolsMergeSourcesWithAttestations, @@ -398,6 +399,70 @@ func testImagetoolsAppend(t *testing.T, sb integration.Sandbox) { 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, images.MediaTypeDockerSchema2ManifestList, 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) { From 3281dce28ce758baa12d7423d3903e119a947cd7 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 10 Mar 2026 21:11:08 -0700 Subject: [PATCH 8/8] tests: cover imagetools dry-run Add dry-run assertions to imagetools merge and platform filter integration tests to verify emitted manifest JSON before push. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 53 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/tests/imagetools.go b/tests/imagetools.go index f8b0de171..5a109ff0a 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -303,6 +303,31 @@ func testImagetoolsCreatePlatformFilter(t *testing.T, sb integration.Sandbox) { } 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() @@ -317,8 +342,8 @@ func testImagetoolsCreatePlatformFilter(t *testing.T, sb integration.Sandbox) { require.NoError(t, err) require.Len(t, filteredIdx.Manifests, 2) - platformCount := 0 - attestationCount := 0 + platformCount = 0 + attestationCount = 0 for _, desc := range filteredIdx.Manifests { if desc.Annotations["vnd.docker.reference.type"] == "attestation-manifest" { attestationCount++ @@ -908,16 +933,12 @@ func testImagetoolsMergeSourcesWithMode(t *testing.T, sb integration.Sandbox, mo } merged := registryMerged + "/buildx/imtools-merge:latest" - cmd := buildxCmd(sb, withArgs("imagetools", "create", "-t", merged, src1, src2, src3)) + cmd := buildxCmd(sb, withArgs("imagetools", "create", "--dry-run", 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) + var dryRunIdx ocispecs.Index + err = json.Unmarshal(dt, &dryRunIdx) require.NoError(t, err) expectedManifestCount := 5 @@ -930,6 +951,20 @@ func testImagetoolsMergeSourcesWithMode(t *testing.T, sb integration.Sandbox, mo 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) }