From 48cdfbc17c2926a6739e5e1315fe267075879fb0 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 6 Jan 2023 16:39:30 +0800 Subject: [PATCH 01/19] refactor: use Parser to process option input Signed-off-by: Billy Zha --- cmd/oras/attach.go | 2 +- cmd/oras/blob/delete.go | 2 +- cmd/oras/blob/fetch.go | 2 +- cmd/oras/blob/push.go | 2 +- cmd/oras/cp.go | 11 +++--- cmd/oras/discover.go | 8 ++--- cmd/oras/internal/option/parser.go | 41 +++++++++++++++++++++++ cmd/oras/internal/option/platform.go | 24 ++++++------- cmd/oras/internal/option/platform_test.go | 37 ++++++++++---------- cmd/oras/internal/option/remote.go | 4 +-- cmd/oras/login.go | 8 ++--- cmd/oras/manifest/delete.go | 2 +- cmd/oras/manifest/fetch.go | 12 ++----- cmd/oras/manifest/fetch_config.go | 10 ++---- cmd/oras/manifest/push.go | 2 +- cmd/oras/pull.go | 10 ++---- cmd/oras/push.go | 2 +- cmd/oras/repository/ls.go | 2 +- cmd/oras/repository/tags.go | 2 +- cmd/oras/tag/tag.go | 2 +- 20 files changed, 99 insertions(+), 86 deletions(-) create mode 100644 cmd/oras/internal/option/parser.go diff --git a/cmd/oras/attach.go b/cmd/oras/attach.go index c188a01ab..ed032ef6b 100644 --- a/cmd/oras/attach.go +++ b/cmd/oras/attach.go @@ -65,7 +65,7 @@ Example - Attach file 'hi.txt' and export the pushed manifest to 'manifest.json' `, Args: cobra.MinimumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.targetRef = args[0] diff --git a/cmd/oras/blob/delete.go b/cmd/oras/blob/delete.go index 4f0f64118..88bf88c2d 100644 --- a/cmd/oras/blob/delete.go +++ b/cmd/oras/blob/delete.go @@ -59,7 +59,7 @@ Example - Delete a blob and print its descriptor: if opts.OutputDescriptor && !opts.Confirmed { return errors.New("must apply --force to confirm the deletion if the descriptor is outputted") } - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.targetRef = args[0] diff --git a/cmd/oras/blob/fetch.go b/cmd/oras/blob/fetch.go index 8e501c7e2..71dee5e1b 100644 --- a/cmd/oras/blob/fetch.go +++ b/cmd/oras/blob/fetch.go @@ -70,7 +70,7 @@ Example - Fetch the blob, save it to a local file and print the descriptor: return errors.New("`--output -` cannot be used with `--descriptor` at the same time") } - return opts.ReadPassword() + return option.Parse(&opts) }, Aliases: []string{"get"}, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/oras/blob/push.go b/cmd/oras/blob/push.go index f0f9537d4..0ae50e950 100644 --- a/cmd/oras/blob/push.go +++ b/cmd/oras/blob/push.go @@ -81,7 +81,7 @@ Example - Push blob without TLS: return errors.New("`--size` must be provided if the blob is read from stdin") } } - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { return pushBlob(opts) diff --git a/cmd/oras/cp.go b/cmd/oras/cp.go index 59efbaaf9..960954921 100644 --- a/cmd/oras/cp.go +++ b/cmd/oras/cp.go @@ -68,6 +68,9 @@ Example - Copy the artifact tagged with 'v1' from repository 'localhost:5000/net oras cp --concurrency 6 localhost:5000/net-monitor:v1 localhost:5000/net-monitor-copy:v1,tag2,tag3 `, Args: cobra.ExactArgs(2), + PreRunE: func(cmd *cobra.Command, args []string) error { + return option.Parse(&opts) + }, RunE: func(cmd *cobra.Command, args []string) error { opts.srcRef = args[0] refs := strings.Split(args[1], ",") @@ -88,10 +91,6 @@ Example - Copy the artifact tagged with 'v1' from repository 'localhost:5000/net func runCopy(opts copyOptions) error { ctx, _ := opts.SetLoggerLevel() - targetPlatform, err := opts.Parse() - if err != nil { - return err - } // Prepare source src, err := opts.src.NewRepository(opts.srcRef, opts.Common) @@ -145,9 +144,7 @@ func runCopy(opts copyOptions) error { copyOptions := oras.CopyOptions{ CopyGraphOptions: extendedCopyOptions.CopyGraphOptions, } - if targetPlatform != nil { - copyOptions.WithTargetPlatform(targetPlatform) - } + copyOptions.WithTargetPlatform(opts.OCIPlatform) desc, err = oras.Copy(ctx, src, opts.srcRef, dst, opts.dstRef, copyOptions) } } diff --git a/cmd/oras/discover.go b/cmd/oras/discover.go index 5401520b9..7b61f8739 100644 --- a/cmd/oras/discover.go +++ b/cmd/oras/discover.go @@ -64,7 +64,7 @@ Example - Discover referrers with type 'test-artifact' of manifest 'hello:latest `, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.targetRef = args[0] @@ -87,14 +87,10 @@ func runDiscover(opts discoverOptions) error { if repo.Reference.Reference == "" { return errors.NewErrInvalidReference(repo.Reference) } - targetPlatform, err := opts.Parse() - if err != nil { - return err - } // discover artifacts resolveOpts := oras.DefaultResolveOptions - resolveOpts.TargetPlatform = targetPlatform + resolveOpts.TargetPlatform = opts.OCIPlatform desc, err := oras.Resolve(ctx, repo, repo.Reference.Reference, resolveOpts) if err != nil { return err diff --git a/cmd/oras/internal/option/parser.go b/cmd/oras/internal/option/parser.go new file mode 100644 index 000000000..70bc5e91b --- /dev/null +++ b/cmd/oras/internal/option/parser.go @@ -0,0 +1,41 @@ +/* +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 option + +import ( + "reflect" +) + +// FlagParser parses flags in an option. +type FlagParser interface { + Parse() error +} + +// Parse parses applicable fields of the passed-in option pointer and returns +// error during parsing. +func Parse(optsPtr interface{}) error { + v := reflect.ValueOf(optsPtr).Elem() + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + if f.CanSet() { + iface := f.Addr().Interface() + if a, ok := iface.(FlagParser); ok { + if err := a.Parse(); err != nil { + return err + } + } + } + } + return nil +} diff --git a/cmd/oras/internal/option/platform.go b/cmd/oras/internal/option/platform.go index 64736487f..bbad6f84b 100644 --- a/cmd/oras/internal/option/platform.go +++ b/cmd/oras/internal/option/platform.go @@ -3,9 +3,7 @@ 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. @@ -26,25 +24,26 @@ import ( // Platform option struct. type Platform struct { - Platform string + platform string + OCIPlatform *ocispec.Platform } // ApplyFlags applies flags to a command flag set. func (opts *Platform) ApplyFlags(fs *pflag.FlagSet) { - fs.StringVarP(&opts.Platform, "platform", "", "", "request platform in the form of `os[/arch][/variant][:os_version]`") + fs.StringVarP(&opts.platform, "platform", "", "", "request platform in the form of `os[/arch][/variant][:os_version]`") } // parse parses the input platform flag to an oci platform type. -func (opts *Platform) Parse() (*ocispec.Platform, error) { - if opts.Platform == "" { - return nil, nil +func (opts *Platform) Parse() error { + if opts.platform == "" { + return nil } // OS[/Arch[/Variant]][:OSVersion] // If Arch is not provided, will use GOARCH instead var platformStr string var p ocispec.Platform - platformStr, p.OSVersion, _ = strings.Cut(opts.Platform, ":") + platformStr, p.OSVersion, _ = strings.Cut(opts.platform, ":") parts := strings.Split(platformStr, "/") switch len(parts) { case 3: @@ -55,14 +54,15 @@ func (opts *Platform) Parse() (*ocispec.Platform, error) { case 1: p.Architecture = runtime.GOARCH default: - return nil, fmt.Errorf("failed to parse platform %q: expected format os[/arch[/variant]]", opts.Platform) + return fmt.Errorf("failed to parse platform %q: expected format os[/arch[/variant]]", opts.platform) } p.OS = parts[0] if p.OS == "" { - return nil, fmt.Errorf("invalid platform: OS cannot be empty") + return fmt.Errorf("invalid platform: OS cannot be empty") } if p.Architecture == "" { - return nil, fmt.Errorf("invalid platform: Architecture cannot be empty") + return fmt.Errorf("invalid platform: Architecture cannot be empty") } - return &p, nil + opts.OCIPlatform = &p + return nil } diff --git a/cmd/oras/internal/option/platform_test.go b/cmd/oras/internal/option/platform_test.go index 246997654..3e01ec01a 100644 --- a/cmd/oras/internal/option/platform_test.go +++ b/cmd/oras/internal/option/platform_test.go @@ -3,9 +3,7 @@ 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. @@ -27,8 +25,8 @@ import ( func TestPlatform_ApplyFlags(t *testing.T) { var test struct{ Platform } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) - if test.Platform.Platform != "" { - t.Fatalf("expecting platform to be empty but got: %v", test.Platform.Platform) + if test.Platform.platform != "" { + t.Fatalf("expecting platform to be empty but got: %v", test.Platform.platform) } } @@ -37,15 +35,15 @@ func TestPlatform_Parse_err(t *testing.T) { name string opts *Platform }{ - {name: "empty arch 1", opts: &Platform{"os/"}}, - {name: "empty arch 2", opts: &Platform{"os//variant"}}, - {name: "empty os", opts: &Platform{"/arch"}}, - {name: "empty os with variant", opts: &Platform{"/arch/variant"}}, - {name: "trailing slash", opts: &Platform{"os/arch/variant/llama"}}, + {name: "empty arch 1", opts: &Platform{"os/", nil}}, + {name: "empty arch 2", opts: &Platform{"os//variant", nil}}, + {name: "empty os", opts: &Platform{"/arch", nil}}, + {name: "empty os with variant", opts: &Platform{"/arch/variant", nil}}, + {name: "trailing slash", opts: &Platform{"os/arch/variant/llama", nil}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := tt.opts.Parse() + err := tt.opts.Parse() if err == nil { t.Errorf("Platform.Parse() error = %v, wantErr %v", err, true) return @@ -60,21 +58,20 @@ func TestPlatform_Parse(t *testing.T) { opts *Platform want *ocispec.Platform }{ - {name: "empty", opts: &Platform{""}, want: nil}, - {name: "default arch", opts: &Platform{"os"}, want: &ocispec.Platform{OS: "os", Architecture: runtime.GOARCH}}, - {name: "os&arch", opts: &Platform{"os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}}, - {name: "empty variant", opts: &Platform{"os/aRcH/"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: ""}}, - {name: "os&arch&variant", opts: &Platform{"os/aRcH/vAriAnt"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt"}}, - {name: "os version", opts: &Platform{"os/aRcH/vAriAnt:osversion"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt", OSVersion: "osversion"}}, - {name: "long os version", opts: &Platform{"os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}}, + {name: "empty", opts: &Platform{platform: ""}, want: nil}, + {name: "default arch", opts: &Platform{platform: "os"}, want: &ocispec.Platform{OS: "os", Architecture: runtime.GOARCH}}, + {name: "os&arch", opts: &Platform{platform: "os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}}, + {name: "empty variant", opts: &Platform{platform: "os/aRcH/"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: ""}}, + {name: "os&arch&variant", opts: &Platform{platform: "os/aRcH/vAriAnt"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt"}}, + {name: "os version", opts: &Platform{platform: "os/aRcH/vAriAnt:osversion"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt", OSVersion: "osversion"}}, + {name: "long os version", opts: &Platform{platform: "os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.opts.Parse() - if err != nil { + if err := tt.opts.Parse(); err != nil { t.Errorf("Platform.Parse() error = %v", err) - return } + got := tt.opts.OCIPlatform if !reflect.DeepEqual(got, tt.want) { t.Errorf("Platform.Parse() = %v, want %v", got, tt.want) } diff --git a/cmd/oras/internal/option/remote.go b/cmd/oras/internal/option/remote.go index 118d75559..4e05f5aeb 100644 --- a/cmd/oras/internal/option/remote.go +++ b/cmd/oras/internal/option/remote.go @@ -87,8 +87,8 @@ func (opts *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description } } -// ReadPassword tries to read password with optional cmd prompt. -func (opts *Remote) ReadPassword() (err error) { +// Parse tries to read password with optional cmd prompt. +func (opts *Remote) Parse() error { if opts.Password != "" { fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") } else if opts.PasswordFromStdin { diff --git a/cmd/oras/login.go b/cmd/oras/login.go index e896d2252..b102444a8 100644 --- a/cmd/oras/login.go +++ b/cmd/oras/login.go @@ -61,7 +61,7 @@ Example - Log in with username and password in an interactive terminal and no TL `, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.Hostname = args[0] @@ -130,9 +130,9 @@ func runLogin(opts loginOptions) (err error) { return nil } -func readLine(prompt string, slient bool) (string, error) { +func readLine(prompt string, silent bool) (string, error) { fmt.Print(prompt) - if slient { + if silent { fd := os.Stdin.Fd() state, err := term.SaveState(fd) if err != nil { @@ -147,7 +147,7 @@ func readLine(prompt string, slient bool) (string, error) { if err != nil { return "", err } - if slient { + if silent { fmt.Println() } diff --git a/cmd/oras/manifest/delete.go b/cmd/oras/manifest/delete.go index 568eefd91..09751eb2e 100644 --- a/cmd/oras/manifest/delete.go +++ b/cmd/oras/manifest/delete.go @@ -63,7 +63,7 @@ Example - Delete a manifest by digest 'sha256:99e4703fbf30916f549cd6bfa9cdbab614 if opts.OutputDescriptor && !opts.Confirmed { return errors.New("must apply --force to confirm the deletion if the descriptor is outputted") } - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(_ *cobra.Command, args []string) error { opts.targetRef = args[0] diff --git a/cmd/oras/manifest/fetch.go b/cmd/oras/manifest/fetch.go index 7173a5c42..e4c911f21 100644 --- a/cmd/oras/manifest/fetch.go +++ b/cmd/oras/manifest/fetch.go @@ -69,8 +69,7 @@ Example - Fetch manifest with prettified json result: if opts.outputPath == "-" && opts.OutputDescriptor { return errors.New("`--output -` cannot be used with `--descriptor` at the same time") } - - return opts.ReadPassword() + return option.Parse(&opts) }, Aliases: []string{"get"}, RunE: func(cmd *cobra.Command, args []string) error { @@ -98,11 +97,6 @@ func fetchManifest(opts fetchOptions) (fetchErr error) { } repo.ManifestMediaTypes = opts.mediaTypes - targetPlatform, err := opts.Parse() - if err != nil { - return err - } - src, err := opts.CachedTarget(repo) if err != nil { return err @@ -112,7 +106,7 @@ func fetchManifest(opts fetchOptions) (fetchErr error) { if opts.OutputDescriptor && opts.outputPath == "" { // fetch manifest descriptor only fetchOpts := oras.DefaultResolveOptions - fetchOpts.TargetPlatform = targetPlatform + fetchOpts.TargetPlatform = opts.OCIPlatform desc, err = oras.Resolve(ctx, src, opts.targetRef, fetchOpts) if err != nil { return err @@ -121,7 +115,7 @@ func fetchManifest(opts fetchOptions) (fetchErr error) { // fetch manifest content var content []byte fetchOpts := oras.DefaultFetchBytesOptions - fetchOpts.TargetPlatform = targetPlatform + fetchOpts.TargetPlatform = opts.OCIPlatform desc, content, err = oras.FetchBytes(ctx, src, opts.targetRef, fetchOpts) if err != nil { return err diff --git a/cmd/oras/manifest/fetch_config.go b/cmd/oras/manifest/fetch_config.go index d57fc62ff..2e54eb478 100644 --- a/cmd/oras/manifest/fetch_config.go +++ b/cmd/oras/manifest/fetch_config.go @@ -76,8 +76,7 @@ Example - Fetch and print the prettified descriptor of the config: if opts.outputPath == "-" && opts.OutputDescriptor { return errors.New("`--output -` cannot be used with `--descriptor` at the same time") } - - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.targetRef = args[0] @@ -102,18 +101,13 @@ func fetchConfig(opts fetchConfigOptions) (fetchErr error) { return oerrors.NewErrInvalidReference(repo.Reference) } - targetPlatform, err := opts.Parse() - if err != nil { - return err - } - src, err := opts.CachedTarget(repo) if err != nil { return err } // fetch config descriptor - configDesc, err := fetchConfigDesc(ctx, src, opts.targetRef, targetPlatform) + configDesc, err := fetchConfigDesc(ctx, src, opts.targetRef, opts.OCIPlatform) if err != nil { return err } diff --git a/cmd/oras/manifest/push.go b/cmd/oras/manifest/push.go index 6249cef30..0c172ade5 100644 --- a/cmd/oras/manifest/push.go +++ b/cmd/oras/manifest/push.go @@ -81,7 +81,7 @@ Example - Push a manifest to repository 'locahost:5000/hello' and tag with 'tag1 if opts.fileRef == "-" && opts.PasswordFromStdin { return errors.New("`-` read file from input and `--password-stdin` read password from input cannot be both used") } - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(_ *cobra.Command, args []string) error { refs := strings.Split(args[0], ",") diff --git a/cmd/oras/pull.go b/cmd/oras/pull.go index c57cc6eb2..630392e07 100644 --- a/cmd/oras/pull.go +++ b/cmd/oras/pull.go @@ -79,7 +79,7 @@ Example - Pull all files with concurrency level tuned: `, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.targetRef = args[0] @@ -99,10 +99,6 @@ Example - Pull all files with concurrency level tuned: func runPull(opts pullOptions) error { var printed sync.Map - targetPlatform, err := opts.Parse() - if err != nil { - return err - } repo, err := opts.NewRepository(opts.targetRef, opts.Common) if err != nil { return err @@ -125,9 +121,7 @@ func runPull(opts pullOptions) error { return err } } - if targetPlatform != nil { - copyOptions.WithTargetPlatform(targetPlatform) - } + copyOptions.WithTargetPlatform(opts.OCIPlatform) var getConfigOnce sync.Once copyOptions.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { statusFetcher := content.FetcherFunc(func(ctx context.Context, target ocispec.Descriptor) (fetched io.ReadCloser, fetchErr error) { diff --git a/cmd/oras/push.go b/cmd/oras/push.go index e43c28d07..7a9f15a02 100644 --- a/cmd/oras/push.go +++ b/cmd/oras/push.go @@ -96,7 +96,7 @@ Example - Push file "hi.txt" with multiple tags and concurrency level tuned: if opts.artifactType != "" && opts.manifestConfigRef != "" { return errors.New("--artifact-type and --config cannot both be provided") } - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { refs := strings.Split(args[0], ",") diff --git a/cmd/oras/repository/ls.go b/cmd/oras/repository/ls.go index 40963b404..4355ba307 100644 --- a/cmd/oras/repository/ls.go +++ b/cmd/oras/repository/ls.go @@ -47,7 +47,7 @@ Example - List the repositories under the registry that include values lexically Args: cobra.ExactArgs(1), Aliases: []string{"list"}, PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.hostname = args[0] diff --git a/cmd/oras/repository/tags.go b/cmd/oras/repository/tags.go index abc6cc202..4cbf7287d 100644 --- a/cmd/oras/repository/tags.go +++ b/cmd/oras/repository/tags.go @@ -53,7 +53,7 @@ Example - Show tags of the target repository that include values lexically after Args: cobra.ExactArgs(1), Aliases: []string{"show-tags"}, PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { opts.targetRef = args[0] diff --git a/cmd/oras/tag/tag.go b/cmd/oras/tag/tag.go index c4fa8a336..bcad5f796 100644 --- a/cmd/oras/tag/tag.go +++ b/cmd/oras/tag/tag.go @@ -55,7 +55,7 @@ Example - Tag the manifest 'v1.0.1' in 'localhost:5000/hello' to 'v1.0.1', 'v1.0 `, Args: cobra.MinimumNArgs(2), PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.ReadPassword() + return option.Parse(&opts) }, RunE: func(_ *cobra.Command, args []string) error { opts.srcRef = args[0] From 9b14cbbfd7ab4eb20dd3cc68189d3fceccb9ca44 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Jan 2023 20:15:37 +0800 Subject: [PATCH 02/19] feat: add compatibility flag Signed-off-by: Billy Zha --- cmd/oras/attach.go | 3 ++- cmd/oras/internal/option/packer.go | 27 ++++++++++++++++++- cmd/oras/push.go | 18 ++++++++++--- internal/registry/compatibility.go | 43 ++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 internal/registry/compatibility.go diff --git a/cmd/oras/attach.go b/cmd/oras/attach.go index ed032ef6b..277708e9a 100644 --- a/cmd/oras/attach.go +++ b/cmd/oras/attach.go @@ -27,6 +27,7 @@ import ( "oras.land/oras-go/v2/content/file" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" + "oras.land/oras/internal/registry" ) type attachOptions struct { @@ -137,7 +138,7 @@ func runAttach(opts attachOptions) error { return oras.CopyGraph(ctx, store, dst, root, graphCopyOptions) } - root, err := pushArtifact(dst, pack, &packOpts, copy, &graphCopyOptions, opts.Verbose) + root, err := pushArtifact(dst, pack, &packOpts, copy, &graphCopyOptions, opts.ManifestSupportState == registry.ManifestSupportUnknown, opts.Verbose) if err != nil { return err } diff --git a/cmd/oras/internal/option/packer.go b/cmd/oras/internal/option/packer.go index 7fe058b83..ec68aa27e 100644 --- a/cmd/oras/internal/option/packer.go +++ b/cmd/oras/internal/option/packer.go @@ -26,6 +26,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" "oras.land/oras-go/v2/content" + "oras.land/oras/internal/registry" ) // Pre-defined annotation keys for annotation file @@ -42,12 +43,35 @@ var ( // Packer option struct. type Packer struct { + ManifestSupportState registry.ManifestSupportState + ReferrersApiSupportState registry.ReferrersApiSupportState + ManifestExportPath string PathValidationDisabled bool AnnotationFilePath string ManifestAnnotations []string - FileRefs []string + FileRefs []string + compatibility string +} + +// Parse parses flags into the option. +func (opts *Packer) Parse(fs *pflag.FlagSet) error { + switch opts.compatibility { + case "artifact": + opts.ManifestSupportState = registry.OCIArtifact + opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown + case "min": + opts.ManifestSupportState = registry.OCIArtifact + opts.ReferrersApiSupportState = registry.ReferrersApiSupported + case "max": + opts.ManifestSupportState = registry.OCIImage + opts.ReferrersApiSupportState = registry.ReferrersApiUnsupported + case "image": + opts.ManifestSupportState = registry.OCIImage + opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown + } + return nil } // ApplyFlags applies flags to a command flag set. @@ -56,6 +80,7 @@ func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") fs.StringVarP(&opts.AnnotationFilePath, "annotation-file", "", "", "path of the annotation file") fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") + fs.StringVar(&opts.compatibility, "compatibility", "", "set compatibility mode for pushing towards") } // ExportManifest saves the pushed manifest to a local file. diff --git a/cmd/oras/push.go b/cmd/oras/push.go index 7a9f15a02..206cb04e4 100644 --- a/cmd/oras/push.go +++ b/cmd/oras/push.go @@ -34,6 +34,7 @@ import ( "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/fileref" "oras.land/oras/cmd/oras/internal/option" + "oras.land/oras/internal/registry" ) type pushOptions struct { @@ -99,6 +100,9 @@ Example - Push file "hi.txt" with multiple tags and concurrency level tuned: return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { + if opts.ManifestSupportState == registry.OCIArtifact && opts.manifestConfigRef != "" { + return errors.New("cannot pack an OCI artifact with manifest config at the same time") + } refs := strings.Split(args[0], ",") opts.targetRef = refs[0] opts.extraRefs = refs[1:] @@ -142,6 +146,9 @@ func runPush(opts pushOptions) error { packOpts.ConfigDescriptor = &desc packOpts.PackImageManifest = true } + if opts.ManifestSupportState == registry.OCIImage { + packOpts.PackImageManifest = true + } descs, err := loadFiles(ctx, store, annotations, opts.FileRefs, opts.Verbose) if err != nil { return err @@ -162,6 +169,11 @@ func runPush(opts pushOptions) error { if err != nil { return err } + if opts.ReferrersApiSupportState != registry.ReferrersApiSupportUnknown { + if err := dst.SetReferrersCapability(opts.ReferrersApiSupportState == registry.ReferrersApiSupported); err != nil { + return err + } + } copyOptions := oras.DefaultCopyOptions copyOptions.Concurrency = opts.concurrency updateDisplayOption(©Options.CopyGraphOptions, store, opts.Verbose) @@ -175,7 +187,7 @@ func runPush(opts pushOptions) error { } // Push - root, err := pushArtifact(dst, pack, &packOpts, copy, ©Options.CopyGraphOptions, opts.Verbose) + root, err := pushArtifact(dst, pack, &packOpts, copy, ©Options.CopyGraphOptions, opts.ManifestSupportState == registry.ManifestSupportUnknown, opts.Verbose) if err != nil { return err } @@ -218,7 +230,7 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, store content.Fetcher, ver type packFunc func() (ocispec.Descriptor, error) type copyFunc func(desc ocispec.Descriptor) error -func pushArtifact(dst *remote.Repository, pack packFunc, packOpts *oras.PackOptions, copy copyFunc, copyOpts *oras.CopyGraphOptions, verbose bool) (ocispec.Descriptor, error) { +func pushArtifact(dst *remote.Repository, pack packFunc, packOpts *oras.PackOptions, copy copyFunc, copyOpts *oras.CopyGraphOptions, fallback bool, verbose bool) (ocispec.Descriptor, error) { root, err := pack() if err != nil { return ocispec.Descriptor{}, err @@ -243,7 +255,7 @@ func pushArtifact(dst *remote.Repository, pack packFunc, packOpts *oras.PackOpti return root, nil } - if !copyRootAttempted || root.MediaType != ocispec.MediaTypeArtifactManifest || + if !fallback || !copyRootAttempted || root.MediaType != ocispec.MediaTypeArtifactManifest || !isManifestUnsupported(err) { return ocispec.Descriptor{}, err } diff --git a/internal/registry/compatibility.go b/internal/registry/compatibility.go new file mode 100644 index 000000000..7807c8e49 --- /dev/null +++ b/internal/registry/compatibility.go @@ -0,0 +1,43 @@ +/* +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 registry + +// ManifestSupportState represents the state of remote registry support for +// manifest type. +type ManifestSupportState = int32 + +const ( + // ManifestSupportUnknown represents an unknown state of Referrers API. + ManifestSupportUnknown ManifestSupportState = iota + // OCIArtifact means that the remote registry is known to support oci artifact manifest type. + OCIArtifact + // OCIImage means that the remote registry is known to + // support oci image manifest type only. + OCIImage +) + +// ReferrersApiSupportState represents the state of remote registry support for +// referrers API. +type ReferrersApiSupportState = int32 + +const ( + // ReferrersApiSupportUnknown represents an unknown state of Referrers API. + ReferrersApiSupportUnknown ReferrersApiSupportState = iota + // ReferrersApiSupported means that the remote registry is known to supporting referrers API. + ReferrersApiSupported + // ReferrersApiUnsupported means that the remote registry is known to not supporting referrers API. + ReferrersApiUnsupported +) From a17a690ee5f5bd8d22bc7b20eecda5c516e62248 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Jan 2023 20:49:45 +0800 Subject: [PATCH 03/19] add error checking for packer options Signed-off-by: Billy Zha --- cmd/oras/internal/option/packer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/oras/internal/option/packer.go b/cmd/oras/internal/option/packer.go index ec68aa27e..7bea5126f 100644 --- a/cmd/oras/internal/option/packer.go +++ b/cmd/oras/internal/option/packer.go @@ -70,6 +70,8 @@ func (opts *Packer) Parse(fs *pflag.FlagSet) error { case "image": opts.ManifestSupportState = registry.OCIImage opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown + default: + return fmt.Errorf("unknown compatibility mode: %q", opts.compatibility) } return nil } @@ -80,7 +82,7 @@ func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") fs.StringVarP(&opts.AnnotationFilePath, "annotation-file", "", "", "path of the annotation file") fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") - fs.StringVar(&opts.compatibility, "compatibility", "", "set compatibility mode for pushing towards") + fs.StringVar(&opts.compatibility, "compatibility", "", "set compatibility mode for registry, `artifact, image, min max`") } // ExportManifest saves the pushed manifest to a local file. From 54236d6d1d40816c9332a5a246f33d9293f0f923 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Jan 2023 14:46:48 +0800 Subject: [PATCH 04/19] doc clean Signed-off-by: Billy Zha --- cmd/oras/internal/option/packer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/option/packer.go b/cmd/oras/internal/option/packer.go index 7bea5126f..14e63c5be 100644 --- a/cmd/oras/internal/option/packer.go +++ b/cmd/oras/internal/option/packer.go @@ -82,7 +82,7 @@ func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") fs.StringVarP(&opts.AnnotationFilePath, "annotation-file", "", "", "path of the annotation file") fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") - fs.StringVar(&opts.compatibility, "compatibility", "", "set compatibility mode for registry, `artifact, image, min max`") + fs.StringVar(&opts.compatibility, "compatibility", "", "set compatibility mode for registry, `artifact,image,min,max`") } // ExportManifest saves the pushed manifest to a local file. From aef998db735afc355abfb9a65d5a1613c8f4a8cf Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Jan 2023 14:50:33 +0800 Subject: [PATCH 05/19] add auto as default Signed-off-by: Billy Zha --- cmd/oras/internal/option/packer.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/oras/internal/option/packer.go b/cmd/oras/internal/option/packer.go index 14e63c5be..54986dbb9 100644 --- a/cmd/oras/internal/option/packer.go +++ b/cmd/oras/internal/option/packer.go @@ -58,6 +58,9 @@ type Packer struct { // Parse parses flags into the option. func (opts *Packer) Parse(fs *pflag.FlagSet) error { switch opts.compatibility { + case "auto": + opts.ManifestSupportState = registry.ManifestSupportUnknown + opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown case "artifact": opts.ManifestSupportState = registry.OCIArtifact opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown @@ -82,7 +85,7 @@ func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") fs.StringVarP(&opts.AnnotationFilePath, "annotation-file", "", "", "path of the annotation file") fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") - fs.StringVar(&opts.compatibility, "compatibility", "", "set compatibility mode for registry, `artifact,image,min,max`") + fs.StringVar(&opts.compatibility, "compatibility", "auto", "set compatibility mode for registry, `artifact,image,min,max,auto`") } // ExportManifest saves the pushed manifest to a local file. From c154ef8a3c9bc43a50d0f876d7db8f8221cff41f Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Jan 2023 20:47:47 +0800 Subject: [PATCH 06/19] split spec flag into two Signed-off-by: Billy Zha --- cmd/oras/attach.go | 6 +++ cmd/oras/internal/option/packer.go | 32 +----------- cmd/oras/internal/option/spec.go | 79 ++++++++++++++++++++++++++++++ cmd/oras/push.go | 6 +-- 4 files changed, 87 insertions(+), 36 deletions(-) create mode 100644 cmd/oras/internal/option/spec.go diff --git a/cmd/oras/attach.go b/cmd/oras/attach.go index 277708e9a..1ae4fd1a1 100644 --- a/cmd/oras/attach.go +++ b/cmd/oras/attach.go @@ -34,6 +34,8 @@ type attachOptions struct { option.Common option.Remote option.Packer + option.ImageSpec + option.DistributionSpec targetRef string artifactType string @@ -104,6 +106,9 @@ func runAttach(opts attachOptions) error { if dst.Reference.Reference == "" { return oerrors.NewErrInvalidReference(dst.Reference) } + if opts.ReferrersApiSupportState != registry.ReferrersApiSupportUnknown { + dst.SetReferrersCapability(opts.ReferrersApiSupportState == registry.ReferrersApiSupported) + } subject, err := dst.Resolve(ctx, dst.Reference.Reference) if err != nil { return err @@ -117,6 +122,7 @@ func runAttach(opts attachOptions) error { packOpts := oras.PackOptions{ Subject: &subject, ManifestAnnotations: annotations[option.AnnotationManifest], + PackImageManifest: opts.ManifestSupportState == registry.OCIImage, } pack := func() (ocispec.Descriptor, error) { return oras.Pack(ctx, store, opts.artifactType, descs, packOpts) diff --git a/cmd/oras/internal/option/packer.go b/cmd/oras/internal/option/packer.go index 54986dbb9..7fe058b83 100644 --- a/cmd/oras/internal/option/packer.go +++ b/cmd/oras/internal/option/packer.go @@ -26,7 +26,6 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" "oras.land/oras-go/v2/content" - "oras.land/oras/internal/registry" ) // Pre-defined annotation keys for annotation file @@ -43,40 +42,12 @@ var ( // Packer option struct. type Packer struct { - ManifestSupportState registry.ManifestSupportState - ReferrersApiSupportState registry.ReferrersApiSupportState - ManifestExportPath string PathValidationDisabled bool AnnotationFilePath string ManifestAnnotations []string - FileRefs []string - compatibility string -} - -// Parse parses flags into the option. -func (opts *Packer) Parse(fs *pflag.FlagSet) error { - switch opts.compatibility { - case "auto": - opts.ManifestSupportState = registry.ManifestSupportUnknown - opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown - case "artifact": - opts.ManifestSupportState = registry.OCIArtifact - opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown - case "min": - opts.ManifestSupportState = registry.OCIArtifact - opts.ReferrersApiSupportState = registry.ReferrersApiSupported - case "max": - opts.ManifestSupportState = registry.OCIImage - opts.ReferrersApiSupportState = registry.ReferrersApiUnsupported - case "image": - opts.ManifestSupportState = registry.OCIImage - opts.ReferrersApiSupportState = registry.ReferrersApiSupportUnknown - default: - return fmt.Errorf("unknown compatibility mode: %q", opts.compatibility) - } - return nil + FileRefs []string } // ApplyFlags applies flags to a command flag set. @@ -85,7 +56,6 @@ func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") fs.StringVarP(&opts.AnnotationFilePath, "annotation-file", "", "", "path of the annotation file") fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") - fs.StringVar(&opts.compatibility, "compatibility", "auto", "set compatibility mode for registry, `artifact,image,min,max,auto`") } // ExportManifest saves the pushed manifest to a local file. diff --git a/cmd/oras/internal/option/spec.go b/cmd/oras/internal/option/spec.go new file mode 100644 index 000000000..7ddecc367 --- /dev/null +++ b/cmd/oras/internal/option/spec.go @@ -0,0 +1,79 @@ +/* +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 option + +import ( + "fmt" + + "github.com/spf13/pflag" + "oras.land/oras/internal/registry" +) + +// ImageSpec option struct. +type ImageSpec struct { + ManifestSupportState registry.ManifestSupportState + + // should be provided in form of `-` + specFlag string +} + +// Parse parses flags into the option. +func (opts *ImageSpec) Parse() error { + switch opts.specFlag { + case "": + opts.ManifestSupportState = registry.ManifestSupportUnknown + case "v1.1-image": + opts.ManifestSupportState = registry.OCIImage + case "v1.1-artifact": + opts.ManifestSupportState = registry.OCIArtifact + default: + return fmt.Errorf("unknown image specification flag: %q", opts.specFlag) + } + return nil +} + +// ApplyFlags applies flags to a command flag set. +func (opts *ImageSpec) ApplyFlags(fs *pflag.FlagSet) { + fs.StringVar(&opts.specFlag, "image-spec", "", "set OCI image spec version and request manifest type. E.g. `v1.1-image,v1.1-artifact`") +} + +// DistributionSpec option struct. +type DistributionSpec struct { + ReferrersApiSupportState registry.ReferrersApiSupportState + + // should be provided in form of`--