diff --git a/cmd/oras/internal/option/spec.go b/cmd/oras/internal/option/spec.go index 6f2d7a2b3..7160098f3 100644 --- a/cmd/oras/internal/option/spec.go +++ b/cmd/oras/internal/option/spec.go @@ -45,7 +45,7 @@ func (is *ImageSpec) Set(value string) error { is.flag = value switch value { case ImageSpecV1_1: - is.PackVersion = oras.PackManifestVersion1_1_RC4 + is.PackVersion = oras.PackManifestVersion1_1 case ImageSpecV1_0: is.PackVersion = oras.PackManifestVersion1_0 default: @@ -78,7 +78,7 @@ func (is *ImageSpec) String() string { // ApplyFlags applies flags to a command flag set. func (is *ImageSpec) ApplyFlags(fs *pflag.FlagSet) { // default to v1.1-rc.4 - is.PackVersion = oras.PackManifestVersion1_1_RC4 + is.PackVersion = oras.PackManifestVersion1_1 defaultFlag := ImageSpecV1_1 fs.Var(is, "image-spec", fmt.Sprintf(`[Experimental] specify manifest type for building artifact. Options: %s (default %q)`, is.Options(), defaultFlag)) } diff --git a/cmd/oras/internal/option/target.go b/cmd/oras/internal/option/target.go index 56da7587d..db5ac8357 100644 --- a/cmd/oras/internal/option/target.go +++ b/cmd/oras/internal/option/target.go @@ -102,14 +102,12 @@ func (opts *Target) Parse() error { if len(opts.headerFlags) != 0 { return errors.New("custom header flags cannot be used on an OCI image layout target") } - return nil + return opts.parseOCILayoutReference() default: opts.Type = TargetTypeRemote - if ref, err := registry.ParseReference(opts.RawReference); err != nil { - return err - } else if ref.Registry == "" || ref.Repository == "" { + if _, err := registry.ParseReference(opts.RawReference); err != nil { return &oerrors.Error{ - Err: fmt.Errorf("%q is an invalid reference", opts.RawReference), + Err: fmt.Errorf("%q: %w", opts.RawReference, err), Recommendation: "Please make sure the provided reference is in the form of /[:tag|@digest]", } } @@ -118,24 +116,28 @@ func (opts *Target) Parse() error { } // parseOCILayoutReference parses the raw in format of [:|@] -func parseOCILayoutReference(raw string) (path string, ref string, err error) { +func (opts *Target) parseOCILayoutReference() error { + raw := opts.RawReference + var path string + var ref string if idx := strings.LastIndex(raw, "@"); idx != -1 { // `digest` found path = raw[:idx] ref = raw[idx+1:] } else { // find `tag` + var err error path, ref, err = fileref.Parse(raw, "") + if err != nil { + return errors.Join(err, errdef.ErrInvalidReference) + } } - return + opts.Path = path + opts.Reference = ref + return nil } func (opts *Target) newOCIStore() (*oci.Store, error) { - var err error - opts.Path, opts.Reference, err = parseOCILayoutReference(opts.RawReference) - if err != nil { - return nil, err - } return oci.New(opts.Path) } @@ -208,11 +210,6 @@ type ReadOnlyGraphTagFinderTarget interface { func (opts *Target) NewReadonlyTarget(ctx context.Context, common Common, logger logrus.FieldLogger) (ReadOnlyGraphTagFinderTarget, error) { switch opts.Type { case TargetTypeOCILayout: - var err error - opts.Path, opts.Reference, err = parseOCILayoutReference(opts.RawReference) - if err != nil { - return nil, err - } info, err := os.Stat(opts.Path) if err != nil { if errors.Is(err, fs.ErrNotExist) { diff --git a/cmd/oras/internal/option/target_test.go b/cmd/oras/internal/option/target_test.go index d3cdf217f..5dc94249d 100644 --- a/cmd/oras/internal/option/target_test.go +++ b/cmd/oras/internal/option/target_test.go @@ -23,15 +23,16 @@ import ( "testing" "github.com/spf13/cobra" + "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote/errcode" oerrors "oras.land/oras/cmd/oras/internal/errors" ) func TestTarget_Parse_oci(t *testing.T) { opts := Target{IsOCILayout: true} - - if err := opts.Parse(); err != nil { - t.Errorf("Target.Parse() error = %v", err) + err := opts.Parse() + if !errors.Is(err, errdef.ErrInvalidReference) { + t.Errorf("Target.Parse() error = %v, expect %v", err, errdef.ErrInvalidReference) } if opts.Type != TargetTypeOCILayout { t.Errorf("Target.Parse() failed, got %q, want %q", opts.Type, TargetTypeOCILayout) @@ -62,36 +63,38 @@ func TestTarget_Parse_remote_err(t *testing.T) { } func Test_parseOCILayoutReference(t *testing.T) { - type args struct { - raw string + opts := Target{ + RawReference: "/test", + IsOCILayout: false, } tests := []struct { name string - args args + raw string want string want1 string wantErr bool }{ - {"Empty input", args{raw: ""}, "", "", true}, - {"Empty path and tag", args{raw: ":"}, "", "", true}, - {"Empty path and digest", args{raw: "@"}, "", "", false}, - {"Empty digest", args{raw: "path@"}, "path", "", false}, - {"Empty tag", args{raw: "path:"}, "path", "", false}, - {"path and digest", args{raw: "path@digest"}, "path", "digest", false}, - {"path and tag", args{raw: "path:tag"}, "path", "tag", false}, + {"Empty input", "", "", "", true}, + {"Empty path and tag", ":", "", "", true}, + {"Empty path and digest", "@", "", "", false}, + {"Empty digest", "path@", "path", "", false}, + {"Empty tag", "path:", "path", "", false}, + {"path and digest", "path@digest", "path", "digest", false}, + {"path and tag", "path:tag", "path", "tag", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1, err := parseOCILayoutReference(tt.args.raw) + opts.RawReference = tt.raw + err := opts.parseOCILayoutReference() if (err != nil) != tt.wantErr { t.Errorf("parseOCILayoutReference() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.want { - t.Errorf("parseOCILayoutReference() got = %v, want %v", got, tt.want) + if opts.Path != tt.want { + t.Errorf("parseOCILayoutReference() got = %v, want %v", opts.Path, tt.want) } - if got1 != tt.want1 { - t.Errorf("parseOCILayoutReference() got1 = %v, want %v", got1, tt.want1) + if opts.Reference != tt.want1 { + t.Errorf("parseOCILayoutReference() got1 = %v, want %v", opts.Reference, tt.want1) } }) } diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 944810b2f..0e6bbbe75 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -156,7 +156,7 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error { Layers: descs, } pack := func() (ocispec.Descriptor, error) { - return oras.PackManifest(ctx, store, oras.PackManifestVersion1_1_RC4, opts.artifactType, packOpts) + return oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, opts.artifactType, packOpts) } copy := func(root ocispec.Descriptor) error { diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index fb357e71e..a6541286e 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -113,7 +113,7 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t if opts.manifestConfigRef != "" && opts.artifactType != "" { return errors.New("--artifact-type and --config cannot both be provided for 1.0 OCI image") } - case oras.PackManifestVersion1_1_RC4: + case oras.PackManifestVersion1_1: if opts.manifestConfigRef == "" && opts.artifactType == "" { opts.artifactType = oras.MediaTypeUnknownArtifact } diff --git a/cmd/oras/root/tag.go b/cmd/oras/root/tag.go index 42576f7ba..7dc923be8 100644 --- a/cmd/oras/root/tag.go +++ b/cmd/oras/root/tag.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2" - "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/errdef" "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/display/status" oerrors "oras.land/oras/cmd/oras/internal/errors" @@ -74,11 +74,16 @@ Example - Tag the manifest 'v1.0.1' to 'v1.0.2' in an OCI image layout folder 'l }, PreRunE: func(cmd *cobra.Command, args []string) error { opts.RawReference = args[0] - if _, err := registry.ParseReference(opts.RawReference); err != nil { - return fmt.Errorf("unable to add tag for '%s': %w", opts.RawReference, err) - } opts.targetRefs = args[1:] - return option.Parse(&opts) + if err := option.Parse(&opts); err != nil { + if inner, ok := err.(*oerrors.Error); ok { + if errors.Is(inner, errdef.ErrInvalidReference) { + inner.Err = fmt.Errorf("unable to add tag for '%s': %w", opts.RawReference, inner.Err) + } + } + return err + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { return tagManifest(cmd, &opts) diff --git a/go.mod b/go.mod index 2a91fd58f..1ca0f0c8c 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( golang.org/x/sync v0.6.0 golang.org/x/term v0.18.0 gopkg.in/yaml.v3 v3.0.1 - oras.land/oras-go/v2 v2.4.0 + oras.land/oras-go/v2 v2.5.0 ) require ( diff --git a/go.sum b/go.sum index b4139fdeb..f55f57036 100644 --- a/go.sum +++ b/go.sum @@ -109,5 +109,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -oras.land/oras-go/v2 v2.4.0 h1:i+Wt5oCaMHu99guBD0yuBjdLvX7Lz8ukPbwXdR7uBMs= -oras.land/oras-go/v2 v2.4.0/go.mod h1:osvtg0/ClRq1KkydMAEu/IxFieyjItcsQ4ut4PPF+f8= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= diff --git a/test/e2e/go.mod b/test/e2e/go.mod index fa5083e6e..8119550d7 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -8,7 +8,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 gopkg.in/yaml.v2 v2.4.0 - oras.land/oras-go/v2 v2.4.0 + oras.land/oras-go/v2 v2.5.0 ) require ( diff --git a/test/e2e/go.sum b/test/e2e/go.sum index ce312454a..50510c9ab 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -47,5 +47,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -oras.land/oras-go/v2 v2.4.0 h1:i+Wt5oCaMHu99guBD0yuBjdLvX7Lz8ukPbwXdR7uBMs= -oras.land/oras-go/v2 v2.4.0/go.mod h1:osvtg0/ClRq1KkydMAEu/IxFieyjItcsQ4ut4PPF+f8= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index 19a89ad39..741c70e20 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -51,7 +51,7 @@ var _ = Describe("ORAS beginners:", func() { It("should fail if the provided reference is not valid", func() { err := ORAS("push", "/oras").ExpectFailure().Exec().Err - gomega.Expect(err).Should(gbytes.Say(`Error: "/oras" is an invalid reference`)) + gomega.Expect(err).Should(gbytes.Say(`Error: "/oras": invalid reference: invalid registry ""`)) gomega.Expect(err).Should(gbytes.Say(regexp.QuoteMeta("Please make sure the provided reference is in the form of /[:tag|@digest]"))) }) diff --git a/test/e2e/suite/command/resolve.go b/test/e2e/suite/command/resolve.go index adbef52ba..449271830 100644 --- a/test/e2e/suite/command/resolve.go +++ b/test/e2e/suite/command/resolve.go @@ -41,7 +41,7 @@ var _ = Describe("ORAS beginners:", func() { ORAS("resolve").ExpectFailure().MatchErrKeyWords("Error:").Exec() }) It("should fail when repo is invalid", func() { - ORAS("resolve", fmt.Sprintf("%s/%s", ZOTHost, InvalidRepo)).ExpectFailure().MatchErrKeyWords("Error:", fmt.Sprintf("invalid reference: invalid repository %q", InvalidRepo)).Exec() + ORAS("resolve", fmt.Sprintf("%s/%s", ZOTHost, InvalidRepo)).ExpectFailure().MatchErrKeyWords("Error:", "localhost:7000/INVALID", "invalid reference", "/[:tag|@digest]").Exec() }) It("should fail when no tag or digest provided", func() { ORAS("resolve", RegistryRef(ZOTHost, ImageRepo, "")).ExpectFailure().MatchErrKeyWords("Error:", `no tag or digest specified`, "oras resolve [flags] {:|@}", "Please specify a reference").Exec() diff --git a/test/e2e/suite/command/tag.go b/test/e2e/suite/command/tag.go index 4532d7655..d1536d1ab 100644 --- a/test/e2e/suite/command/tag.go +++ b/test/e2e/suite/command/tag.go @@ -17,6 +17,7 @@ package command import ( "fmt" + "path/filepath" "regexp" . "github.com/onsi/ginkgo/v2" @@ -36,6 +37,10 @@ var _ = Describe("ORAS beginners:", func() { ORAS("tag", RegistryRef(ZOTHost, ImageRepo, InvalidTag), "tagged").ExpectFailure().MatchErrKeyWords(RegistryErrorPrefix).Exec() }) + It("should fail when the given reference is invalid", func() { + ORAS("tag", fmt.Sprintf("%s/%s:%s", ZOTHost, InvalidRepo, "test"), "latest").ExpectFailure().MatchErrKeyWords("Error:", "unable to add tag", "invalid reference").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")) @@ -108,5 +113,13 @@ var _ = Describe("OCI image layout users:", func() { It("should add multiple tags to an existent manifest when providing tag reference", func() { tagAndValidate(PrepareTempOCI(ImageRepo), multi_arch.Tag, multi_arch.Digest, "tag1-via-tag", "tag1-via-tag", "tag1-via-tag") }) + It("should be able to retag a manifest at the current directory", func() { + root := PrepareTempOCI(ImageRepo) + dir := filepath.Dir(root) + ref := filepath.Base(root) + ORAS("tag", LayoutRef(ref, multi_arch.Tag), Flags.Layout, "latest").WithWorkDir(dir).MatchKeyWords("Tagging [oci-layout]", "Tagged latest").Exec() + ORAS("tag", LayoutRef(ref, multi_arch.Tag), Flags.Layout, "tag2").WithWorkDir(dir).MatchKeyWords("Tagging [oci-layout]", "Tagged tag2").Exec() + ORAS("repo", "tags", Flags.Layout, LayoutRef(ref, multi_arch.Tag)).WithWorkDir(dir).MatchKeyWords(multi_arch.Tag, "latest", "tag2").Exec() + }) }) })