From 6eb48d9c8cae48d70c02ca8d1c3d06aaabb02ddd Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 12 Mar 2026 22:54:35 -0700 Subject: [PATCH] imagetools: fix oci-layout index update when blob exists When pushing to an OCI layout where the top-level descriptor blob already existed, pushOCILayout returned early without updating index.json or writing pending referrers. Restructure the control flow so the blob-exists case skips only the write but still updates the index and flushes referrers. Signed-off-by: Tonis Tiigi --- tests/imagetools.go | 85 +++++++++++++++++++++++++++++++++++++++ util/imagetools/create.go | 14 +++---- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/tests/imagetools.go b/tests/imagetools.go index 867f9a4be..788374fbd 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -35,6 +35,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){ testImagetoolsOCILayoutInspect, testImagetoolsOCILayoutCreateSourceAndTarget, testImagetoolsOCILayoutReferrers, + testImagetoolsOCILayoutExistingContent, testImagetoolsOCILayoutMergeSources, testImagetoolsOCILayoutTargetDigest, testImagetoolsAppend, @@ -566,6 +567,90 @@ func testImagetoolsOCILayoutReferrers(t *testing.T, sb integration.Sandbox) { } } +// 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) { diff --git a/util/imagetools/create.go b/util/imagetools/create.go index ccf2c26cb..2490bb86a 100644 --- a/util/imagetools/create.go +++ b/util/imagetools/create.go @@ -558,14 +558,14 @@ func (r *Resolver) pushOCILayout(ctx context.Context, ref *Location, desc ocispe } w, err := store.Writer(ctx, content.WithRef(desc.Digest.String()), content.WithDescriptor(desc)) if err != nil { - if errdefs.IsAlreadyExists(err) { - return nil + if !errdefs.IsAlreadyExists(err) { + return err + } + } else { + err = content.Copy(ctx, w, bytes.NewReader(dt), desc.Size, desc.Digest) + if err != nil && !errdefs.IsAlreadyExists(err) { + return err } - return err - } - err = content.Copy(ctx, w, bytes.NewReader(dt), desc.Size, desc.Digest) - if err != nil && !errdefs.IsAlreadyExists(err) { - return err } idx := ociindex.NewStoreIndex(ref.OCILayout().Path)