From 67dad877e35ea984f8d9fe648af30662034012eb Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 18 Mar 2024 09:56:03 +0000 Subject: [PATCH 01/36] feat: support `--format` in `manifest fetch` command Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 4 ++ .../internal/display/metadata/interface.go | 5 ++ .../display/metadata/model/mappable.go | 55 ++++++++++++++++ .../metadata/template/manifest_fetch.go | 41 ++++++++++++ cmd/oras/internal/option/pretty.go | 6 +- cmd/oras/internal/option/pretty_test.go | 8 +-- cmd/oras/root/manifest/fetch.go | 63 +++++++++++++++---- .../e2e/internal/testdata/multi_arch/const.go | 1 + test/e2e/suite/command/manifest.go | 21 +++++++ 9 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 cmd/oras/internal/display/metadata/model/mappable.go create mode 100644 cmd/oras/internal/display/metadata/template/manifest_fetch.go diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 56e05ae63..836f43f3e 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -72,3 +72,7 @@ func NewAttachHandler(format string, tty *os.File, verbose bool) (status.AttachH return statusHandler, metadataHandler } + +func NewManifestFetchHandler(format string) metadata.ManifestFetchHandler { + return template.NewManifestFetchHandler(format) +} diff --git a/cmd/oras/internal/display/metadata/interface.go b/cmd/oras/internal/display/metadata/interface.go index 577f9eb2e..c8ba1b41f 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -30,3 +30,8 @@ type PushHandler interface { type AttachHandler interface { OnCompleted(opts *option.Target, root, subject ocispec.Descriptor) error } + +// ManifestFetchHandler handles metadata output for manifest fetch events. +type ManifestFetchHandler interface { + OnFetched(manifest ocispec.Manifest) error +} diff --git a/cmd/oras/internal/display/metadata/model/mappable.go b/cmd/oras/internal/display/metadata/model/mappable.go new file mode 100644 index 000000000..ca2ab686c --- /dev/null +++ b/cmd/oras/internal/display/metadata/model/mappable.go @@ -0,0 +1,55 @@ +/* +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 ( + "reflect" + "strings" +) + +// ToMappable converts an reflect value into a map[string]any with json tags as +// key. +func ToMappable(v reflect.Value) any { + switch v.Kind() { + case reflect.Struct: + t := v.Type() + numField := t.NumField() + ret := make(map[string]any) + for i := 0; i < numField; i++ { + fv := v.Field(i) + tag := t.Field(i).Tag.Get("json") + if tag == "" { + continue + } + key, _, _ := strings.Cut(tag, ",") + ret[key] = ToMappable(fv) + } + return ret + case reflect.Slice, reflect.Array: + ret := make([]any, v.Len()) + for i := 0; i < v.Len(); i++ { + ret[i] = ToMappable(v.Index(i)) + } + return ret + case reflect.Ptr, reflect.Interface: + if v.IsNil() { + return nil + } + elem := ToMappable(v.Elem()) + return &elem + } + return v.Interface() +} 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..256193974 --- /dev/null +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.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 template + +import ( + "reflect" + + 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 +} + +// NewManifestFetchHandler creates a new handler for manifest fetch events. +func NewManifestFetchHandler(template string) metadata.ManifestFetchHandler { + return &ManifestFetchHandler{ + template: template, + } +} + +// OnFetched is called after the manifest fetch is completed. +func (ph *ManifestFetchHandler) OnFetched(manifest ocispec.Manifest) error { + return parseAndWrite(model.ToMappable(reflect.ValueOf(manifest)), ph.template) +} diff --git a/cmd/oras/internal/option/pretty.go b/cmd/oras/internal/option/pretty.go index 0e7524473..de889522e 100644 --- a/cmd/oras/internal/option/pretty.go +++ b/cmd/oras/internal/option/pretty.go @@ -26,18 +26,18 @@ import ( // 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 { + if opts.Pretty { buf := bytes.NewBuffer(nil) if err := json.Indent(buf, content, "", " "); err != nil { return fmt.Errorf("failed to prettify: %w", err) 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 5726552a4..2a43a7659 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -17,7 +17,6 @@ package manifest import ( "encoding/json" - "errors" "fmt" "os" @@ -26,8 +25,10 @@ import ( "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" + "oras.land/oras/internal/docker" ) type fetchOptions struct { @@ -37,6 +38,7 @@ type fetchOptions struct { option.Platform option.Pretty option.Target + option.Format mediaTypes []string outputPath string @@ -72,9 +74,22 @@ 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") + toCheck := []struct { + name string + isPresent func() bool + }{ + {"--output -", func() bool { return opts.outputPath == "-" }}, + {"--format", func() bool { return opts.Template != "" }}, + {"--descriptor", func() bool { return opts.OutputDescriptor }}, } + for i := range toCheck { + for j := i + 1; j < len(toCheck); j++ { + if toCheck[i].isPresent() && toCheck[j].isPresent() { + return fmt.Errorf("`%s` cannot be used with `%s` at the same time", toCheck[i].name, toCheck[j].name) + } + } + } + opts.RawReference = args[0] return option.Parse(&opts) }, @@ -86,6 +101,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", "", `Format output using a custom template: +'json': Print manifest in prettified JSON format +'$TEMPLATE': Print output using the given Go template.`) option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } @@ -110,6 +128,7 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return err } + handler := display.NewManifestFetchHandler(opts.Template) var desc ocispec.Descriptor if opts.OutputDescriptor && opts.outputPath == "" { @@ -130,14 +149,37 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { 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) + if opts.Template != "" { + if opts.Template == "json" { + // output prettified json manifest content + opts.Pretty.Pretty = true + if err := opts.Output(os.Stdout, content); err != nil { + return err + } + } else { + // output formatted data + switch desc.MediaType { + case ocispec.MediaTypeImageManifest, docker.MediaTypeManifest: + var manifest ocispec.Manifest + if err := json.Unmarshal(content, &manifest); err != nil { + return err + } + if err = handler.OnFetched(manifest); err != nil { + return err + } + default: + return fmt.Errorf("cannot apply template to %q: unsupported media type %s", opts.RawReference, desc.MediaType) + } + } } - - // save manifest content into the local file if the output path is provided - if err = os.WriteFile(opts.outputPath, content, 0666); err != nil { - return err + if opts.outputPath != "" && opts.outputPath != "-" { + // save manifest content into the local file if the output path is provided + if err = os.WriteFile(opts.outputPath, content, 0666); err != nil { + return err + } + } else if opts.Template == "" { + // output raw manifest content + return opts.Output(os.Stdout, content) } } @@ -149,6 +191,5 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { } return opts.Output(os.Stdout, descBytes) } - return nil } 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..5e227c91e 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() { @@ -229,6 +238,18 @@ var _ = Describe("1.1 registry users:", func() { MatchContent(multi_arch.LinuxAMD64Manifest).Exec() }) + It("should fetch manifest with platform validation and output json", func() { + out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "json"). + 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 .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() From fa0f2a1b7549dcb7b4e8316849e6ab1b835415e8 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 18 Mar 2024 10:03:22 +0000 Subject: [PATCH 02/36] add comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 836f43f3e..3a92622f5 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -73,6 +73,7 @@ func NewAttachHandler(format string, tty *os.File, verbose bool) (status.AttachH return statusHandler, metadataHandler } +// NewManifestFetchHandler returns a manifest fetch handler. func NewManifestFetchHandler(format string) metadata.ManifestFetchHandler { return template.NewManifestFetchHandler(format) } From e9c122680d06c2cc750fbb7a39fd39624017f49a Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 1 Apr 2024 04:59:18 +0000 Subject: [PATCH 03/36] refactor Signed-off-by: Billy Zha --- cmd/oras/root/manifest/fetch.go | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 2a43a7659..4173cccfb 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -74,22 +74,14 @@ 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 { - toCheck := []struct { - name string - isPresent func() bool - }{ - {"--output -", func() bool { return opts.outputPath == "-" }}, - {"--format", func() bool { return opts.Template != "" }}, - {"--descriptor", func() bool { return opts.OutputDescriptor }}, + switch { + case opts.outputPath == "-" && opts.Template != "": + return fmt.Errorf("`--output -` cannot be used with `--format` at the same time") + case opts.OutputDescriptor && opts.Template != "": + return fmt.Errorf("`--descriptor` cannot be used with `--format` at the same time") + case opts.OutputDescriptor && opts.outputPath != "": + return fmt.Errorf("`--descriptor` cannot be used with `--output` at the same time") } - for i := range toCheck { - for j := i + 1; j < len(toCheck); j++ { - if toCheck[i].isPresent() && toCheck[j].isPresent() { - return fmt.Errorf("`%s` cannot be used with `%s` at the same time", toCheck[i].name, toCheck[j].name) - } - } - } - opts.RawReference = args[0] return option.Parse(&opts) }, From 6614c9287dff40b6fdfdda11304e998883464454 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 1 Apr 2024 05:06:18 +0000 Subject: [PATCH 04/36] use new printer Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 4 ++-- .../internal/display/metadata/template/manifest_fetch.go | 9 ++++++--- cmd/oras/root/manifest/fetch.go | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index f694be28c..49e961ac8 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -75,6 +75,6 @@ func NewAttachHandler(format string, tty *os.File, out io.Writer, verbose bool) } // NewManifestFetchHandler returns a manifest fetch handler. -func NewManifestFetchHandler(format string) metadata.ManifestFetchHandler { - return template.NewManifestFetchHandler(format) +func NewManifestFetchHandler(out io.Writer, format string) metadata.ManifestFetchHandler { + return template.NewManifestFetchHandler(out, format) } diff --git a/cmd/oras/internal/display/metadata/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go index 256193974..5f999e70c 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -16,6 +16,7 @@ limitations under the License. package template import ( + "io" "reflect" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -26,16 +27,18 @@ import ( // 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(template string) metadata.ManifestFetchHandler { +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 (ph *ManifestFetchHandler) OnFetched(manifest ocispec.Manifest) error { - return parseAndWrite(model.ToMappable(reflect.ValueOf(manifest)), ph.template) +func (h *ManifestFetchHandler) OnFetched(manifest ocispec.Manifest) error { + return parseAndWrite(h.out, model.ToMappable(reflect.ValueOf(manifest)), h.template) } diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 5cbe23406..e3a5d0ddf 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -120,7 +120,7 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return err } - handler := display.NewManifestFetchHandler(opts.Template) + handler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Template) var desc ocispec.Descriptor if opts.OutputDescriptor && opts.outputPath == "" { From eb5f62d9f19759e3a857b0bb6542da6a03dc2805 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 1 Apr 2024 05:31:26 +0000 Subject: [PATCH 05/36] bug fix Signed-off-by: Billy Zha --- cmd/oras/root/manifest/fetch.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index e3a5d0ddf..9d5e5f5f1 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -79,8 +79,8 @@ Example - Fetch raw manifest from an OCI layout archive file 'layout.tar': return fmt.Errorf("`--output -` cannot be used with `--format` at the same time") case opts.OutputDescriptor && opts.Template != "": return fmt.Errorf("`--descriptor` cannot be used with `--format` at the same time") - case opts.OutputDescriptor && opts.outputPath != "": - return fmt.Errorf("`--descriptor` cannot be used with `--output` at the same time") + 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) From ce6bc89ce5a6cc92001aad3ddc1cc043a95ffbb9 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 2 Apr 2024 08:28:43 +0000 Subject: [PATCH 06/36] fix struct fields with json tags Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 18 +++- cmd/oras/internal/display/metadata/discard.go | 28 +++++++ .../internal/display/metadata/interface.go | 3 +- .../display/metadata/json/manifest_fetch.go | 54 ++++++++++++ .../display/metadata/model/mappable.go | 28 ++++--- .../metadata/template/manifest_fetch.go | 16 +++- cmd/oras/internal/display/raw/discard.go | 33 ++++++++ cmd/oras/internal/display/raw/interface.go | 29 +++++++ .../internal/display/raw/manifest_fetch.go | 76 +++++++++++++++++ cmd/oras/root/manifest/fetch.go | 84 +++++-------------- test/e2e/suite/command/manifest.go | 2 +- 11 files changed, 290 insertions(+), 81 deletions(-) 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/raw/discard.go create mode 100644 cmd/oras/internal/display/raw/interface.go create mode 100644 cmd/oras/internal/display/raw/manifest_fetch.go diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 49e961ac8..e2dba6654 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -23,6 +23,7 @@ import ( "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" + "oras.land/oras/cmd/oras/internal/display/raw" "oras.land/oras/cmd/oras/internal/display/status" ) @@ -75,6 +76,19 @@ func NewAttachHandler(format string, tty *os.File, out io.Writer, verbose bool) } // NewManifestFetchHandler returns a manifest fetch handler. -func NewManifestFetchHandler(out io.Writer, format string) metadata.ManifestFetchHandler { - return template.NewManifestFetchHandler(out, format) +func NewManifestFetchHandler(out io.Writer, outputPath string, format string, pretty bool) (metadata.ManifestFetchHandler, raw.ManifestFetchHandler) { + var metadataHandler metadata.ManifestFetchHandler + var rawContentHandler raw.ManifestFetchHandler + switch format { + case "raw": + metadataHandler = metadata.NewDiscardHandler() + rawContentHandler = raw.NewManifestFetchHandler(out, pretty) + case "json": + metadataHandler = json.NewManifestFetchHandler(out) + rawContentHandler = raw.NewDiscardHandler() + default: + metadataHandler = template.NewManifestFetchHandler(out, format) + rawContentHandler = raw.NewDiscardHandler() + } + return metadataHandler, rawContentHandler } diff --git a/cmd/oras/internal/display/metadata/discard.go b/cmd/oras/internal/display/metadata/discard.go new file mode 100644 index 000000000..89f0f2d69 --- /dev/null +++ b/cmd/oras/internal/display/metadata/discard.go @@ -0,0 +1,28 @@ +/* +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([]byte, ocispec.Descriptor) error { return nil } diff --git a/cmd/oras/internal/display/metadata/interface.go b/cmd/oras/internal/display/metadata/interface.go index c8ba1b41f..c95c5eb12 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -33,5 +33,6 @@ type AttachHandler interface { // ManifestFetchHandler handles metadata output for manifest fetch events. type ManifestFetchHandler interface { - OnFetched(manifest ocispec.Manifest) error + // OnFetched is called after the manifest content is fetched. + OnFetched([]byte, ocispec.Descriptor) error } 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..01d4ba82a --- /dev/null +++ b/cmd/oras/internal/display/metadata/json/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 json + +import ( + "encoding/json" + "fmt" + "io" + "reflect" + + 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" + "oras.land/oras/internal/docker" +) + +// 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(content []byte, desc ocispec.Descriptor) error { + switch desc.MediaType { + case ocispec.MediaTypeImageManifest, docker.MediaTypeManifest: + var manifest ocispec.Manifest + if err := json.Unmarshal(content, &manifest); err != nil { + return err + } + return printJSON(h.out, model.ToMappable(reflect.ValueOf(manifest))) + default: + return fmt.Errorf("cannot apply template: unsupported media type %s", desc.MediaType) + } +} diff --git a/cmd/oras/internal/display/metadata/model/mappable.go b/cmd/oras/internal/display/metadata/model/mappable.go index ca2ab686c..dca57fb01 100644 --- a/cmd/oras/internal/display/metadata/model/mappable.go +++ b/cmd/oras/internal/display/metadata/model/mappable.go @@ -25,18 +25,8 @@ import ( func ToMappable(v reflect.Value) any { switch v.Kind() { case reflect.Struct: - t := v.Type() - numField := t.NumField() ret := make(map[string]any) - for i := 0; i < numField; i++ { - fv := v.Field(i) - tag := t.Field(i).Tag.Get("json") - if tag == "" { - continue - } - key, _, _ := strings.Cut(tag, ",") - ret[key] = ToMappable(fv) - } + addToMap(ret, v) return ret case reflect.Slice, reflect.Array: ret := make([]any, v.Len()) @@ -53,3 +43,19 @@ func ToMappable(v reflect.Value) any { } return v.Interface() } + +func addToMap(ret map[string]any, v reflect.Value) { + t := v.Type() + numField := t.NumField() + for i := 0; i < numField; i++ { + fv := v.Field(i) + ft := t.Field(i) + tag := ft.Tag.Get("json") + if tag == "" { + addToMap(ret, fv) + } else { + key, _, _ := strings.Cut(tag, ",") + ret[key] = ToMappable(fv) + } + } +} diff --git a/cmd/oras/internal/display/metadata/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go index 5f999e70c..c2bdf8b32 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -16,12 +16,15 @@ limitations under the License. package template import ( + "encoding/json" + "fmt" "io" "reflect" 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" + "oras.land/oras/internal/docker" ) // ManifestFetchHandler handles JSON metadata output for manifest fetch events. @@ -39,6 +42,15 @@ func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFe } // OnFetched is called after the manifest fetch is completed. -func (h *ManifestFetchHandler) OnFetched(manifest ocispec.Manifest) error { - return parseAndWrite(h.out, model.ToMappable(reflect.ValueOf(manifest)), h.template) +func (h *ManifestFetchHandler) OnFetched(content []byte, desc ocispec.Descriptor) error { + switch desc.MediaType { + case ocispec.MediaTypeImageManifest, docker.MediaTypeManifest: + var manifest ocispec.Manifest + if err := json.Unmarshal(content, &manifest); err != nil { + return err + } + return parseAndWrite(h.out, model.ToMappable(reflect.ValueOf(manifest)), h.template) + default: + return fmt.Errorf("cannot apply template: unsupported media type %s", desc.MediaType) + } } diff --git a/cmd/oras/internal/display/raw/discard.go b/cmd/oras/internal/display/raw/discard.go new file mode 100644 index 000000000..17eeab434 --- /dev/null +++ b/cmd/oras/internal/display/raw/discard.go @@ -0,0 +1,33 @@ +/* +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 raw + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type discard struct{} + +// OnContentFetched implements ManifestFetchHandler. +func (discard) OnContentFetched(string, []byte) error { return nil } + +// OnDescriptorFetched implements ManifestFetchHandler. +func (discard) OnDescriptorFetched(desc ocispec.Descriptor) error { return nil } + +// NewManifestFetchHandler creates a new handler. +func NewDiscardHandler() ManifestFetchHandler { + return discard{} +} diff --git a/cmd/oras/internal/display/raw/interface.go b/cmd/oras/internal/display/raw/interface.go new file mode 100644 index 000000000..a2476fcef --- /dev/null +++ b/cmd/oras/internal/display/raw/interface.go @@ -0,0 +1,29 @@ +/* +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 raw + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// ManifestFetchHandler handles raw output for manifest fetch events. +type ManifestFetchHandler interface { + // OnFetched is called after the manifest content is fetched. + OnContentFetched(outputPath string, content []byte) error + // OnDescriptorFetched is called after the manifest descriptor is + // fetched. + OnDescriptorFetched(desc ocispec.Descriptor) error +} diff --git a/cmd/oras/internal/display/raw/manifest_fetch.go b/cmd/oras/internal/display/raw/manifest_fetch.go new file mode 100644 index 000000000..c24ac2fe0 --- /dev/null +++ b/cmd/oras/internal/display/raw/manifest_fetch.go @@ -0,0 +1,76 @@ +/* +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 raw + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// RawManifestFetch handles raw content output. +type RawManifestFetch struct { + pretty bool + stdout io.Writer +} + +// OnContentFetched implements ManifestFetchHandler. +func (h *RawManifestFetch) OnContentFetched(outputPath string, manifest []byte) error { + out := h.stdout + if outputPath != "-" && outputPath != "" { + f, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return fmt.Errorf("failed to open %q: %w", outputPath, err) + } + defer f.Close() + } + return h.output(out, manifest) +} + +// OnDescriptorFetched implements ManifestFetchHandler. +func (h *RawManifestFetch) OnDescriptorFetched(desc ocispec.Descriptor) error { + descBytes, err := json.Marshal(desc) + if err != nil { + return fmt.Errorf("invalid descriptor: %w", err) + } + return h.output(h.stdout, descBytes) +} + +// NewManifestFetchHandler creates a new handler. +func NewManifestFetchHandler(out io.Writer, pretty bool) ManifestFetchHandler { + return &RawManifestFetch{ + pretty: pretty, + stdout: out, + } +} + +// OnFetched is called after the content is fetched. +func (h *RawManifestFetch) output(out io.Writer, data []byte) error { + if h.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/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 9d5e5f5f1..7f98623d7 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -16,11 +16,8 @@ limitations under the License. package manifest import ( - "encoding/json" "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" @@ -28,7 +25,6 @@ import ( "oras.land/oras/cmd/oras/internal/display" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" - "oras.land/oras/internal/docker" ) type fetchOptions struct { @@ -75,10 +71,10 @@ 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 != "": - return fmt.Errorf("`--output -` cannot be used with `--format` at the same time") - case opts.OutputDescriptor && opts.Template != "": - return fmt.Errorf("`--descriptor` cannot be used with `--format` at the same time") + case opts.outputPath == "-" && opts.Template != "raw": + return fmt.Errorf("`--output -` cannot be used with `--format %s` at the same time", opts.Template) + case opts.OutputDescriptor && opts.Template != "raw": + 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") } @@ -93,7 +89,8 @@ 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", "", `Format output using a custom template: + cmd.Flags().StringVar(&opts.Template, "format", "raw", `Format output using a custom template: +'raw': Print raw manifest content 'json': Print manifest in prettified JSON format '$TEMPLATE': Print output using the given Go template.`) option.ApplyFlags(&opts, cmd.Flags()) @@ -120,68 +117,27 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return err } - handler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Template) + metadataHandler, contentHandler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.outputPath, opts.Template, opts.Pretty.Pretty) - var desc ocispec.Descriptor - if opts.OutputDescriptor && opts.outputPath == "" { + if opts.OutputDescriptor { // fetch manifest descriptor only fetchOpts := oras.DefaultResolveOptions fetchOpts.TargetPlatform = opts.Platform.Platform - desc, err = oras.Resolve(ctx, src, opts.Reference, fetchOpts) + desc, err := oras.Resolve(ctx, src, opts.Reference, fetchOpts) if err != nil { return fmt.Errorf("failed to find %q: %w", opts.RawReference, err) } - } else { - // fetch manifest content - var content []byte - 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.Template != "" { - if opts.Template == "json" { - // output prettified json manifest content - opts.Pretty.Pretty = true - if err := opts.Output(os.Stdout, content); err != nil { - return err - } - } else { - // output formatted data - switch desc.MediaType { - case ocispec.MediaTypeImageManifest, docker.MediaTypeManifest: - var manifest ocispec.Manifest - if err := json.Unmarshal(content, &manifest); err != nil { - return err - } - if err = handler.OnFetched(manifest); err != nil { - return err - } - default: - return fmt.Errorf("cannot apply template to %q: unsupported media type %s", opts.RawReference, desc.MediaType) - } - } - } - if opts.outputPath != "" && opts.outputPath != "-" { - // save manifest content into the local file if the output path is provided - if err = os.WriteFile(opts.outputPath, content, 0666); err != nil { - return err - } - } else if opts.Template == "" { - // output raw manifest content - return opts.Output(os.Stdout, content) - } + return contentHandler.OnDescriptorFetched(desc) } - - // 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) + // fetch manifest 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 err = metadataHandler.OnFetched(content, desc); err != nil { + return nil } - return nil + return contentHandler.OnContentFetched(opts.outputPath, content) } diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 5e227c91e..8b0108e70 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -198,7 +198,7 @@ var _ = Describe("ORAS beginners:", func() { }) }) -var _ = Describe("1.1 registry users:", func() { +var _ = Describe("1.1 registry users:", Focus, func() { repoFmt := fmt.Sprintf("command/manifest/%%s/%d/%%s", GinkgoRandomSeed()) When("running `manifest fetch`", func() { It("should fetch manifest list with digest", func() { From c13ee29ea1ea3cd83f74cf196ca626532e40cf3d Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 3 Apr 2024 08:46:27 +0000 Subject: [PATCH 07/36] remove raw discarder Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 8 ++--- cmd/oras/internal/display/raw/discard.go | 33 ------------------- .../internal/display/raw/manifest_fetch.go | 1 + cmd/oras/root/manifest/fetch.go | 4 +-- test/e2e/suite/command/manifest.go | 4 ++- 5 files changed, 8 insertions(+), 42 deletions(-) delete mode 100644 cmd/oras/internal/display/raw/discard.go diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index e2dba6654..d3c0e4cd3 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -76,19 +76,15 @@ func NewAttachHandler(format string, tty *os.File, out io.Writer, verbose bool) } // NewManifestFetchHandler returns a manifest fetch handler. -func NewManifestFetchHandler(out io.Writer, outputPath string, format string, pretty bool) (metadata.ManifestFetchHandler, raw.ManifestFetchHandler) { +func NewManifestFetchHandler(out io.Writer, format string, pretty bool) (metadata.ManifestFetchHandler, raw.ManifestFetchHandler) { var metadataHandler metadata.ManifestFetchHandler - var rawContentHandler raw.ManifestFetchHandler switch format { case "raw": metadataHandler = metadata.NewDiscardHandler() - rawContentHandler = raw.NewManifestFetchHandler(out, pretty) case "json": metadataHandler = json.NewManifestFetchHandler(out) - rawContentHandler = raw.NewDiscardHandler() default: metadataHandler = template.NewManifestFetchHandler(out, format) - rawContentHandler = raw.NewDiscardHandler() } - return metadataHandler, rawContentHandler + return metadataHandler, raw.NewManifestFetchHandler(out, pretty) } diff --git a/cmd/oras/internal/display/raw/discard.go b/cmd/oras/internal/display/raw/discard.go deleted file mode 100644 index 17eeab434..000000000 --- a/cmd/oras/internal/display/raw/discard.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -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 raw - -import ( - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -type discard struct{} - -// OnContentFetched implements ManifestFetchHandler. -func (discard) OnContentFetched(string, []byte) error { return nil } - -// OnDescriptorFetched implements ManifestFetchHandler. -func (discard) OnDescriptorFetched(desc ocispec.Descriptor) error { return nil } - -// NewManifestFetchHandler creates a new handler. -func NewDiscardHandler() ManifestFetchHandler { - return discard{} -} diff --git a/cmd/oras/internal/display/raw/manifest_fetch.go b/cmd/oras/internal/display/raw/manifest_fetch.go index c24ac2fe0..6c9fd6cc0 100644 --- a/cmd/oras/internal/display/raw/manifest_fetch.go +++ b/cmd/oras/internal/display/raw/manifest_fetch.go @@ -40,6 +40,7 @@ func (h *RawManifestFetch) OnContentFetched(outputPath string, manifest []byte) return fmt.Errorf("failed to open %q: %w", outputPath, err) } defer f.Close() + out = f } return h.output(out, manifest) } diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 7f98623d7..7ba05d836 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -117,7 +117,7 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return err } - metadataHandler, contentHandler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.outputPath, opts.Template, opts.Pretty.Pretty) + metadataHandler, contentHandler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Template, opts.Pretty.Pretty) if opts.OutputDescriptor { // fetch manifest descriptor only @@ -137,7 +137,7 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { return fmt.Errorf("failed to fetch the content of %q: %w", opts.RawReference, err) } if err = metadataHandler.OnFetched(content, desc); err != nil { - return nil + return err } return contentHandler.OnContentFetched(opts.outputPath, content) } diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 8b0108e70..7d5808b5f 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -241,7 +241,9 @@ var _ = Describe("1.1 registry users:", Focus, func() { It("should fetch manifest with platform validation and output json", func() { out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "json"). Exec().Out.Contents() - Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) + var manifest ocispec.Manifest + Expect(json.Unmarshal(out, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.MediaType).To(Equal(multi_arch.LinuxAMD64.MediaType)) }) It("should fetch manifest and format output", func() { From 4b1e2f9c969e434e81b016c4fedc53895b929667 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 3 Apr 2024 08:51:55 +0000 Subject: [PATCH 08/36] fix e2e Signed-off-by: Billy Zha --- cmd/oras/root/manifest/fetch.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 7ba05d836..c5691914f 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -119,7 +119,7 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { } metadataHandler, contentHandler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Template, opts.Pretty.Pretty) - if opts.OutputDescriptor { + if opts.OutputDescriptor && opts.outputPath == "" { // fetch manifest descriptor only fetchOpts := oras.DefaultResolveOptions fetchOpts.TargetPlatform = opts.Platform.Platform @@ -136,8 +136,11 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return fmt.Errorf("failed to fetch the content of %q: %w", opts.RawReference, err) } - if err = metadataHandler.OnFetched(content, desc); err != nil { + if err = contentHandler.OnDescriptorFetched(desc); err != nil { return err } - return contentHandler.OnContentFetched(opts.outputPath, content) + if err = contentHandler.OnContentFetched(opts.outputPath, content); err != nil { + return err + } + return metadataHandler.OnFetched(content, desc) } From c7488d6e5696f7177647454535c9437b2f4f6224 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 10:40:53 +0000 Subject: [PATCH 09/36] fix e2e Signed-off-by: Billy Zha --- cmd/oras/root/manifest/fetch.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index c5691914f..0af5ec1f1 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -136,8 +136,10 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return fmt.Errorf("failed to fetch the content of %q: %w", opts.RawReference, err) } - if err = contentHandler.OnDescriptorFetched(desc); err != nil { - return err + if opts.OutputDescriptor { + if err = contentHandler.OnDescriptorFetched(desc); err != nil { + return err + } } if err = contentHandler.OnContentFetched(opts.outputPath, content); err != nil { return err From 42ab0a363102014cfe491f5b94943cb6d6ab52a4 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 10:48:43 +0000 Subject: [PATCH 10/36] refactor Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 4 +-- cmd/oras/internal/display/raw/interface.go | 4 +-- .../internal/display/raw/manifest_fetch.go | 27 +++++++++++++------ cmd/oras/root/manifest/fetch.go | 9 ++----- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index d3c0e4cd3..021b6ffb3 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -76,7 +76,7 @@ func NewAttachHandler(format string, tty *os.File, out io.Writer, verbose bool) } // NewManifestFetchHandler returns a manifest fetch handler. -func NewManifestFetchHandler(out io.Writer, format string, pretty bool) (metadata.ManifestFetchHandler, raw.ManifestFetchHandler) { +func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool, pretty bool, outputPath string) (metadata.ManifestFetchHandler, raw.ManifestFetchHandler) { var metadataHandler metadata.ManifestFetchHandler switch format { case "raw": @@ -86,5 +86,5 @@ func NewManifestFetchHandler(out io.Writer, format string, pretty bool) (metadat default: metadataHandler = template.NewManifestFetchHandler(out, format) } - return metadataHandler, raw.NewManifestFetchHandler(out, pretty) + return metadataHandler, raw.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) } diff --git a/cmd/oras/internal/display/raw/interface.go b/cmd/oras/internal/display/raw/interface.go index a2476fcef..44653d212 100644 --- a/cmd/oras/internal/display/raw/interface.go +++ b/cmd/oras/internal/display/raw/interface.go @@ -21,9 +21,9 @@ import ( // ManifestFetchHandler handles raw output for manifest fetch events. type ManifestFetchHandler interface { - // OnFetched is called after the manifest content is fetched. - OnContentFetched(outputPath string, content []byte) error // OnDescriptorFetched is called after the manifest descriptor is // fetched. OnDescriptorFetched(desc ocispec.Descriptor) error + // OnContentFetched is called after the manifest content is fetched. + OnContentFetched(desc ocispec.Descriptor, content []byte) error } diff --git a/cmd/oras/internal/display/raw/manifest_fetch.go b/cmd/oras/internal/display/raw/manifest_fetch.go index 6c9fd6cc0..e8a96d990 100644 --- a/cmd/oras/internal/display/raw/manifest_fetch.go +++ b/cmd/oras/internal/display/raw/manifest_fetch.go @@ -27,12 +27,22 @@ import ( // RawManifestFetch handles raw content output. type RawManifestFetch struct { - pretty bool - stdout io.Writer + pretty bool + stdout io.Writer + outputDescriptor bool + outputPath string } -// OnContentFetched implements ManifestFetchHandler. -func (h *RawManifestFetch) OnContentFetched(outputPath string, manifest []byte) error { +func (h *RawManifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byte) error { + if h.outputDescriptor { + if err := h.OnDescriptorFetched(desc); err != nil { + return err + } + } + return h.onContentFetched(h.outputPath, manifest) +} + +func (h *RawManifestFetch) onContentFetched(outputPath string, manifest []byte) error { out := h.stdout if outputPath != "-" && outputPath != "" { f, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) @@ -45,7 +55,6 @@ func (h *RawManifestFetch) OnContentFetched(outputPath string, manifest []byte) return h.output(out, manifest) } -// OnDescriptorFetched implements ManifestFetchHandler. func (h *RawManifestFetch) OnDescriptorFetched(desc ocispec.Descriptor) error { descBytes, err := json.Marshal(desc) if err != nil { @@ -55,10 +64,12 @@ func (h *RawManifestFetch) OnDescriptorFetched(desc ocispec.Descriptor) error { } // NewManifestFetchHandler creates a new handler. -func NewManifestFetchHandler(out io.Writer, pretty bool) ManifestFetchHandler { +func NewManifestFetchHandler(out io.Writer, outputDescriptor bool, pretty bool, outputPath string) ManifestFetchHandler { return &RawManifestFetch{ - pretty: pretty, - stdout: out, + pretty: pretty, + stdout: out, + outputDescriptor: outputDescriptor, + outputPath: outputPath, } } diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 0af5ec1f1..0c758c7ab 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -117,7 +117,7 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return err } - metadataHandler, contentHandler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Template, opts.Pretty.Pretty) + metadataHandler, contentHandler := display.NewManifestFetchHandler(cmd.OutOrStdout(), opts.Template, opts.OutputDescriptor, opts.Pretty.Pretty, opts.outputPath) if opts.OutputDescriptor && opts.outputPath == "" { // fetch manifest descriptor only @@ -136,12 +136,7 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { if err != nil { return fmt.Errorf("failed to fetch the content of %q: %w", opts.RawReference, err) } - if opts.OutputDescriptor { - if err = contentHandler.OnDescriptorFetched(desc); err != nil { - return err - } - } - if err = contentHandler.OnContentFetched(opts.outputPath, content); err != nil { + if err = contentHandler.OnContentFetched(desc, content); err != nil { return err } return metadataHandler.OnFetched(content, desc) From d954405de69451f3376d800771df08937cd6c685 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 10:56:56 +0000 Subject: [PATCH 11/36] add discard handler for raw output Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 9 +++--- cmd/oras/internal/display/raw/discard.go | 35 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 cmd/oras/internal/display/raw/discard.go diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 021b6ffb3..8adfec53e 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -77,14 +77,13 @@ func NewAttachHandler(format string, tty *os.File, out io.Writer, verbose bool) // NewManifestFetchHandler returns a manifest fetch handler. func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool, pretty bool, outputPath string) (metadata.ManifestFetchHandler, raw.ManifestFetchHandler) { - var metadataHandler metadata.ManifestFetchHandler + discardHandler := raw.NewDiscardHandler() switch format { case "raw": - metadataHandler = metadata.NewDiscardHandler() + return metadata.NewDiscardHandler(), raw.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) case "json": - metadataHandler = json.NewManifestFetchHandler(out) + return json.NewManifestFetchHandler(out), discardHandler default: - metadataHandler = template.NewManifestFetchHandler(out, format) + return template.NewManifestFetchHandler(out, format), discardHandler } - return metadataHandler, raw.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) } diff --git a/cmd/oras/internal/display/raw/discard.go b/cmd/oras/internal/display/raw/discard.go new file mode 100644 index 000000000..ff40a0e93 --- /dev/null +++ b/cmd/oras/internal/display/raw/discard.go @@ -0,0 +1,35 @@ +/* +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 raw + +import v1 "github.com/opencontainers/image-spec/specs-go/v1" + +type discardHandler struct{} + +// OnContentFetched implements ManifestFetchHandler. +func (discardHandler) OnContentFetched(v1.Descriptor, []byte) error { + return nil +} + +// OnDescriptorFetched implements ManifestFetchHandler. +func (discardHandler) OnDescriptorFetched(v1.Descriptor) error { + return nil +} + +// NewDiscardHandler returns a new discard handler. +func NewDiscardHandler() ManifestFetchHandler { + return discardHandler{} +} From 74600606b16cb9fa0156cfb19aaf9fb7d69a74e9 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 11:01:32 +0000 Subject: [PATCH 12/36] revert focus Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 7d5808b5f..8c4fad3c0 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -198,7 +198,7 @@ var _ = Describe("ORAS beginners:", func() { }) }) -var _ = Describe("1.1 registry users:", Focus, func() { +var _ = Describe("1.1 registry users:", func() { repoFmt := fmt.Sprintf("command/manifest/%%s/%d/%%s", GinkgoRandomSeed()) When("running `manifest fetch`", func() { It("should fetch manifest list with digest", func() { From de6acf1c686a1f3ecec5fec1baa20e17bd64140c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 11:14:57 +0000 Subject: [PATCH 13/36] rename to content handler Signed-off-by: Billy Zha --- .../display/{raw => content}/discard.go | 2 +- .../display/{raw => content}/interface.go | 2 +- .../display/{raw => content}/manifest_fetch.go | 2 +- cmd/oras/internal/display/handler.go | 17 +++++++++++------ 4 files changed, 14 insertions(+), 9 deletions(-) rename cmd/oras/internal/display/{raw => content}/discard.go (98%) rename cmd/oras/internal/display/{raw => content}/interface.go (98%) rename cmd/oras/internal/display/{raw => content}/manifest_fetch.go (99%) diff --git a/cmd/oras/internal/display/raw/discard.go b/cmd/oras/internal/display/content/discard.go similarity index 98% rename from cmd/oras/internal/display/raw/discard.go rename to cmd/oras/internal/display/content/discard.go index ff40a0e93..f62e879a1 100644 --- a/cmd/oras/internal/display/raw/discard.go +++ b/cmd/oras/internal/display/content/discard.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package raw +package content import v1 "github.com/opencontainers/image-spec/specs-go/v1" diff --git a/cmd/oras/internal/display/raw/interface.go b/cmd/oras/internal/display/content/interface.go similarity index 98% rename from cmd/oras/internal/display/raw/interface.go rename to cmd/oras/internal/display/content/interface.go index 44653d212..815d1c346 100644 --- a/cmd/oras/internal/display/raw/interface.go +++ b/cmd/oras/internal/display/content/interface.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package raw +package content import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" diff --git a/cmd/oras/internal/display/raw/manifest_fetch.go b/cmd/oras/internal/display/content/manifest_fetch.go similarity index 99% rename from cmd/oras/internal/display/raw/manifest_fetch.go rename to cmd/oras/internal/display/content/manifest_fetch.go index e8a96d990..ad35a2792 100644 --- a/cmd/oras/internal/display/raw/manifest_fetch.go +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package raw +package content import ( "bytes" diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 7df177361..5a8f4962c 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -19,11 +19,11 @@ 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/json" "oras.land/oras/cmd/oras/internal/display/metadata/template" "oras.land/oras/cmd/oras/internal/display/metadata/text" - "oras.land/oras/cmd/oras/internal/display/raw" "oras.land/oras/cmd/oras/internal/display/status" ) @@ -97,14 +97,19 @@ func NewPullHandler(format string, path string, tty *os.File, out io.Writer, ver } // NewManifestFetchHandler returns a manifest fetch handler. -func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool, pretty bool, outputPath string) (metadata.ManifestFetchHandler, raw.ManifestFetchHandler) { - discardHandler := raw.NewDiscardHandler() +func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) { + var metadataHandler metadata.ManifestFetchHandler + var contentHandler content.ManifestFetchHandler = content.NewDiscardHandler() + if outputPath != "" { + contentHandler = content.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) + } switch format { case "raw": - return metadata.NewDiscardHandler(), raw.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) + metadataHandler = metadata.NewDiscardHandler() case "json": - return json.NewManifestFetchHandler(out), discardHandler + metadataHandler = json.NewManifestFetchHandler(out) default: - return template.NewManifestFetchHandler(out, format), discardHandler + metadataHandler = template.NewManifestFetchHandler(out, format) } + return metadataHandler, contentHandler } From 4709b62ab7768d654d46337dc5365c10b4daa492 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 11:20:39 +0000 Subject: [PATCH 14/36] fix e2e Signed-off-by: Billy Zha --- cmd/oras/internal/display/handler.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 5a8f4962c..207392bf2 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -99,17 +99,22 @@ func NewPullHandler(format string, path string, tty *os.File, out io.Writer, ver // NewManifestFetchHandler returns a manifest fetch handler. func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) { var metadataHandler metadata.ManifestFetchHandler - var contentHandler content.ManifestFetchHandler = content.NewDiscardHandler() - if outputPath != "" { - contentHandler = content.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) - } + var contentHandler content.ManifestFetchHandler = content.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) + var discardHandler = content.NewDiscardHandler() + switch format { case "raw": metadataHandler = metadata.NewDiscardHandler() case "json": metadataHandler = json.NewManifestFetchHandler(out) + if outputPath == "" { + contentHandler = discardHandler + } default: metadataHandler = template.NewManifestFetchHandler(out, format) + if outputPath == "" { + contentHandler = discardHandler + } } return metadataHandler, contentHandler } From 3fcab699c625bca9a3f9550422460ead1f74bc59 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 11:27:36 +0000 Subject: [PATCH 15/36] add e2e Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 8c4fad3c0..9b5ab26f3 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -223,6 +223,15 @@ var _ = Describe("1.1 registry users:", func() { MatchFile(fetchPath, multi_arch.Manifest, DefaultTimeout) }) + It("should fetch manifest to file and output full ref", func() { + fetchPath := filepath.Join(GinkgoT().TempDir(), "fetchedImage") + digest := multi_arch.LinuxAMD64.Digest.String() + ref := RegistryRef(ZOTHost, ImageRepo, digest) + ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{.digest}}"). + MatchContent(digest).Exec() + MatchFile(fetchPath, multi_arch.LinuxAMD64Manifest, DefaultTimeout) + }) + 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() From f8c2dbfcbb39816eb4011ebe6fda27eb3cb70bfd Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 7 Apr 2024 11:28:40 +0000 Subject: [PATCH 16/36] fix e2e Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 9b5ab26f3..6e5be1a72 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -227,8 +227,8 @@ var _ = Describe("1.1 registry users:", func() { fetchPath := filepath.Join(GinkgoT().TempDir(), "fetchedImage") digest := multi_arch.LinuxAMD64.Digest.String() ref := RegistryRef(ZOTHost, ImageRepo, digest) - ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{.digest}}"). - MatchContent(digest).Exec() + ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{.mediaType}}"). + MatchContent("application/vnd.oci.image.manifest.v1+json").Exec() MatchFile(fetchPath, multi_arch.LinuxAMD64Manifest, DefaultTimeout) }) From 90a31417bc9eece6adc375e38cad3979291aa6f8 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 02:42:21 +0000 Subject: [PATCH 17/36] use unmarshal and enhance e2e Signed-off-by: Billy Zha --- .../display/metadata/json/manifest_fetch.go | 17 ++---- .../display/metadata/model/mappable.go | 61 ------------------- .../metadata/template/manifest_fetch.go | 18 ++---- test/e2e/suite/command/manifest.go | 10 ++- 4 files changed, 13 insertions(+), 93 deletions(-) delete mode 100644 cmd/oras/internal/display/metadata/model/mappable.go diff --git a/cmd/oras/internal/display/metadata/json/manifest_fetch.go b/cmd/oras/internal/display/metadata/json/manifest_fetch.go index 01d4ba82a..4d717e666 100644 --- a/cmd/oras/internal/display/metadata/json/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/json/manifest_fetch.go @@ -17,14 +17,10 @@ package json import ( "encoding/json" - "fmt" "io" - "reflect" 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" - "oras.land/oras/internal/docker" ) // ManifestFetchHandler handles JSON metadata output for manifest fetch events. @@ -41,14 +37,9 @@ func NewManifestFetchHandler(out io.Writer) metadata.ManifestFetchHandler { // OnFetched is called after the manifest fetch is completed. func (h *ManifestFetchHandler) OnFetched(content []byte, desc ocispec.Descriptor) error { - switch desc.MediaType { - case ocispec.MediaTypeImageManifest, docker.MediaTypeManifest: - var manifest ocispec.Manifest - if err := json.Unmarshal(content, &manifest); err != nil { - return err - } - return printJSON(h.out, model.ToMappable(reflect.ValueOf(manifest))) - default: - return fmt.Errorf("cannot apply template: unsupported media type %s", desc.MediaType) + var manifest map[string]any + if err := json.Unmarshal(content, &manifest); err != nil { + return err } + return printJSON(h.out, manifest) } diff --git a/cmd/oras/internal/display/metadata/model/mappable.go b/cmd/oras/internal/display/metadata/model/mappable.go deleted file mode 100644 index dca57fb01..000000000 --- a/cmd/oras/internal/display/metadata/model/mappable.go +++ /dev/null @@ -1,61 +0,0 @@ -/* -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 ( - "reflect" - "strings" -) - -// ToMappable converts an reflect value into a map[string]any with json tags as -// key. -func ToMappable(v reflect.Value) any { - switch v.Kind() { - case reflect.Struct: - ret := make(map[string]any) - addToMap(ret, v) - return ret - case reflect.Slice, reflect.Array: - ret := make([]any, v.Len()) - for i := 0; i < v.Len(); i++ { - ret[i] = ToMappable(v.Index(i)) - } - return ret - case reflect.Ptr, reflect.Interface: - if v.IsNil() { - return nil - } - elem := ToMappable(v.Elem()) - return &elem - } - return v.Interface() -} - -func addToMap(ret map[string]any, v reflect.Value) { - t := v.Type() - numField := t.NumField() - for i := 0; i < numField; i++ { - fv := v.Field(i) - ft := t.Field(i) - tag := ft.Tag.Get("json") - if tag == "" { - addToMap(ret, fv) - } else { - key, _, _ := strings.Cut(tag, ",") - ret[key] = ToMappable(fv) - } - } -} diff --git a/cmd/oras/internal/display/metadata/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go index c2bdf8b32..6934d7825 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -17,14 +17,10 @@ package template import ( "encoding/json" - "fmt" "io" - "reflect" 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" - "oras.land/oras/internal/docker" ) // ManifestFetchHandler handles JSON metadata output for manifest fetch events. @@ -43,14 +39,10 @@ func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFe // OnFetched is called after the manifest fetch is completed. func (h *ManifestFetchHandler) OnFetched(content []byte, desc ocispec.Descriptor) error { - switch desc.MediaType { - case ocispec.MediaTypeImageManifest, docker.MediaTypeManifest: - var manifest ocispec.Manifest - if err := json.Unmarshal(content, &manifest); err != nil { - return err - } - return parseAndWrite(h.out, model.ToMappable(reflect.ValueOf(manifest)), h.template) - default: - return fmt.Errorf("cannot apply template: unsupported media type %s", desc.MediaType) + var manifest map[string]any + if err := json.Unmarshal(content, &manifest); err != nil { + return err } + // return parseAndWrite(h.out, model.ToMappable(reflect.ValueOf(manifest)), h.template) + return parseAndWrite(h.out, manifest, h.template) } diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 6e5be1a72..2b74b2ad9 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -223,12 +223,12 @@ var _ = Describe("1.1 registry users:", func() { MatchFile(fetchPath, multi_arch.Manifest, DefaultTimeout) }) - It("should fetch manifest to file and output full ref", func() { + 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) - ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{.mediaType}}"). - MatchContent("application/vnd.oci.image.manifest.v1+json").Exec() + out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "json").Exec().Out.Contents() + Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) MatchFile(fetchPath, multi_arch.LinuxAMD64Manifest, DefaultTimeout) }) @@ -250,9 +250,7 @@ var _ = Describe("1.1 registry users:", func() { It("should fetch manifest with platform validation and output json", func() { out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "json"). Exec().Out.Contents() - var manifest ocispec.Manifest - Expect(json.Unmarshal(out, &manifest)).ShouldNot(HaveOccurred()) - Expect(manifest.MediaType).To(Equal(multi_arch.LinuxAMD64.MediaType)) + Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) }) It("should fetch manifest and format output", func() { From ca0f082173a7d1a8aa8a656a75ef5fceadddd642 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 06:29:27 +0000 Subject: [PATCH 18/36] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/content/manifest_fetch.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/oras/internal/display/content/manifest_fetch.go b/cmd/oras/internal/display/content/manifest_fetch.go index ad35a2792..d99c46e92 100644 --- a/cmd/oras/internal/display/content/manifest_fetch.go +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -73,7 +73,6 @@ func NewManifestFetchHandler(out io.Writer, outputDescriptor bool, pretty bool, } } -// OnFetched is called after the content is fetched. func (h *RawManifestFetch) output(out io.Writer, data []byte) error { if h.pretty { buf := bytes.NewBuffer(nil) From a1addb43f845af4ec7a79bdc0da339bec8c97421 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 08:37:19 +0000 Subject: [PATCH 19/36] resolve comments Signed-off-by: Billy Zha --- cmd/oras/internal/display/content/discard.go | 6 +- .../internal/display/content/interface.go | 3 - .../display/content/manifest_fetch.go | 57 ++++--------------- cmd/oras/internal/display/handler.go | 19 +++++-- .../display/metadata/descriptor/interface.go | 24 ++++++++ .../metadata/descriptor/manifest_fetch.go | 49 ++++++++++++++++ cmd/oras/internal/display/metadata/discard.go | 2 +- .../internal/display/metadata/interface.go | 2 +- .../display/metadata/json/manifest_fetch.go | 2 +- .../metadata/template/manifest_fetch.go | 2 +- cmd/oras/internal/display/utils/utils.go | 23 +++++++- cmd/oras/root/manifest/fetch.go | 32 ++++++----- 12 files changed, 144 insertions(+), 77 deletions(-) create mode 100644 cmd/oras/internal/display/metadata/descriptor/interface.go create mode 100644 cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go diff --git a/cmd/oras/internal/display/content/discard.go b/cmd/oras/internal/display/content/discard.go index f62e879a1..3c08fb695 100644 --- a/cmd/oras/internal/display/content/discard.go +++ b/cmd/oras/internal/display/content/discard.go @@ -15,17 +15,17 @@ limitations under the License. package content -import v1 "github.com/opencontainers/image-spec/specs-go/v1" +import ocispec "github.com/opencontainers/image-spec/specs-go/v1" type discardHandler struct{} // OnContentFetched implements ManifestFetchHandler. -func (discardHandler) OnContentFetched(v1.Descriptor, []byte) error { +func (discardHandler) OnContentFetched(ocispec.Descriptor, []byte) error { return nil } // OnDescriptorFetched implements ManifestFetchHandler. -func (discardHandler) OnDescriptorFetched(v1.Descriptor) error { +func (discardHandler) OnDescriptorFetched(ocispec.Descriptor) error { return nil } diff --git a/cmd/oras/internal/display/content/interface.go b/cmd/oras/internal/display/content/interface.go index 815d1c346..2c35fc552 100644 --- a/cmd/oras/internal/display/content/interface.go +++ b/cmd/oras/internal/display/content/interface.go @@ -21,9 +21,6 @@ import ( // ManifestFetchHandler handles raw output for manifest fetch events. type ManifestFetchHandler interface { - // OnDescriptorFetched is called after the manifest descriptor is - // fetched. - OnDescriptorFetched(desc ocispec.Descriptor) error // 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 index d99c46e92..6da0172b1 100644 --- a/cmd/oras/internal/display/content/manifest_fetch.go +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -16,72 +16,39 @@ limitations under the License. package content import ( - "bytes" - "encoding/json" "fmt" "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/utils" ) // RawManifestFetch handles raw content output. type RawManifestFetch struct { - pretty bool - stdout io.Writer - outputDescriptor bool - outputPath string + pretty bool + stdout io.Writer + outputPath string } func (h *RawManifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byte) error { - if h.outputDescriptor { - if err := h.OnDescriptorFetched(desc); err != nil { - return err - } - } - return h.onContentFetched(h.outputPath, manifest) -} - -func (h *RawManifestFetch) onContentFetched(outputPath string, manifest []byte) error { out := h.stdout - if outputPath != "-" && outputPath != "" { - f, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if h.outputPath != "-" && h.outputPath != "" { + f, err := os.OpenFile(h.outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { - return fmt.Errorf("failed to open %q: %w", outputPath, err) + return fmt.Errorf("failed to open %q: %w", h.outputPath, err) } defer f.Close() out = f } - return h.output(out, manifest) -} - -func (h *RawManifestFetch) OnDescriptorFetched(desc ocispec.Descriptor) error { - descBytes, err := json.Marshal(desc) - if err != nil { - return fmt.Errorf("invalid descriptor: %w", err) - } - return h.output(h.stdout, descBytes) + return utils.Output(out, manifest, h.pretty) } // NewManifestFetchHandler creates a new handler. -func NewManifestFetchHandler(out io.Writer, outputDescriptor bool, pretty bool, outputPath string) ManifestFetchHandler { +func NewManifestFetchHandler(out io.Writer, pretty bool, outputPath string) ManifestFetchHandler { return &RawManifestFetch{ - pretty: pretty, - stdout: out, - outputDescriptor: outputDescriptor, - outputPath: outputPath, - } -} - -func (h *RawManifestFetch) output(out io.Writer, data []byte) error { - if h.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() + pretty: pretty, + stdout: out, + outputPath: outputPath, } - _, err := out.Write(data) - return err } diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 207392bf2..29cc2d921 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -21,6 +21,7 @@ import ( "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" @@ -99,21 +100,27 @@ func NewPullHandler(format string, path string, tty *os.File, out io.Writer, ver // NewManifestFetchHandler returns a manifest fetch handler. func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) { var metadataHandler metadata.ManifestFetchHandler - var contentHandler content.ManifestFetchHandler = content.NewManifestFetchHandler(out, outputDescriptor, pretty, outputPath) - var discardHandler = content.NewDiscardHandler() + var contentHandler content.ManifestFetchHandler = content.NewManifestFetchHandler(out, pretty, outputPath) switch format { - case "raw": - metadataHandler = metadata.NewDiscardHandler() + case "": + // raw + if outputDescriptor { + metadataHandler = descriptor.NewManifestFetchHandler(out, pretty) + } else { + metadataHandler = metadata.NewDiscardHandler() + } case "json": + // json metadataHandler = json.NewManifestFetchHandler(out) if outputPath == "" { - contentHandler = discardHandler + contentHandler = content.NewDiscardHandler() } default: + // go template metadataHandler = template.NewManifestFetchHandler(out, format) if outputPath == "" { - contentHandler = discardHandler + contentHandler = content.NewDiscardHandler() } } return metadataHandler, contentHandler diff --git a/cmd/oras/internal/display/metadata/descriptor/interface.go b/cmd/oras/internal/display/metadata/descriptor/interface.go new file mode 100644 index 000000000..94958f7f4 --- /dev/null +++ b/cmd/oras/internal/display/metadata/descriptor/interface.go @@ -0,0 +1,24 @@ +/* +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 ocispec "github.com/opencontainers/image-spec/specs-go/v1" + +// ManifestFetchHandler handles metadata output for manifest fetch events. +type ManifestFetchHandler interface { + // OnFetched is called after the manifest content is fetched. + OnFetched(ocispec.Descriptor) error +} 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..2a55b503d --- /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 ManifestFetch struct { + pretty bool + out io.Writer +} + +// OnFetched implements ManifestFetchHandler. +func (h *ManifestFetch) OnFetched(desc ocispec.Descriptor, _ []byte) error { + descBytes, err := json.Marshal(desc) + if err != nil { + return fmt.Errorf("invalid descriptor: %w", err) + } + return utils.Output(h.out, descBytes, h.pretty) +} + +// NewManifestFetchHandler creates a new handler. +func NewManifestFetchHandler(out io.Writer, pretty bool) metadata.ManifestFetchHandler { + return &ManifestFetch{ + pretty: pretty, + out: out, + } +} diff --git a/cmd/oras/internal/display/metadata/discard.go b/cmd/oras/internal/display/metadata/discard.go index 89f0f2d69..dc31bae0f 100644 --- a/cmd/oras/internal/display/metadata/discard.go +++ b/cmd/oras/internal/display/metadata/discard.go @@ -25,4 +25,4 @@ func NewDiscardHandler() ManifestFetchHandler { } // OnFetched implements ManifestFetchHandler. -func (discard) OnFetched([]byte, ocispec.Descriptor) error { return nil } +func (discard) OnFetched(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 fdeedfaf6..7a06b8635 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -34,7 +34,7 @@ type AttachHandler interface { // ManifestFetchHandler handles metadata output for manifest fetch events. type ManifestFetchHandler interface { // OnFetched is called after the manifest content is fetched. - OnFetched([]byte, ocispec.Descriptor) error + OnFetched(ocispec.Descriptor, []byte) error } // PullHandler handles metadata output for pull events. diff --git a/cmd/oras/internal/display/metadata/json/manifest_fetch.go b/cmd/oras/internal/display/metadata/json/manifest_fetch.go index 4d717e666..42bf6ebb4 100644 --- a/cmd/oras/internal/display/metadata/json/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/json/manifest_fetch.go @@ -36,7 +36,7 @@ func NewManifestFetchHandler(out io.Writer) metadata.ManifestFetchHandler { } // OnFetched is called after the manifest fetch is completed. -func (h *ManifestFetchHandler) OnFetched(content []byte, desc ocispec.Descriptor) error { +func (h *ManifestFetchHandler) OnFetched(desc ocispec.Descriptor, content []byte) error { var manifest map[string]any if err := json.Unmarshal(content, &manifest); err != nil { return err diff --git a/cmd/oras/internal/display/metadata/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go index 6934d7825..3c777670d 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -38,7 +38,7 @@ func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFe } // OnFetched is called after the manifest fetch is completed. -func (h *ManifestFetchHandler) OnFetched(content []byte, desc ocispec.Descriptor) error { +func (h *ManifestFetchHandler) OnFetched(desc ocispec.Descriptor, content []byte) error { var manifest map[string]any if err := json.Unmarshal(content, &manifest); err != nil { return err diff --git a/cmd/oras/internal/display/utils/utils.go b/cmd/oras/internal/display/utils/utils.go index 6a88c7c02..b8463bef6 100644 --- a/cmd/oras/internal/display/utils/utils.go +++ b/cmd/oras/internal/display/utils/utils.go @@ -15,7 +15,14 @@ limitations under the License. package utils -import v1 "github.com/opencontainers/image-spec/specs-go/v1" +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + 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. @@ -31,3 +38,17 @@ const ( PullPromptRestored = "Restored " PullPromptDownloaded = "Downloaded " ) + +// Output writes the data to the output stream, optionally prettifying it. +func Output(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/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 0c758c7ab..ff0ffbb86 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -18,6 +18,7 @@ package manifest import ( "fmt" + 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" @@ -89,8 +90,7 @@ 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", "raw", `Format output using a custom template: -'raw': Print raw manifest content + cmd.Flags().StringVar(&opts.Template, "format", "", `Format output using a custom template: 'json': Print manifest in prettified JSON format '$TEMPLATE': Print output using the given Go template.`) option.ApplyFlags(&opts, cmd.Flags()) @@ -119,25 +119,27 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { } 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 fetchOpts.TargetPlatform = opts.Platform.Platform - desc, err := oras.Resolve(ctx, src, opts.Reference, fetchOpts) + desc, err = oras.Resolve(ctx, src, opts.Reference, fetchOpts) if err != nil { return fmt.Errorf("failed to find %q: %w", opts.RawReference, err) } - return contentHandler.OnDescriptorFetched(desc) - } - // fetch manifest 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 err = contentHandler.OnContentFetched(desc, content); err != nil { - return err + } else { + // 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 err = contentHandler.OnContentFetched(desc, content); err != nil { + return err + } } - return metadataHandler.OnFetched(content, desc) + return metadataHandler.OnFetched(desc, content) } From 998ad767950ae64a5f4db8484e853685f5496a52 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 08:39:51 +0000 Subject: [PATCH 20/36] fix e2e Signed-off-by: Billy Zha --- cmd/oras/root/manifest/fetch.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index ff0ffbb86..585f4fb55 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -72,9 +72,9 @@ 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 != "raw": + 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 != "raw": + 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") From f93ba89b6be93aaf039e37ad3e443d929a0bb1be Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 08:40:41 +0000 Subject: [PATCH 21/36] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/content/discard.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cmd/oras/internal/display/content/discard.go b/cmd/oras/internal/display/content/discard.go index 3c08fb695..cdf543797 100644 --- a/cmd/oras/internal/display/content/discard.go +++ b/cmd/oras/internal/display/content/discard.go @@ -24,11 +24,6 @@ func (discardHandler) OnContentFetched(ocispec.Descriptor, []byte) error { return nil } -// OnDescriptorFetched implements ManifestFetchHandler. -func (discardHandler) OnDescriptorFetched(ocispec.Descriptor) error { - return nil -} - // NewDiscardHandler returns a new discard handler. func NewDiscardHandler() ManifestFetchHandler { return discardHandler{} From b54a2e6686842ec85f7827869da3cf0fc90be335 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 11:38:06 +0000 Subject: [PATCH 22/36] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/content/manifest_fetch.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/oras/internal/display/content/manifest_fetch.go b/cmd/oras/internal/display/content/manifest_fetch.go index 6da0172b1..b6c9b5d81 100644 --- a/cmd/oras/internal/display/content/manifest_fetch.go +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -24,14 +24,14 @@ import ( "oras.land/oras/cmd/oras/internal/display/utils" ) -// RawManifestFetch handles raw content output. -type RawManifestFetch struct { +// manifestFetch handles raw content output. +type manifestFetch struct { pretty bool stdout io.Writer outputPath string } -func (h *RawManifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byte) error { +func (h *manifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byte) error { out := h.stdout if h.outputPath != "-" && h.outputPath != "" { f, err := os.OpenFile(h.outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) @@ -46,7 +46,7 @@ func (h *RawManifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest [] // NewManifestFetchHandler creates a new handler. func NewManifestFetchHandler(out io.Writer, pretty bool, outputPath string) ManifestFetchHandler { - return &RawManifestFetch{ + return &manifestFetch{ pretty: pretty, stdout: out, outputPath: outputPath, From e682458b661ae1e5deb30e15b8dae29ec20d0648 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 11:38:52 +0000 Subject: [PATCH 23/36] code clean Signed-off-by: Billy Zha --- .../display/metadata/descriptor/interface.go | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 cmd/oras/internal/display/metadata/descriptor/interface.go diff --git a/cmd/oras/internal/display/metadata/descriptor/interface.go b/cmd/oras/internal/display/metadata/descriptor/interface.go deleted file mode 100644 index 94958f7f4..000000000 --- a/cmd/oras/internal/display/metadata/descriptor/interface.go +++ /dev/null @@ -1,24 +0,0 @@ -/* -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 ocispec "github.com/opencontainers/image-spec/specs-go/v1" - -// ManifestFetchHandler handles metadata output for manifest fetch events. -type ManifestFetchHandler interface { - // OnFetched is called after the manifest content is fetched. - OnFetched(ocispec.Descriptor) error -} From faa8c636ac88c81752415b18f91fb893e5020db6 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 8 Apr 2024 12:32:53 +0000 Subject: [PATCH 24/36] code clean Signed-off-by: Billy Zha --- .../display/metadata/descriptor/manifest_fetch.go | 8 ++++---- .../internal/display/metadata/template/manifest_fetch.go | 8 ++++---- cmd/oras/internal/display/utils/utils.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go b/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go index 2a55b503d..bbdedc2e7 100644 --- a/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go @@ -25,14 +25,14 @@ import ( "oras.land/oras/cmd/oras/internal/display/utils" ) -// ManifestFetchHandler handles metadata descriptor output. -type ManifestFetch struct { +// manifestFetchHandler handles metadata descriptor output. +type manifestFetchHandler struct { pretty bool out io.Writer } // OnFetched implements ManifestFetchHandler. -func (h *ManifestFetch) OnFetched(desc ocispec.Descriptor, _ []byte) error { +func (h *manifestFetchHandler) OnFetched(desc ocispec.Descriptor, _ []byte) error { descBytes, err := json.Marshal(desc) if err != nil { return fmt.Errorf("invalid descriptor: %w", err) @@ -42,7 +42,7 @@ func (h *ManifestFetch) OnFetched(desc ocispec.Descriptor, _ []byte) error { // NewManifestFetchHandler creates a new handler. func NewManifestFetchHandler(out io.Writer, pretty bool) metadata.ManifestFetchHandler { - return &ManifestFetch{ + return &manifestFetchHandler{ pretty: pretty, out: out, } diff --git a/cmd/oras/internal/display/metadata/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go index 3c777670d..186d63eca 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -23,22 +23,22 @@ import ( "oras.land/oras/cmd/oras/internal/display/metadata" ) -// ManifestFetchHandler handles JSON metadata output for manifest fetch events. -type ManifestFetchHandler struct { +// 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{ + return &manifestFetchHandler{ template: template, out: out, } } // OnFetched is called after the manifest fetch is completed. -func (h *ManifestFetchHandler) OnFetched(desc ocispec.Descriptor, content []byte) error { +func (h *manifestFetchHandler) OnFetched(desc ocispec.Descriptor, content []byte) error { var manifest map[string]any if err := json.Unmarshal(content, &manifest); err != nil { return err diff --git a/cmd/oras/internal/display/utils/utils.go b/cmd/oras/internal/display/utils/utils.go index b8463bef6..8cde3dd43 100644 --- a/cmd/oras/internal/display/utils/utils.go +++ b/cmd/oras/internal/display/utils/utils.go @@ -21,13 +21,13 @@ import ( "fmt" "io" - v1 "github.com/opencontainers/image-spec/specs-go/v1" + ocispec "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] +func GenerateContentKey(desc ocispec.Descriptor) string { + return desc.Digest.String() + desc.Annotations[ocispec.AnnotationTitle] } const ( From 4eb5b9f5895b3e23e7af3dd3fd1908f3081eec99 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 01:38:30 +0000 Subject: [PATCH 25/36] add fetched model Signed-off-by: Billy Zha --- .../metadata/descriptor/manifest_fetch.go | 2 +- cmd/oras/internal/display/metadata/discard.go | 2 +- .../internal/display/metadata/interface.go | 2 +- .../display/metadata/json/manifest_fetch.go | 11 ++++--- .../display/metadata/model/fetched.go | 31 +++++++++++++++++++ .../metadata/template/manifest_fetch.go | 5 +-- cmd/oras/root/manifest/fetch.go | 2 +- test/e2e/suite/command/manifest.go | 4 +-- 8 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 cmd/oras/internal/display/metadata/model/fetched.go diff --git a/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go b/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go index bbdedc2e7..db7f9ede8 100644 --- a/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go @@ -32,7 +32,7 @@ type manifestFetchHandler struct { } // OnFetched implements ManifestFetchHandler. -func (h *manifestFetchHandler) OnFetched(desc ocispec.Descriptor, _ []byte) error { +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) diff --git a/cmd/oras/internal/display/metadata/discard.go b/cmd/oras/internal/display/metadata/discard.go index dc31bae0f..2e2f0a382 100644 --- a/cmd/oras/internal/display/metadata/discard.go +++ b/cmd/oras/internal/display/metadata/discard.go @@ -25,4 +25,4 @@ func NewDiscardHandler() ManifestFetchHandler { } // OnFetched implements ManifestFetchHandler. -func (discard) OnFetched(ocispec.Descriptor, []byte) error { return nil } +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 7a06b8635..c7815aa12 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -34,7 +34,7 @@ type AttachHandler interface { // ManifestFetchHandler handles metadata output for manifest fetch events. type ManifestFetchHandler interface { // OnFetched is called after the manifest content is fetched. - OnFetched(ocispec.Descriptor, []byte) error + OnFetched(path string, desc ocispec.Descriptor, content []byte) error } // PullHandler handles metadata output for pull events. diff --git a/cmd/oras/internal/display/metadata/json/manifest_fetch.go b/cmd/oras/internal/display/metadata/json/manifest_fetch.go index 42bf6ebb4..081d7d827 100644 --- a/cmd/oras/internal/display/metadata/json/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/json/manifest_fetch.go @@ -21,25 +21,26 @@ import ( 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 { +// 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{ + return &manifestFetchHandler{ out: out, } } // OnFetched is called after the manifest fetch is completed. -func (h *ManifestFetchHandler) OnFetched(desc ocispec.Descriptor, content []byte) error { +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 { return err } - return printJSON(h.out, manifest) + 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 index 186d63eca..2a42d53fd 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -21,6 +21,7 @@ import ( 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. @@ -38,11 +39,11 @@ func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFe } // OnFetched is called after the manifest fetch is completed. -func (h *manifestFetchHandler) OnFetched(desc ocispec.Descriptor, content []byte) error { +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 { return err } // return parseAndWrite(h.out, model.ToMappable(reflect.ValueOf(manifest)), h.template) - return parseAndWrite(h.out, manifest, h.template) + return parseAndWrite(h.out, model.NewFetched(path, desc, manifest), h.template) } diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 585f4fb55..690bdeb0c 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -141,5 +141,5 @@ func fetchManifest(cmd *cobra.Command, opts *fetchOptions) (fetchErr error) { return err } } - return metadataHandler.OnFetched(desc, content) + return metadataHandler.OnFetched(opts.Path, desc, content) } diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 2b74b2ad9..03e764ae8 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -227,7 +227,7 @@ var _ = Describe("1.1 registry users:", func() { fetchPath := filepath.Join(GinkgoT().TempDir(), "fetchedImage") digest := multi_arch.LinuxAMD64.Digest.String() ref := RegistryRef(ZOTHost, ImageRepo, digest) - out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "json").Exec().Out.Contents() + out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{.Content}}").Exec().Out.Contents() Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) MatchFile(fetchPath, multi_arch.LinuxAMD64Manifest, DefaultTimeout) }) @@ -248,7 +248,7 @@ var _ = Describe("1.1 registry users:", func() { }) It("should fetch manifest with platform validation and output json", func() { - out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "json"). + out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "{{.Content}}"). Exec().Out.Contents() Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) }) From 3cbbf9b84cdb63c37442e9db34c7bae0a944e4ed Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 01:43:39 +0000 Subject: [PATCH 26/36] add experimental mark Signed-off-by: Billy Zha --- cmd/oras/root/manifest/fetch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 690bdeb0c..0e2103619 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -90,7 +90,7 @@ 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", "", `Format output using a custom template: + cmd.Flags().StringVar(&opts.Template, "format", "", `[Experimental] Format fetched manifest using a custom template: 'json': Print manifest in prettified JSON format '$TEMPLATE': Print output using the given Go template.`) option.ApplyFlags(&opts, cmd.Flags()) From 7b09792a0ba46a3f7285d7d715ee11f79c8c1467 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 01:49:48 +0000 Subject: [PATCH 27/36] fix e2e Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 03e764ae8..6c3f21bcc 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -227,7 +227,7 @@ var _ = Describe("1.1 registry users:", func() { fetchPath := filepath.Join(GinkgoT().TempDir(), "fetchedImage") digest := multi_arch.LinuxAMD64.Digest.String() ref := RegistryRef(ZOTHost, ImageRepo, digest) - out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{.Content}}").Exec().Out.Contents() + out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{toJson .Content}}").Exec().Out.Contents() Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) MatchFile(fetchPath, multi_arch.LinuxAMD64Manifest, DefaultTimeout) }) @@ -248,7 +248,7 @@ var _ = Describe("1.1 registry users:", func() { }) It("should fetch manifest with platform validation and output json", func() { - out := ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.Tag), "--platform", "linux/amd64", "--format", "{{.Content}}"). + 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)) }) From cb98a26ed8d7037455fb6ee27cb9a9db6a689e44 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 01:53:52 +0000 Subject: [PATCH 28/36] fix e2e Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 6c3f21bcc..b657d37ca 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -254,7 +254,7 @@ var _ = Describe("1.1 registry users:", func() { }) It("should fetch manifest and format output", func() { - ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.LinuxAMD64.Digest.String()), "--format", "{{(first .layers).digest}}"). + ORAS("manifest", "fetch", RegistryRef(ZOTHost, ImageRepo, multi_arch.LinuxAMD64.Digest.String()), "--format", "{{(first .Content.layers).digest}}"). MatchContent(multi_arch.LayerDigest). Exec() }) From 6615478377234df3ae7afae893590d9d9ce82ef8 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 02:06:57 +0000 Subject: [PATCH 29/36] resolve comments Signed-off-by: Billy Zha --- .../internal/display/content/manifest_fetch.go | 2 +- cmd/oras/internal/display/handler.go | 8 ++++++-- .../metadata/descriptor/manifest_fetch.go | 2 +- cmd/oras/internal/display/metadata/discard.go | 4 +++- cmd/oras/internal/display/utils/utils.go | 4 ++-- cmd/oras/internal/option/pretty.go | 16 ++-------------- 6 files changed, 15 insertions(+), 21 deletions(-) diff --git a/cmd/oras/internal/display/content/manifest_fetch.go b/cmd/oras/internal/display/content/manifest_fetch.go index b6c9b5d81..8a826337e 100644 --- a/cmd/oras/internal/display/content/manifest_fetch.go +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -41,7 +41,7 @@ func (h *manifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byt defer f.Close() out = f } - return utils.Output(out, manifest, h.pretty) + return utils.PrintJSON(out, manifest, h.pretty) } // NewManifestFetchHandler creates a new handler. diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 29cc2d921..884b7427e 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -98,9 +98,9 @@ func NewPullHandler(format string, path string, tty *os.File, out io.Writer, ver } // NewManifestFetchHandler returns a manifest fetch handler. -func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) { +func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) { var metadataHandler metadata.ManifestFetchHandler - var contentHandler content.ManifestFetchHandler = content.NewManifestFetchHandler(out, pretty, outputPath) + var contentHandler content.ManifestFetchHandler switch format { case "": @@ -123,5 +123,9 @@ func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor bool 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 index db7f9ede8..a55407494 100644 --- a/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go @@ -37,7 +37,7 @@ func (h *manifestFetchHandler) OnFetched(_ string, desc ocispec.Descriptor, _ [] if err != nil { return fmt.Errorf("invalid descriptor: %w", err) } - return utils.Output(h.out, descBytes, h.pretty) + return utils.PrintJSON(h.out, descBytes, h.pretty) } // NewManifestFetchHandler creates a new handler. diff --git a/cmd/oras/internal/display/metadata/discard.go b/cmd/oras/internal/display/metadata/discard.go index 2e2f0a382..4b1941f59 100644 --- a/cmd/oras/internal/display/metadata/discard.go +++ b/cmd/oras/internal/display/metadata/discard.go @@ -25,4 +25,6 @@ func NewDiscardHandler() ManifestFetchHandler { } // OnFetched implements ManifestFetchHandler. -func (discard) OnFetched(string, ocispec.Descriptor, []byte) error { return nil } +func (discard) OnFetched(string, ocispec.Descriptor, []byte) error { + return nil +} diff --git a/cmd/oras/internal/display/utils/utils.go b/cmd/oras/internal/display/utils/utils.go index 8cde3dd43..776be54df 100644 --- a/cmd/oras/internal/display/utils/utils.go +++ b/cmd/oras/internal/display/utils/utils.go @@ -39,8 +39,8 @@ const ( PullPromptDownloaded = "Downloaded " ) -// Output writes the data to the output stream, optionally prettifying it. -func Output(out io.Writer, data []byte, pretty bool) error { +// 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 { diff --git a/cmd/oras/internal/option/pretty.go b/cmd/oras/internal/option/pretty.go index de889522e..6013be141 100644 --- a/cmd/oras/internal/option/pretty.go +++ b/cmd/oras/internal/option/pretty.go @@ -16,12 +16,10 @@ 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. @@ -37,15 +35,5 @@ func (opts *Pretty) ApplyFlags(fs *pflag.FlagSet) { // 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) } From a8c3de8d0ac2f23e8a9fe303bf3930509414c6a9 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 02:20:02 +0000 Subject: [PATCH 30/36] add json test Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index b657d37ca..6e55d7aae 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -227,8 +227,11 @@ var _ = Describe("1.1 registry users:", func() { fetchPath := filepath.Join(GinkgoT().TempDir(), "fetchedImage") digest := multi_arch.LinuxAMD64.Digest.String() ref := RegistryRef(ZOTHost, ImageRepo, digest) - out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{toJson .Content}}").Exec().Out.Contents() - Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) + // 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) }) @@ -247,7 +250,7 @@ var _ = Describe("1.1 registry users:", func() { MatchContent(multi_arch.LinuxAMD64Manifest).Exec() }) - It("should fetch manifest with platform validation and output json", 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}}"). Exec().Out.Contents() Expect(out).To(MatchJSON(multi_arch.LinuxAMD64Manifest)) From 1dfd284770a01e4438df8699c6cafd937ae8b077 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 02:35:17 +0000 Subject: [PATCH 31/36] fix e2e Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 6e55d7aae..0e96113ba 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -228,7 +228,7 @@ var _ = Describe("1.1 registry users:", func() { digest := multi_arch.LinuxAMD64.Digest.String() ref := RegistryRef(ZOTHost, ImageRepo, digest) // test - out := ORAS("manifest", "fetch", ref, "--output", fetchPath, "--format", "{{json}}").Exec().Out.Contents() + 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()) From dfb8dce7d15b4fbf4d4f6e7cf443f5292427a66f Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 06:00:39 +0000 Subject: [PATCH 32/36] resolve comments Signed-off-by: Billy Zha --- cmd/oras/internal/display/metadata/json/manifest_fetch.go | 4 +--- .../internal/display/metadata/template/manifest_fetch.go | 5 +---- cmd/oras/root/manifest/fetch.go | 6 +++--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/cmd/oras/internal/display/metadata/json/manifest_fetch.go b/cmd/oras/internal/display/metadata/json/manifest_fetch.go index 081d7d827..993a1a054 100644 --- a/cmd/oras/internal/display/metadata/json/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/json/manifest_fetch.go @@ -39,8 +39,6 @@ func NewManifestFetchHandler(out io.Writer) metadata.ManifestFetchHandler { // 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 { - return err - } + _ = json.Unmarshal(content, &manifest) return printJSON(h.out, model.NewFetched(path, desc, manifest)) } diff --git a/cmd/oras/internal/display/metadata/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go index 2a42d53fd..dee02a4ba 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -41,9 +41,6 @@ func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFe // 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 { - return err - } - // return parseAndWrite(h.out, model.ToMappable(reflect.ValueOf(manifest)), h.template) + _ = json.Unmarshal(content, &manifest) return parseAndWrite(h.out, model.NewFetched(path, desc, manifest), h.template) } diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 0e2103619..2eab3c2ff 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -90,9 +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 fetched manifest using a custom template: -'json': Print manifest in prettified JSON format -'$TEMPLATE': Print output using the given Go template.`) + cmd.Flags().StringVar(&opts.Template, "format", "", `[Experimental] Format operation 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) } From 6c5e2cfbcf437ac8229af914bf8a7e318ec99caf Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 07:11:30 +0000 Subject: [PATCH 33/36] resolve comments Signed-off-by: Billy Zha --- cmd/oras/internal/display/metadata/json/manifest_fetch.go | 4 +++- cmd/oras/internal/display/metadata/template/manifest_fetch.go | 4 +++- cmd/oras/root/manifest/fetch.go | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/oras/internal/display/metadata/json/manifest_fetch.go b/cmd/oras/internal/display/metadata/json/manifest_fetch.go index 993a1a054..01fe57a02 100644 --- a/cmd/oras/internal/display/metadata/json/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/json/manifest_fetch.go @@ -39,6 +39,8 @@ func NewManifestFetchHandler(out io.Writer) metadata.ManifestFetchHandler { // 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 - _ = json.Unmarshal(content, &manifest) + 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/template/manifest_fetch.go b/cmd/oras/internal/display/metadata/template/manifest_fetch.go index dee02a4ba..8148f13d6 100644 --- a/cmd/oras/internal/display/metadata/template/manifest_fetch.go +++ b/cmd/oras/internal/display/metadata/template/manifest_fetch.go @@ -41,6 +41,8 @@ func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFe // 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 - _ = json.Unmarshal(content, &manifest) + 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/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 2eab3c2ff..abe211c8f 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -90,7 +90,7 @@ 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 operation metadata using a custom template: + 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()) From bb69b393944cd0230f29eb748156ce1d8beb719b Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 08:01:09 +0000 Subject: [PATCH 34/36] add e2e coverage Signed-off-by: Billy Zha --- test/e2e/suite/command/manifest.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 0e96113ba..bf644acb1 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -235,6 +235,15 @@ var _ = Describe("1.1 registry users:", func() { 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() From 57d2078dae55c683324673da2715166982e34299 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 08:03:22 +0000 Subject: [PATCH 35/36] remove used code Signed-off-by: Billy Zha --- cmd/oras/internal/display/utils/utils.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cmd/oras/internal/display/utils/utils.go b/cmd/oras/internal/display/utils/utils.go index 776be54df..2f3205067 100644 --- a/cmd/oras/internal/display/utils/utils.go +++ b/cmd/oras/internal/display/utils/utils.go @@ -20,16 +20,9 @@ import ( "encoding/json" "fmt" "io" - - ocispec "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 ocispec.Descriptor) string { - return desc.Digest.String() + desc.Annotations[ocispec.AnnotationTitle] -} - +// Prompt constants for pull. const ( PullPromptDownloading = "Downloading" PullPromptPulled = "Pulled " From 4ec65f97eb380c6609fe7d394f30a9b0d802548b Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 10 Apr 2024 08:10:41 +0000 Subject: [PATCH 36/36] use create file instead of open file Signed-off-by: Billy Zha --- cmd/oras/internal/display/content/manifest_fetch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/content/manifest_fetch.go b/cmd/oras/internal/display/content/manifest_fetch.go index 8a826337e..20707b04b 100644 --- a/cmd/oras/internal/display/content/manifest_fetch.go +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -34,7 +34,7 @@ type manifestFetch struct { func (h *manifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byte) error { out := h.stdout if h.outputPath != "-" && h.outputPath != "" { - f, err := os.OpenFile(h.outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + f, err := os.Create(h.outputPath) if err != nil { return fmt.Errorf("failed to open %q: %w", h.outputPath, err) }