From fb5ffaeb6677a3d4b537e341e7effb9d216dca93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 16 Oct 2024 11:09:33 -0500 Subject: [PATCH 1/3] cmd/cp: Add oci-layout-path flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The oci-layout can't be used when the image reference contains a slash ( see issue 1505). This PR introduces a new oci-layout-path that explicitly receives the path of the oci layout fixing the parsing ambiguity. The usage of this flag is: oras cp --from-oci-layout-path Signed-off-by: Mauricio Vásquez --- cmd/oras/internal/option/target.go | 9 ++++++ cmd/oras/internal/option/target_test.go | 42 +++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/cmd/oras/internal/option/target.go b/cmd/oras/internal/option/target.go index 489eeb820..839f376db 100644 --- a/cmd/oras/internal/option/target.go +++ b/cmd/oras/internal/option/target.go @@ -85,6 +85,7 @@ func (opts *Target) AnnotatedReference() string { func (opts *Target) applyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) { flagPrefix, notePrefix := applyPrefix(prefix, description) fs.BoolVarP(&opts.IsOCILayout, flagPrefix+"oci-layout", "", false, "set "+notePrefix+"target as an OCI image layout") + fs.StringVar(&opts.Path, flagPrefix+"oci-layout-path", "", "set the path for the "+notePrefix+"OCI image layout target") } // ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string. @@ -96,6 +97,10 @@ func (opts *Target) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description // Parse gets target options from user input. func (opts *Target) Parse(cmd *cobra.Command) error { + if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), opts.flagPrefix+"oci-layout-path", opts.flagPrefix+"oci-layout"); err != nil { + return err + } + switch { case opts.IsOCILayout: opts.Type = TargetTypeOCILayout @@ -103,6 +108,10 @@ func (opts *Target) Parse(cmd *cobra.Command) error { return errors.New("custom header flags cannot be used on an OCI image layout target") } return opts.parseOCILayoutReference() + case opts.Path != "": + opts.Type = TargetTypeOCILayout + opts.Reference = opts.RawReference + return nil default: opts.Type = TargetTypeRemote if ref, err := registry.ParseReference(opts.RawReference); err != nil { diff --git a/cmd/oras/internal/option/target_test.go b/cmd/oras/internal/option/target_test.go index 9a227b768..52931bdf9 100644 --- a/cmd/oras/internal/option/target_test.go +++ b/cmd/oras/internal/option/target_test.go @@ -20,6 +20,7 @@ import ( "net/http" "net/url" "reflect" + "strings" "testing" "github.com/spf13/cobra" @@ -28,9 +29,26 @@ import ( oerrors "oras.land/oras/cmd/oras/internal/errors" ) +func TestTarget_Parse_oci_path(t *testing.T) { + opts := Target{ + Path: "foo", + RawReference: "mocked/test", + } + cmd := &cobra.Command{} + ApplyFlags(&opts, cmd.Flags()) + if err := opts.Parse(cmd); err != nil { + t.Errorf("Target.Parse() error = %v", err) + } + if opts.Type != TargetTypeOCILayout { + t.Errorf("Target.Parse() failed, got %q, want %q", opts.Type, TargetTypeOCILayout) + } +} + func TestTarget_Parse_oci(t *testing.T) { opts := Target{IsOCILayout: true} - err := opts.Parse(nil) + cmd := &cobra.Command{} + ApplyFlags(&opts, cmd.Flags()) + err := opts.Parse(cmd) if !errors.Is(err, errdef.ErrInvalidReference) { t.Errorf("Target.Parse() error = %v, expect %v", err, errdef.ErrInvalidReference) } @@ -39,6 +57,24 @@ func TestTarget_Parse_oci(t *testing.T) { } } +func TestTarget_Parse_oci_and_oci_path(t *testing.T) { + opts := Target{} + cmd := &cobra.Command{} + opts.ApplyFlags(cmd.Flags()) + cmd.SetArgs([]string{"--oci-layout", "foo", "--oci-layout-path", "foo"}) + if err := cmd.Execute(); err != nil { + t.Errorf("cmd.Execute() error = %v", err) + } + err := opts.Parse(cmd) + if err == nil { + t.Errorf("expect Target.Parse() to fail but not") + } + if !strings.Contains(err.Error(), "cannot be used at the same time") { + t.Errorf("expect error message to contain 'cannot be used at the same time' but not") + } + +} + func TestTarget_Parse_remote(t *testing.T) { opts := Target{ RawReference: "mocked/test", @@ -59,7 +95,9 @@ func TestTarget_Parse_remote_err(t *testing.T) { RawReference: "/test", IsOCILayout: false, } - if err := opts.Parse(nil); err == nil { + cmd := &cobra.Command{} + ApplyFlags(&opts, cmd.Flags()) + if err := opts.Parse(cmd); err == nil { t.Errorf("expect Target.Parse() to fail but not") } } From 5b9afa7f32a476f47fb4e7dd293a7421d09572a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 30 Oct 2024 09:22:59 -0500 Subject: [PATCH 2/3] test/e2e/command/cp: Use digest instead of tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mauricio Vásquez --- test/e2e/suite/command/cp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/command/cp.go b/test/e2e/suite/command/cp.go index f548dd0be..c48910ec5 100644 --- a/test/e2e/suite/command/cp.go +++ b/test/e2e/suite/command/cp.go @@ -441,7 +441,7 @@ var _ = Describe("OCI layout users:", func() { It("should copy an image from a registry to an OCI image layout via digest", func() { dstDir := GinkgoT().TempDir() - src := RegistryRef(ZOTHost, ImageRepo, foobar.Tag) + src := RegistryRef(ZOTHost, ImageRepo, foobar.Digest) ORAS("cp", src, dstDir, "-v", Flags.ToLayout).MatchStatus(foobarStates, true, len(foobarStates)).Exec() // validate srcManifest := ORAS("manifest", "fetch", src).WithDescription("fetch from source to validate").Exec().Out.Contents() From 9718f09127c9ad4ff61fb373484fe24ba7c5f5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Wed, 30 Oct 2024 09:23:38 -0500 Subject: [PATCH 3/3] test/e2e/suite/command/cp: Add --oci-layout-path tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mauricio Vásquez --- test/e2e/internal/utils/const.go | 4 ++ test/e2e/suite/command/cp.go | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/test/e2e/internal/utils/const.go b/test/e2e/internal/utils/const.go index 6c247aa29..4ea0940fd 100644 --- a/test/e2e/internal/utils/const.go +++ b/test/e2e/internal/utils/const.go @@ -20,12 +20,16 @@ var ( Layout string FromLayout string ToLayout string + FromLayoutPath string + ToLayoutPath string DistributionSpec string ImageSpec string }{ "--oci-layout", "--from-oci-layout", "--to-oci-layout", + "--from-oci-layout-path", + "--to-oci-layout-path", "--distribution-spec", "--image-spec", } diff --git a/test/e2e/suite/command/cp.go b/test/e2e/suite/command/cp.go index c48910ec5..70b278f64 100644 --- a/test/e2e/suite/command/cp.go +++ b/test/e2e/suite/command/cp.go @@ -625,6 +625,91 @@ var _ = Describe("OCI layout users:", func() { Expect(len(index.Manifests)).To(Equal(1)) Expect(index.Manifests[0].Digest.String()).To(Equal(ma.LinuxAMD64Referrer.Digest.String())) }) + + // oci-layout-path tests + + It("should copy an image from a registry to an OCI image layout via tag using --oci-layout-path", func() { + layoutDir := GinkgoT().TempDir() + src := RegistryRef(ZOTHost, ImageRepo, foobar.Tag) + ref := "copied" + dst := LayoutRef(layoutDir, ref) + ORAS("cp", src, ref, "-v", Flags.ToLayoutPath, layoutDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst, Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image from an OCI image layout to a registry via tag using --oci-layout-path", func() { + layoutDir := GinkgoT().TempDir() + ref := "copied" + src := LayoutRef(layoutDir, ref) + dst := RegistryRef(ZOTHost, cpTestRepo("from-layout-tag-path"), foobar.Tag) + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), src, Flags.ToLayout).Exec() + // test + ORAS("cp", ref, dst, "-v", Flags.FromLayoutPath, layoutDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image between OCI image layouts via tag using --oci-layout-path", func() { + srcDir := GinkgoT().TempDir() + toDir := GinkgoT().TempDir() + srcRef := "from" + dstRef := "to" + src := LayoutRef(srcDir, srcRef) + dst := LayoutRef(toDir, dstRef) + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), src, Flags.ToLayout).Exec() + // test + ORAS("cp", srcRef, dstRef, "-v", Flags.FromLayoutPath, srcDir, Flags.ToLayoutPath, toDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst, Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image from a registry to an OCI image layout via digest using --oci-layout-path", func() { + dstDir := GinkgoT().TempDir() + src := RegistryRef(ZOTHost, ImageRepo, foobar.Digest) + ORAS("cp", src, foobar.Digest, "-v", Flags.ToLayoutPath, dstDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", LayoutRef(dstDir, foobar.Digest), Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image from an OCI image layout to a registry via digest using --oci-layout-path", func() { + layoutDir := GinkgoT().TempDir() + src := LayoutRef(layoutDir, foobar.Digest) + dst := RegistryRef(ZOTHost, cpTestRepo("from-layout-digest-path"), "copied") + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), layoutDir, Flags.ToLayout).Exec() + // test + ORAS("cp", foobar.Digest, dst, "-v", Flags.FromLayoutPath, layoutDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image between OCI image layouts via digest", func() { + srcDir := GinkgoT().TempDir() + toDir := GinkgoT().TempDir() + src := LayoutRef(srcDir, foobar.Digest) + dst := LayoutRef(toDir, foobar.Digest) + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), srcDir, Flags.ToLayout).Exec() + // test + ORAS("cp", foobar.Digest, foobar.Digest, "-v", Flags.FromLayoutPath, srcDir, Flags.ToLayoutPath, toDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst, Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) }) })