Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add --template and support --format go-template=TEMPLATE experience #1377

Merged
merged 35 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d956bca
feat: redefine go template experience
qweeah May 7, 2024
bb9e950
add default value
qweeah May 7, 2024
477063e
update default
qweeah May 7, 2024
241c1b2
Merge remote-tracking branch 'upstream/main' into format-template
qweeah May 8, 2024
05fc9a8
fix: discover init error
qweeah May 8, 2024
2d86e91
add tests
qweeah May 8, 2024
b65dd71
fix e2e
qweeah May 8, 2024
799e313
add unit test
qweeah May 9, 2024
467ab8e
code clean
qweeah May 9, 2024
4f0334c
make ut easier
qweeah May 9, 2024
ddb9b0a
try make UT easier
qweeah May 9, 2024
97b8cc0
add unit tests
qweeah May 9, 2024
ae6b4f2
code clean
qweeah May 9, 2024
10510b7
bug fix && add e2e
qweeah May 9, 2024
69afad6
bug fix
qweeah May 9, 2024
e0eb0fb
remove focus
qweeah May 9, 2024
10b7016
revert unnecessary change
qweeah May 10, 2024
53392e2
code clean
qweeah May 10, 2024
0014f3a
code clean
qweeah May 11, 2024
6db1020
code clean
qweeah May 11, 2024
1224dd2
add experimental to template flag
qweeah May 11, 2024
15647d0
Merge remote-tracking branch 'upstream/main' into format-template
qweeah May 13, 2024
74522e5
resolve comments
qweeah May 13, 2024
91584be
add flushing
qweeah May 13, 2024
c1dfafd
add e2e
qweeah May 13, 2024
aa60110
refine recommendation
qweeah May 13, 2024
927573a
Merge remote-tracking branch 'upstream/main' into format-template
qweeah May 13, 2024
5c53b24
add default format help doc
qweeah May 13, 2024
cc2c8e3
fix e2e
qweeah May 13, 2024
364729f
code clean
qweeah May 13, 2024
e571ad2
resolve nit comment
qweeah May 13, 2024
bc81c38
recommend only primary usage
qweeah May 13, 2024
61c5f1f
correct description
qweeah May 13, 2024
47c35ec
fix build error
qweeah May 13, 2024
70cd7f0
add parameter to format type
qweeah May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 47 additions & 33 deletions cmd/oras/internal/display/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,120 +30,134 @@
"oras.land/oras/cmd/oras/internal/display/metadata/text"
"oras.land/oras/cmd/oras/internal/display/metadata/tree"
"oras.land/oras/cmd/oras/internal/display/status"
"oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
)

