From e5ac9d944e3aa44a739775e0d127f5f4ba173aaa Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 18:32:04 +0800 Subject: [PATCH] feat: support `--format` in `manifest fetch` command (#1295) Signed-off-by: Billy Zha --- cmd/oras/internal/display/content/discard.go | 30 +++++++++++ .../internal/display/content/interface.go | 26 +++++++++ .../display/content/manifest_fetch.go | 54 +++++++++++++++++++ cmd/oras/internal/display/handler.go | 35 ++++++++++++ .../metadata/descriptor/manifest_fetch.go | 49 +++++++++++++++++ cmd/oras/internal/display/metadata/discard.go | 30 +++++++++++ .../internal/display/metadata/interface.go | 6 +++ .../display/metadata/json/manifest_fetch.go | 46 ++++++++++++++++ .../display/metadata/model/fetched.go | 31 +++++++++++ .../metadata/template/manifest_fetch.go | 48 +++++++++++++++++ cmd/oras/internal/display/utils/utils.go | 28 +++++++--- cmd/oras/internal/option/pretty.go | 20 ++----- cmd/oras/internal/option/pretty_test.go | 8 +-- cmd/oras/root/manifest/fetch.go | 43 ++++++--------- .../e2e/internal/testdata/multi_arch/const.go | 1 + test/e2e/suite/command/manifest.go | 42 +++++++++++++++ 16 files changed, 444 insertions(+), 53 deletions(-) create mode 100644 cmd/oras/internal/display/content/discard.go create mode 100644 cmd/oras/internal/display/content/interface.go create mode 100644 cmd/oras/internal/display/content/manifest_fetch.go create mode 100644 cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go create mode 100644 cmd/oras/internal/display/metadata/discard.go create mode 100644 cmd/oras/internal/display/metadata/json/manifest_fetch.go create mode 100644 cmd/oras/internal/display/metadata/model/fetched.go create mode 100644 cmd/oras/internal/display/metadata/template/manifest_fetch.go diff --git a/cmd/oras/internal/display/content/discard.go b/cmd/oras/internal/display/content/discard.go new file mode 100644 index 000000000..cdf543797 --- /dev/null +++ b/cmd/oras/internal/display/content/discard.go @@ -0,0 +1,30 @@ +/* +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 content + +import ocispec "github.com/opencontainers/image-spec/specs-go/v1" + +type discardHandler struct{} + +// OnContentFetched implements ManifestFetchHandler. +func (discardHandler) OnContentFetched(ocispec.Descriptor, []byte) error { + return nil +} + +// NewDiscardHandler returns a new discard handler. +func NewDiscardHandler() ManifestFetchHandler { + return discardHandler{} +} diff --git a/cmd/oras/internal/display/content/interface.go b/cmd/oras/internal/display/content/interface.go new file mode 100644 index 000000000..2c35fc552 --- /dev/null +++ b/cmd/oras/internal/display/content/interface.go @@ -0,0 +1,26 @@ +/* +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 content + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// ManifestFetchHandler handles raw output for manifest fetch events. +type ManifestFetchHandler interface { + // OnContentFetched is called after the manifest content is fetched. + OnContentFetched(desc ocispec.Descriptor, content []byte) error +} diff --git a/cmd/oras/internal/display/content/manifest_fetch.go b/cmd/oras/internal/display/content/manifest_fetch.go new file mode 100644 index 000000000..20707b04b --- /dev/null +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -0,0 +1,54 @@ +/* +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 content + +import ( + "fmt" + "io" + "os" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/utils" +) + +// manifestFetch handles raw content output. +type manifestFetch struct { + pretty bool + stdout io.Writer + outputPath string +} + +func (h *manifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byte) error { + out := h.stdout + if h.outputPath != "-" && h.outputPath != "" { + f, err := os.Create(h.outputPath) + if err != nil { + return fmt.Errorf("failed to open %q: %w", h.outputPath, err) + } + defer f.Close() + out = f + } + return utils.PrintJSON(out, manifest, h.pretty) +} + +// NewManifestFetchHandler creates a new handler. +func NewManifestFetchHandler(out io.Writer, pretty bool, outputPath string) ManifestFetchHandler { + return &manifestFetch{ + pretty: pretty, + stdout: out, + outputPath: outputPath, + } +} diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 66aa468a4..884b7427e 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -19,7 +19,9 @@ import ( "io" "os" + "oras.land/oras/cmd/oras/internal/display/content" "oras.land/oras/cmd/oras/internal/display/metadata" + "oras.land/oras/cmd/oras/internal/display/metadata/descriptor" "oras.land/oras/cmd/oras/internal/display/metadata/json" "oras.land/oras/cmd/oras/internal/display/metadata/template" "oras.land/oras/cmd/oras/internal/display/metadata/text" @@ -94,3 +96,36 @@ func NewPullHandler(format string, path string, tty *os.File, out io.Writer, ver } return statusHandler, metadataHandler } + +// NewManifestFetchHandler returns a manifest fetch handler. +func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) { + var metadataHandler metadata.ManifestFetchHandler + var contentHandler content.ManifestFetchHandler + + switch format { + case "": + // raw + if outputDescriptor { + metadataHandler = descriptor.NewManifestFetchHandler(out, pretty) + } else { + metadataHandler = metadata.NewDiscardHandler() + } + case "json": + // json + metadataHandler = json.NewManifestFetchHandler(out) + if outputPath == "" { + contentHandler = content.NewDiscardHandler() + } + default: + // go template + metadataHandler = template.NewManifestFetchHandler(out, format) + if outputPath == "" { + contentHandler = content.NewDiscardHandler() + } + } + + if contentHandler == nil { + contentHandler = content.NewManifestFetchHandler(out, pretty, outputPath) + } + return metadataHandler, contentHandler +} diff --git a/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go b/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go new file mode 100644 index 000000000..a55407494 --- /dev/null +++ b/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go @@ -0,0 +1,49 @@ +/* +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 descriptor + +import ( + "encoding/json" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/metadata" + "oras.land/oras/cmd/oras/internal/display/utils" +) + +// manifestFetchHandler handles metadata descriptor output. +type manifestFetchHandler struct { + pretty bool + out io.Writer +} + +// OnFetched implements ManifestFetchHandler. +func (h *manifestFetchHandler) OnFetched(_ string, desc ocispec.Descriptor, _ []byte) error { + descBytes, err := json.Marshal(desc) + if err != nil { + return fmt.Errorf("invalid descriptor: %w", err) + } + return utils.PrintJSON(h.out, descBytes, h.pretty) +} + +// NewManifestFetchHandler creates a new handler. +func NewManifestFetchHandler(out io.Writer, pretty bool) metadata.ManifestFetchHandler { + return &manifestFetchHandler{ + pretty: pretty, + out: out, + } +} diff --git a/cmd/oras/internal/display/metadata/discard.go b/cmd/oras/internal/display/metadata/discard.go new file mode 100644 index 000000000..4b1941f59 --- /dev/null +++ b/cmd/oras/internal/display/metadata/discard.go @@ -0,0 +1,30 @@ +/* +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 metadata + +import ocispec "github.com/opencontainers/image-spec/specs-go/v1" + +type discard struct{} + +// NewDiscardHandler creates a new handler that discards output for all events. +func NewDiscardHandler() ManifestFetchHandler { + return discard{} +} + +// OnFetched implements ManifestFetchHandler. +func (discard) OnFetched(string, ocispec.Descriptor, []byte) error { + return nil +} diff --git a/cmd/oras/internal/display/metadata/interface.go b/cmd/oras/internal/display/metadata/interface.go index 0709a0578..c7815aa12 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -31,6 +31,12 @@ type AttachHandler interface { OnCompleted(opts *option.Target, root, subject ocispec.Descriptor) error } +// ManifestFetchHandler handles metadata output for manifest fetch events. +type ManifestFetchHandler interface { + // OnFetched is called after the manifest content is fetched. + OnFetched(path string, desc ocispec.Descriptor, content []byte) error +} + // PullHandler handles metadata output for pull events. type PullHandler interface { // OnLayerSkipped is called when a layer is skipped. diff --git a/cmd/oras/internal/display/metadata/json/manifest_fetch.go b/cmd/oras/internal/display/metadata/json/manifest_fetch.go new file mode 100644 index 000000000..01fe57a02 --- /dev/null +++ b/cmd/oras/internal/display/metadata/json/manifest_fetch.go @@ -0,0 +1,46 @@ +/* +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 json + +import ( + "encoding/json" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/metadata" + "oras.land/oras/cmd/oras/internal/display/metadata/model" +) + +// manifestFetchHandler handles JSON metadata output for manifest fetch events. +type manifestFetchHandler struct { + out io.Writer +} + +// NewManifestFetchHandler creates a new handler for manifest fetch events. +func NewManifestFetchHandler(out io.Writer) metadata.ManifestFetchHandler { + return &manifestFetchHandler{ + out: out, + } +} + +// OnFetched is called after the manifest fetch is completed. +func (h *manifestFetchHandler) OnFetched(path string, desc ocispec.Descriptor, content []byte) error { + var manifest map[string]any + if err := json.Unmarshal(content, &manifest); err != nil { + manifest = nil + } + return printJSON(h.out, model.NewFetched(path, desc, manifest)) +} diff --git a/cmd/oras/internal/display/metadata/model/fetched.go b/cmd/oras/internal/display/metadata/model/fetched.go new file mode 100644 index 000000000..be764839f --- /dev/null +++ b/cmd/oras/internal/display/metadata/model/fetched.go @@ -0,0 +1,31 @@ +/* +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 model + +import ocispec "github.com/opencontainers/image-spec/specs-go/v1" + +type fetched struct { + Descriptor + Content any +} + +// NewFetched creates a new fetched metadata. +func NewFetched(path string, desc ocispec.Descriptor, content any) any { + return &fetched{ + Descriptor: FromDescriptor(path, desc), + Content: content, + } +} diff --git a/cmd/oras/internal/display/metadata/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go new file mode 100644 index 000000000..8148f13d6 --- /dev/null +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -0,0 +1,48 @@ +/* +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 template + +import ( + "encoding/json" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/metadata" + "oras.land/oras/cmd/oras/internal/display/metadata/model" +) + +// manifestFetchHandler handles JSON metadata output for manifest fetch events. +type manifestFetchHandler struct { + template string + out io.Writer +} + +// NewManifestFetchHandler creates a new handler for manifest fetch events. +func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFetchHandler { + return &manifestFetchHandler{ + template: template, + out: out, + } +} + +// OnFetched is called after the manifest fetch is completed. +func (h *manifestFetchHandler) OnFetched(path string, desc ocispec.Descriptor, content []byte) error { + var manifest map[string]any + if err := json.Unmarshal(content, &manifest); err != nil { + manifest = nil + } + return parseAndWrite(h.out, model.NewFetched(path, desc, manifest), h.template) +} diff --git a/cmd/oras/internal/display/utils/utils.go b/cmd/oras/internal/display/utils/utils.go index 6a88c7c02..2f3205067 100644 --- a/cmd/oras/internal/display/utils/utils.go +++ b/cmd/oras/internal/display/utils/utils.go @@ -15,14 +15,14 @@ limitations under the License. package utils -import v1 "github.com/opencontainers/image-spec/specs-go/v1" - -// GenerateContentKey generates a unique key for each content descriptor, using -// its digest and name if applicable. -func GenerateContentKey(desc v1.Descriptor) string { - return desc.Digest.String() + desc.Annotations[v1.AnnotationTitle] -} +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) +// Prompt constants for pull. const ( PullPromptDownloading = "Downloading" PullPromptPulled = "Pulled " @@ -31,3 +31,17 @@ const ( PullPromptRestored = "Restored " PullPromptDownloaded = "Downloaded " ) + +// PrintJSON writes the data to the output stream, optionally prettifying it. +func PrintJSON(out io.Writer, data []byte, pretty bool) error { + if pretty { + buf := bytes.NewBuffer(nil) + if err := json.Indent(buf, data, "", " "); err != nil { + return fmt.Errorf("failed to prettify: %w", err) + } + buf.WriteByte('\n') + data = buf.Bytes() + } + _, err := out.Write(data) + return err +} diff --git a/cmd/oras/internal/option/pretty.go b/cmd/oras/internal/option/pretty.go index 0e7524473..6013be141 100644 --- a/cmd/oras/internal/option/pretty.go +++ b/cmd/oras/internal/option/pretty.go @@ -16,36 +16,24 @@ limitations under the License. package option import ( - "bytes" - "encoding/json" - "fmt" "io" "github.com/spf13/pflag" + "oras.land/oras/cmd/oras/internal/display/utils" ) // Pretty option struct. type Pretty struct { - pretty bool + Pretty bool } // ApplyFlags applies flags to a command flag set. func (opts *Pretty) ApplyFlags(fs *pflag.FlagSet) { - fs.BoolVarP(&opts.pretty, "pretty", "", false, "prettify JSON objects printed to stdout") + fs.BoolVarP(&opts.Pretty, "pretty", "", false, "prettify JSON objects printed to stdout") } // Output outputs the prettified content if `--pretty` flag is used. Otherwise // outputs the original content. func (opts *Pretty) Output(w io.Writer, content []byte) error { - if opts.pretty { - buf := bytes.NewBuffer(nil) - if err := json.Indent(buf, content, "", " "); err != nil { - return fmt.Errorf("failed to prettify: %w", err) - } - buf.WriteByte('\n') - content = buf.Bytes() - } - - _, err := w.Write(content) - return err + return utils.PrintJSON(w, content, opts.Pretty) } diff --git a/cmd/oras/internal/option/pretty_test.go b/cmd/oras/internal/option/pretty_test.go index 75aedd804..f42aa22d9 100644 --- a/cmd/oras/internal/option/pretty_test.go +++ b/cmd/oras/internal/option/pretty_test.go @@ -28,8 +28,8 @@ import ( func TestPretty_ApplyFlags(t *testing.T) { var test struct{ Pretty } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) - if test.Pretty.pretty != false { - t.Fatalf("expecting pretty to be false but got: %v", test.Pretty.pretty) + if test.Pretty.Pretty != false { + t.Fatalf("expecting pretty to be false but got: %v", test.Pretty.Pretty) } } @@ -49,7 +49,7 @@ func TestPretty_Output(t *testing.T) { // test unprettified content opts := Pretty{ - pretty: false, + Pretty: false, } err = opts.Output(fp, raw) if err != nil { @@ -76,7 +76,7 @@ func TestPretty_Output(t *testing.T) { // test prettified content opts = Pretty{ - pretty: true, + Pretty: true, } err = opts.Output(fp, raw) if err != nil { diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 245883443..abe211c8f 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -16,16 +16,14 @@ limitations under the License. package manifest import ( - "encoding/json" - "errors" "fmt" - "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" "oras.land/oras-go/v2" "oras.land/oras-go/v2/registry/remote" "oras.land/oras/cmd/oras/internal/argument" + "oras.land/oras/cmd/oras/internal/display" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" ) @@ -37,6 +35,7 @@ type fetchOptions struct { option.Platform option.Pretty option.Target + option.Format mediaTypes []string outputPath string @@ -72,8 +71,13 @@ 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 { - if opts.outputPath == "-" && opts.OutputDescriptor { - return errors.New("`--output -` cannot be used with `--descriptor` at the same time") + switch { + case opts.outputPath == "-" && opts.Template != "": + return fmt.Errorf("`--output -` cannot be used with `--format %s` at the same time", opts.Template) + case opts.OutputDescriptor && opts.Template != "": + return fmt.Errorf("`--descriptor` cannot be used with `--format %s` at the same time", opts.Template) + case opts.OutputDescriptor && opts.outputPath == "-": + return fmt.Errorf("`--descriptor` cannot be used with `--output -` at the same time") } opts.RawReference = args[0] return option.Parse(cmd, &opts) @@ -86,6 +90,9 @@ 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.`) option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } @@ -110,8 +117,10 @@ 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 == "" { // fetch manifest descriptor only fetchOpts := oras.DefaultResolveOptions @@ -121,34 +130,16 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { return fmt.Errorf("failed to find %q: %w", opts.RawReference, err) } } else { - // fetch manifest content - var content []byte + // fetch manifest descriptor and content fetchOpts := oras.DefaultFetchBytesOptions fetchOpts.TargetPlatform = opts.Platform.Platform desc, content, err = oras.FetchBytes(ctx, src, opts.Reference, fetchOpts) if err != nil { return fmt.Errorf("failed to fetch the content of %q: %w", opts.RawReference, err) } - - if opts.outputPath == "" || opts.outputPath == "-" { - // output manifest content - return opts.Output(os.Stdout, content) - } - - // save manifest content into the local file if the output path is provided - if err = os.WriteFile(opts.outputPath, content, 0666); err != nil { + if err = contentHandler.OnContentFetched(desc, content); err != nil { return err } } - - // output manifest's descriptor if `--descriptor` is used - if opts.OutputDescriptor { - descBytes, err := json.Marshal(desc) - if err != nil { - return err - } - return opts.Output(os.Stdout, descBytes) - } - - return nil + return metadataHandler.OnFetched(opts.Path, desc, content) } diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index 5915b5942..d4a3ae380 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -64,6 +64,7 @@ var ( Size: 482, } LayerName = "hello.tar" + LayerDigest = "sha256:2ef548696ac7dd66ef38aab5cc8fc5cc1fb637dfaedb3a9afc89bf16db9277e1" LinuxAMD64ReferrerStateKey = match.StateKey{Digest: "c5e00045954a", Name: "application/vnd.oci.image.manifest.v1+json"} LinuxAMD64ReferrerConfigStateKey = match.StateKey{Digest: "44136fa355b3", Name: "referrer/image"} LinuxAMD64StateKeys = []match.StateKey{ diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index f7e52ad01..bf644acb1 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -97,6 +97,15 @@ var _ = Describe("ORAS beginners:", func() { It("should fail with suggestion if no tag or digest is provided", func() { ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, "")).ExpectFailure().MatchErrKeyWords("Error:", "no tag or digest specified", "oras manifest fetch [flags] {:|@}", "Please specify a reference").Exec() }) + + It("should fail if stdout is used inpropriately", func() { + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--output", "-", "--format", "test"). + ExpectFailure().Exec() + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--descriptor", "--format", "test"). + ExpectFailure().Exec() + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "--output", "-", "--descriptor"). + ExpectFailure().Exec() + }) }) When("running `manifest delete`", func() { @@ -214,6 +223,27 @@ var _ = Describe("1.1 registry users:", func() { MatchFile(fetchPath, multi_arch.Manifest, DefaultTimeout) }) + It("should fetch manifest to file and output json", func() { + fetchPath := filepath.Join(GinkgoT().TempDir(), "fetchedImage") + digest := multi_arch.LinuxAMD64.Digest.String() + ref := RegistryRef(ZOTHost, ImageRepo, digest) + // test + out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "json").Exec().Out.Contents() + // validate + var content = struct{ Content any }{} + Expect(json.Unmarshal(out, &content)).ShouldNot(HaveOccurred()) + MatchFile(fetchPath, multi_arch.LinuxAMD64Manifest, DefaultTimeout) + }) + + It("should fetch manifest and output json", func() { + ref := RegistryRef(ZOTHost, ImageRepo, multi_arch.LinuxAMD64.Digest.String()) + // test + out := ORAS("manifest", "fetch", ref, "--format", "json").Exec().Out.Contents() + // validate + var content = struct{ Content any }{} + Expect(json.Unmarshal(out, &content)).ShouldNot(HaveOccurred()) + }) + It("should fetch manifest via tag with platform selection", func() { ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64"). MatchContent(multi_arch.LinuxAMD64Manifest).Exec() @@ -229,6 +259,18 @@ var _ = Describe("1.1 registry users:", func() { MatchContent(multi_arch.LinuxAMD64Manifest).Exec() }) + 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}}"). + 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}}"). + MatchContent(multi_arch.LayerDigest). + Exec() + }) + It("should fetch descriptor via digest", func() { ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Digest), "--descriptor"). MatchContent(multi_arch.Descriptor).Exec()