diff --git a/cmd/oras/root/manifest/index/update.go b/cmd/oras/root/manifest/index/update.go index d63b20d49..d8a43b07c 100644 --- a/cmd/oras/root/manifest/index/update.go +++ b/cmd/oras/root/manifest/index/update.go @@ -19,6 +19,7 @@ import ( "context" "encoding/json" "fmt" + "os" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -39,11 +40,13 @@ import ( type updateOptions struct { option.Common option.Target + option.Pretty addArguments []string mergeArguments []string removeArguments []string tags []string + outputPath string } func updateCmd() *cobra.Command { @@ -64,9 +67,18 @@ Example - Merge manifests from the index 'v2-windows' to the index 'v2': Example - Update an index and tag the updated index as 'v2.1.0' and 'v2': oras manifest index update localhost:5000/hello@sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 --add linux-amd64 --tag "v2.1.0" --tag "v2" + +Example - Update an index and save it locally to index.json, auto push will be disabled: + oras manifest index update --output index.json localhost:5000/hello:v2 --add v2-linux-amd64 + +Example - Update an index and output the index to stdout, auto push will be disabled: + oras manifest index update --output - --pretty localhost:5000/hello:v2 --remove sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 `, Args: oerrors.CheckArgs(argument.Exactly(1), "the target index to update"), PreRunE: func(cmd *cobra.Command, args []string) error { + if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), "tag", "output"); err != nil { + return err + } opts.RawReference = args[0] for _, manifestRef := range opts.removeArguments { if !contentutil.IsDigest(manifestRef) { @@ -84,6 +96,7 @@ Example - Update an index and tag the updated index as 'v2.1.0' and 'v2': cmd.Flags().StringArrayVarP(&opts.mergeArguments, "merge", "", nil, "indexes to be merged into the index") cmd.Flags().StringArrayVarP(&opts.removeArguments, "remove", "", nil, "manifests to remove from the index, must be digests") cmd.Flags().StringArrayVarP(&opts.tags, "tag", "", nil, "extra tags for the updated index") + cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the created index to, use - for stdout") return oerrors.Command(cmd, &opts.Target) } @@ -127,7 +140,17 @@ func updateIndex(cmd *cobra.Command, opts updateOptions) error { printUpdateStatus(status.IndexPromptUpdated, string(desc.Digest), "", opts.Printer) path := getPushPath(opts.RawReference, opts.Type, opts.Reference, opts.Path) - return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.tags, path, opts.Printer) + switch opts.outputPath { + case "": + err = pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.tags, path, 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 fetchIndex(ctx context.Context, target oras.ReadOnlyTarget, opts updateOptions) (ocispec.Index, error) { diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index 531e6e5c6..f8a1f3d8b 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -100,5 +100,5 @@ 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"}}]}` + OutputIndex = `{"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 e90a8f2ed..0bcbd8a79 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -155,14 +155,14 @@ var _ = Describe("1.1 registry users:", func() { 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) + MatchFile(filePath, multi_arch.OutputIndex, 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() + "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() }) It("should fail if given a reference that does not exist in the repo", func() { @@ -251,6 +251,28 @@ var _ = Describe("1.1 registry users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output updated index to file", func() { + testRepo := indexTestRepo("update", "output-to-file") + CopyZOTRepo(ImageRepo, testRepo) + filePath := filepath.Join(GinkgoT().TempDir(), "updatedIndex") + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "v1")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "v1"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.OutputIndex, DefaultTimeout) + }) + + It("should output updated index to stdout", func() { + testRepo := indexTestRepo("update", "output-to-stdout") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "v1")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "v1"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() + }) + It("should tell user nothing to update if no update flags are used", func() { testRepo := indexTestRepo("update", "no-flags") CopyZOTRepo(ImageRepo, testRepo) @@ -321,6 +343,15 @@ var _ = Describe("1.1 registry users:", func() { "--remove", string(multi_arch.LinuxARM64.Digest)).ExpectFailure(). MatchErrKeyWords("Error", "does not exist").Exec() }) + + It("should fail if --tag is used with --output", func() { + testRepo := indexTestRepo("update", "tag-and-output") + CopyZOTRepo(ImageRepo, testRepo) + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "v1"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-", "--tag", "v2"). + ExpectFailure().MatchErrKeyWords("--tag, --output cannot be used at the same time").Exec() + }) }) }) @@ -413,14 +444,14 @@ var _ = Describe("OCI image layout users:", func() { 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) + MatchFile(filePath, multi_arch.OutputIndex, 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() + "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() }) It("should fail if given a reference that does not exist in the repo", func() { @@ -488,6 +519,26 @@ var _ = Describe("OCI image layout users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output updated index to file", func() { + root := PrepareTempOCI(ImageRepo) + filePath := filepath.Join(GinkgoT().TempDir(), "updatedIndex") + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, LayoutRef(root, "index01")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, LayoutRef(root, "index01"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.OutputIndex, DefaultTimeout) + }) + + It("should output updated index to stdout", func() { + root := PrepareTempOCI(ImageRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, LayoutRef(root, "index01")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, LayoutRef(root, "index01"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() + }) + It("should tell user nothing to update if no update flags are used", func() { root := PrepareTempOCI(ImageRepo) indexRef := LayoutRef(root, "latest")