diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go index 3e5a2754f..66bdecb6e 100644 --- a/cmd/oras/root/manifest/index/create.go +++ b/cmd/oras/root/manifest/index/create.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "os" "strings" "github.com/opencontainers/image-spec/specs-go" @@ -44,9 +45,11 @@ var maxConfigSize int64 = 4 * 1024 * 1024 // 4 MiB type createOptions struct { option.Common option.Target + option.Pretty - sources []string - extraRefs []string + sources []string + extraRefs []string + outputPath string } func createCmd() *cobra.Command { @@ -70,6 +73,12 @@ Example - create an index and push it with multiple tags: 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 + +Example - create an index and save it locally to index.json, auto push will be disabled: + oras manifest index create --output index.json localhost:5000/hello linux-amd64 linux-arm64 + +Example - create an index and output the index to stdout, auto push will be disabled: + oras manifest index create localhost:5000/hello linux-arm64 --output - --pretty `, Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination index to create."), PreRunE: func(cmd *cobra.Command, args []string) error { @@ -84,6 +93,7 @@ Example - create an index and push to an OCI image layout folder 'layout-dir' an return createIndex(cmd, opts) }, } + cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the created index to, use - for stdout") option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } @@ -108,7 +118,18 @@ func createIndex(cmd *cobra.Command, opts createOptions) error { indexBytes, _ := json.Marshal(index) desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexBytes) opts.Println(status.IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex) - return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer) + + switch opts.outputPath { + case "": + err = pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer) + case "-": + opts.Println("Digest:", desc.Digest) + err = opts.Output(os.Stdout, indexBytes) + default: + opts.Println("Digest:", desc.Digest) + err = os.WriteFile(opts.outputPath, indexBytes, 0666) + } + return err } func fetchSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts createOptions) ([]ocispec.Descriptor, error) { diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index c1062a42c..531e6e5c6 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -97,3 +97,8 @@ var ( }, } ) + +// exported index +var ( + CreatedIndex = `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}}]}` +) diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go index cfd23e8ac..fe6d35139 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -18,6 +18,7 @@ package command import ( "encoding/json" "fmt" + "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -130,6 +131,21 @@ var _ = Describe("1.1 registry users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output created index to file", func() { + testRepo := indexTestRepo("create", "output-to-file") + CopyZOTRepo(ImageRepo, testRepo) + filePath := filepath.Join(GinkgoT().TempDir(), "createdIndex") + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.CreatedIndex, DefaultTimeout) + }) + + It("should output created index to stdout", func() { + testRepo := indexTestRepo("create", "output-to-stdout") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), string(multi_arch.LinuxAMD64.Digest), + "--output", "-").MatchKeyWords(multi_arch.CreatedIndex).Exec() + }) + It("should fail if given a reference that does not exist in the repo", func() { testRepo := indexTestRepo("create", "nonexist-ref") CopyZOTRepo(ImageRepo, testRepo) @@ -211,6 +227,21 @@ var _ = Describe("OCI image layout users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output created index to file", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "output-to-file") + filePath := filepath.Join(GinkgoT().TempDir(), "createdIndex") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.CreatedIndex, DefaultTimeout) + }) + + It("should output created index to stdout", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "output-to-stdout") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest), + "--output", "-").MatchKeyWords(multi_arch.CreatedIndex).Exec() + }) + It("should fail if given a reference that does not exist in the repo", func() { root := PrepareTempOCI(ImageRepo) indexRef := LayoutRef(root, "latest")