From 674b16eb1c31cca28778bf8a8e59f64ea6333cbd Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 14 May 2024 09:48:44 +0800 Subject: [PATCH] feat: add `--template` and support `--format go-template=TEMPLATE` experience (#1377) Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 80 +++++++++------ cmd/oras/internal/errors/errors.go | 8 ++ cmd/oras/internal/option/format.go | 131 ++++++++++++++++++++++-- cmd/oras/root/attach.go | 7 +- cmd/oras/root/attach_test.go | 43 ++++++++ cmd/oras/root/discover.go | 28 +++-- cmd/oras/root/manifest/fetch.go | 15 +-- cmd/oras/root/manifest/fetch_test.go | 43 ++++++++ cmd/oras/root/pull.go | 6 +- cmd/oras/root/pull_test.go | 43 ++++++++ cmd/oras/root/push.go | 7 +- cmd/oras/root/push_test.go | 43 ++++++++ test/e2e/suite/command/attach.go | 36 +++---- test/e2e/suite/command/cp.go | 10 +- test/e2e/suite/command/discover.go | 7 +- test/e2e/suite/command/manifest.go | 10 +- test/e2e/suite/command/pull.go | 56 +++++++++- test/e2e/suite/command/push.go | 4 +- test/e2e/suite/scenario/oci_artifact.go | 4 +- 19 files changed, 480 insertions(+), 101 deletions(-) create mode 100644 cmd/oras/root/attach_test.go create mode 100644 cmd/oras/root/manifest/fetch_test.go create mode 100644 cmd/oras/root/pull_test.go create mode 100644 cmd/oras/root/push_test.go diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 96a7ed68f..3b3da398c 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -30,97 +30,109 @@ import ( "oras.land/oras/cmd/oras/internal/display/metadata/text" "oras.land/oras/cmd/oras/internal/display/metadata/tree" "oras.land/oras/cmd/oras/internal/display/status" + "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" ) // NewPushHandler returns status and metadata handlers for push command. -func NewPushHandler(out io.Writer, format string, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler) { +func NewPushHandler(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler, error) { var statusHandler status.PushHandler if tty != nil { statusHandler = status.NewTTYPushHandler(tty) - } else if format == "" { + } else if format.Type == "" { statusHandler = status.NewTextPushHandler(out, verbose) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.PushHandler - switch format { + switch format.Type { case "": metadataHandler = text.NewPushHandler(out) - case "json": + case option.FormatTypeJSON.Name: metadataHandler = json.NewPushHandler(out) + case option.FormatTypeGoTemplate.Name: + metadataHandler = template.NewPushHandler(out, format.Template) default: - metadataHandler = template.NewPushHandler(out, format) + return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } - return statusHandler, metadataHandler + return statusHandler, metadataHandler, nil } // NewAttachHandler returns status and metadata handlers for attach command. -func NewAttachHandler(out io.Writer, format string, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler) { +func NewAttachHandler(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler, error) { var statusHandler status.AttachHandler if tty != nil { statusHandler = status.NewTTYAttachHandler(tty) - } else if format == "" { + } else if format.Type == "" { statusHandler = status.NewTextAttachHandler(out, verbose) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.AttachHandler - switch format { + switch format.Type { case "": metadataHandler = text.NewAttachHandler(out) - case "json": + case option.FormatTypeJSON.Name: metadataHandler = json.NewAttachHandler(out) + case option.FormatTypeGoTemplate.Name: + metadataHandler = template.NewAttachHandler(out, format.Template) default: - metadataHandler = template.NewAttachHandler(out, format) + return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } - return statusHandler, metadataHandler + return statusHandler, metadataHandler, nil } // NewPullHandler returns status and metadata handlers for pull command. -func NewPullHandler(out io.Writer, format string, path string, tty *os.File, verbose bool) (status.PullHandler, metadata.PullHandler) { +func NewPullHandler(out io.Writer, format option.Format, path string, tty *os.File, verbose bool) (status.PullHandler, metadata.PullHandler, error) { var statusHandler status.PullHandler if tty != nil { statusHandler = status.NewTTYPullHandler(tty) - } else if format == "" { + } else if format.Type == "" { statusHandler = status.NewTextPullHandler(out, verbose) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.PullHandler - switch format { + switch format.Type { case "": metadataHandler = text.NewPullHandler(out) - case "json": + case option.FormatTypeJSON.Name: metadataHandler = json.NewPullHandler(out, path) + case option.FormatTypeGoTemplate.Name: + metadataHandler = template.NewPullHandler(out, path, format.Template) default: - metadataHandler = template.NewPullHandler(out, path, format) + return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } - return statusHandler, metadataHandler + return statusHandler, metadataHandler, nil } // NewDiscoverHandler returns status and metadata handlers for discover command. -func NewDiscoverHandler(out io.Writer, outputType string, path string, rawReference string, desc ocispec.Descriptor, verbose bool) metadata.DiscoverHandler { - switch outputType { - case "tree", "": - return tree.NewDiscoverHandler(out, path, desc, verbose) - case "table": - return table.NewDiscoverHandler(out, rawReference, desc, verbose) - case "json": - return json.NewDiscoverHandler(out, desc, path) +func NewDiscoverHandler(out io.Writer, format option.Format, path string, rawReference string, desc ocispec.Descriptor, verbose bool) (metadata.DiscoverHandler, error) { + var handler metadata.DiscoverHandler + switch format.Type { + case option.FormatTypeTree.Name, "": + handler = tree.NewDiscoverHandler(out, path, desc, verbose) + case option.FormatTypeTable.Name: + handler = table.NewDiscoverHandler(out, rawReference, desc, verbose) + case option.FormatTypeJSON.Name: + handler = json.NewDiscoverHandler(out, desc, path) + case option.FormatTypeGoTemplate.Name: + handler = template.NewDiscoverHandler(out, desc, path, format.Template) default: - return template.NewDiscoverHandler(out, desc, path, outputType) + return nil, errors.UnsupportedFormatTypeError(format.Type) } + return handler, nil } // NewManifestFetchHandler returns a manifest fetch handler. -func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) { +func NewManifestFetchHandler(out io.Writer, format option.Format, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler, error) { var metadataHandler metadata.ManifestFetchHandler var contentHandler content.ManifestFetchHandler - switch format { + switch format.Type { case "": // raw if outputDescriptor { @@ -128,22 +140,24 @@ func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor, pre } else { metadataHandler = metadata.NewDiscardHandler() } - case "json": + case option.FormatTypeJSON.Name: // json metadataHandler = json.NewManifestFetchHandler(out) if outputPath == "" { contentHandler = content.NewDiscardHandler() } - default: + case option.FormatTypeGoTemplate.Name: // go template - metadataHandler = template.NewManifestFetchHandler(out, format) + metadataHandler = template.NewManifestFetchHandler(out, format.Template) if outputPath == "" { contentHandler = content.NewDiscardHandler() } + default: + return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } if contentHandler == nil { contentHandler = content.NewManifestFetchHandler(out, pretty, outputPath) } - return metadataHandler, contentHandler + return metadataHandler, contentHandler, nil } diff --git a/cmd/oras/internal/errors/errors.go b/cmd/oras/internal/errors/errors.go index bb7784660..6e48556ef 100644 --- a/cmd/oras/internal/errors/errors.go +++ b/cmd/oras/internal/errors/errors.go @@ -29,6 +29,14 @@ import ( // RegistryErrorPrefix is the commandline prefix for errors from registry. const RegistryErrorPrefix = "Error response from registry:" +// UnsupportedFormatTypeError generates the error message for an invalid type. +type UnsupportedFormatTypeError string + +// Error implements the error interface. +func (e UnsupportedFormatTypeError) Error() string { + return "unsupported format type: " + string(e) +} + // Error is the error type for CLI error messaging. type Error struct { Err error diff --git a/cmd/oras/internal/option/format.go b/cmd/oras/internal/option/format.go index 07e9489b9..888fa6bf7 100644 --- a/cmd/oras/internal/option/format.go +++ b/cmd/oras/internal/option/format.go @@ -15,21 +15,130 @@ limitations under the License. package option -import "github.com/spf13/pflag" +import ( + "bytes" + "fmt" + "strings" + "text/tabwriter" -// Format is a flag to format metadata into output. + "github.com/spf13/cobra" + "github.com/spf13/pflag" + oerrors "oras.land/oras/cmd/oras/internal/errors" +) + +// FormatType represents a format type. +type FormatType struct { + // Name is the format type name. + Name string + // Usage is the usage string in help doc. + Usage string + // HasParams indicates whether the format type has parameters. + HasParams bool +} + +// WithUsage returns a new format type with provided usage string. +func (ft *FormatType) WithUsage(usage string) *FormatType { + return &FormatType{ + Name: ft.Name, + HasParams: ft.HasParams, + Usage: usage, + } +} + +// format types +var ( + FormatTypeJSON = &FormatType{ + Name: "json", + Usage: "Print in JSON format", + } + FormatTypeGoTemplate = &FormatType{ + Name: "go-template", + Usage: "Print output using the given Go template", + HasParams: true, + } + FormatTypeTable = &FormatType{ + Name: "table", + Usage: "Get direct referrers and output in table format", + } + FormatTypeTree = &FormatType{ + Name: "tree", + Usage: "Get referrers recursively and print in tree format", + } +) + +// Format contains input and parsed options for formatted output flags. type Format struct { - Template string + FormatFlag string + Type string + Template string + AllowedTypes []*FormatType } // ApplyFlag implements FlagProvider.ApplyFlag. func (opts *Format) ApplyFlags(fs *pflag.FlagSet) { - const name = "format" - if fs.Lookup(name) != nil { - // allow command to overwrite the flag - return - } - fs.StringVar(&opts.Template, name, "", `[Experimental] Format output using a custom template: -'json': Print in JSON format -'$TEMPLATE': Print output using the given Go template.`) + buf := bytes.NewBufferString("[Experimental] Format output using a custom template:") + w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) + for _, t := range opts.AllowedTypes { + _, _ = fmt.Fprintf(w, "\n'%s':\t%s", t.Name, t.Usage) + } + w.Flush() + // apply flags + fs.StringVar(&opts.FormatFlag, "format", opts.FormatFlag, buf.String()) + fs.StringVar(&opts.Template, "template", "", "[Experimental] Template string used to format output") +} + +// Parse parses the input format flag. +func (opts *Format) Parse(_ *cobra.Command) error { + if err := opts.parseFlag(); err != nil { + return err + } + + if opts.Type == "" { + // flag not specified + return nil + } + + if opts.Type == FormatTypeGoTemplate.Name && opts.Template == "" { + return &oerrors.Error{ + Err: fmt.Errorf("%q format specified but no template given", opts.Type), + Recommendation: fmt.Sprintf("use `--format %s=TEMPLATE` to specify the template", opts.Type), + } + } + + var optionalTypes []string + for _, t := range opts.AllowedTypes { + if opts.Type == t.Name { + // type validation passed + return nil + } + optionalTypes = append(optionalTypes, t.Name) + } + return &oerrors.Error{ + Err: fmt.Errorf("invalid format type: %q", opts.Type), + Recommendation: fmt.Sprintf("supported types: %s", strings.Join(optionalTypes, ", ")), + } +} + +func (opts *Format) parseFlag() error { + opts.Type = opts.FormatFlag + if opts.Template != "" { + // template explicitly set + if opts.Type != FormatTypeGoTemplate.Name { + return fmt.Errorf("--template must be used with --format %s", FormatTypeGoTemplate.Name) + } + return nil + } + + for _, t := range opts.AllowedTypes { + if !t.HasParams { + continue + } + prefix := t.Name + "=" + if strings.HasPrefix(opts.FormatFlag, prefix) { + // parse type and add parameter to template + opts.Type = t.Name + opts.Template = opts.FormatFlag[len(prefix):] + } + } + return nil } diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index f86c6a0a1..ddc77aeba 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -102,12 +102,18 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder opts.FlagDescription = "[Preview] attach to an arch-specific subject" _ = cmd.MarkFlagRequired("artifact-type") opts.EnableDistributionSpecFlag() + opts.AllowedTypes = []*option.FormatType{option.FormatTypeJSON, option.FormatTypeGoTemplate} option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } func runAttach(cmd *cobra.Command, opts *attachOptions) error { ctx, logger := command.GetLogger(cmd, &opts.Common) + displayStatus, displayMetadata, err := display.NewAttachHandler(cmd.OutOrStdout(), opts.Format, opts.TTY, opts.Verbose) + if err != nil { + return err + } + annotations, err := opts.LoadManifestAnnotations() if err != nil { return err @@ -119,7 +125,6 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error { Recommendation: `To attach to an existing artifact, please provide files via argument or annotations via flag "--annotation". Run "oras attach -h" for more options and examples`, } } - displayStatus, displayMetadata := display.NewAttachHandler(cmd.OutOrStdout(), opts.Template, opts.TTY, opts.Verbose) // prepare manifest store, err := file.New("") diff --git a/cmd/oras/root/attach_test.go b/cmd/oras/root/attach_test.go new file mode 100644 index 000000000..bbd443ee2 --- /dev/null +++ b/cmd/oras/root/attach_test.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 root + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" +) + +func Test_runAttach_errType(t *testing.T) { + // prpare + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + // test + opts := &attachOptions{ + Format: option.Format{ + Type: "unknown", + }, + } + got := runAttach(cmd, opts).Error() + want := errors.UnsupportedFormatTypeError(opts.Format.Type).Error() + if got != want { + t.Fatalf("got %v, want %v", got, want) + } +} diff --git a/cmd/oras/root/discover.go b/cmd/oras/root/discover.go index 89e439837..5cde8cfa4 100644 --- a/cmd/oras/root/discover.go +++ b/cmd/oras/root/discover.go @@ -78,16 +78,19 @@ Example - Discover referrers of the manifest tagged 'v1' in an OCI image layout if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), "format", "output"); err != nil { return err } + opts.RawReference = args[0] + if err := option.Parse(cmd, &opts); err != nil { + return err + } if cmd.Flags().Changed("output") { - switch opts.Template { + switch opts.Format.Type { case "tree", "json", "table": fmt.Fprintf(cmd.ErrOrStderr(), "[DEPRECATED] --output is deprecated, try `--format %s` instead\n", opts.Template) default: return errors.New("output type can only be tree, table or json") } } - opts.RawReference = args[0] - return option.Parse(cmd, &opts) + return nil }, RunE: func(cmd *cobra.Command, args []string) error { return runDiscover(cmd, &opts) @@ -95,12 +98,14 @@ Example - Discover referrers of the manifest tagged 'v1' in an OCI image layout } cmd.Flags().StringVarP(&opts.artifactType, "artifact-type", "", "", "artifact type") - cmd.Flags().StringVarP(&opts.Template, "output", "o", "tree", "[Deprecated] format in which to display referrers (table, json, or tree). tree format will also show indirect referrers") - cmd.Flags().StringVarP(&opts.Template, "format", "", "", `[Experimental] Format output using a custom template: -'tree': Get referrers recursively and print in tree format (default) -'table': Get direct referrers and output in table format -'json': Get direct referrers and output in JSON format -'$TEMPLATE': Print direct referrers using the given Go template`) + cmd.Flags().StringVarP(&opts.Format.FormatFlag, "output", "o", "tree", "[Deprecated] format in which to display referrers (table, json, or tree). tree format will also show indirect referrers") + opts.FormatFlag = option.FormatTypeTree.Name + opts.AllowedTypes = []*option.FormatType{ + option.FormatTypeTree, + option.FormatTypeTable, + option.FormatTypeJSON.WithUsage("Get direct referrers and output in JSON format"), + option.FormatTypeGoTemplate.WithUsage("Print direct referrers using the given Go template"), + } opts.EnableDistributionSpecFlag() option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) @@ -124,7 +129,10 @@ func runDiscover(cmd *cobra.Command, opts *discoverOptions) error { return err } - handler := display.NewDiscoverHandler(cmd.OutOrStdout(), opts.Template, opts.Path, opts.RawReference, desc, opts.Verbose) + handler, err := display.NewDiscoverHandler(cmd.OutOrStdout(), opts.Format, opts.Path, opts.RawReference, desc, opts.Verbose) + if err != nil { + return err + } if handler.MultiLevelSupported() { if err := fetchAllReferrers(ctx, repo, desc, opts.artifactType, handler); err != nil { return err diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 499c14403..c38afe413 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -73,7 +73,7 @@ Example - Fetch raw manifest from an OCI layout archive file 'layout.tar': Args: oerrors.CheckArgs(argument.Exactly(1), "the manifest to fetch"), PreRunE: func(cmd *cobra.Command, args []string) error { switch { - case opts.outputPath == "-" && opts.Template != "": + case opts.outputPath == "-" && opts.FormatFlag != "": return fmt.Errorf("`--output -` cannot be used with `--format %s` at the same time", opts.Template) case opts.outputPath == "-" && opts.OutputDescriptor: return fmt.Errorf("`--descriptor` cannot be used with `--output -` at the same time") @@ -98,15 +98,20 @@ Example - Fetch raw manifest from an OCI layout archive file 'layout.tar': cmd.Flags().StringSliceVarP(&opts.mediaTypes, "media-type", "", nil, "accepted media types") cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the fetched manifest to, use - for stdout") - cmd.Flags().StringVar(&opts.Template, "format", "", `[Experimental] Format metadata using a custom template: -'json': Print in prettified JSON format -'$TEMPLATE': Print using the given Go template.`) + opts.AllowedTypes = []*option.FormatType{ + option.FormatTypeJSON.WithUsage("Print in prettified JSON format"), + option.FormatTypeGoTemplate.WithUsage("Print using the given Go template"), + } option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { ctx, logger := command.GetLogger(cmd, &opts.Common) + metadataHandler, contentHandler, err := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Format, opts.OutputDescriptor, opts.Pretty.Pretty, opts.outputPath) + if err != nil { + return err + } target, err := opts.NewReadonlyTarget(ctx, opts.Common, logger) if err != nil { @@ -125,8 +130,6 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return err } - metadataHandler, contentHandler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Template, opts.OutputDescriptor, opts.Pretty.Pretty, opts.outputPath) - var desc ocispec.Descriptor var content []byte if opts.OutputDescriptor && opts.outputPath == "" { diff --git a/cmd/oras/root/manifest/fetch_test.go b/cmd/oras/root/manifest/fetch_test.go new file mode 100644 index 000000000..7e4d7aa07 --- /dev/null +++ b/cmd/oras/root/manifest/fetch_test.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 manifest + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" +) + +func Test_fetchManifest_errType(t *testing.T) { + // prpare + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + // test + opts := &fetchOptions{ + Format: option.Format{ + Type: "unknown", + }, + } + got := fetchManifest(cmd, opts).Error() + want := errors.UnsupportedFormatTypeError(opts.Format.Type).Error() + if got != want { + t.Fatalf("got %v, want %v", got, want) + } +} diff --git a/cmd/oras/root/pull.go b/cmd/oras/root/pull.go index 1ed0ef4de..061cd1b79 100644 --- a/cmd/oras/root/pull.go +++ b/cmd/oras/root/pull.go @@ -104,12 +104,17 @@ Example - Pull artifact files from an OCI layout archive 'layout.tar': cmd.Flags().StringVarP(&opts.Output, "output", "o", ".", "output directory") cmd.Flags().StringVarP(&opts.ManifestConfigRef, "config", "", "", "output manifest config file") cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 3, "concurrency level") + opts.AllowedTypes = []*option.FormatType{option.FormatTypeJSON, option.FormatTypeGoTemplate} option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } func runPull(cmd *cobra.Command, opts *pullOptions) error { ctx, logger := command.GetLogger(cmd, &opts.Common) + statusHandler, metadataHandler, err := display.NewPullHandler(cmd.OutOrStdout(), opts.Format, opts.Path, opts.TTY, opts.Verbose) + if err != nil { + return err + } // Copy Options copyOptions := oras.DefaultCopyOptions copyOptions.Concurrency = opts.concurrency @@ -135,7 +140,6 @@ func runPull(cmd *cobra.Command, opts *pullOptions) error { dst.AllowPathTraversalOnWrite = opts.PathTraversal dst.DisableOverwrite = opts.KeepOldFiles - statusHandler, metadataHandler := display.NewPullHandler(cmd.OutOrStdout(), opts.Template, opts.Path, opts.TTY, opts.Verbose) desc, err := doPull(ctx, src, dst, copyOptions, metadataHandler, statusHandler, opts) if err != nil { if errors.Is(err, file.ErrPathTraversalDisallowed) { diff --git a/cmd/oras/root/pull_test.go b/cmd/oras/root/pull_test.go new file mode 100644 index 000000000..8e710c1fc --- /dev/null +++ b/cmd/oras/root/pull_test.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 root + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" +) + +func Test_runPull_errType(t *testing.T) { + // prpare + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + // test + opts := &pullOptions{ + Format: option.Format{ + Type: "unknown", + }, + } + got := runPull(cmd, opts).Error() + want := errors.UnsupportedFormatTypeError(opts.Format.Type).Error() + if got != want { + t.Fatalf("got %v, want %v", got, want) + } +} diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 6cf4c44f8..ad6145373 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -144,18 +144,21 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t cmd.Flags().StringVarP(&opts.manifestConfigRef, "config", "", "", "`path` of image config file") cmd.Flags().StringVarP(&opts.artifactType, "artifact-type", "", "", "artifact type") cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 5, "concurrency level") - + opts.AllowedTypes = []*option.FormatType{option.FormatTypeJSON, option.FormatTypeGoTemplate} option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } func runPush(cmd *cobra.Command, opts *pushOptions) error { ctx, logger := command.GetLogger(cmd, &opts.Common) + displayStatus, displayMetadata, err := display.NewPushHandler(cmd.OutOrStdout(), opts.Format, opts.TTY, opts.Verbose) + if err != nil { + return err + } annotations, err := opts.LoadManifestAnnotations() if err != nil { return err } - displayStatus, displayMetadata := display.NewPushHandler(cmd.OutOrStdout(), opts.Template, opts.TTY, opts.Verbose) // prepare pack packOpts := oras.PackManifestOptions{ diff --git a/cmd/oras/root/push_test.go b/cmd/oras/root/push_test.go new file mode 100644 index 000000000..296711979 --- /dev/null +++ b/cmd/oras/root/push_test.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 root + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" +) + +func Test_runPush_errType(t *testing.T) { + // prpare + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + // test + opts := &pushOptions{ + Format: option.Format{ + Type: "unknown", + }, + } + got := runPush(cmd, opts).Error() + want := errors.UnsupportedFormatTypeError(opts.Format.Type).Error() + if got != want { + t.Fatalf("got %v, want %v", got, want) + } +} diff --git a/test/e2e/suite/command/attach.go b/test/e2e/suite/command/attach.go index 4d9f4dc11..3f14f915a 100644 --- a/test/e2e/suite/command/attach.go +++ b/test/e2e/suite/command/attach.go @@ -107,7 +107,7 @@ var _ = Describe("1.1 registry users:", func() { ORAS("cp", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), subjectRef).Exec() artifactType := "test/attach" // test - out := ORAS("attach", "--artifact-type", artifactType, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.digest}}", "--platform", "linux/amd64"). + out := ORAS("attach", "--artifact-type", artifactType, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.digest}}", "--platform", "linux/amd64"). WithWorkDir(PrepareTempFiles()).Exec().Out.Contents() // validate ORAS("discover", "--artifact-type", artifactType, RegistryRef(ZOTHost, testRepo, multi_arch.LinuxAMD64.Digest.String())).MatchKeyWords(string(out)).Exec() @@ -121,7 +121,7 @@ var _ = Describe("1.1 registry users:", func() { subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag) CopyZOTRepo(ImageRepo, testRepo) // test - ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--export-manifest", exportName, "--format", "{{.reference}}"). + ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--export-manifest", exportName, "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out.Contents() // validate fetched := ORAS("manifest", "fetch", string(ref)).Exec().Out.Contents() @@ -137,7 +137,7 @@ var _ = Describe("1.1 registry users:", func() { CopyZOTRepo(ImageRepo, testRepo) // test delimitter := "---" - output := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--export-manifest", exportName, "--format", fmt.Sprintf("{{.reference}}%s{{.artifactType}}", delimitter)). + output := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--export-manifest", exportName, "--format", fmt.Sprintf("go-template={{.reference}}%s{{.artifactType}}", delimitter)). WithWorkDir(tempDir).Exec().Out.Contents() ref, artifactType, _ := strings.Cut(string(output), delimitter) // validate @@ -167,12 +167,12 @@ var _ = Describe("1.1 registry users:", func() { subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag) CopyZOTRepo(ImageRepo, testRepo) // test - ref1 := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.reference}}"). + ref1 := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out.Contents() - ref2 := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.reference}}"). + ref2 := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out.Contents() // validate - ORAS("discover", subjectRef, "--format", "{{range .manifests}}{{println .reference}}{{end}}").MatchKeyWords(string(ref1), string(ref2)).Exec() + ORAS("discover", subjectRef, "--format", "go-template={{range .manifests}}{{println .reference}}{{end}}").MatchKeyWords(string(ref1), string(ref2)).Exec() }) It("should attach a file via a OCI Image", func() { @@ -181,10 +181,10 @@ var _ = Describe("1.1 registry users:", func() { subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag) CopyZOTRepo(ImageRepo, testRepo) // test - ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.reference}}"). + ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out.Contents() // validate - out := ORAS("discover", subjectRef, "--format", "{{range .manifests}}{{println .reference}}{{end}}").Exec().Out + out := ORAS("discover", subjectRef, "--format", "go-template={{range .manifests}}{{println .reference}}{{end}}").Exec().Out Expect(out).To(gbytes.Say(string(ref))) }) @@ -222,10 +222,10 @@ var _ = Describe("1.0 registry users:", func() { subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) // test - ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.reference}}"). + ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out.Contents() // validate - out := ORAS("discover", subjectRef, "--format", "{{range .manifests}}{{println .reference}}{{end}}").Exec().Out + out := ORAS("discover", subjectRef, "--format", "go-template={{range .manifests}}{{println .reference}}{{end}}").Exec().Out Expect(out).To(gbytes.Say(string(ref))) }) @@ -235,11 +235,11 @@ var _ = Describe("1.0 registry users:", func() { subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) // test - ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.reference}}"). + ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out.Contents() // validate - out := ORAS("discover", subjectRef, "--format", "{{range .manifests}}{{println .reference}}{{end}}").Exec().Out + out := ORAS("discover", subjectRef, "--format", "go-template={{range .manifests}}{{println .reference}}{{end}}").Exec().Out Expect(out).To(gbytes.Say(string(ref))) }) @@ -249,11 +249,11 @@ var _ = Describe("1.0 registry users:", func() { subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) // test - ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--distribution-spec", "v1.1-referrers-tag", "--format", "{{.reference}}"). + ref := ORAS("attach", "--artifact-type", "test/attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--distribution-spec", "v1.1-referrers-tag", "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out.Contents() // validate - out := ORAS("discover", subjectRef, "--format", "{{range .manifests}}{{println .reference}}{{end}}").Exec().Out + out := ORAS("discover", subjectRef, "--format", "go-template={{range .manifests}}{{println .reference}}{{end}}").Exec().Out Expect(out).To(gbytes.Say(string(ref))) }) }) @@ -275,7 +275,7 @@ var _ = Describe("OCI image layout users:", func() { root := PrepareTempOCI(ImageRepo) subjectRef := LayoutRef(root, foobar.Tag) // test - ref := ORAS("attach", "--artifact-type", "test/attach", Flags.Layout, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--export-manifest", exportName, "--format", "{{.reference}}"). + ref := ORAS("attach", "--artifact-type", "test/attach", Flags.Layout, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--export-manifest", exportName, "--format", "go-template={{.reference}}"). WithWorkDir(root).Exec().Out.Contents() // validate fetched := ORAS("manifest", "fetch", Flags.Layout, string(ref)).Exec().Out.Contents() @@ -287,7 +287,7 @@ var _ = Describe("OCI image layout users:", func() { subjectRef := LayoutRef(root, multi_arch.Tag) artifactType := "test/attach" // test - out := ORAS("attach", Flags.Layout, "--artifact-type", artifactType, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.digest}}", "--platform", "linux/amd64"). + out := ORAS("attach", Flags.Layout, "--artifact-type", artifactType, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.digest}}", "--platform", "linux/amd64"). WithWorkDir(PrepareTempFiles()).Exec().Out.Contents() // validate ORAS("discover", Flags.Layout, "--artifact-type", artifactType, LayoutRef(root, multi_arch.LinuxAMD64.Digest.String())).MatchKeyWords(string(out)).Exec() @@ -297,10 +297,10 @@ var _ = Describe("OCI image layout users:", func() { root := PrepareTempOCI(ImageRepo) subjectRef := LayoutRef(root, foobar.Tag) // test - ref := ORAS("attach", "--artifact-type", "test/attach", Flags.Layout, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "{{.reference}}"). + ref := ORAS("attach", "--artifact-type", "test/attach", Flags.Layout, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--format", "go-template={{.reference}}"). WithWorkDir(root).Exec().Out.Contents() // validate - out := ORAS("discover", Flags.Layout, subjectRef, "--format", "{{range .manifests}}{{println .reference}}{{end}}").Exec().Out + out := ORAS("discover", Flags.Layout, subjectRef, "--format", "go-template={{range .manifests}}{{println .reference}}{{end}}").Exec().Out Expect(out).To(gbytes.Say(string(ref))) }) }) diff --git a/test/e2e/suite/command/cp.go b/test/e2e/suite/command/cp.go index 5b1a16c28..f548dd0be 100644 --- a/test/e2e/suite/command/cp.go +++ b/test/e2e/suite/command/cp.go @@ -188,7 +188,7 @@ var _ = Describe("1.1 registry users:", func() { // validate CompareRef(RegistryRef(ZOTHost, ImageRepo, ma.Digest), dst) - digests := ORAS("discover", dst, "--format", "{{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() + digests := ORAS("discover", dst, "--format", "go-template={{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() for _, digest := range strings.Split(strings.TrimSpace(string(digests)), "\n") { CompareRef(RegistryRef(ZOTHost, ArtifactRepo, digest), RegistryRef(ZOTHost, dstRepo, digest)) } @@ -206,7 +206,7 @@ var _ = Describe("1.1 registry users:", func() { Exec() // validate CompareRef(RegistryRef(ZOTHost, ImageRepo, ma.Digest), dst) - digests := ORAS("discover", dst, "--format", "{{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() + digests := ORAS("discover", dst, "--format", "go-template={{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() for _, digest := range strings.Split(strings.TrimSpace(string(digests)), "\n") { CompareRef(RegistryRef(ZOTHost, ArtifactRepo, digest), RegistryRef(ZOTHost, dstRepo, digest)) } @@ -233,7 +233,7 @@ var _ = Describe("1.1 registry users:", func() { Exec() // validate CompareRef(RegistryRef(ZOTHost, ImageRepo, ma.Digest), dst) - digests := ORAS("discover", dst, "--format", "{{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() + digests := ORAS("discover", dst, "--format", "go-template={{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() for _, digest := range strings.Split(strings.TrimSpace(string(digests)), "\n") { CompareRef(RegistryRef(ZOTHost, ArtifactRepo, digest), RegistryRef(ZOTHost, dstRepo, digest)) } @@ -273,7 +273,7 @@ var _ = Describe("1.1 registry users:", func() { Exec() // validate CompareRef(RegistryRef(ZOTHost, ArtifactRepo, digest), dst) - digests := ORAS("discover", dst, "--format", "{{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() + digests := ORAS("discover", dst, "--format", "go-template={{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() for _, digest := range strings.Split(strings.TrimSpace(string(digests)), "\n") { CompareRef(RegistryRef(ZOTHost, ArtifactRepo, digest), RegistryRef(ZOTHost, dstRepo, digest)) } @@ -291,7 +291,7 @@ var _ = Describe("1.1 registry users:", func() { Exec() // validate CompareRef(RegistryRef(ZOTHost, ArtifactRepo, digest), RegistryRef(ZOTHost, dstRepo, digest)) - digests := ORAS("discover", RegistryRef(ZOTHost, dstRepo, digest), "--format", "{{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() + digests := ORAS("discover", RegistryRef(ZOTHost, dstRepo, digest), "--format", "go-template={{range .manifests}}{{println .digest}}{{end}}").Exec().Out.Contents() for _, digest := range strings.Split(strings.TrimSpace(string(digests)), "\n") { CompareRef(RegistryRef(ZOTHost, ArtifactRepo, digest), RegistryRef(ZOTHost, dstRepo, digest)) } diff --git a/test/e2e/suite/command/discover.go b/test/e2e/suite/command/discover.go index 082ee4eb3..8cdcf5ff1 100644 --- a/test/e2e/suite/command/discover.go +++ b/test/e2e/suite/command/discover.go @@ -77,9 +77,10 @@ var _ = Describe("ORAS beginners:", func() { }) It("should fail if invalid output type is used", func() { - ORAS("discover", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--output", "ukpkmkk"). + invalidType := "ukpkmkk" + ORAS("discover", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--output", invalidType). ExpectFailure(). - MatchErrKeyWords("Error:", "output type can only be tree, table or json"). + MatchErrKeyWords("Error:", "invalid format type", invalidType, "tree", "table", "json", "go-template"). Exec() }) @@ -204,7 +205,7 @@ var _ = Describe("1.1 registry users:", func() { }) When("running discover command with go-template output", func() { It("should show referrers digest of a subject", func() { - ORAS("discover", subjectRef, "--format", "{{(first .manifests).reference}}"). + ORAS("discover", subjectRef, "--format", "go-template={{(first .manifests).reference}}"). MatchContent(RegistryRef(ZOTHost, ArtifactRepo, foobar.SBOMImageReferrer.Digest.String())). Exec() }) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index ca92e4abe..adbf34ee7 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -106,13 +106,13 @@ var _ = Describe("ORAS beginners:", func() { }) It("should fail if stdout is used inpropriately", func() { - ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--output", "-", "--format", "test"). + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--output", "-", "--format", "json"). ExpectFailure().Exec() - ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--descriptor", "--format", "test"). + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--descriptor", "--format", "json"). ExpectFailure().Exec() ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--output", "-", "--descriptor"). ExpectFailure().Exec() - ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--format", "test", "--pretty"). + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--format", "json", "--pretty"). ExpectFailure().Exec() }) }) @@ -276,13 +276,13 @@ var _ = Describe("1.1 registry users:", func() { }) It("should fetch manifest with platform validation and output content", func() { - out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "{{toJson .content}}"). + out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "go-template={{toJson .content}}"). Exec().Out.Contents() Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) }) It("should fetch manifest and format output", func() { - ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.LinuxAMD64.Digest.String()), "--format", "{{(first .content.layers).digest}}"). + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.LinuxAMD64.Digest.String()), "--format", "go-template={{(first .content.layers).digest}}"). MatchContent(multi_arch.LayerDigest). Exec() }) diff --git a/test/e2e/suite/command/pull.go b/test/e2e/suite/command/pull.go index 7d292646d..bbeafe43b 100644 --- a/test/e2e/suite/command/pull.go +++ b/test/e2e/suite/command/pull.go @@ -78,10 +78,52 @@ var _ = Describe("ORAS beginners:", func() { It("should not show hint for go template output", func() { tempDir := PrepareTempFiles() ref := RegistryRef(ZOTHost, ArtifactRepo, unnamed.Tag) - out := ORAS("pull", ref, "--format", "{{.}}").WithWorkDir(tempDir).Exec().Out + out := ORAS("pull", ref, "--format", "go-template={{.}}").WithWorkDir(tempDir).Exec().Out gomega.Expect(out).ShouldNot(gbytes.Say(hintMsg(ref))) }) + It("should fail if template is specified without go template format", func() { + tempDir := PrepareTempFiles() + ref := RegistryRef(ZOTHost, ArtifactRepo, foobar.Tag) + ORAS("pull", ref, "--format", "json", "--template", "{{.}}"). + WithWorkDir(tempDir). + ExpectFailure(). + MatchErrKeyWords("--template must be used with --format go-template"). + Exec() + ORAS("pull", ref, "--template", "{{.}}"). + WithWorkDir(tempDir). + ExpectFailure(). + MatchErrKeyWords("--template must be used with --format go-template"). + Exec() + }) + + It("should fail if template format is invalid", func() { + tempDir := PrepareTempFiles() + invalidPrompt := "invalid format type" + ref := RegistryRef(ZOTHost, ArtifactRepo, foobar.Tag) + ORAS("pull", ref, "--format", "json", "--format", "="). + WithWorkDir(tempDir). + ExpectFailure(). + MatchErrKeyWords(invalidPrompt). + Exec() + ORAS("pull", ref, "--format", "json", "--format", "={{.}}"). + WithWorkDir(tempDir). + ExpectFailure(). + MatchErrKeyWords(invalidPrompt). + Exec() + noTemplatePrompt := "format specified but no template given" + ORAS("pull", ref, "--format", "json", "--format", "go-template="). + WithWorkDir(tempDir). + ExpectFailure(). + MatchErrKeyWords(noTemplatePrompt). + Exec() + ORAS("pull", ref, "--format", "json", "--format", "go-template"). + WithWorkDir(tempDir). + ExpectFailure(). + MatchErrKeyWords(noTemplatePrompt). + Exec() + }) + It("should fail and show detailed error description if no argument provided", func() { err := ORAS("pull").ExpectFailure().Exec().Err Expect(err).Should(gbytes.Say("Error")) @@ -194,7 +236,17 @@ var _ = Describe("OCI spec 1.1 registry users:", func() { for _, p := range foobar.ImageLayerNames { paths = append(paths, filepath.Join(tempDir, p)) } - ORAS("pull", RegistryRef(ZOTHost, ArtifactRepo, foobar.Tag), "--format", "{{range .files}}{{println .path}}{{end}}"). + ORAS("pull", RegistryRef(ZOTHost, ArtifactRepo, foobar.Tag), "--format", "go-template={{range .files}}{{println .path}}{{end}}"). + WithWorkDir(tempDir).MatchKeyWords(paths...).Exec() + }) + + It("should pull and output downloaded file paths using --template", func() { + tempDir := GinkgoT().TempDir() + var paths []string + for _, p := range foobar.ImageLayerNames { + paths = append(paths, filepath.Join(tempDir, p)) + } + ORAS("pull", RegistryRef(ZOTHost, ArtifactRepo, foobar.Tag), "--format", "go-template", "--template", "{{range .files}}{{println .path}}{{end}}"). WithWorkDir(tempDir).MatchKeyWords(paths...).Exec() }) diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index 39beace43..8ed2c2595 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -180,7 +180,7 @@ var _ = Describe("Remote registry users:", func() { tempDir := PrepareTempFiles() extraTag := "2e2" - ORAS("push", fmt.Sprintf("%s,%s", RegistryRef(ZOTHost, repo, tag), extraTag), foobar.FileBarName, "-v", "--format", "{{range .referenceAsTags}}{{println .}}{{end}}"). + ORAS("push", fmt.Sprintf("%s,%s", RegistryRef(ZOTHost, repo, tag), extraTag), foobar.FileBarName, "-v", "--format", "go-template={{range .referenceAsTags}}{{println .}}{{end}}"). MatchContent(fmt.Sprintf("%s\n%s\n", RegistryRef(ZOTHost, repo, extraTag), RegistryRef(ZOTHost, repo, tag))). WithWorkDir(tempDir).Exec() @@ -367,7 +367,7 @@ var _ = Describe("Remote registry users:", func() { annotationValue := "value" // test - out := ORAS("push", RegistryRef(ZOTHost, repo, tag), "-a", fmt.Sprintf("%s=%s", annotationKey, annotationValue), "--format", "{{.reference}}"). + out := ORAS("push", RegistryRef(ZOTHost, repo, tag), "-a", fmt.Sprintf("%s=%s", annotationKey, annotationValue), "--format", "go-template={{.reference}}"). WithWorkDir(tempDir).Exec().Out // validate diff --git a/test/e2e/suite/scenario/oci_artifact.go b/test/e2e/suite/scenario/oci_artifact.go index 41703764e..ee726f8f4 100644 --- a/test/e2e/suite/scenario/oci_artifact.go +++ b/test/e2e/suite/scenario/oci_artifact.go @@ -64,7 +64,7 @@ var _ = Describe("OCI artifact users:", Ordered, func() { WithWorkDir(tempDir). WithDescription("attach with manifest exported").Exec() - ref := string(ORAS("discover", subject, "--format", "{{(first .manifests).reference}}").Exec().Out.Contents()) + ref := string(ORAS("discover", subject, "--format", "go-template={{(first .manifests).reference}}").Exec().Out.Contents()) fetched := ORAS("manifest", "fetch", ref).MatchKeyWords(foobar.AttachFileMedia).Exec() MatchFile(filepath.Join(tempDir, pulledManifest), string(fetched.Out.Contents()), DefaultTimeout) @@ -81,7 +81,7 @@ var _ = Describe("OCI artifact users:", Ordered, func() { WithWorkDir(tempDir). WithDescription("attach again with manifest exported").Exec() - ref = string(ORAS("discover", subject, "--format", "{{(first .manifests).reference}}", "--artifact-type", "test/artifact2").Exec().Out.Contents()) + ref = string(ORAS("discover", subject, "--format", "go-template={{(first .manifests).reference}}", "--artifact-type", "test/artifact2").Exec().Out.Contents()) fetched = ORAS("manifest", "fetch", ref).MatchKeyWords(foobar.AttachFileMedia).Exec() MatchFile(filepath.Join(tempDir, pulledManifest), string(fetched.Out.Contents()), DefaultTimeout)