imagetools: support oci-layout referrers

Handle OCI layout referrers via subject-annotated index entries and add
integration coverage for copying signed attestations through oci-layout.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-03-12 22:45:01 -07:00
parent 9894189361
commit 3e5c05cb1f
4 changed files with 363 additions and 74 deletions
+116 -1
View File
@@ -34,6 +34,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){
testImagetoolsCreatePlatformFilter,
testImagetoolsOCILayoutInspect,
testImagetoolsOCILayoutCreateSourceAndTarget,
testImagetoolsOCILayoutReferrers,
testImagetoolsOCILayoutMergeSources,
testImagetoolsOCILayoutTargetDigest,
testImagetoolsAppend,
@@ -450,6 +451,121 @@ func testImagetoolsOCILayoutCreateSourceAndTarget(t *testing.T, sb integration.S
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)
}
}
// testImagetoolsOCILayoutMergeSources verifies create merges registry and local OCI layout sources.
func testImagetoolsOCILayoutMergeSources(t *testing.T, sb integration.Sandbox) {
if !isDockerContainerWorker(sb) {
@@ -871,7 +987,6 @@ func testImagetoolsCopyAttestationWithSignature(t *testing.T, sb integration.San
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