// NewPushHandler returns status and metadata handlers for push command.
func NewPushHandler(out io.Writer, format string, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler) {
func NewPushHandler(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler, error) {
var statusHandler status.PushHandler
if tty != nil {
statusHandler = status.NewTTYPushHandler(tty)
} else if format == "" {
} else if format.Type == "" {
statusHandler = status.NewTextPushHandler(out, verbose)
} else {
statusHandler = status.NewDiscardHandler()
}

var metadataHandler metadata.PushHandler
switch format {
switch format.Type {
case "":
metadataHandler = text.NewPushHandler(out)
case "json":
case option.FormatTypeJSON.Name:
metadataHandler = json.NewPushHandler(out)
case option.FormatTypeGoTemplate.Name:
metadataHandler = template.NewPushHandler(out, format.Template)
default:
metadataHandler = template.NewPushHandler(out, format)
return nil, nil, errors.UnsupportedFormatTypeError(format.Type)
}
return statusHandler, metadataHandler
return statusHandler, metadataHandler, nil
}

// NewAttachHandler returns status and metadata handlers for attach command.
func NewAttachHandler(out io.Writer, format string, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler) {
func NewAttachHandler(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler, error) {
var statusHandler status.AttachHandler
if tty != nil {
statusHandler = status.NewTTYAttachHandler(tty)
} else if format == "" {
} else if format.Type == "" {
statusHandler = status.NewTextAttachHandler(out, verbose)
} else {
statusHandler = status.NewDiscardHandler()
}

var metadataHandler metadata.AttachHandler
switch format {
switch format.Type {
case "":
metadataHandler = text.NewAttachHandler(out)
case "json":
case option.FormatTypeJSON.Name:
metadataHandler = json.NewAttachHandler(out)
case option.FormatTypeGoTemplate.Name:
metadataHandler = template.NewAttachHandler(out, format.Template)
default:
metadataHandler = template.NewAttachHandler(out, format)
return nil, nil, errors.UnsupportedFormatTypeError(format.Type)
}
return statusHandler, metadataHandler
return statusHandler, metadataHandler, nil
}

// NewPullHandler returns status and metadata handlers for pull command.
func NewPullHandler(out io.Writer, format string, path string, tty *os.File, verbose bool) (status.PullHandler, metadata.PullHandler) {
func NewPullHandler(out io.Writer, format option.Format, path string, tty *os.File, verbose bool) (status.PullHandler, metadata.PullHandler, error) {
var statusHandler status.PullHandler
if tty != nil {
statusHandler = status.NewTTYPullHandler(tty)
} else if format == "" {
} else if format.Type == "" {
statusHandler = status.NewTextPullHandler(out, verbose)
} else {
statusHandler = status.NewDiscardHandler()
}

var metadataHandler metadata.PullHandler
switch format {
switch format.Type {
case "":
metadataHandler = text.NewPullHandler(out)
case "json":
case option.FormatTypeJSON.Name:
metadataHandler = json.NewPullHandler(out, path)
case option.FormatTypeGoTemplate.Name:
metadataHandler = template.NewPullHandler(out, path, format.Template)
default:
metadataHandler = template.NewPullHandler(out, path, format)
return nil, nil, errors.UnsupportedFormatTypeError(format.Type)
}
return statusHandler, metadataHandler
return statusHandler, metadataHandler, nil
}

// NewDiscoverHandler returns status and metadata handlers for discover command.
func NewDiscoverHandler(out io.Writer, outputType string, path string, rawReference string, desc ocispec.Descriptor, verbose bool) metadata.DiscoverHandler {
switch outputType {
case "tree", "":
return tree.NewDiscoverHandler(out, path, desc, verbose)
case "table":
return table.NewDiscoverHandler(out, rawReference, desc, verbose)
case "json":
return json.NewDiscoverHandler(out, desc, path)
func NewDiscoverHandler(out io.Writer, format option.Format, path string, rawReference string, desc ocispec.Descriptor, verbose bool) (metadata.DiscoverHandler, error) {
var handler metadata.DiscoverHandler
switch format.Type {
case option.FormatTypeTree.Name, "":
handler = tree.NewDiscoverHandler(out, path, desc, verbose)
case option.FormatTypeTable.Name:
handler = table.NewDiscoverHandler(out, rawReference, desc, verbose)
case option.FormatTypeJSON.Name:
handler = json.NewDiscoverHandler(out, desc, path)
case option.FormatTypeGoTemplate.Name:
handler = template.NewDiscoverHandler(out, desc, path, format.Template)
default:
return template.NewDiscoverHandler(out, desc, path, outputType)
return nil, errors.UnsupportedFormatTypeError(format.Type)

Check warning on line 125 in cmd/oras/internal/display/handler.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/handler.go#L125

Added line #L125 was not covered by tests
}
return handler, nil
}

// NewManifestFetchHandler returns a manifest fetch handler.
func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) {
func NewManifestFetchHandler(out io.Writer, format option.Format, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler, error) {
var metadataHandler metadata.ManifestFetchHandler
var contentHandler content.ManifestFetchHandler

switch format {
switch format.Type {
case "":
// raw
if outputDescriptor {
metadataHandler = descriptor.NewManifestFetchHandler(out, pretty)
} else {
metadataHandler = metadata.NewDiscardHandler()
}
case "json":
case option.FormatTypeJSON.Name:
// json
metadataHandler = json.NewManifestFetchHandler(out)
if outputPath == "" {
contentHandler = content.NewDiscardHandler()
}
default:
case option.FormatTypeGoTemplate.Name:
// go template
metadataHandler = template.NewManifestFetchHandler(out, format)
metadataHandler = template.NewManifestFetchHandler(out, format.Template)
if outputPath == "" {
contentHandler = content.NewDiscardHandler()
}
default:
return nil, nil, errors.UnsupportedFormatTypeError(format.Type)
}

if contentHandler == nil {
contentHandler = content.NewManifestFetchHandler(out, pretty, outputPath)
}
return metadataHandler, contentHandler
return metadataHandler, contentHandler, nil
}
8 changes: 8 additions & 0 deletions cmd/oras/internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ import (
// RegistryErrorPrefix is the commandline prefix for errors from registry.
const RegistryErrorPrefix = "Error response from registry:"

// UnsupportedFormatTypeError generates the error message for an invalid type.
type UnsupportedFormatTypeError string

// Error implements the error interface.
func (e UnsupportedFormatTypeError) Error() string {
return "unsupported format type: " + string(e)
}

// Error is the error type for CLI error messaging.
type Error struct {
Err error
Expand Down
131 changes: 120 additions & 11 deletions cmd/oras/internal/option/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,130 @@ limitations under the License.

package option

import "github.com/spf13/pflag"
import (
"bytes"
"fmt"
"strings"
"text/tabwriter"

// Format is a flag to format metadata into output.
"github.com/spf13/cobra"
"github.com/spf13/pflag"
oerrors "oras.land/oras/cmd/oras/internal/errors"
)

// FormatType represents a format type.
type FormatType struct {
// Name is the format type name.
Name string
// Usage is the usage string in help doc.
Usage string
// HasParams indicates whether the format type has parameters.
HasParams bool
}

// WithUsage returns a new format type with provided usage string.
func (ft *FormatType) WithUsage(usage string) *FormatType {
return &FormatType{
Name: ft.Name,
HasParams: ft.HasParams,
Usage: usage,
}
}

// format types
var (
FormatTypeJSON = &FormatType{
Name: "json",
Usage: "Print in JSON format",
}
FormatTypeGoTemplate = &FormatType{
Name: "go-template",
Usage: "Print output using the given Go template",
HasParams: true,
}
FormatTypeTable = &FormatType{
Name: "table",
Usage: "Get direct referrers and output in table format",
}
FormatTypeTree = &FormatType{
Name: "tree",
Usage: "Get referrers recursively and print in tree format",
}
)

// Format contains input and parsed options for formatted output flags.
type Format struct {
Template string
FormatFlag string
Type string
Template string
AllowedTypes []*FormatType
}

// ApplyFlag implements FlagProvider.ApplyFlag.
func (opts *Format) ApplyFlags(fs *pflag.FlagSet) {
const name = "format"
if fs.Lookup(name) != nil {
// allow command to overwrite the flag
return
}
fs.StringVar(&opts.Template, name, "", `[Experimental] Format output using a custom template:
'json': Print in JSON format
'$TEMPLATE': Print output using the given Go template.`)
buf := bytes.NewBufferString("[Experimental] Format output using a custom template:")
w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0)
for _, t := range opts.AllowedTypes {
_, _ = fmt.Fprintf(w, "\n'%s':\t%s", t.Name, t.Usage)
}
w.Flush()
// apply flags
fs.StringVar(&opts.FormatFlag, "format", opts.FormatFlag, buf.String())
fs.StringVar(&opts.Template, "template", "", "[Experimental] Template string used to format output")
}

// Parse parses the input format flag.
func (opts *Format) Parse(_ *cobra.Command) error {
if err := opts.parseFlag(); err != nil {
return err
}

if opts.Type == "" {
// flag not specified
return nil
}

if opts.Type == FormatTypeGoTemplate.Name && opts.Template == "" {
return &oerrors.Error{
Err: fmt.Errorf("%q format specified but no template given", opts.Type),
Recommendation: fmt.Sprintf("use `--format %s=TEMPLATE` to specify the template", opts.Type),
}
}

var optionalTypes []string
for _, t := range opts.AllowedTypes {
if opts.Type == t.Name {
// type validation passed
return nil
}
optionalTypes = append(optionalTypes, t.Name)
}
return &oerrors.Error{
Err: fmt.Errorf("invalid format type: %q", opts.Type),
Recommendation: fmt.Sprintf("supported types: %s", strings.Join(optionalTypes, ", ")),
}
}

func (opts *Format) parseFlag() error {
opts.Type = opts.FormatFlag
if opts.Template != "" {
// template explicitly set
if opts.Type != FormatTypeGoTemplate.Name {
return fmt.Errorf("--template must be used with --format %s", FormatTypeGoTemplate.Name)
}
return nil
}

for _, t := range opts.AllowedTypes {
if !t.HasParams {
continue
}
prefix := t.Name + "="
if strings.HasPrefix(opts.FormatFlag, prefix) {
// parse type and add parameter to template
opts.Type = t.Name
opts.Template = opts.FormatFlag[len(prefix):]
}
}
return nil
}
7 changes: 6 additions & 1 deletion cmd/oras/root/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,18 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder
opts.FlagDescription = "[Preview] attach to an arch-specific subject"
_ = cmd.MarkFlagRequired("artifact-type")
opts.EnableDistributionSpecFlag()
opts.AllowedTypes = []*option.FormatType{option.FormatTypeJSON, option.FormatTypeGoTemplate}
option.ApplyFlags(&opts, cmd.Flags())
return oerrors.Command(cmd, &opts.Target)
}

func runAttach(cmd *cobra.Command, opts *attachOptions) error {
ctx, logger := command.GetLogger(cmd, &opts.Common)
displayStatus, displayMetadata, err := display.NewAttachHandler(cmd.OutOrStdout(), opts.Format, opts.TTY, opts.Verbose)
if err != nil {
return err
}

annotations, err := opts.LoadManifestAnnotations()
if err != nil {
return err
Expand All @@ -119,7 +125,6 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error {
Recommendation: `To attach to an existing artifact, please provide files via argument or annotations via flag "--annotation". Run "oras attach -h" for more options and examples`,
}
}
displayStatus, displayMetadata := display.NewAttachHandler(cmd.OutOrStdout(), opts.Template, opts.TTY, opts.Verbose)

// prepare manifest
store, err := file.New("")
Expand Down
43 changes: 43 additions & 0 deletions cmd/oras/root/attach_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package root

import (
"context"
"testing"

"github.com/spf13/cobra"
"oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
)

func Test_runAttach_errType(t *testing.T) {
// prpare
cmd := &cobra.Command{}
cmd.SetContext(context.Background())

// test
opts := &attachOptions{
Format: option.Format{
Type: "unknown",
},
}
got := runAttach(cmd, opts).Error()
want := errors.UnsupportedFormatTypeError(opts.Format.Type).Error()
if got != want {
t.Fatalf("got %v, want %v", got, want)
}
}
Loading
Loading