From b01cf2cf180f38293de0e9b19df1212b038af310 Mon Sep 17 00:00:00 2001 From: Zoey Li <108315938+lizMSFT@users.noreply.github.com> Date: Tue, 6 Sep 2022 18:47:53 +0800 Subject: [PATCH] feat: add Descriptor and Pretty options for ORAS command (#536) Signed-off-by: Zoey Li --- cmd/oras/internal/option/descriptor.go | 43 ++++++++ cmd/oras/internal/option/descriptor_test.go | 59 +++++++++++ cmd/oras/internal/option/pretty.go | 51 ++++++++++ cmd/oras/internal/option/pretty_test.go | 106 ++++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 cmd/oras/internal/option/descriptor.go create mode 100644 cmd/oras/internal/option/descriptor_test.go create mode 100644 cmd/oras/internal/option/pretty.go create mode 100644 cmd/oras/internal/option/pretty_test.go diff --git a/cmd/oras/internal/option/descriptor.go b/cmd/oras/internal/option/descriptor.go new file mode 100644 index 000000000..20017d4b5 --- /dev/null +++ b/cmd/oras/internal/option/descriptor.go @@ -0,0 +1,43 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "encoding/json" + "fmt" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/pflag" +) + +// Descriptor option struct. +type Descriptor struct { + OutputDescriptor bool +} + +// ApplyFlags applies flags to a command flag set. +func (opts *Descriptor) ApplyFlags(fs *pflag.FlagSet) { + fs.BoolVarP(&opts.OutputDescriptor, "descriptor", "", false, "output the descriptor") +} + +// Marshal returns the JSON encoding of descriptor. +func (opts *Descriptor) Marshal(desc ocispec.Descriptor) ([]byte, error) { + b, err := json.Marshal(desc) + if err != nil { + return nil, fmt.Errorf("failed to marshal descriptor: %w", err) + } + return b, nil +} diff --git a/cmd/oras/internal/option/descriptor_test.go b/cmd/oras/internal/option/descriptor_test.go new file mode 100644 index 000000000..ca17e09d4 --- /dev/null +++ b/cmd/oras/internal/option/descriptor_test.go @@ -0,0 +1,59 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/pflag" +) + +func TestDescriptor_ApplyFlags(t *testing.T) { + var test struct{ Descriptor } + ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) + if test.Descriptor.OutputDescriptor != false { + t.Fatalf("expecting OutputDescriptor to be false but got: %v", test.Descriptor.OutputDescriptor) + } +} + +func TestDescriptor_Marshal(t *testing.T) { + // generate test content + blob := []byte("hello world") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + want, err := json.Marshal(desc) + if err != nil { + t.Fatal("error calling json.Marshal(), error =", err) + } + + opts := Descriptor{ + OutputDescriptor: true, + } + got, err := opts.Marshal(desc) + if err != nil { + t.Fatal("Descriptor.Marshal() error =", err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("Descriptor.Marshal() got %v, want %v", got, want) + } +} diff --git a/cmd/oras/internal/option/pretty.go b/cmd/oras/internal/option/pretty.go new file mode 100644 index 000000000..0e7524473 --- /dev/null +++ b/cmd/oras/internal/option/pretty.go @@ -0,0 +1,51 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + "github.com/spf13/pflag" +) + +// Pretty option struct. +type Pretty struct { + 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") +} + +// 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 +} diff --git a/cmd/oras/internal/option/pretty_test.go b/cmd/oras/internal/option/pretty_test.go new file mode 100644 index 000000000..5d0049048 --- /dev/null +++ b/cmd/oras/internal/option/pretty_test.go @@ -0,0 +1,106 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/pflag" +) + +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) + } +} + +func TestPretty_Output(t *testing.T) { + // generate test content + blob := []byte("hello world") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + want, err := json.Marshal(desc) + if err != nil { + t.Fatal("error calling json.Marshal(), error =", err) + } + + tempDir := t.TempDir() + fileName := "test.txt" + path := filepath.Join(tempDir, fileName) + fp, err := os.Create(path) + if err != nil { + t.Fatal("error calling os.Create(), error =", err) + } + defer fp.Close() + + // outputs unprettified content + opts := Pretty{ + pretty: false, + } + err = opts.Output(fp, want) + if err != nil { + t.Fatal("Pretty.Output() error =", err) + } + if _, err = fp.Seek(0, io.SeekStart); err != nil { + t.Fatal("error calling File.Seek(), error =", err) + } + got, err := io.ReadAll(fp) + if err != nil { + t.Fatal("error calling io.ReadAll(), error =", err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("Pretty.Output() got %v, want %v", got, want) + } + + // remove all content in the file + if err := os.Truncate(path, 0); err != nil { + t.Fatal("error calling os.Truncate(), error =", err) + } + if _, err = fp.Seek(0, io.SeekStart); err != nil { + t.Fatal("error calling File.Seek(), error =", err) + } + + // outputs prettified content + opts = Pretty{ + pretty: true, + } + err = opts.Output(fp, want) + if err != nil { + t.Fatal("Pretty.Output() error =", err) + } + if _, err = fp.Seek(0, io.SeekStart); err != nil { + t.Fatal("error calling File.Seek(), error =", err) + } + got, err = io.ReadAll(fp) + if err != nil { + t.Fatal("error calling io.ReadAll(), error =", err) + } + if reflect.DeepEqual(got, want) { + t.Fatalf("Pretty.Output() failed to prettified the content: %v", got) + } +}