Skip to content

Commit a4a4b33

Browse files
authored
chore(ux): improve error message when attaching without subject artifact (oras-project#1430)
Signed-off-by: Billy Zha <[email protected]>
1 parent e7ffb65 commit a4a4b33

File tree

4 files changed

+48
-14
lines changed

4 files changed

+48
-14
lines changed

cmd/oras/internal/errors/errors.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ import (
2626
"oras.land/oras-go/v2/registry/remote/errcode"
2727
)
2828

29+
// OperationType stands for certain type of operations.
30+
type OperationType int
31+
32+
const (
33+
// OperationTypeParseArtifactReference represents parsing artifact
34+
// reference operation.
35+
OperationTypeParseArtifactReference OperationType = iota + 1
36+
)
37+
2938
// RegistryErrorPrefix is the commandline prefix for errors from registry.
3039
const RegistryErrorPrefix = "Error response from registry:"
3140

@@ -39,6 +48,7 @@ func (e UnsupportedFormatTypeError) Error() string {
3948

4049
// Error is the error type for CLI error messaging.
4150
type Error struct {
51+
OperationType OperationType
4252
Err error
4353
Usage string
4454
Recommendation string
@@ -160,6 +170,7 @@ func NewErrEmptyTagOrDigest(ref string, cmd *cobra.Command, needsTag bool) error
160170
errMsg = "no tag or digest specified"
161171
}
162172
return &Error{
173+
OperationType: OperationTypeParseArtifactReference,
163174
Err: fmt.Errorf(`"%s": %s`, ref, errMsg),
164175
Usage: fmt.Sprintf("%s %s", cmd.Parent().CommandPath(), cmd.Use),
165176
Recommendation: fmt.Sprintf(`Please specify a reference in the form of %s. Run "%s -h" for more options and examples`, form, cmd.CommandPath()),

cmd/oras/internal/option/target.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,14 @@ func (opts *Target) Parse(cmd *cobra.Command) error {
105105
return opts.parseOCILayoutReference()
106106
default:
107107
opts.Type = TargetTypeRemote
108-
if _, err := registry.ParseReference(opts.RawReference); err != nil {
108+
if ref, err := registry.ParseReference(opts.RawReference); err != nil {
109109
return &oerrors.Error{
110+
OperationType: oerrors.OperationTypeParseArtifactReference,
110111
Err: fmt.Errorf("%q: %w", opts.RawReference, err),
111112
Recommendation: "Please make sure the provided reference is in the form of <registry>/<repo>[:tag|@digest]",
112113
}
114+
} else {
115+
opts.Reference = ref.Reference
113116
}
114117
return opts.Remote.Parse(cmd)
115118
}
@@ -243,9 +246,9 @@ func (opts *Target) NewReadonlyTarget(ctx context.Context, common Common, logger
243246
}
244247

245248
// EnsureReferenceNotEmpty returns formalized error when the reference is empty.
246-
func (opts *Target) EnsureReferenceNotEmpty(cmd *cobra.Command, needsTag bool) error {
249+
func (opts *Target) EnsureReferenceNotEmpty(cmd *cobra.Command, allowTag bool) error {
247250
if opts.Reference == "" {
248-
return oerrors.NewErrEmptyTagOrDigest(opts.RawReference, cmd, needsTag)
251+
return oerrors.NewErrEmptyTagOrDigest(opts.RawReference, cmd, allowTag)
249252
}
250253
return nil
251254
}

cmd/oras/root/attach.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type attachOptions struct {
4949
func attachCmd() *cobra.Command {
5050
var opts attachOptions
5151
cmd := &cobra.Command{
52-
Use: "attach [flags] --artifact-type=<type> <name>{:<tag>|@<digest>} <file>[:<layer_media_type>] [...]",
52+
Use: "attach [flags] --artifact-type=<type> <name>{:<tag>|@<digest>} {<file>[:<layer_media_type>]|--annotation <key>=<value>} [...]",
5353
Short: "[Preview] Attach files to an existing artifact",
5454
Long: `[Preview] Attach files to an existing artifact
5555
@@ -87,10 +87,20 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder
8787
PreRunE: func(cmd *cobra.Command, args []string) error {
8888
opts.RawReference = args[0]
8989
opts.FileRefs = args[1:]
90-
if err := option.Parse(cmd, &opts); err != nil {
91-
return err
90+
err := option.Parse(cmd, &opts)
91+
if err == nil {
92+
if err = opts.EnsureReferenceNotEmpty(cmd, true); err == nil {
93+
return nil
94+
}
95+
}
96+
if len(opts.FileRefs) == 0 {
97+
// no file argument provided
98+
if err, ok := err.(*oerrors.Error); ok && err.OperationType == oerrors.OperationTypeParseArtifactReference {
99+
// invalid reference
100+
err.Recommendation = fmt.Sprintf("Are you missing an artifact reference to attach to? %s", err.Recommendation)
101+
}
92102
}
93-
return nil
103+
return err
94104
},
95105
RunE: func(cmd *cobra.Command, args []string) error {
96106
return runAttach(cmd, &opts)
@@ -137,9 +147,6 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error {
137147
if err != nil {
138148
return err
139149
}
140-
if err := opts.EnsureReferenceNotEmpty(cmd, true); err != nil {
141-
return err
142-
}
143150
// add both pull and push scope hints for dst repository
144151
// to save potential push-scope token requests during copy
145152
ctx = registryutil.WithScopeHint(ctx, dst, auth.ActionPull, auth.ActionPush)

test/e2e/suite/command/attach.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,25 @@ var _ = Describe("ORAS beginners:", func() {
6969
ExpectFailure().MatchErrKeyWords("unknown distribution specification flag").Exec()
7070
})
7171

72+
It("should fail with error suggesting subject missed", func() {
73+
err := ORAS("attach", "--artifact-type", "oras/test", RegistryRef(ZOTHost, ImageRepo, "")).ExpectFailure().Exec().Err
74+
Expect(err).Should(gbytes.Say("Error"))
75+
Expect(err).Should(gbytes.Say("\nAre you missing an artifact reference to attach to?"))
76+
})
77+
78+
It("should fail with error suggesting right form", func() {
79+
err := ORAS("attach", "--artifact-type", "oras/test", RegistryRef(ZOTHost, ImageRepo, ""), "./test.json").ExpectFailure().Exec().Err
80+
Expect(err).Should(gbytes.Say("Error"))
81+
Expect(err).Should(gbytes.Say("no tag or digest specified"))
82+
Expect(err).ShouldNot(gbytes.Say("\nAre you missing an artifact reference to attach to?"))
83+
})
84+
7285
It("should fail and show detailed error description if no argument provided", func() {
7386
err := ORAS("attach").ExpectFailure().Exec().Err
74-
gomega.Expect(err).Should(gbytes.Say("Error"))
75-
gomega.Expect(err).Should(gbytes.Say("\nUsage: oras attach"))
76-
gomega.Expect(err).Should(gbytes.Say("\n"))
77-
gomega.Expect(err).Should(gbytes.Say(`Run "oras attach -h"`))
87+
Expect(err).Should(gbytes.Say("Error"))
88+
Expect(err).Should(gbytes.Say("\nUsage: oras attach"))
89+
Expect(err).Should(gbytes.Say("\n"))
90+
Expect(err).Should(gbytes.Say(`Run "oras attach -h"`))
7891
})
7992

8093
It("should fail if distribution spec is not valid", func() {

0 commit comments

Comments
 (0)