diff --git a/cmd/oras/internal/errors/errors.go b/cmd/oras/internal/errors/errors.go index b93144428..8af80303d 100644 --- a/cmd/oras/internal/errors/errors.go +++ b/cmd/oras/internal/errors/errors.go @@ -17,11 +17,16 @@ package errors import ( "fmt" + "strings" "github.com/spf13/cobra" "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote/errcode" ) +// RegistryErrorPrefix is the commandline prefix for errors from registry. +const RegistryErrorPrefix = "Error response from registry:" + // Error is the error type for CLI error messaging. type Error struct { Err error @@ -60,6 +65,60 @@ func CheckArgs(checker func(args []string) (bool, string), Usage string) cobra.P } } +// Modifier modifies the error during cmd execution. +type Modifier interface { + Modify(cmd *cobra.Command, err error) (modifiedErr error, modified bool) +} + +// Command returns an error-handled cobra command. +func Command(cmd *cobra.Command, handler Modifier) *cobra.Command { + runE := cmd.RunE + cmd.RunE = func(cmd *cobra.Command, args []string) error { + err := runE(cmd, args) + if err != nil { + err, _ = handler.Modify(cmd, err) + return err + } + return nil + } + return cmd +} + +// Trim tries to trim toTrim from err. +func Trim(err error, toTrim error) error { + var inner error + if errResp, ok := toTrim.(*errcode.ErrorResponse); ok { + if len(errResp.Errors) == 0 { + return fmt.Errorf("recognizable error message not found: %w", toTrim) + } + inner = errResp.Errors + } else { + return err + } + + if rewrapped := reWrap(err, toTrim, inner); rewrapped != nil { + return rewrapped + } + return inner +} + +// reWrap re-wraps errA to errC and trims out errB, returns nil if scrub fails. +// +---------- errA ----------+ +// | +---- errB ----+ | +---- errA ----+ +// | | errC | | => | errC | +// | +--------------+ | +--------------+ +// +--------------------------+ +func reWrap(errA, errB, errC error) error { + // TODO: trim dedicated error type when + // https://github.com/oras-project/oras-go/issues/677 is done + contentA := errA.Error() + contentB := errB.Error() + if idx := strings.Index(contentA, contentB); idx > 0 { + return fmt.Errorf("%s%w", contentA[:idx], errC) + } + return nil +} + // NewErrEmptyTagOrDigest creates a new error based on the reference string. func NewErrEmptyTagOrDigest(ref registry.Reference) error { return NewErrEmptyTagOrDigestStr(ref.String()) diff --git a/cmd/oras/internal/option/remote.go b/cmd/oras/internal/option/remote.go index 5547569e5..255393934 100644 --- a/cmd/oras/internal/option/remote.go +++ b/cmd/oras/internal/option/remote.go @@ -30,11 +30,14 @@ import ( credentials "github.com/oras-project/oras-credentials-go" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "github.com/spf13/pflag" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/errcode" "oras.land/oras-go/v2/registry/remote/retry" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/internal/credential" "oras.land/oras/internal/crypto" onet "oras.land/oras/internal/net" @@ -42,7 +45,8 @@ import ( "oras.land/oras/internal/version" ) -// Remote options struct. +// Remote options struct contains flags and arguments specifying one registry. +// Remote implements oerrors.Handler and interface. type Remote struct { DistributionSpec CACertFilePath string @@ -322,3 +326,15 @@ func (opts *Remote) isPlainHttp(registry string) bool { } return plainHTTP } + +// Modify modifies error during cmd execution. +func (opts *Remote) Modify(cmd *cobra.Command, err error) (error, bool) { + var errResp *errcode.ErrorResponse + if errors.As(err, &errResp) { + cmd.SetErrPrefix(oerrors.RegistryErrorPrefix) + return &oerrors.Error{ + Err: oerrors.Trim(err, errResp), + }, true + } + return err, false +} diff --git a/cmd/oras/internal/option/target.go b/cmd/oras/internal/option/target.go index a2f0e6995..d4b8878fc 100644 --- a/cmd/oras/internal/option/target.go +++ b/cmd/oras/internal/option/target.go @@ -21,17 +21,21 @@ import ( "fmt" "io" "io/fs" + "net/http" "os" "strings" "sync" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "github.com/spf13/pflag" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/oci" + "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/errcode" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/fileref" ) @@ -43,6 +47,7 @@ const ( // Target struct contains flags and arguments specifying one registry or image // layout. +// Target implements oerrors.Handler interface. type Target struct { Remote RawReference string @@ -239,8 +244,54 @@ func (opts *Target) EnsureReferenceNotEmpty() error { return nil } +// Modify handles error during cmd execution. +func (opts *Target) Modify(cmd *cobra.Command, err error) (error, bool) { + if opts.IsOCILayout { + return err, false + } + + if errors.Is(err, errdef.ErrNotFound) { + cmd.SetErrPrefix(oerrors.RegistryErrorPrefix) + return err, true + } + + var errResp *errcode.ErrorResponse + if errors.As(err, &errResp) { + ref := registry.Reference{Registry: opts.RawReference} + if errResp.URL.Host != ref.Host() { + // raw reference is not registry host + var parseErr error + ref, parseErr = registry.ParseReference(opts.RawReference) + if parseErr != nil { + // this should not happen + return err, false + } + if errResp.URL.Host != ref.Host() { + // not handle if the error is not from the target + return err, false + } + } + + cmd.SetErrPrefix(oerrors.RegistryErrorPrefix) + ret := &oerrors.Error{ + Err: oerrors.Trim(err, errResp), + } + + if ref.Registry == "docker.io" && errResp.StatusCode == http.StatusUnauthorized { + if ref.Repository != "" && !strings.Contains(ref.Repository, "/") { + // docker.io/xxx -> docker.io/library/xxx + ref.Repository = "library/" + ref.Repository + ret.Recommendation = fmt.Sprintf("Namespace seems missing. Do you mean `%s %s`?", cmd.CommandPath(), ref) + } + } + return ret, true + } + return err, false +} + // BinaryTarget struct contains flags and arguments specifying two registries or // image layouts. +// BinaryTarget implements errors.Handler interface. type BinaryTarget struct { From Target To Target @@ -269,3 +320,11 @@ func (opts *BinaryTarget) Parse() error { opts.To.resolveFlag = append(opts.resolveFlag, opts.To.resolveFlag...) return Parse(opts) } + +// Modify handles error during cmd execution. +func (opts *BinaryTarget) Modify(cmd *cobra.Command, err error) (error, bool) { + if modifiedErr, modified := opts.From.Modify(cmd, err); modified { + return modifiedErr, modified + } + return opts.To.Modify(cmd, err) +} diff --git a/cmd/oras/internal/option/target_test.go b/cmd/oras/internal/option/target_test.go index fd13221ae..8ab6f8459 100644 --- a/cmd/oras/internal/option/target_test.go +++ b/cmd/oras/internal/option/target_test.go @@ -16,7 +16,15 @@ limitations under the License. package option import ( + "errors" + "net/http" + "net/url" + "reflect" "testing" + + "github.com/spf13/cobra" + "oras.land/oras-go/v2/registry/remote/errcode" + oerrors "oras.land/oras/cmd/oras/internal/errors" ) func TestTarget_Parse_oci(t *testing.T) { @@ -75,3 +83,159 @@ func Test_parseOCILayoutReference(t *testing.T) { }) } } + +func TestTarget_Modify_ociLayout(t *testing.T) { + errClient := errors.New("client error") + opts := &Target{} + got, modified := opts.Modify(&cobra.Command{}, errClient) + + if modified { + t.Errorf("expect error not to be modified but received true") + } + if got != errClient { + t.Errorf("unexpected output from Target.Process() = %v", got) + } +} + +func TestTarget_Modify_errInvalidReference(t *testing.T) { + errResp := &errcode.ErrorResponse{ + URL: &url.URL{Host: "registry-1.docker.io"}, + StatusCode: http.StatusUnauthorized, + Errors: errcode.Errors{ + errcode.Error{ + Code: "000", + Message: "mocked message", + Detail: map[string]string{"mocked key": "mocked value"}, + }, + }, + } + opts := &Target{ + RawReference: "invalid-reference", + } + got, modified := opts.Modify(&cobra.Command{}, errResp) + + if modified { + t.Errorf("expect error not to be modified but received true") + } + if got != errResp { + t.Errorf("unexpected output from Target.Process() = %v", got) + } +} + +func TestTarget_Modify_errHostNotMatching(t *testing.T) { + errResp := &errcode.ErrorResponse{ + URL: &url.URL{Host: "registry-1.docker.io"}, + StatusCode: http.StatusUnauthorized, + Errors: errcode.Errors{ + errcode.Error{ + Code: "000", + Message: "mocked message", + Detail: map[string]string{"mocked key": "mocked value"}, + }, + }, + } + + opts := &Target{ + RawReference: "registry-2.docker.io/test:tag", + } + _, modified := opts.Modify(&cobra.Command{}, errResp) + if modified { + t.Errorf("expect error not to be modified but received true") + } +} + +func TestTarget_Modify_dockerHint(t *testing.T) { + type fields struct { + Remote Remote + RawReference string + Type string + Reference string + Path string + IsOCILayout bool + } + errs := errcode.Errors{ + errcode.Error{ + Code: "000", + Message: "mocked message", + Detail: map[string]string{"mocked key": "mocked value"}, + }, + } + tests := []struct { + name string + fields fields + err error + modifiedErr *oerrors.Error + }{ + { + "namespace already exists", + fields{RawReference: "docker.io/library/alpine:latest"}, + &errcode.ErrorResponse{ + URL: &url.URL{Host: "registry-1.docker.io"}, + StatusCode: http.StatusUnauthorized, + Errors: errs, + }, + &oerrors.Error{Err: errs}, + }, + { + "no namespace", + fields{RawReference: "docker.io"}, + &errcode.ErrorResponse{ + URL: &url.URL{Host: "registry-1.docker.io"}, + StatusCode: http.StatusUnauthorized, + Errors: errs, + }, + &oerrors.Error{Err: errs}, + }, + { + "not 401", + fields{RawReference: "docker.io"}, + &errcode.ErrorResponse{ + URL: &url.URL{Host: "registry-1.docker.io"}, + StatusCode: http.StatusConflict, + Errors: errs, + }, + &oerrors.Error{Err: errs}, + }, + { + "should hint", + fields{ + RawReference: "docker.io/alpine", + Path: "oras test", + }, + &errcode.ErrorResponse{ + URL: &url.URL{Host: "registry-1.docker.io"}, + StatusCode: http.StatusUnauthorized, + Errors: errs, + }, + &oerrors.Error{ + Err: errs, + Recommendation: "Namespace seems missing. Do you mean ` docker.io/library/alpine`?", + }, + }, + } + + cmd := &cobra.Command{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := &Target{ + Remote: tt.fields.Remote, + RawReference: tt.fields.RawReference, + Type: tt.fields.Type, + Reference: tt.fields.Reference, + Path: tt.fields.Path, + IsOCILayout: tt.fields.IsOCILayout, + } + got, modified := opts.Modify(cmd, tt.err) + gotErr, ok := got.(*oerrors.Error) + if !ok { + t.Errorf("expecting error to be *oerrors.Error but received %T", got) + } + if !reflect.DeepEqual(gotErr.Err, tt.modifiedErr.Err) || gotErr.Usage != tt.modifiedErr.Usage || gotErr.Recommendation != tt.modifiedErr.Recommendation { + t.Errorf("Target.Modify() error = %v, wantErr %v", gotErr, tt.modifiedErr) + } + if !modified { + t.Errorf("Failed to modify %v", tt.err) + } + }) + } +} diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 528a01b47..f16d0fc4d 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -96,7 +96,7 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder _ = cmd.MarkFlagRequired("artifact-type") opts.EnableDistributionSpecFlag() option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func runAttach(ctx context.Context, opts attachOptions) error { diff --git a/cmd/oras/root/blob/delete.go b/cmd/oras/root/blob/delete.go index fe22a1389..866aaf40f 100644 --- a/cmd/oras/root/blob/delete.go +++ b/cmd/oras/root/blob/delete.go @@ -69,7 +69,7 @@ Example - Delete a blob and print its descriptor: } option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func deleteBlob(ctx context.Context, opts deleteBlobOptions) (err error) { diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 3ebb4187f..fdfdb7abc 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -89,7 +89,7 @@ Example - Fetch and print a blob from OCI image layout archive file 'layout.tar' cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "output file `path`, use - for stdout") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func fetchBlob(ctx context.Context, opts fetchBlobOptions) (fetchErr error) { diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 801874048..1f892a6c2 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -97,7 +97,7 @@ Example - Push blob 'hi.txt' into an OCI image layout folder 'layout-dir': cmd.Flags().Int64VarP(&opts.size, "size", "", -1, "provide the blob size") cmd.Flags().StringVarP(&opts.mediaType, "media-type", "", ocispec.MediaTypeImageLayer, "specify the returned media type in the descriptor if --descriptor is used") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 28d61f275..94eb52044 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -102,7 +102,7 @@ Example - Copy an artifact with multiple tags with concurrency tuned: cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 3, "concurrency level") opts.EnableDistributionSpecFlag() option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.BinaryTarget) } func runCopy(ctx context.Context, opts copyOptions) error { diff --git a/cmd/oras/root/discover.go b/cmd/oras/root/discover.go index 6b98d4a22..a35db60a2 100644 --- a/cmd/oras/root/discover.go +++ b/cmd/oras/root/discover.go @@ -89,7 +89,7 @@ Example - Discover referrers of the manifest tagged 'v1' in an OCI image layout cmd.Flags().StringVarP(&opts.outputType, "output", "o", "table", "format in which to display referrers (table, json, or tree). tree format will also show indirect referrers") opts.EnableDistributionSpecFlag() option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func runDiscover(ctx context.Context, opts discoverOptions) error { diff --git a/cmd/oras/root/login.go b/cmd/oras/root/login.go index 4eca66986..07e3f43e0 100644 --- a/cmd/oras/root/login.go +++ b/cmd/oras/root/login.go @@ -73,7 +73,7 @@ Example - Log in with username and password in an interactive terminal and no TL }, } option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Remote) } func runLogin(ctx context.Context, opts loginOptions) (err error) { diff --git a/cmd/oras/root/manifest/delete.go b/cmd/oras/root/manifest/delete.go index b8e43f0c6..fa4cf75fe 100644 --- a/cmd/oras/root/manifest/delete.go +++ b/cmd/oras/root/manifest/delete.go @@ -73,7 +73,7 @@ Example - Delete a manifest by digest 'sha256:99e4703fbf30916f549cd6bfa9cdbab614 opts.EnableDistributionSpecFlag() option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func deleteManifest(ctx context.Context, opts deleteOptions) error { diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index dad6917c1..4862891b8 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -88,7 +88,7 @@ Example - Fetch raw manifest from an OCI layout archive file 'layout.tar': cmd.Flags().StringSliceVarP(&opts.mediaTypes, "media-type", "", nil, "accepted media types") cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the fetched manifest to, use - for stdout") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func fetchManifest(ctx context.Context, opts fetchOptions) (fetchErr error) { diff --git a/cmd/oras/root/manifest/fetch_config.go b/cmd/oras/root/manifest/fetch_config.go index 2ce4a8e6b..2519e491b 100644 --- a/cmd/oras/root/manifest/fetch_config.go +++ b/cmd/oras/root/manifest/fetch_config.go @@ -84,7 +84,7 @@ Example - Fetch and print the prettified descriptor of the config: cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the fetched config to, use - for stdout") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func fetchConfig(ctx context.Context, opts fetchConfigOptions) (fetchErr error) { diff --git a/cmd/oras/root/manifest/push.go b/cmd/oras/root/manifest/push.go index d001ffd2f..2043b3625 100644 --- a/cmd/oras/root/manifest/push.go +++ b/cmd/oras/root/manifest/push.go @@ -102,7 +102,7 @@ Example - Push a manifest to an OCI image layout folder 'layout-dir' and tag wit option.ApplyFlags(&opts, cmd.Flags()) cmd.Flags().StringVarP(&opts.mediaType, "media-type", "", "", "media type of manifest") cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 5, "concurrency level") - return cmd + return oerrors.Command(cmd, &opts.Target) } func pushManifest(ctx context.Context, opts pushOptions) error { diff --git a/cmd/oras/root/pull.go b/cmd/oras/root/pull.go index bf39e05e6..d59343002 100644 --- a/cmd/oras/root/pull.go +++ b/cmd/oras/root/pull.go @@ -92,7 +92,7 @@ Example - Pull artifact files from an OCI layout archive 'layout.tar': return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { - return runPull(cmd.Context(), opts) + return runPull(cmd.Context(), &opts) }, } @@ -103,10 +103,10 @@ Example - Pull artifact files from an OCI layout archive 'layout.tar': cmd.Flags().StringVarP(&opts.ManifestConfigRef, "config", "", "", "output manifest config file") cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 3, "concurrency level") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } -func runPull(ctx context.Context, opts pullOptions) error { +func runPull(ctx context.Context, opts *pullOptions) error { ctx, logger := opts.WithContext(ctx) // Copy Options copyOptions := oras.DefaultCopyOptions @@ -133,7 +133,7 @@ func runPull(ctx context.Context, opts pullOptions) error { dst.AllowPathTraversalOnWrite = opts.PathTraversal dst.DisableOverwrite = opts.KeepOldFiles - desc, layerSkipped, err := doPull(ctx, src, dst, copyOptions, &opts) + desc, layerSkipped, err := doPull(ctx, src, dst, copyOptions, opts) if err != nil { if errors.Is(err, file.ErrPathTraversalDisallowed) { err = fmt.Errorf("%s: %w", "use flag --allow-path-traversal to allow insecurely pulling files outside of working directory", err) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 0ee581a5f..c4825de09 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -132,7 +132,7 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 5, "concurrency level") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func runPush(ctx context.Context, opts pushOptions) error { diff --git a/cmd/oras/root/repo/ls.go b/cmd/oras/root/repo/ls.go index ea64b1968..84ffe2e06 100644 --- a/cmd/oras/root/repo/ls.go +++ b/cmd/oras/root/repo/ls.go @@ -68,7 +68,7 @@ Example - List the repositories under the registry that include values lexically cmd.Flags().StringVar(&opts.last, "last", "", "start after the repository specified by `last`") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Remote) } func listRepository(ctx context.Context, opts repositoryOptions) error { diff --git a/cmd/oras/root/repo/tags.go b/cmd/oras/root/repo/tags.go index 6eddc504b..eacb87594 100644 --- a/cmd/oras/root/repo/tags.go +++ b/cmd/oras/root/repo/tags.go @@ -76,7 +76,7 @@ Example - [Experimental] Show tags associated with a digest: cmd.Flags().StringVar(&opts.last, "last", "", "start after the tag specified by `last`") cmd.Flags().BoolVar(&opts.excludeDigestTag, "exclude-digest-tags", false, "[Preview] exclude all digest-like tags such as 'sha256-aaaa...'") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func showTags(ctx context.Context, opts showTagsOptions) error { diff --git a/cmd/oras/root/resolve.go b/cmd/oras/root/resolve.go index 976fac3cc..c1d533d34 100644 --- a/cmd/oras/root/resolve.go +++ b/cmd/oras/root/resolve.go @@ -58,7 +58,7 @@ Example - Resolve digest of the target artifact: cmd.Flags().BoolVarP(&opts.fullRef, "full-reference", "l", false, "print the full artifact reference with digest") option.ApplyFlags(&opts, cmd.Flags()) - return cmd + return oerrors.Command(cmd, &opts.Target) } func runResolve(ctx context.Context, opts resolveOptions) error { diff --git a/cmd/oras/root/tag.go b/cmd/oras/root/tag.go index dcc857f1f..51c6d81cb 100644 --- a/cmd/oras/root/tag.go +++ b/cmd/oras/root/tag.go @@ -88,7 +88,7 @@ Example - Tag the manifest 'v1.0.1' to 'v1.0.2' in an OCI image layout folder 'l option.ApplyFlags(&opts, cmd.Flags()) cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 5, "concurrency level") - return cmd + return oerrors.Command(cmd, &opts.Target) } func tagManifest(ctx context.Context, opts tagOptions) error { diff --git a/test/e2e/internal/utils/flags.go b/test/e2e/internal/utils/const.go similarity index 56% rename from test/e2e/internal/utils/flags.go rename to test/e2e/internal/utils/const.go index 893fa575d..6c247aa29 100644 --- a/test/e2e/internal/utils/flags.go +++ b/test/e2e/internal/utils/const.go @@ -15,16 +15,21 @@ limitations under the License. package utils -var Flags = struct { - Layout string - FromLayout string - ToLayout string - DistributionSpec string - ImageSpec string -}{ - "--oci-layout", - "--from-oci-layout", - "--to-oci-layout", - "--distribution-spec", - "--image-spec", -} +var ( + Flags = struct { + Layout string + FromLayout string + ToLayout string + DistributionSpec string + ImageSpec string + }{ + "--oci-layout", + "--from-oci-layout", + "--to-oci-layout", + "--distribution-spec", + "--image-spec", + } + RegistryErrorPrefix = "Error response from registry: " + EmptyBodyPrefix = "recognizable error message not found: " + InvalidTag = "i-dont-think-this-tag-exists" +) diff --git a/test/e2e/suite/auth/auth.go b/test/e2e/suite/auth/auth.go index ada87713f..5d192dab6 100644 --- a/test/e2e/suite/auth/auth.go +++ b/test/e2e/suite/auth/auth.go @@ -49,6 +49,23 @@ var _ = Describe("Common registry user", func() { }) }) + When("credential is invalid", func() { + It("should fail with registry error", func() { + RunWithInvalidCreds("attach", ZOTHost+"/repo:tag", "-a", "test=true", "--artifact-type", "doc/example") + RunWithInvalidCreds("discover", ZOTHost+"/repo:tag") + RunWithInvalidCreds("push", "-a", "key=value", ZOTHost+"/repo:tag") + RunWithInvalidCreds("pull", ZOTHost+"/repo:tag") + RunWithInvalidCreds("manifest", "fetch", ZOTHost+"/repo:tag") + RunWithInvalidCreds("blob", "delete", ZOTHost+"/repo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + RunWithInvalidCreds("blob", "push", ZOTHost+"/repo", WriteTempFile("blob", "test")) + RunWithInvalidCreds("tag", ZOTHost+"/repo:tag", "tag1") + RunWithInvalidCreds("resolve", ZOTHost+"/repo:tag") + RunWithInvalidCreds("repo", "ls", ZOTHost) + RunWithInvalidCreds("repo", "tags", RegistryRef(ZOTHost, "repo", "")) + RunWithInvalidCreds("manifest", "fetch-config", ZOTHost+"/repo:tag") + }) + }) + When("logging in", func() { tmpConfigName := "test.config" It("should succeed to use basic auth", func() { @@ -90,6 +107,14 @@ var _ = Describe("Common registry user", func() { WithInput(strings.NewReader(fmt.Sprintf("%s\n\n", Username))).ExpectFailure().Exec() }) + It("should fail if password is wrong with registry error prefix", func() { + ORAS("login", ZOTHost, "--registry-config", filepath.Join(GinkgoT().TempDir(), tmpConfigName)). + WithTimeOut(20*time.Second). + MatchKeyWords("Username: ", "Password: "). + MatchErrKeyWords(RegistryErrorPrefix). + WithInput(strings.NewReader(fmt.Sprintf("%s\n???\n", Username))).ExpectFailure().Exec() + }) + It("should fail if no token input", func() { ORAS("login", ZOTHost, "--registry-config", filepath.Join(GinkgoT().TempDir(), tmpConfigName)). WithTimeOut(20*time.Second). @@ -129,3 +154,9 @@ func RunWithoutLogin(args ...string) { MatchErrKeyWords("Error:", "credential required"). WithDescription("fail without logging in").Exec() } + +func RunWithInvalidCreds(args ...string) { + ORAS(append(args, "-u", Username, "-p", Password+"1")...).ExpectFailure(). + MatchErrKeyWords(RegistryErrorPrefix). + WithDescription("fail with invalid credentials").Exec() +} diff --git a/test/e2e/suite/command/cp.go b/test/e2e/suite/command/cp.go index 45718fa29..c0bb31f57 100644 --- a/test/e2e/suite/command/cp.go +++ b/test/e2e/suite/command/cp.go @@ -57,7 +57,7 @@ var _ = Describe("ORAS beginners:", func() { }) It("should fail when source doesn't exist", func() { - ORAS("cp", RegistryRef(ZOTHost, ImageRepo, "i-dont-think-this-tag-exists"), RegistryRef(ZOTHost, cpTestRepo("nonexistent-source"), "")).ExpectFailure().MatchErrKeyWords("Error:").Exec() + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, InvalidTag), RegistryRef(ZOTHost, cpTestRepo("nonexistent-source"), "")).ExpectFailure().MatchErrKeyWords(InvalidTag).Exec() }) It("should fail and show detailed error description if no argument provided", func() { @@ -75,6 +75,26 @@ var _ = Describe("ORAS beginners:", func() { Expect(err).Should(gbytes.Say("\n")) Expect(err).Should(gbytes.Say(`Run "oras cp -h"`)) }) + + It("should fail and show registry error prefix if source not found", func() { + src := RegistryRef(ZOTHost, ArtifactRepo, InvalidTag) + dst := GinkgoT().TempDir() + ORAS("cp", src, Flags.ToLayout, dst).MatchErrKeyWords(RegistryErrorPrefix).ExpectFailure().Exec() + }) + + It("should fail and show registry error prefix if destination registry is not logged in", func() { + src := PrepareTempOCI(ArtifactRepo) + dst := RegistryRef(ZOTHost, cpTestRepo("dest-not-logged-in"), "") + ORAS("cp", Flags.FromLayout, LayoutRef(src, foobar.Tag), dst, "--to-username", Username, "--to-password", Password+"?"). + MatchErrKeyWords(RegistryErrorPrefix).ExpectFailure().Exec() + }) + + It("should fail and show registry error prefix if source registry is not logged in", func() { + src := RegistryRef(ZOTHost, cpTestRepo("src-not-logged-in"), foobar.Tag) + dst := RegistryRef(ZOTHost, ArtifactRepo, "") + ORAS("cp", src, dst, "--from-username", Username, "--from-password", Password+"?"). + MatchErrKeyWords(RegistryErrorPrefix).ExpectFailure().Exec() + }) }) }) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 6f7c10ebc..48e29356a 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -557,7 +557,7 @@ var _ = Describe("1.0 registry users:", func() { It("should fail to fetch image if media type assertion fails", func() { ORAS("manifest", "fetch", RegistryRef(FallbackHost, ImageRepo, multi_arch.LinuxAMD64.Digest.String()), "--media-type", "this.will.not.be.found"). ExpectFailure(). - MatchErrKeyWords(multi_arch.LinuxAMD64.Digest.String(), "error: ", "not found").Exec() + MatchErrKeyWords(multi_arch.LinuxAMD64.Digest.String(), RegistryErrorPrefix, "not found").Exec() }) }) diff --git a/test/e2e/suite/command/pull.go b/test/e2e/suite/command/pull.go index c841820a9..ebd5cf61b 100644 --- a/test/e2e/suite/command/pull.go +++ b/test/e2e/suite/command/pull.go @@ -74,6 +74,22 @@ var _ = Describe("ORAS beginners:", func() { gomega.Expect(err).Should(gbytes.Say("\n")) gomega.Expect(err).Should(gbytes.Say(`Run "oras pull -h"`)) }) + + It("should fail if password is wrong with registry error prefix", func() { + ORAS("pull", RegistryRef(ZOTHost, ArtifactRepo, empty.Tag), "-u", Username, "-p", "???"). + MatchErrKeyWords(RegistryErrorPrefix).ExpectFailure().Exec() + }) + + It("should fail if artifact is not found with registry error prefix", func() { + ORAS("pull", RegistryRef(ZOTHost, ArtifactRepo, InvalidTag)). + MatchErrKeyWords(RegistryErrorPrefix).ExpectFailure().Exec() + }) + + It("should fail if artifact is not found from OCI layout", func() { + root := PrepareTempOCI(ArtifactRepo) + ORAS("pull", Flags.Layout, LayoutRef(root, InvalidTag)). + MatchErrKeyWords("Error: ").ExpectFailure().Exec() + }) }) }) diff --git a/test/e2e/suite/command/repo.go b/test/e2e/suite/command/repo.go index 0aab9b2bc..8bc924498 100644 --- a/test/e2e/suite/command/repo.go +++ b/test/e2e/suite/command/repo.go @@ -52,6 +52,11 @@ var _ = Describe("ORAS beginners:", func() { gomega.Expect(err).Should(gbytes.Say("\n")) gomega.Expect(err).Should(gbytes.Say(`Run "oras repo ls -h"`)) }) + + It("should fail if password is wrong with registry error prefix", func() { + ORAS("repo", "ls", ZOTHost, "-u", Username, "-p", "???"). + MatchErrKeyWords(RegistryErrorPrefix).ExpectFailure().Exec() + }) }) When("running `repo tags`", func() { It("should show help description with feature flags", func() { @@ -63,10 +68,10 @@ var _ = Describe("ORAS beginners:", func() { ORAS("repository", "show-tags", "--help").MatchKeyWords(ExampleDesc).Exec() }) - It("should fail listing repositories if wrong registry provided", func() { + It("should fail listing repositories if wrong reference provided", func() { ORAS("repo", "tags").ExpectFailure().MatchErrKeyWords("Error:").Exec() ORAS("repo", "tags", ZOTHost).ExpectFailure().MatchErrKeyWords("Error:").Exec() - ORAS("repo", "tags", RegistryRef(ZOTHost, ImageRepo, "some-tag")).ExpectFailure().MatchErrKeyWords("Error:").Exec() + ORAS("repo", "tags", RegistryRef(ZOTHost, ImageRepo, "some-tag")).ExpectFailure().MatchErrKeyWords(RegistryErrorPrefix).Exec() }) It("should fail and show detailed error description if no argument provided", func() { diff --git a/test/e2e/suite/command/resolve.go b/test/e2e/suite/command/resolve.go index fac8266b4..9a345f11e 100644 --- a/test/e2e/suite/command/resolve.go +++ b/test/e2e/suite/command/resolve.go @@ -47,7 +47,13 @@ var _ = Describe("ORAS beginners:", 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() + ORAS("resolve", RegistryRef(ZOTHost, ImageRepo, "i-dont-think-this-tag-exists")).ExpectFailure().MatchErrKeyWords(RegistryErrorPrefix, "failed to resolve digest:", "not found").Exec() + }) + It("should fail with empty response when returned response doesn't have body", func() { + ORAS("resolve", RegistryRef(ZOTHost, ImageRepo, InvalidTag), "-u", Username, "-p", Password+"1"). + ExpectFailure(). + MatchErrKeyWords(RegistryErrorPrefix, EmptyBodyPrefix, "response status code 401: Unauthorized"). + Exec() }) It("should fail and show detailed error description if no argument provided", func() { diff --git a/test/e2e/suite/command/tag.go b/test/e2e/suite/command/tag.go index 3c98dcea3..4532d7655 100644 --- a/test/e2e/suite/command/tag.go +++ b/test/e2e/suite/command/tag.go @@ -33,7 +33,7 @@ var _ = Describe("ORAS beginners:", func() { }) It("should fail when provided manifest reference is not found", func() { - ORAS("tag", RegistryRef(ZOTHost, ImageRepo, "i-dont-think-this-tag-exists"), "tagged").ExpectFailure().MatchErrKeyWords("Error:").Exec() + ORAS("tag", RegistryRef(ZOTHost, ImageRepo, InvalidTag), "tagged").ExpectFailure().MatchErrKeyWords(RegistryErrorPrefix).Exec() }) It("should fail and show detailed error description if no argument provided", func() {