From 82e461ff9d15907d73fc0275ceecd97105ba5e41 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Fri, 9 Oct 2020 06:40:55 -0400 Subject: [PATCH 1/2] Switch to using errors.Wrapf rather then fmt.Errorsf We are using both functions throughout the code. Pick one and stick with it. Signed-off-by: Daniel J Walsh --- cmd/skopeo/inspect.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 140f90f9..670a3aac 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -90,7 +90,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) defer func() { if err := src.Close(); err != nil { - retErr = errors.Wrapf(retErr, fmt.Sprintf("(could not close image: %v) ", err)) + retErr = errors.Wrapf(retErr, "could not close image") } }() @@ -104,14 +104,14 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) if opts.raw && !opts.config { _, err := stdout.Write(rawManifest) if err != nil { - return fmt.Errorf("Error writing manifest to standard output: %v", err) + return errors.Wrapf(err, "Error writing manifest to standard output") } return nil } img, err := image.FromUnparsedImage(ctx, sys, image.UnparsedInstance(src, nil)) if err != nil { - return fmt.Errorf("Error parsing manifest for image: %v", err) + return errors.Wrapf(err, "Error parsing manifest for image") } if opts.config && opts.raw { @@ -124,7 +124,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) } _, err = stdout.Write(configBlob) if err != nil { - return fmt.Errorf("Error writing configuration blob to standard output: %v", err) + return errors.Wrapf(err, "Error writing configuration blob to standard output") } return nil } else if opts.config { @@ -137,7 +137,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) } err = json.NewEncoder(stdout).Encode(config) if err != nil { - return fmt.Errorf("Error writing OCI-formatted configuration data to standard output: %v", err) + return errors.Wrapf(err, "Error writing OCI-formatted configuration data to standard output") } return nil } @@ -164,7 +164,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) } outputData.Digest, err = manifest.Digest(rawManifest) if err != nil { - return fmt.Errorf("Error computing manifest digest: %v", err) + return errors.Wrapf(err, "Error computing manifest digest") } if dockerRef := img.Reference().DockerReference(); dockerRef != nil { outputData.Name = dockerRef.Name() @@ -182,7 +182,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) // In addition, AWS ECR rejects it with 403 (Forbidden) if the "ecr:ListImages" // action is not allowed. if !strings.Contains(err.Error(), "401") && !strings.Contains(err.Error(), "403") { - return fmt.Errorf("Error determining repository tags: %v", err) + return errors.Wrapf(err, "Error determining repository tags") } logrus.Warnf("Registry disallows tag list retrieval; skipping") } From 5d73dea5778de42ffe05b988a57e70ad7d32385b Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Tue, 17 Nov 2020 09:03:48 -0500 Subject: [PATCH 2/2] Add --format option to skopeo inspect Signed-off-by: Daniel J Walsh --- cmd/skopeo/inspect.go | 59 +++++++-- completions/bash/skopeo | 1 + docs/skopeo-inspect.1.md | 81 ++++++++---- .../common/pkg/report/camelcase/LICENSE.md | 20 +++ .../common/pkg/report/camelcase/README.md | 58 +++++++++ .../common/pkg/report/camelcase/camelcase.go | 91 ++++++++++++++ .../containers/common/pkg/report/doc.go | 46 +++++++ .../containers/common/pkg/report/template.go | 115 ++++++++++++++++++ .../containers/common/pkg/report/validate.go | 13 ++ .../containers/common/pkg/report/writer.go | 27 ++++ vendor/modules.txt | 2 + 11 files changed, 479 insertions(+), 34 deletions(-) create mode 100644 vendor/github.com/containers/common/pkg/report/camelcase/LICENSE.md create mode 100644 vendor/github.com/containers/common/pkg/report/camelcase/README.md create mode 100644 vendor/github.com/containers/common/pkg/report/camelcase/camelcase.go create mode 100644 vendor/github.com/containers/common/pkg/report/doc.go create mode 100644 vendor/github.com/containers/common/pkg/report/template.go create mode 100644 vendor/github.com/containers/common/pkg/report/validate.go create mode 100644 vendor/github.com/containers/common/pkg/report/writer.go diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 670a3aac..9f519c1c 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -4,8 +4,12 @@ import ( "encoding/json" "fmt" "io" + "os" "strings" + "text/tabwriter" + "text/template" + "github.com/containers/common/pkg/report" "github.com/containers/common/pkg/retry" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/image" @@ -23,6 +27,7 @@ type inspectOptions struct { global *globalOptions image *imageOptions retryOpts *retry.RetryOptions + format string raw bool // Output the raw manifest instead of parsing information about the image config bool // Output the raw config blob instead of parsing information about the image } @@ -45,13 +50,16 @@ Supported transports: See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), - RunE: commandAction(opts.run), - Example: `skopeo inspect docker://docker.io/fedora`, + RunE: commandAction(opts.run), + Example: `skopeo inspect docker://registry.fedoraproject.org/fedora + skopeo inspect --config docker://docker.io/alpine + skopeo inspect --format "Name: {{.Name}} Digest: {{.Digest}}" docker://registry.access.redhat.com/ubi8`, } adjustUsage(cmd) flags := cmd.Flags() flags.BoolVar(&opts.raw, "raw", false, "output raw manifest or configuration") flags.BoolVar(&opts.config, "config", false, "output configuration") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output to a Go template") flags.AddFlagSet(&sharedFlags) flags.AddFlagSet(&imageFlags) flags.AddFlagSet(&retryFlags) @@ -63,6 +71,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) rawManifest []byte src types.ImageSource imgInspect *types.ImageInspectInfo + data []interface{} ) ctx, cancel := opts.global.commandTimeoutContext() defer cancel() @@ -70,6 +79,9 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) if len(args) != 1 { return errors.New("Exactly one argument expected") } + if opts.raw && opts.format != "" { + return errors.New("raw output does not support format option") + } imageName := args[0] if err := reexecIfNecessaryForImages(imageName); err != nil { @@ -90,7 +102,7 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) defer func() { if err := src.Close(); err != nil { - retErr = errors.Wrapf(retErr, "could not close image") + retErr = errors.Wrapf(retErr, fmt.Sprintf("(could not close image: %v) ", err)) } }() @@ -104,8 +116,9 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) if opts.raw && !opts.config { _, err := stdout.Write(rawManifest) if err != nil { - return errors.Wrapf(err, "Error writing manifest to standard output") + return fmt.Errorf("Error writing manifest to standard output: %v", err) } + return nil } @@ -135,7 +148,17 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) }, opts.retryOpts); err != nil { return errors.Wrapf(err, "Error reading OCI-formatted configuration data") } - err = json.NewEncoder(stdout).Encode(config) + if report.IsJSON(opts.format) || opts.format == "" { + var out []byte + out, err = json.MarshalIndent(config, "", " ") + if err == nil { + fmt.Fprintf(stdout, "%s\n", string(out)) + } + } else { + row := "{{range . }}" + report.NormalizeFormat(opts.format) + "{{end}}" + data = append(data, config) + err = printTmpl(row, data) + } if err != nil { return errors.Wrapf(err, "Error writing OCI-formatted configuration data to standard output") } @@ -187,10 +210,30 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) logrus.Warnf("Registry disallows tag list retrieval; skipping") } } - out, err := json.MarshalIndent(outputData, "", " ") + if report.IsJSON(opts.format) || opts.format == "" { + out, err := json.MarshalIndent(outputData, "", " ") + if err == nil { + fmt.Fprintf(stdout, "%s\n", string(out)) + } + return err + } + row := "{{range . }}" + report.NormalizeFormat(opts.format) + "{{end}}" + data = append(data, outputData) + return printTmpl(row, data) +} + +func inspectNormalize(row string) string { + r := strings.NewReplacer( + ".ImageID", ".Image", + ) + return r.Replace(row) +} + +func printTmpl(row string, data []interface{}) error { + t, err := template.New("skopeo inspect").Parse(row) if err != nil { return err } - fmt.Fprintf(stdout, "%s\n", string(out)) - return nil + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + return t.Execute(w, data) } diff --git a/completions/bash/skopeo b/completions/bash/skopeo index 05bfa6dd..ffc34d64 100644 --- a/completions/bash/skopeo +++ b/completions/bash/skopeo @@ -75,6 +75,7 @@ _skopeo_inspect() { --authfile --creds --cert-dir + --format --retry-times --registry-token " diff --git a/docs/skopeo-inspect.1.md b/docs/skopeo-inspect.1.md index 0659849e..5400fb74 100644 --- a/docs/skopeo-inspect.1.md +++ b/docs/skopeo-inspect.1.md @@ -4,38 +4,58 @@ skopeo\-inspect - Return low-level information about _image-name_ in a registry. ## SYNOPSIS -**skopeo inspect** [**--raw**] [**--config**] _image-name_ +**skopeo inspect** [*options*] _image-name_ + +## DESCRIPTION Return low-level information about _image-name_ in a registry - **--raw** output raw manifest, default is to format in JSON +_image-name_ name of image to retrieve information about - _image-name_ name of image to retrieve information about +## OPTIONS - **--config** output configuration in OCI format, default is to format in JSON +**--authfile** _path_ - _image-name_ name of image to retrieve configuration for +Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `skopeo login`. +If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. - **--config** **--raw** output configuration in raw format +**--cert-dir** _path_ - _image-name_ name of image to retrieve configuration for +Use certificates at _path_ (\*.crt, \*.cert, \*.key) to connect to the registry. - **--authfile** _path_ +**--config** - Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `skopeo login`. - If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. +Output configuration in OCI format, default is to format in JSON format. - **--creds** _username[:password]_ for accessing the registry. +**--creds** _username[:password]_ - **--cert-dir** _path_ Use certificates at _path_ (\*.crt, \*.cert, \*.key) to connect to the registry. +Username and password for accessing the registry. - **--retry-times** the number of times to retry, retry wait time will be exponentially increased based on the number of failed attempts. +**--format**, **-f**=*format* - **--tls-verify** _bool-value_ Require HTTPS and verify certificates when talking to container registries (defaults to true). +Format the output using the given Go template. +The keys of the returned JSON can be used as the values for the --format flag (see examples below). - **--no-creds** _bool-value_ Access the registry anonymously. +**--no-creds** - **--registry-token** _Bearer token_ for accessing the registry. +Access the registry anonymously. + +**--raw** + +Output raw manifest or config data depending on --config option. +The --format option is not supported with --raw option. + +**--registry-token** _Bearer token_ + +Registry token for accessing the registry. + +**--retry-times** + +The number of times to retry; retry wait time will be exponentially increased based on the number of failed attempts. + +**--tls-verify** + +Require HTTPS and verify certificates when talking to container registries (defaults to true). ## EXAMPLES @@ -46,14 +66,14 @@ $ skopeo inspect docker://docker.io/fedora "Name": "docker.io/library/fedora", "Digest": "sha256:a97914edb6ba15deb5c5acf87bd6bd5b6b0408c96f48a5cbd450b5b04509bb7d", "RepoTags": [ - "20", - "21", - "22", - "23", - "24", - "heisenbug", - "latest", - "rawhide" + "20", + "21", + "22", + "23", + "24", + "heisenbug", + "latest", + "rawhide" ], "Created": "2016-06-20T19:33:43.220526898Z", "DockerVersion": "1.10.3", @@ -61,15 +81,24 @@ $ skopeo inspect docker://docker.io/fedora "Architecture": "amd64", "Os": "linux", "Layers": [ - "sha256:7c91a140e7a1025c3bc3aace4c80c0d9933ac4ee24b8630a6b0b5d8b9ce6b9d4" + "sha256:7c91a140e7a1025c3bc3aace4c80c0d9933ac4ee24b8630a6b0b5d8b9ce6b9d4" ] } ``` +``` +$ /bin/skopeo inspect --config docker://registry.fedoraproject.org/fedora --format "{{ .Architecture }}" +amd64 +``` + +``` +$ /bin/skopeo inspect --format '{{ .Env }}' docker://registry.access.redhat.com/ubi8 +[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin container=oci] +``` + # SEE ALSO skopeo(1), skopeo-login(1), docker-login(1), containers-auth.json(5) ## AUTHORS Antonio Murdaca , Miloslav Trmac , Jhon Honce - diff --git a/vendor/github.com/containers/common/pkg/report/camelcase/LICENSE.md b/vendor/github.com/containers/common/pkg/report/camelcase/LICENSE.md new file mode 100644 index 00000000..aa4a536c --- /dev/null +++ b/vendor/github.com/containers/common/pkg/report/camelcase/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Fatih Arslan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/containers/common/pkg/report/camelcase/README.md b/vendor/github.com/containers/common/pkg/report/camelcase/README.md new file mode 100644 index 00000000..105a6ae3 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/report/camelcase/README.md @@ -0,0 +1,58 @@ +# CamelCase [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/camelcase) [![Build Status](http://img.shields.io/travis/fatih/camelcase.svg?style=flat-square)](https://travis-ci.org/fatih/camelcase) + +CamelCase is a Golang (Go) package to split the words of a camelcase type +string into a slice of words. It can be used to convert a camelcase word (lower +or upper case) into any type of word. + +## Splitting rules: + +1. If string is not valid UTF-8, return it without splitting as + single item array. +2. Assign all unicode characters into one of 4 sets: lower case + letters, upper case letters, numbers, and all other characters. +3. Iterate through characters of string, introducing splits + between adjacent characters that belong to different sets. +4. Iterate through array of split strings, and if a given string + is upper case: + * if subsequent string is lower case: + * move last character of upper case string to beginning of + lower case string + +## Install + +```bash +go get github.com/fatih/camelcase +``` + +## Usage and examples + +```go +splitted := camelcase.Split("GolangPackage") + +fmt.Println(splitted[0], splitted[1]) // prints: "Golang", "Package" +``` + +Both lower camel case and upper camel case are supported. For more info please +check: [http://en.wikipedia.org/wiki/CamelCase](http://en.wikipedia.org/wiki/CamelCase) + +Below are some example cases: + +``` +"" => [] +"lowercase" => ["lowercase"] +"Class" => ["Class"] +"MyClass" => ["My", "Class"] +"MyC" => ["My", "C"] +"HTML" => ["HTML"] +"PDFLoader" => ["PDF", "Loader"] +"AString" => ["A", "String"] +"SimpleXMLParser" => ["Simple", "XML", "Parser"] +"vimRPCPlugin" => ["vim", "RPC", "Plugin"] +"GL11Version" => ["GL", "11", "Version"] +"99Bottles" => ["99", "Bottles"] +"May5" => ["May", "5"] +"BFG9000" => ["BFG", "9000"] +"BöseÜberraschung" => ["Böse", "Überraschung"] +"Two spaces" => ["Two", " ", "spaces"] +"BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] +``` diff --git a/vendor/github.com/containers/common/pkg/report/camelcase/camelcase.go b/vendor/github.com/containers/common/pkg/report/camelcase/camelcase.go new file mode 100644 index 00000000..0a82d100 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/report/camelcase/camelcase.go @@ -0,0 +1,91 @@ +// Package camelcase is a micro package to split the words of a camelcase type +// string into a slice of words. +package camelcase + +import ( + "unicode" + "unicode/utf8" +) + +// Split splits the camelcase word and returns a list of words. It also +// supports digits. Both lower camel case and upper camel case are supported. +// For more info please check: http://en.wikipedia.org/wiki/CamelCase +// +// Examples +// +// "" => [""] +// "lowercase" => ["lowercase"] +// "Class" => ["Class"] +// "MyClass" => ["My", "Class"] +// "MyC" => ["My", "C"] +// "HTML" => ["HTML"] +// "PDFLoader" => ["PDF", "Loader"] +// "AString" => ["A", "String"] +// "SimpleXMLParser" => ["Simple", "XML", "Parser"] +// "vimRPCPlugin" => ["vim", "RPC", "Plugin"] +// "GL11Version" => ["GL", "11", "Version"] +// "99Bottles" => ["99", "Bottles"] +// "May5" => ["May", "5"] +// "BFG9000" => ["BFG", "9000"] +// "BöseÜberraschung" => ["Böse", "Überraschung"] +// "Two spaces" => ["Two", " ", "spaces"] +// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] +// +// Splitting rules +// +// 1) If string is not valid UTF-8, return it without splitting as +// single item array. +// 2) Assign all unicode characters into one of 4 sets: lower case +// letters, upper case letters, numbers, and all other characters. +// 3) Iterate through characters of string, introducing splits +// between adjacent characters that belong to different sets. +// 4) Iterate through array of split strings, and if a given string +// is upper case: +// if subsequent string is lower case: +// move last character of upper case string to beginning of +// lower case string +func Split(src string) (entries []string) { + // don't split invalid utf8 + if !utf8.ValidString(src) { + return []string{src} + } + entries = []string{} + var runes [][]rune + lastClass := 0 + class := 0 + // split into fields based on class of unicode character + for _, r := range src { + switch { + case unicode.IsLower(r): + class = 1 + case unicode.IsUpper(r): + class = 2 + case unicode.IsDigit(r): + class = 3 + default: + class = 4 + } + if class == lastClass { + runes[len(runes)-1] = append(runes[len(runes)-1], r) + } else { + runes = append(runes, []rune{r}) + } + lastClass = class + } + // handle upper case -> lower case sequences, e.g. + // "PDFL", "oader" -> "PDF", "Loader" + for i := 0; i < len(runes)-1; i++ { + if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) { + runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) + runes[i] = runes[i][:len(runes[i])-1] + } + } + // construct []string from results + for _, s := range runes { + if len(s) > 0 { + entries = append(entries, string(s)) + } + } + + return entries +} diff --git a/vendor/github.com/containers/common/pkg/report/doc.go b/vendor/github.com/containers/common/pkg/report/doc.go new file mode 100644 index 00000000..60d954d7 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/report/doc.go @@ -0,0 +1,46 @@ +/* +Package report provides helper structs/methods/funcs for formatting output + +To format output for an array of structs: + + w := report.NewWriterDefault(os.Stdout) + defer w.Flush() + + headers := report.Headers(struct { + ID string + }{}, nil) + t, _ := report.NewTemplate("command name").Parse("{{range .}}{{.ID}}{{end}}") + t.Execute(t, headers) + t.Execute(t, map[string]string{ + "ID":"fa85da03b40141899f3af3de6d27852b", + }) + // t.IsTable() == false + +or + + w := report.NewWriterDefault(os.Stdout) + defer w.Flush() + + headers := report.Headers(struct { + CID string + }{}, map[string]string{ + "CID":"ID"}) + t, _ := report.NewTemplate("command name").Parse("table {{.CID}}") + t.Execute(t, headers) + t.Execute(t,map[string]string{ + "CID":"fa85da03b40141899f3af3de6d27852b", + }) + // t.IsTable() == true + +Helpers: + + if report.IsJSON(cmd.Flag("format").Value.String()) { + ... process JSON and output + } + +and + + +Note: Your code should not ignore errors +*/ +package report diff --git a/vendor/github.com/containers/common/pkg/report/template.go b/vendor/github.com/containers/common/pkg/report/template.go new file mode 100644 index 00000000..551fbb3c --- /dev/null +++ b/vendor/github.com/containers/common/pkg/report/template.go @@ -0,0 +1,115 @@ +package report + +import ( + "reflect" + "strings" + "text/template" + + "github.com/containers/common/pkg/report/camelcase" +) + +// Template embeds template.Template to add functionality to methods +type Template struct { + *template.Template + isTable bool +} + +// FuncMap is aliased from template.FuncMap +type FuncMap template.FuncMap + +// tableReplacer will remove 'table ' prefix and clean up tabs +var tableReplacer = strings.NewReplacer( + "table ", "", + `\t`, "\t", + `\n`, "\n", + " ", "\t", +) + +// escapedReplacer will clean up escaped characters from CLI +var escapedReplacer = strings.NewReplacer( + `\t`, "\t", + `\n`, "\n", +) + +// NormalizeFormat reads given go template format provided by CLI and munges it into what we need +func NormalizeFormat(format string) string { + var f string + // two replacers used so we only remove the prefix keyword `table` + if strings.HasPrefix(format, "table ") { + f = tableReplacer.Replace(format) + } else { + f = escapedReplacer.Replace(format) + } + + if !strings.HasSuffix(f, "\n") { + f += "\n" + } + return f +} + +// Headers queries the interface for field names. +// Array of map is returned to support range templates +// Note: unexported fields can be supported by adding field to overrides +// Note: It is left to the developer to write out said headers +// Podman commands use the general rules of: +// 1) unchanged --format includes headers +// 2) --format '{{.ID}" # no headers +// 3) --format 'table {{.ID}}' # includes headers +func Headers(object interface{}, overrides map[string]string) []map[string]string { + value := reflect.ValueOf(object) + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + // Column header will be field name upper-cased. + headers := make(map[string]string, value.NumField()) + for i := 0; i < value.Type().NumField(); i++ { + field := value.Type().Field(i) + // Recurse to find field names from promoted structs + if field.Type.Kind() == reflect.Struct && field.Anonymous { + h := Headers(reflect.New(field.Type).Interface(), nil) + for k, v := range h[0] { + headers[k] = v + } + continue + } + name := strings.Join(camelcase.Split(field.Name), " ") + headers[field.Name] = strings.ToUpper(name) + } + + if len(overrides) > 0 { + // Override column header as provided + for k, v := range overrides { + headers[k] = strings.ToUpper(v) + } + } + return []map[string]string{headers} +} + +// NewTemplate creates a new template object +func NewTemplate(name string) *Template { + return &Template{template.New(name), false} +} + +// Parse parses text as a template body for t +func (t *Template) Parse(text string) (*Template, error) { + if strings.HasPrefix(text, "table ") { + t.isTable = true + text = "{{range .}}" + NormalizeFormat(text) + "{{end}}" + } else { + text = NormalizeFormat(text) + } + + tt, err := t.Template.Parse(text) + return &Template{tt, t.isTable}, err +} + +// Funcs adds the elements of the argument map to the template's function map +func (t *Template) Funcs(funcMap FuncMap) *Template { + return &Template{t.Template.Funcs(template.FuncMap(funcMap)), t.isTable} +} + +// IsTable returns true if format string defines a "table" +func (t *Template) IsTable() bool { + return t.isTable +} diff --git a/vendor/github.com/containers/common/pkg/report/validate.go b/vendor/github.com/containers/common/pkg/report/validate.go new file mode 100644 index 00000000..a5eac532 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/report/validate.go @@ -0,0 +1,13 @@ +package report + +import "regexp" + +var jsonRegex = regexp.MustCompile(`^\s*(json|{{\s*json\s*(\.)?\s*}})\s*$`) + +// JSONFormat test CLI --format string to be a JSON request +// if report.IsJSON(cmd.Flag("format").Value.String()) { +// ... process JSON and output +// } +func IsJSON(s string) bool { + return jsonRegex.MatchString(s) +} diff --git a/vendor/github.com/containers/common/pkg/report/writer.go b/vendor/github.com/containers/common/pkg/report/writer.go new file mode 100644 index 00000000..360ef826 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/report/writer.go @@ -0,0 +1,27 @@ +package report + +import ( + "io" + "text/tabwriter" +) + +// Writer aliases tabwriter.Writer to provide Podman defaults +type Writer struct { + *tabwriter.Writer +} + +// NewWriter initializes a new report.Writer with given values +func NewWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) (*Writer, error) { + t := tabwriter.NewWriter(output, minwidth, tabwidth, padding, padchar, flags) + return &Writer{t}, nil +} + +// NewWriterDefault initializes a new report.Writer with Podman defaults +func NewWriterDefault(output io.Writer) (*Writer, error) { + return NewWriter(output, 12, 2, 2, ' ', 0) +} + +// Flush any output left in buffers +func (w *Writer) Flush() error { + return w.Writer.Flush() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 50b54e1b..77a12ae3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -38,6 +38,8 @@ github.com/containerd/containerd/errdefs github.com/containers/common/pkg/auth github.com/containers/common/pkg/capabilities github.com/containers/common/pkg/completion +github.com/containers/common/pkg/report +github.com/containers/common/pkg/report/camelcase github.com/containers/common/pkg/retry # github.com/containers/image/v5 v5.7.0 github.com/containers/image/v5/copy