diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go index 1d6164d5d..ee1b3e657 100644 --- a/cmd/oras/root/manifest/index/create.go +++ b/cmd/oras/root/manifest/index/create.go @@ -48,9 +48,11 @@ type createOptions struct { option.Target option.Pretty - sources []string - extraRefs []string - outputPath string + sources []string + extraRefs []string + rawAnnotations []string + indexAnnotations map[string]string + outputPath string } func createCmd() *cobra.Command { @@ -72,6 +74,9 @@ Example - Create an index from source manifests using both tags and digests, and Example - Create an index and push it with multiple tags: oras manifest index create localhost:5000/hello:tag1,tag2,tag3 linux-amd64 linux-arm64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 +Example - Create and push an index with annotations: + oras manifest index create localhost:5000/hello:v1 linux-amd64 --annotation "key=val" + Example - Create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1': oras manifest index create layout-dir:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 @@ -87,6 +92,10 @@ Example - Create an index and output the index to stdout, auto push will be disa opts.RawReference = refs[0] opts.extraRefs = refs[1:] opts.sources = args[1:] + opts.indexAnnotations = make(map[string]string) + if err := parseAnnotations(opts.rawAnnotations, opts.indexAnnotations); err != nil { + return err + } return option.Parse(cmd, &opts) }, Aliases: []string{"pack"}, @@ -95,6 +104,7 @@ Example - Create an index and output the index to stdout, auto push will be disa }, } cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the created index to, use - for stdout") + cmd.Flags().StringArrayVarP(&opts.rawAnnotations, "annotation", "a", nil, "index annotations") option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } @@ -113,8 +123,9 @@ func createIndex(cmd *cobra.Command, opts createOptions) error { Versioned: specs.Versioned{ SchemaVersion: 2, }, - MediaType: ocispec.MediaTypeImageIndex, - Manifests: manifests, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: manifests, + Annotations: opts.indexAnnotations, } indexBytes, err := json.Marshal(index) if err != nil { @@ -204,3 +215,14 @@ func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, } return printer.Println("Digest:", desc.Digest) } + +func parseAnnotations(input []string, annotations map[string]string) error { + for _, anno := range input { + key, val, success := strings.Cut(anno, "=") + if !success { + return fmt.Errorf("annotation value doesn't match the required format of \"key=value\"") + } + annotations[key] = val + } + return nil +} diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go index 95eee3165..336e2bae5 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -137,6 +137,19 @@ var _ = Describe("1.1 registry users:", func() { ValidateIndex(content, expectedManifests) }) + It("should create index with annotations", func() { + testRepo := indexTestRepo("create", "with-annotations") + key := "image-anno-key" + value := "image-anno-value" + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "v1"), "--annotation", fmt.Sprintf("%s=%s", key, value)).Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "v1")).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(content, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Annotations[key]).To(Equal(value)) + }) + It("should output created index to file", func() { testRepo := indexTestRepo("create", "output-to-file") CopyZOTRepo(ImageRepo, testRepo) @@ -374,6 +387,19 @@ var _ = Describe("OCI image layout users:", func() { ValidateIndex(content, expectedManifests) }) + It("should create index with annotations", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "with-annotations") + key := "image-anno-key" + value := "image-anno-value" + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "--annotation", fmt.Sprintf("%s=%s", key, value)).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(content, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Annotations[key]).To(Equal(value)) + }) + It("should output created index to file", func() { root := PrepareTempOCI(ImageRepo) indexRef := LayoutRef(root, "output-to-file")