diff --git a/cmd/oras/root/backup.go b/cmd/oras/root/backup.go index a5d3b9d9a..aac0fd576 100644 --- a/cmd/oras/root/backup.go +++ b/cmd/oras/root/backup.go @@ -235,6 +235,14 @@ func runBackup(cmd *cobra.Command, opts *backupOptions) error { } for i, tag := range tags { + // Add the full image reference to the title annotation + root := roots[i] + fullRef := opts.repository + ":" + tag + if root.Annotations == nil { + root.Annotations = make(map[string]string, 1) + } + root.Annotations[ocispec.AnnotationTitle] = fullRef + referrerCount, err := func() (referrerCount int, retErr error) { trackedDst, err := statusHandler.StartTracking(dstOCI) if err != nil { @@ -248,9 +256,9 @@ func runBackup(cmd *cobra.Command, opts *backupOptions) error { }() if opts.includeReferrers { - return backupTagWithReferrers(ctx, srcRepo, trackedDst, tag, roots[i], extCopyGraphOpts) + return backupTagWithReferrers(ctx, srcRepo, trackedDst, tag, root, extCopyGraphOpts) } - return 0, backupTag(ctx, srcRepo, trackedDst, tag, roots[i], copyGraphOpts) + return 0, backupTag(ctx, srcRepo, trackedDst, tag, root, copyGraphOpts) }() if err != nil { return fmt.Errorf("failed to back up tag %q from %q to %q: %w", tag, opts.repository, dstRoot, oerrors.UnwrapCopyError(err)) diff --git a/test/e2e/suite/command/backup.go b/test/e2e/suite/command/backup.go index 02c73f79a..a1663845a 100644 --- a/test/e2e/suite/command/backup.go +++ b/test/e2e/suite/command/backup.go @@ -16,6 +16,7 @@ limitations under the License. package command import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -25,6 +26,7 @@ import ( . "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" @@ -471,6 +473,38 @@ var _ = Describe("ORAS users:", func() { }) }) + When("verifying annotations", func() { + It("should set org.opencontainers.image.title annotation with full reference", func() { + tmpDir := GinkgoT().TempDir() + outDir := filepath.Join(tmpDir, "backup-title-annotation") + repo := backupTestRepo("title-annotation") + tag := "v1.0" + srcRef := RegistryRef(ZOTHost, repo, tag) + + prepare(RegistryRef(ZOTHost, ArtifactRepo, foobar.Tag), srcRef) + + ORAS("backup", "--output", outDir, srcRef).Exec() + + verifyBackupDirectoryStructure(outDir) + + // Read and verify index.json annotations + indexPath := filepath.Join(outDir, "index.json") + indexBytes, err := os.ReadFile(indexPath) + Expect(err).ToNot(HaveOccurred()) + + var index ocispec.Index + err = json.Unmarshal(indexBytes, &index) + Expect(err).ToNot(HaveOccurred()) + Expect(index.Manifests).ToNot(BeEmpty()) + + // Verify annotations on the first manifest + annotations := index.Manifests[0].Annotations + Expect(annotations["org.opencontainers.image.ref.name"]).To(Equal(tag)) + expectedFullRef := fmt.Sprintf("%s/%s:%s", ZOTHost, repo, tag) + Expect(annotations[ocispec.AnnotationTitle]).To(Equal(expectedFullRef)) + }) + }) + When("handling error cases", func() { It("should fail when output directory cannot be created", func() { // Create a file that will conflict with our output path