diff --git a/cmd/oras/root/cmd.go b/cmd/oras/root/cmd.go index 67fd6d231..14e0e468a 100644 --- a/cmd/oras/root/cmd.go +++ b/cmd/oras/root/cmd.go @@ -34,6 +34,7 @@ func New() *cobra.Command { logoutCmd(), versionCmd(), discoverCmd(), + resolveCmd(), copyCmd(), tagCmd(), attachCmd(), diff --git a/cmd/oras/root/resolve.go b/cmd/oras/root/resolve.go new file mode 100644 index 000000000..022fa8b20 --- /dev/null +++ b/cmd/oras/root/resolve.go @@ -0,0 +1,86 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package root + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "oras.land/oras-go/v2" + "oras.land/oras/cmd/oras/internal/option" +) + +type resolveOptions struct { + option.Common + option.Platform + option.Target + + FullRef bool +} + +func resolveCmd() *cobra.Command { + var opts resolveOptions + + cmd := &cobra.Command{ + Use: "resolve [flags] {:|@}", + Short: "[Experimental] Resolves digest of the target artifact", + Long: `[Experimental] Resolves digest of the target artifact + +Example - Resolve digest of the target artifact: + oras resolve localhost:5000/hello-world:v1 +`, + Args: cobra.ExactArgs(1), + Aliases: []string{"digest"}, + PreRunE: func(cmd *cobra.Command, args []string) error { + opts.RawReference = args[0] + return option.Parse(&opts) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runResolve(cmd.Context(), opts) + }, + } + + cmd.Flags().BoolVarP(&opts.FullRef, "full-reference", "l", false, "print the full artifact reference with digest") + option.ApplyFlags(&opts, cmd.Flags()) + return cmd +} + +func runResolve(ctx context.Context, opts resolveOptions) error { + ctx, logger := opts.WithContext(ctx) + repo, err := opts.NewReadonlyTarget(ctx, opts.Common, logger) + if err != nil { + return err + } + if err := opts.EnsureReferenceNotEmpty(); err != nil { + return err + } + resolveOpts := oras.DefaultResolveOptions + resolveOpts.TargetPlatform = opts.Platform.Platform + desc, err := oras.Resolve(ctx, repo, opts.Reference, resolveOpts) + + if err != nil { + return fmt.Errorf("failed to resolve digest: %w", err) + } + + if opts.FullRef { + fmt.Printf("%s@%s\n", opts.Path, desc.Digest) + } else { + fmt.Println(desc.Digest.String()) + } + + return nil +} diff --git a/test/e2e/internal/utils/testdata.go b/test/e2e/internal/utils/testdata.go index b4aab0f84..400164387 100644 --- a/test/e2e/internal/utils/testdata.go +++ b/test/e2e/internal/utils/testdata.go @@ -22,6 +22,7 @@ const ( BlobRepo = "command/blobs" ArtifactRepo = "command/artifacts" Namespace = "command" + InvalidRepo = "INVALID" // env RegHostKey = "ORAS_REGISTRY_HOST" FallbackRegHostKey = "ORAS_REGISTRY_FALLBACK_HOST" diff --git a/test/e2e/suite/command/resolve.go b/test/e2e/suite/command/resolve.go new file mode 100644 index 000000000..fb46d798c --- /dev/null +++ b/test/e2e/suite/command/resolve.go @@ -0,0 +1,58 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "oras.land/oras/test/e2e/internal/testdata/multi_arch" + . "oras.land/oras/test/e2e/internal/utils" +) + +var _ = Describe("ORAS beginners:", func() { + When("running resolve command", func() { + It("should fail when no manifest reference provided", func() { + ORAS("resolve").ExpectFailure().MatchErrKeyWords("Error:").Exec() + }) + It("should fail when repo is invalid", func() { + ORAS("resolve", fmt.Sprintf("%s/%s", ZOTHost, InvalidRepo)).ExpectFailure().MatchErrKeyWords("Error:", fmt.Sprintf("invalid reference: invalid repository %q", InvalidRepo)).Exec() + }) + It("should fail when no tag or digest provided", func() { + ORAS("resolve", RegistryRef(ZOTHost, ImageRepo, "")).ExpectFailure().MatchErrKeyWords("Error:", "no tag or digest when expecting ").Exec() + }) + It("should fail when provided manifest reference is not found", func() { + ORAS("resolve", RegistryRef(ZOTHost, ImageRepo, "i-dont-think-this-tag-exists")).ExpectFailure().MatchErrKeyWords("Error: failed to resolve digest:", "not found").Exec() + }) + It("should resolve with just digest", func() { + out := ORAS("resolve", RegistryRef(ZOTHost, ImageRepo, multi_arch.Digest)).Exec().Out + outString := string(out.Contents()) + outString = strings.TrimSpace(outString) + gomega.Expect(outString).To(gomega.Equal(multi_arch.Digest)) + }) + It("should resolve with a fully qualified reference", func() { + out := ORAS("digest", "-l", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag)).Exec().Out + gomega.Expect(out).To(gbytes.Say(fmt.Sprintf("%s/%s@%s", ZOTHost, ImageRepo, multi_arch.Digest))) + }) + It("should resolve with a fully qualified reference for a platform", func() { + out := ORAS("resolve", "--full-reference", "--platform", "linux/amd64", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag)).Exec().Out + gomega.Expect(out).To(gbytes.Say(fmt.Sprintf("%s/%s@%s", ZOTHost, ImageRepo, multi_arch.LinuxAMD64.Digest))) + }) + }) +})