From ce6bc89ce5a6cc92001aad3ddc1cc043a95ffbb9 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 2 Apr 2024 08:28:43 +0000 Subject: [PATCH] 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() {