diff --git a/cmd/oras/internal/argument/checker.go b/cmd/oras/internal/argument/checker.go new file mode 100644 index 000000000..e187ee1e8 --- /dev/null +++ b/cmd/oras/internal/argument/checker.go @@ -0,0 +1,32 @@ +/* +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 argument + +import "fmt" + +// Exactly checks if the number of arguments is exactly cnt. +func Exactly(cnt int) func(args []string) (bool, string) { + return func(args []string) (bool, string) { + return len(args) == cnt, fmt.Sprintf("exactly %d argument", cnt) + } +} + +// AtLeast checks if the number of arguments is larger or equal to cnt. +func AtLeast(cnt int) func(args []string) (bool, string) { + return func(args []string) (bool, string) { + return len(args) >= cnt, fmt.Sprintf("at least %d argument", cnt) + } +} diff --git a/cmd/oras/internal/errors/errors.go b/cmd/oras/internal/errors/errors.go index 2c3fd8bf5..b93144428 100644 --- a/cmd/oras/internal/errors/errors.go +++ b/cmd/oras/internal/errors/errors.go @@ -18,9 +18,48 @@ package errors import ( "fmt" + "github.com/spf13/cobra" "oras.land/oras-go/v2/registry" ) +// Error is the error type for CLI error messaging. +type Error struct { + Err error + Usage string + Recommendation string +} + +// Unwrap implements the errors.Wrapper interface. +func (o *Error) Unwrap() error { + return o.Err +} + +// Error implements the error interface. +func (o *Error) Error() string { + ret := o.Err.Error() + if o.Usage != "" { + ret += fmt.Sprintf("\nUsage: %s", o.Usage) + } + if o.Recommendation != "" { + ret += fmt.Sprintf("\n%s", o.Recommendation) + } + return ret +} + +// CheckArgs checks the args with the checker function. +func CheckArgs(checker func(args []string) (bool, string), Usage string) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if ok, text := checker(args); !ok { + return &Error{ + Err: fmt.Errorf(`%q requires %s but got %d`, cmd.CommandPath(), text, len(args)), + Usage: fmt.Sprintf("%s %s", cmd.Parent().CommandPath(), cmd.Use), + Recommendation: fmt.Sprintf(`Please specify %s as %s. Run "%s -h" for more options and examples`, text, Usage, cmd.CommandPath()), + } + } + 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/root/attach.go b/cmd/oras/root/attach.go index c485c1927..a8c055bdd 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -27,7 +27,9 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/file" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display/track" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" "oras.land/oras/internal/registryutil" @@ -75,8 +77,8 @@ Example - Attach file 'hi.txt' and export the pushed manifest to 'manifest.json' Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder 'layout-dir': oras attach --oci-layout --artifact-type doc/example layout-dir:v1 hi.txt - `, - Args: cobra.MinimumNArgs(1), +`, + Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination artifact for attaching."), PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] opts.FileRefs = args[1:] diff --git a/cmd/oras/root/blob/delete.go b/cmd/oras/root/blob/delete.go index 50bdf7240..fe22a1389 100644 --- a/cmd/oras/root/blob/delete.go +++ b/cmd/oras/root/blob/delete.go @@ -24,6 +24,8 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/registryutil" ) @@ -53,7 +55,7 @@ Example - Delete a blob without prompting confirmation: Example - Delete a blob and print its descriptor: oras blob delete --descriptor --force localhost:5000/hello@sha256:9a201d228ebd966211f7d1131be19f152be428bd373a92071c71d8deaf83b3e5 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the target blob to delete"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] if opts.OutputDescriptor && !opts.Force { diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index a7c1d3f48..3ebb4187f 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -28,7 +28,9 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry/remote" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display/track" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" ) @@ -67,7 +69,7 @@ Example - Fetch and print a blob from OCI image layout folder 'layout-dir': Example - Fetch and print a blob from OCI image layout archive file 'layout.tar': oras blob fetch --oci-layout --output - layout.tar@sha256:9a201d228ebd966211f7d1131be19f152be428bd373a92071c71d8deaf83b3e5 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the target blob to fetch"), PreRunE: func(cmd *cobra.Command, args []string) error { if opts.outputPath == "" && !opts.OutputDescriptor { return errors.New("either `--output` or `--descriptor` must be provided") diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index b13692c26..801874048 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -25,8 +25,10 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" "oras.land/oras-go/v2" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/display/track" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/file" ) @@ -73,7 +75,7 @@ Example - Push blob without TLS: Example - Push blob 'hi.txt' into an OCI image layout folder 'layout-dir': oras blob push --oci-layout layout-dir hi.txt `, - Args: cobra.ExactArgs(2), + Args: oerrors.CheckArgs(argument.Exactly(2), "the destination to push to and the file to read blob content from"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] opts.fileRef = args[1] diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 2bd463b8c..31d63a85e 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -29,8 +29,10 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/display/track" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/docker" "oras.land/oras/internal/graph" @@ -83,7 +85,7 @@ Example - Copy an artifact with multiple tags: Example - Copy an artifact with multiple tags with concurrency tuned: oras cp --concurrency 10 localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:tag1,tag2,tag3 `, - Args: cobra.ExactArgs(2), + Args: oerrors.CheckArgs(argument.Exactly(2), "the source and destination for copying"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.From.RawReference = args[0] refs := strings.Split(args[1], ",") diff --git a/cmd/oras/root/discover.go b/cmd/oras/root/discover.go index 801710ead..ec67d839c 100644 --- a/cmd/oras/root/discover.go +++ b/cmd/oras/root/discover.go @@ -28,6 +28,8 @@ import ( "gopkg.in/yaml.v3" "oras.land/oras-go/v2" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" "oras.land/oras/internal/tree" @@ -73,7 +75,7 @@ Example - Discover referrers of the manifest tagged 'v1' in an OCI image layout oras discover --oci-layout layout-dir:v1 oras discover --oci-layout -v -o tree layout-dir:v1 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the target artifact to discover referrers from"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] return option.Parse(&opts) diff --git a/cmd/oras/root/login.go b/cmd/oras/root/login.go index d6cc65d5d..4eca66986 100644 --- a/cmd/oras/root/login.go +++ b/cmd/oras/root/login.go @@ -25,6 +25,8 @@ import ( credentials "github.com/oras-project/oras-credentials-go" "github.com/spf13/cobra" "golang.org/x/term" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/credential" "oras.land/oras/internal/io" @@ -61,7 +63,7 @@ Example - Log in with username and password in an interactive terminal: Example - Log in with username and password in an interactive terminal and no TLS check: oras login --insecure localhost:5000 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the registry to log in to"), PreRunE: func(cmd *cobra.Command, args []string) error { return option.Parse(&opts) }, diff --git a/cmd/oras/root/logout.go b/cmd/oras/root/logout.go index bdb39d1f5..fe6241860 100644 --- a/cmd/oras/root/logout.go +++ b/cmd/oras/root/logout.go @@ -21,6 +21,8 @@ import ( credentials "github.com/oras-project/oras-credentials-go" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/internal/credential" ) @@ -41,7 +43,7 @@ func logoutCmd() *cobra.Command { Example - Logout: oras logout localhost:5000 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the registry you want to log out"), RunE: func(cmd *cobra.Command, args []string) error { opts.hostname = args[0] return runLogout(cmd.Context(), opts) diff --git a/cmd/oras/root/manifest/delete.go b/cmd/oras/root/manifest/delete.go index 8064c5ea8..b8e43f0c6 100644 --- a/cmd/oras/root/manifest/delete.go +++ b/cmd/oras/root/manifest/delete.go @@ -24,6 +24,8 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/registryutil" ) @@ -56,7 +58,7 @@ Example - Delete a manifest and print its descriptor: Example - Delete a manifest by digest 'sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9' from repository 'localhost:5000/hello': oras manifest delete localhost:5000/hello@sha:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the manifest to delete"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] if opts.OutputDescriptor && !opts.Force { diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 52ec2f80b..dad6917c1 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -26,6 +26,8 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2" "oras.land/oras-go/v2/registry/remote" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" ) @@ -69,7 +71,7 @@ Example - Fetch raw manifest from an OCI image layout folder 'layout-dir': Example - Fetch raw manifest from an OCI layout archive file 'layout.tar': oras manifest fetch --oci-layout layout.tar:v1 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the manifest to fetch"), PreRunE: func(cmd *cobra.Command, args []string) error { if opts.outputPath == "-" && opts.OutputDescriptor { return errors.New("`--output -` cannot be used with `--descriptor` at the same time") diff --git a/cmd/oras/root/manifest/fetch_config.go b/cmd/oras/root/manifest/fetch_config.go index e0f871f34..2ce4a8e6b 100644 --- a/cmd/oras/root/manifest/fetch_config.go +++ b/cmd/oras/root/manifest/fetch_config.go @@ -26,6 +26,8 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/descriptor" ) @@ -67,7 +69,7 @@ Example - Fetch the descriptor of the config: Example - Fetch and print the prettified descriptor of the config: oras manifest fetch-config --descriptor --pretty localhost:5000/hello:v1 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the manifest config to fetch"), PreRunE: func(cmd *cobra.Command, args []string) error { if opts.outputPath == "-" && opts.OutputDescriptor { return errors.New("`--output -` cannot be used with `--descriptor` at the same time") diff --git a/cmd/oras/root/manifest/push.go b/cmd/oras/root/manifest/push.go index d9e737918..d001ffd2f 100644 --- a/cmd/oras/root/manifest/push.go +++ b/cmd/oras/root/manifest/push.go @@ -28,7 +28,9 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/file" ) @@ -80,7 +82,7 @@ Example - Push a manifest to repository 'localhost:5000/hello' and tag with 'tag Example - Push a manifest to an OCI image layout folder 'layout-dir' and tag with 'v1': oras manifest push --oci-layout layout-dir:v1 manifest.json `, - Args: cobra.ExactArgs(2), + Args: oerrors.CheckArgs(argument.Exactly(2), "the destination to push to and the file to read manifest content from"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.fileRef = args[1] if opts.fileRef == "-" && opts.PasswordFromStdin { diff --git a/cmd/oras/root/pull.go b/cmd/oras/root/pull.go index e98b03691..c96c60ffa 100644 --- a/cmd/oras/root/pull.go +++ b/cmd/oras/root/pull.go @@ -28,8 +28,10 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/file" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/display/track" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/fileref" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" @@ -84,7 +86,7 @@ Example - Pull artifact files from an OCI image layout folder 'layout-dir': Example - Pull artifact files from an OCI layout archive 'layout.tar': oras pull --oci-layout layout.tar:v1 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the artifact reference you want to pull"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] return option.Parse(&opts) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index c9dc0a7f7..c9bda91de 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -30,8 +30,10 @@ import ( "oras.land/oras-go/v2/content/file" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/display/track" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/fileref" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/contentutil" @@ -100,7 +102,7 @@ Example - Push file "hi.txt" with multiple tags and concurrency level tuned: Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with tag 'test': oras push --oci-layout layout-dir:test hi.txt `, - Args: cobra.MinimumNArgs(1), + Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination for pushing"), PreRunE: func(cmd *cobra.Command, args []string) error { refs := strings.Split(args[0], ",") opts.RawReference = refs[0] diff --git a/cmd/oras/root/repo/ls.go b/cmd/oras/root/repo/ls.go index 0d6c3a7c5..ea64b1968 100644 --- a/cmd/oras/root/repo/ls.go +++ b/cmd/oras/root/repo/ls.go @@ -22,6 +22,8 @@ import ( "strings" "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/repository" ) @@ -50,7 +52,7 @@ Example - List the repositories under a namespace in the registry: Example - List the repositories under the registry that include values lexically after last: oras repo ls --last "last_repo" localhost:5000 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the target registry to list repositories from"), Aliases: []string{"list"}, PreRunE: func(cmd *cobra.Command, args []string) error { return option.Parse(&opts) diff --git a/cmd/oras/root/repo/tags.go b/cmd/oras/root/repo/tags.go index 45e37dba9..6eddc504b 100644 --- a/cmd/oras/root/repo/tags.go +++ b/cmd/oras/root/repo/tags.go @@ -22,6 +22,8 @@ import ( "github.com/opencontainers/go-digest" "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" ) @@ -61,7 +63,7 @@ Example - [Experimental] Show tags associated with a particular tagged resource: Example - [Experimental] Show tags associated with a digest: oras repo tags localhost:5000/hello@sha256:c551125a624189cece9135981621f3f3144564ddabe14b523507bf74c2281d9b `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the target repository to list tags from"), Aliases: []string{"show-tags"}, PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] diff --git a/cmd/oras/root/resolve.go b/cmd/oras/root/resolve.go index b98e67144..976fac3cc 100644 --- a/cmd/oras/root/resolve.go +++ b/cmd/oras/root/resolve.go @@ -21,6 +21,8 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2" + "oras.land/oras/cmd/oras/internal/argument" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" ) @@ -43,7 +45,7 @@ func resolveCmd() *cobra.Command { Example - Resolve digest of the target artifact: oras resolve localhost:5000/hello-world:v1 `, - Args: cobra.ExactArgs(1), + Args: oerrors.CheckArgs(argument.Exactly(1), "the target artifact reference to resolve"), Aliases: []string{"digest"}, PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] diff --git a/cmd/oras/root/tag.go b/cmd/oras/root/tag.go index 4524d4fb9..e4f65f830 100644 --- a/cmd/oras/root/tag.go +++ b/cmd/oras/root/tag.go @@ -22,7 +22,9 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2" "oras.land/oras-go/v2/registry" + "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display" + oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" ) @@ -56,7 +58,7 @@ Example - Tag the manifest 'v1.0.1' in 'localhost:5000/hello' to 'v1.0.1', 'v1.0 Example - Tag the manifest 'v1.0.1' to 'v1.0.2' in an OCI image layout folder 'layout-dir': oras tag layout-dir:v1.0.1 v1.0.2 `, - Args: cobra.MinimumNArgs(2), + Args: oerrors.CheckArgs(argument.AtLeast(1), "the to-be-retage artifact and the tags to be added"), PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] if _, err := registry.ParseReference(opts.RawReference); err != nil { diff --git a/cmd/oras/root/version.go b/cmd/oras/root/version.go index b91659afa..fae9d28bf 100644 --- a/cmd/oras/root/version.go +++ b/cmd/oras/root/version.go @@ -17,6 +17,7 @@ package root import ( "fmt" + "os" "runtime" "strings" @@ -34,7 +35,15 @@ func versionCmd() *cobra.Command { Example - print version: oras version `, - Args: cobra.NoArgs, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + _, err := fmt.Fprintf(os.Stderr, "warning: `oras version` requires no argument, %q will be ignored\n", strings.Join(args, ",")) + if err != nil { + return err + } + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { return runVersion() }, diff --git a/test/e2e/suite/auth/auth.go b/test/e2e/suite/auth/auth.go index 1d21b7fc4..ada87713f 100644 --- a/test/e2e/suite/auth/auth.go +++ b/test/e2e/suite/auth/auth.go @@ -22,6 +22,8 @@ import ( "time" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" "oras.land/oras-go/v2" "oras.land/oras/test/e2e/internal/testdata/foobar" . "oras.land/oras/test/e2e/internal/utils" @@ -56,6 +58,14 @@ var _ = Describe("Common registry user", func() { MatchErrKeyWords("WARNING", "Using --password via the CLI is insecure", "Use --password-stdin").Exec() }) + It("should show detailed error description if no argument provided", func() { + err := ORAS("login").ExpectFailure().Exec().Err + Expect(err).Should(gbytes.Say("Error")) + Expect(err).Should(gbytes.Say("\nUsage: oras login")) + Expect(err).Should(gbytes.Say("\n")) + Expect(err).Should(gbytes.Say(`Run "oras login -h"`)) + }) + It("should fail if no username input", func() { ORAS("login", ZOTHost, "--registry-config", filepath.Join(GinkgoT().TempDir(), tmpConfigName)). WithTimeOut(20 * time.Second). diff --git a/test/e2e/suite/command/attach.go b/test/e2e/suite/command/attach.go index ef5c51d2e..63fc1fea4 100644 --- a/test/e2e/suite/command/attach.go +++ b/test/e2e/suite/command/attach.go @@ -64,6 +64,14 @@ var _ = Describe("ORAS beginners:", func() { ORAS("attach", "--artifact-type", "oras/test", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--distribution-spec", "???"). ExpectFailure().MatchErrKeyWords("unknown distribution specification flag").Exec() }) + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("attach").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras attach")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras attach -h"`)) + }) }) }) diff --git a/test/e2e/suite/command/blob.go b/test/e2e/suite/command/blob.go index 34b3d8356..7f0daf5a0 100644 --- a/test/e2e/suite/command/blob.go +++ b/test/e2e/suite/command/blob.go @@ -22,6 +22,8 @@ import ( "strings" . "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" "oras.land/oras/test/e2e/internal/testdata/foobar" . "oras.land/oras/test/e2e/internal/utils" ) @@ -105,6 +107,14 @@ var _ = Describe("ORAS beginners:", func() { ExpectFailure().Exec() }) + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("blob", "fetch").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras blob fetch")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras blob fetch -h"`)) + }) + It("should fail if no digest provided", func() { ORAS("blob", "fetch", RegistryRef(ZOTHost, ImageRepo, "")). ExpectFailure().Exec() @@ -124,6 +134,16 @@ var _ = Describe("ORAS beginners:", func() { ORAS("blob", "fetch").ExpectFailure().Exec() }) }) + + When("running `blob delete`", func() { + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("blob", "delete").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras blob delete")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras blob delete -h"`)) + }) + }) }) When("running `blob delete`", func() { diff --git a/test/e2e/suite/command/cp.go b/test/e2e/suite/command/cp.go index 4191589bd..45718fa29 100644 --- a/test/e2e/suite/command/cp.go +++ b/test/e2e/suite/command/cp.go @@ -59,6 +59,22 @@ 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() }) + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("cp").ExpectFailure().Exec().Err + Expect(err).Should(gbytes.Say("Error")) + Expect(err).Should(gbytes.Say("\nUsage: oras cp")) + Expect(err).Should(gbytes.Say("\n")) + Expect(err).Should(gbytes.Say(`Run "oras cp -h"`)) + }) + + It("should fail and show detailed error description if more than 2 arguments are provided", func() { + err := ORAS("cp", "foo", "bar", "buz").ExpectFailure().Exec().Err + Expect(err).Should(gbytes.Say("Error")) + Expect(err).Should(gbytes.Say("\nUsage: oras cp")) + Expect(err).Should(gbytes.Say("\n")) + Expect(err).Should(gbytes.Say(`Run "oras cp -h"`)) + }) }) }) diff --git a/test/e2e/suite/command/discover.go b/test/e2e/suite/command/discover.go index 42c59da07..fd2f678f5 100644 --- a/test/e2e/suite/command/discover.go +++ b/test/e2e/suite/command/discover.go @@ -63,6 +63,22 @@ var _ = Describe("ORAS beginners:", func() { It("should fail when no tag or digest found in provided subject reference", func() { ORAS("discover", RegistryRef(ZOTHost, ImageRepo, "")).ExpectFailure().MatchErrKeyWords("Error:", "no tag or digest").Exec() }) + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("discover").ExpectFailure().Exec().Err + Expect(err).Should(gbytes.Say("Error")) + Expect(err).Should(gbytes.Say("\nUsage: oras discover")) + Expect(err).Should(gbytes.Say("\n")) + Expect(err).Should(gbytes.Say(`Run "oras discover -h"`)) + }) + + It("should fail and show detailed error description if more than 1 argument are provided", func() { + err := ORAS("discover", "foo", "bar").ExpectFailure().Exec().Err + Expect(err).Should(gbytes.Say("Error")) + Expect(err).Should(gbytes.Say("\nUsage: oras discover")) + Expect(err).Should(gbytes.Say("\n")) + Expect(err).Should(gbytes.Say(`Run "oras discover -h"`)) + }) }) }) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 116979d0d..6f7c10ebc 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -63,11 +63,12 @@ var _ = Describe("ORAS beginners:", func() { Exec() }) - It("should fail pushing without reference provided", func() { - ORAS("manifest", "push"). - ExpectFailure(). - MatchErrKeyWords("Error:"). - Exec() + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("manifest", "push").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras manifest push")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras manifest push -h"`)) }) It("should fail pushing with a manifest from stdin without media type flag", func() { @@ -84,19 +85,30 @@ var _ = Describe("ORAS beginners:", func() { MatchKeyWords(ExampleDesc). Exec() }) - It("should fail fetching manifest without reference provided", func() { - ORAS("manifest", "fetch"). - ExpectFailure(). - MatchErrKeyWords("Error:"). - Exec() + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("manifest", "fetch").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras manifest fetch")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras manifest fetch -h"`)) }) }) + When("running `manifest delete`", func() { It("should show help doc with feature flags", func() { out := ORAS("manifest", "delete", "--help").MatchKeyWords(ExampleDesc).Exec() gomega.Expect(out).Should(gbytes.Say("--distribution-spec string\\s+%s", regexp.QuoteMeta(feature.Preview.Mark))) }) + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("manifest", "delete").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras manifest delete")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras manifest delete -h"`)) + }) + tempTag := "to-delete" It("should cancel deletion without confirmation", func() { dstRepo := fmt.Sprintf(repoFmt, "delete", "no-confirm") @@ -154,8 +166,12 @@ var _ = Describe("ORAS beginners:", func() { MatchKeyWords(ExampleDesc, "\nUsage:").Exec() }) - It("should fail if no manifest reference provided", func() { - ORAS("manifest", "fetch-config").ExpectFailure().Exec() + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("manifest", "fetch-config").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras manifest fetch-config")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras manifest fetch-config -h"`)) }) It("should fail if provided reference does not exist", func() { diff --git a/test/e2e/suite/command/pull.go b/test/e2e/suite/command/pull.go index 06accb879..c841820a9 100644 --- a/test/e2e/suite/command/pull.go +++ b/test/e2e/suite/command/pull.go @@ -66,6 +66,14 @@ var _ = Describe("ORAS beginners:", func() { out := ORAS("pull", ref).WithWorkDir(tempDir).Exec().Out gomega.Expect(out).ShouldNot(gbytes.Say(hintMsg(ref))) }) + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("pull").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras pull")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras pull -h"`)) + }) }) }) diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index 81bd2ddad..123f6a0bd 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -41,6 +41,14 @@ var _ = Describe("ORAS beginners:", func() { gomega.Expect(out).Should(gbytes.Say("--image-spec string\\s+%s", regexp.QuoteMeta(feature.Experimental.Mark))) }) + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("push").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras push")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras push -h"`)) + }) + It("should fail to use --config and --artifact-type at the same time for OCI spec v1.0 registry", func() { tempDir := PrepareTempFiles() repo := pushTestRepo("no-mediatype") diff --git a/test/e2e/suite/command/repo.go b/test/e2e/suite/command/repo.go index 3cbe628cf..0aab9b2bc 100644 --- a/test/e2e/suite/command/repo.go +++ b/test/e2e/suite/command/repo.go @@ -21,6 +21,7 @@ import ( "strings" . "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "oras.land/oras/test/e2e/internal/testdata/feature" @@ -41,9 +42,16 @@ var _ = Describe("ORAS beginners:", func() { }) It("should fail listing repositories if wrong registry provided", func() { - ORAS("repo", "ls").ExpectFailure().MatchErrKeyWords("Error:").Exec() ORAS("repo", "ls", RegistryRef(ZOTHost, ImageRepo, "some-tag")).ExpectFailure().MatchErrKeyWords("Error:").Exec() }) + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("repo", "ls").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras repo ls")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras repo ls -h"`)) + }) }) When("running `repo tags`", func() { It("should show help description with feature flags", func() { @@ -60,6 +68,14 @@ var _ = Describe("ORAS beginners:", func() { ORAS("repo", "tags", ZOTHost).ExpectFailure().MatchErrKeyWords("Error:").Exec() ORAS("repo", "tags", RegistryRef(ZOTHost, ImageRepo, "some-tag")).ExpectFailure().MatchErrKeyWords("Error:").Exec() }) + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("repo", "tags").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras repo tags")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras repo tags -h"`)) + }) }) }) }) diff --git a/test/e2e/suite/command/resolve.go b/test/e2e/suite/command/resolve.go index f3ae46c2b..fac8266b4 100644 --- a/test/e2e/suite/command/resolve.go +++ b/test/e2e/suite/command/resolve.go @@ -50,6 +50,13 @@ var _ = Describe("ORAS beginners:", func() { ORAS("resolve", RegistryRef(ZOTHost, ImageRepo, "i-dont-think-this-tag-exists")).ExpectFailure().MatchErrKeyWords("Error: failed to resolve digest:", "not found").Exec() }) + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("resolve").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras resolve")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras resolve -h"`)) + }) }) }) diff --git a/test/e2e/suite/command/tag.go b/test/e2e/suite/command/tag.go index 3895d0424..0daf4e840 100644 --- a/test/e2e/suite/command/tag.go +++ b/test/e2e/suite/command/tag.go @@ -39,6 +39,14 @@ var _ = Describe("ORAS beginners:", func() { It("should fail when provided invalid reference", func() { ORAS("tag", "list", "tagged").ExpectFailure().MatchErrKeyWords("Error:", "'list'").Exec() }) + + It("should fail and show detailed error description if no argument provided", func() { + err := ORAS("tag").ExpectFailure().Exec().Err + gomega.Expect(err).Should(gbytes.Say("Error")) + gomega.Expect(err).Should(gbytes.Say("\nUsage: oras tag")) + gomega.Expect(err).Should(gbytes.Say("\n")) + gomega.Expect(err).Should(gbytes.Say(`Run "oras tag -h"`)) + }) }) }) diff --git a/test/e2e/suite/command/version.go b/test/e2e/suite/command/version.go index cbc8f71d5..bc2a652d3 100644 --- a/test/e2e/suite/command/version.go +++ b/test/e2e/suite/command/version.go @@ -25,5 +25,9 @@ var _ = Describe("ORAS user:", func() { It("should run version command", func() { ORAS("version").Exec() }) + + It("should run version command and ignore extra arguments with warning", func() { + ORAS("version", "foo", "bar").MatchErrKeyWords("foo", "bar", "warning:").Exec() + }) }) })