Skip to content

Commit

Permalink
feat: improve returned error for invalid arguments (oras-project#1201)
Browse files Browse the repository at this point in the history
Signed-off-by: Billy Zha <jinzha1@microsoft.com>
Signed-off-by: Feynman Zhou <feynmanzhou@microsoft.com>
  • Loading branch information
qweeah authored and FeynmanZhou committed May 11, 2024
1 parent cb4ae83 commit dee4e64
Show file tree
Hide file tree
Showing 33 changed files with 286 additions and 33 deletions.
32 changes: 32 additions & 0 deletions cmd/oras/internal/argument/checker.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
39 changes: 39 additions & 0 deletions cmd/oras/internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
6 changes: 4 additions & 2 deletions cmd/oras/root/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:]
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/blob/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/blob/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/blob/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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], ",")
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
},
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/manifest/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/manifest/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/manifest/fetch_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/manifest/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/repo/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/root/repo/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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]
Expand Down
Loading

0 comments on commit dee4e64

Please sign in to comment.