mirror of
https://github.com/containers/skopeo.git
synced 2025-07-05 19:06:54 +00:00
Merge pull request #1070 from rhatdan/format
Add --format option to skopeo inspect
This commit is contained in:
commit
353f3a23e1
@ -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 {
|
||||
@ -106,12 +118,13 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing manifest to standard output: %v", err)
|
||||
}
|
||||
|
||||
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 +137,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 {
|
||||
@ -135,9 +148,19 @@ 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 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 +187,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,15 +205,35 @@ 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")
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ _skopeo_inspect() {
|
||||
--authfile
|
||||
--creds
|
||||
--cert-dir
|
||||
--format
|
||||
--retry-times
|
||||
--registry-token
|
||||
"
|
||||
|
@ -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 <runcom@redhat.com>, Miloslav Trmac <mitr@redhat.com>, Jhon Honce <jhonce@redhat.com>
|
||||
|
||||
|
20
vendor/github.com/containers/common/pkg/report/camelcase/LICENSE.md
generated
vendored
Normal file
20
vendor/github.com/containers/common/pkg/report/camelcase/LICENSE.md
generated
vendored
Normal file
@ -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.
|
58
vendor/github.com/containers/common/pkg/report/camelcase/README.md
generated
vendored
Normal file
58
vendor/github.com/containers/common/pkg/report/camelcase/README.md
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
# CamelCase [](http://godoc.org/github.com/fatih/camelcase) [](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"]
|
||||
```
|
91
vendor/github.com/containers/common/pkg/report/camelcase/camelcase.go
generated
vendored
Normal file
91
vendor/github.com/containers/common/pkg/report/camelcase/camelcase.go
generated
vendored
Normal file
@ -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
|
||||
}
|
46
vendor/github.com/containers/common/pkg/report/doc.go
generated
vendored
Normal file
46
vendor/github.com/containers/common/pkg/report/doc.go
generated
vendored
Normal file
@ -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
|
115
vendor/github.com/containers/common/pkg/report/template.go
generated
vendored
Normal file
115
vendor/github.com/containers/common/pkg/report/template.go
generated
vendored
Normal file
@ -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
|
||||
}
|
13
vendor/github.com/containers/common/pkg/report/validate.go
generated
vendored
Normal file
13
vendor/github.com/containers/common/pkg/report/validate.go
generated
vendored
Normal file
@ -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)
|
||||
}
|
27
vendor/github.com/containers/common/pkg/report/writer.go
generated
vendored
Normal file
27
vendor/github.com/containers/common/pkg/report/writer.go
generated
vendored
Normal file
@ -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()
|
||||
}
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -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.8.0
|
||||
github.com/containers/image/v5/copy
|
||||
|
Loading…
Reference in New Issue
Block a user