From 1d7cb7cc48d06631e2bdfd0e25eeccc8e87b042f Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Mon, 23 Mar 2020 09:04:31 -0700 Subject: V2 podman images/image list * Updated entities to support flags/options * Updated bindings caused by entities changes * Removed handlers.ImageSummary in favor of entities.ImageSummary * Introduced StringSet() container object to simply error checking Signed-off-by: Jhon Honce --- cmd/podmanV2/images/images.go | 21 +--- cmd/podmanV2/images/list.go | 213 +++++++++++++++++++++++++++++++++++++- cmd/podmanV2/registry/registry.go | 8 ++ cmd/podmanV2/report/templates.go | 29 ++++-- 4 files changed, 242 insertions(+), 29 deletions(-) (limited to 'cmd') diff --git a/cmd/podmanV2/images/images.go b/cmd/podmanV2/images/images.go index a1e56396a..719846b4c 100644 --- a/cmd/podmanV2/images/images.go +++ b/cmd/podmanV2/images/images.go @@ -9,17 +9,16 @@ import ( ) var ( - // podman _images_ + // podman _images_ Alias for podman image _list_ imagesCmd = &cobra.Command{ Use: strings.Replace(listCmd.Use, "list", "images", 1), + Args: listCmd.Args, Short: listCmd.Short, Long: listCmd.Long, PersistentPreRunE: preRunE, - RunE: images, + RunE: listCmd.RunE, Example: strings.Replace(listCmd.Example, "podman image list", "podman images", -1), } - - imagesOpts = entities.ImageListOptions{} ) func init() { @@ -30,17 +29,5 @@ func init() { imagesCmd.SetHelpTemplate(registry.HelpTemplate()) imagesCmd.SetUsageTemplate(registry.UsageTemplate()) - flags := imagesCmd.Flags() - flags.BoolVarP(&imagesOpts.All, "all", "a", false, "Show all images (default hides intermediate images)") - flags.BoolVar(&imagesOpts.Digests, "digests", false, "Show digests") - flags.StringSliceVarP(&imagesOpts.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") - flags.StringVar(&imagesOpts.Format, "format", "", "Change the output format to JSON or a Go template") - flags.BoolVarP(&imagesOpts.Noheading, "noheading", "n", false, "Do not print column headings") - // TODO Need to learn how to deal with second name being a string instead of a char. - // This needs to be "no-trunc, notruncate" - flags.BoolVar(&imagesOpts.NoTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&imagesOpts.NoTrunc, "notruncate", false, "Do not truncate output") - flags.BoolVarP(&imagesOpts.Quiet, "quiet", "q", false, "Display only image IDs") - flags.StringVar(&imagesOpts.Sort, "sort", "created", "Sort by created, id, repository, size, or tag") - flags.BoolVarP(&imagesOpts.History, "history", "", false, "Display the image name history") + imageListFlagSet(imagesCmd.Flags()) } diff --git a/cmd/podmanV2/images/list.go b/cmd/podmanV2/images/list.go index 0441f8fd8..4714af3e4 100644 --- a/cmd/podmanV2/images/list.go +++ b/cmd/podmanV2/images/list.go @@ -1,16 +1,40 @@ package images import ( + "errors" + "fmt" + "os" + "sort" + "strings" + "text/tabwriter" + "text/template" + "time" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/report" "github.com/containers/libpod/pkg/domain/entities" + jsoniter "github.com/json-iterator/go" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) +type listFlagType struct { + format string + history bool + noHeading bool + noTrunc bool + quiet bool + sort string + readOnly bool + digests bool +} + var ( // Command: podman image _list_ listCmd = &cobra.Command{ Use: "list [flag] [IMAGE]", Aliases: []string{"ls"}, + Args: cobra.MaximumNArgs(1), Short: "List images in local storage", Long: "Lists images previously pulled to the system or created on the system.", RunE: images, @@ -18,6 +42,19 @@ var ( podman image list --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}" podman image list --filter dangling=true`, } + + // Options to pull data + listOptions = entities.ImageListOptions{} + + // Options for presenting data + listFlag = listFlagType{} + + sortFields = entities.NewStringSet( + "created", + "id", + "repository", + "size", + "tag") ) func init() { @@ -26,9 +63,181 @@ func init() { Command: listCmd, Parent: imageCmd, }) + imageListFlagSet(listCmd.Flags()) +} + +func imageListFlagSet(flags *pflag.FlagSet) { + flags.BoolVarP(&listOptions.All, "all", "a", false, "Show all images (default hides intermediate images)") + flags.StringSliceVarP(&listOptions.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") + flags.StringVar(&listFlag.format, "format", "", "Change the output format to JSON or a Go template") + flags.BoolVar(&listFlag.digests, "digests", false, "Show digests") + flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings") + flags.BoolVar(&listFlag.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&listFlag.noTrunc, "notruncate", false, "Do not truncate output") + flags.BoolVarP(&listFlag.quiet, "quiet", "q", false, "Display only image IDs") + flags.StringVar(&listFlag.sort, "sort", "created", "Sort by "+sortFields.String()) + flags.BoolVarP(&listFlag.history, "history", "", false, "Display the image name history") } func images(cmd *cobra.Command, args []string) error { - _, _ = registry.Options(cmd) - return nil + if len(listOptions.Filter) > 0 && len(args) > 0 { + return errors.New("cannot specify an image and a filter(s)") + } + + if len(listOptions.Filter) < 1 && len(args) > 0 { + listOptions.Filter = append(listOptions.Filter, "reference="+args[0]) + } + + if cmd.Flag("sort").Changed && !sortFields.Contains(listFlag.sort) { + return fmt.Errorf("\"%s\" is not a valid field for sorting. Choose from: %s", + listFlag.sort, sortFields.String()) + } + + summaries, err := registry.ImageEngine().List(registry.GetContext(), listOptions) + if err != nil { + return err + } + + imageS := summaries + sort.Slice(imageS, sortFunc(listFlag.sort, imageS)) + + if cmd.Flag("format").Changed && listFlag.format == "json" { + return writeJSON(imageS) + } else { + return writeTemplate(imageS, err) + } +} + +func writeJSON(imageS []*entities.ImageSummary) error { + type image struct { + entities.ImageSummary + Created string + } + + imgs := make([]image, 0, len(imageS)) + for _, e := range imageS { + var h image + h.ImageSummary = *e + h.Created = time.Unix(e.Created, 0).Format(time.RFC3339) + h.RepoTags = nil + + imgs = append(imgs, h) + } + + json := jsoniter.ConfigCompatibleWithStandardLibrary + enc := json.NewEncoder(os.Stdout) + return enc.Encode(imgs) +} + +func writeTemplate(imageS []*entities.ImageSummary, err error) error { + type image struct { + entities.ImageSummary + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + } + + imgs := make([]image, 0, len(imageS)) + for _, e := range imageS { + for _, tag := range e.RepoTags { + var h image + h.ImageSummary = *e + h.Repository, h.Tag = tokenRepoTag(tag) + imgs = append(imgs, h) + } + if e.IsReadOnly() { + listFlag.readOnly = true + } + } + + hdr, row := imageListFormat(listFlag) + format := hdr + "{{range . }}" + row + "{{end}}" + + tmpl := template.Must(template.New("report").Funcs(report.PodmanTemplateFuncs()).Parse(format)) + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer w.Flush() + return tmpl.Execute(w, imgs) +} + +func tokenRepoTag(tag string) (string, string) { + tokens := strings.SplitN(tag, ":", 2) + switch len(tokens) { + case 0: + return tag, "" + case 1: + return tokens[0], "" + case 2: + return tokens[0], tokens[1] + default: + return "", "" + } +} + +func sortFunc(key string, data []*entities.ImageSummary) func(i, j int) bool { + switch key { + case "id": + return func(i, j int) bool { + return data[i].ID < data[j].ID + } + case "repository": + return func(i, j int) bool { + return data[i].RepoTags[0] < data[j].RepoTags[0] + } + case "size": + return func(i, j int) bool { + return data[i].Size < data[j].Size + } + case "tag": + return func(i, j int) bool { + return data[i].RepoTags[0] < data[j].RepoTags[0] + } + default: + // case "created": + return func(i, j int) bool { + return data[i].Created >= data[j].Created + } + } +} + +func imageListFormat(flags listFlagType) (string, string) { + if flags.quiet { + return "", "{{slice .ID 0 12}}\n" + } + + // Defaults + hdr := "REPOSITORY\tTAG" + row := "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}{{end}}" + + if flags.digests { + hdr += "\tDIGEST" + row += "\t{{.Digest}}" + } + + hdr += "\tID" + if flags.noTrunc { + row += "\tsha256:{{.ID}}" + } else { + row += "\t{{slice .ID 0 12}}" + } + + hdr += "\tCREATED\tSIZE" + row += "\t{{humanDuration .Created}}\t{{humanSize .Size}}" + + if flags.history { + hdr += "\tHISTORY" + row += "\t{{if .History}}{{join .History \", \"}}{{else}}{{end}}" + } + + if flags.readOnly { + hdr += "\tReadOnly" + row += "\t{{.ReadOnly}}" + } + + if flags.noHeading { + hdr = "" + } else { + hdr += "\n" + } + + row += "\n" + return hdr, row } diff --git a/cmd/podmanV2/registry/registry.go b/cmd/podmanV2/registry/registry.go index 9c12b2456..f0650a7cf 100644 --- a/cmd/podmanV2/registry/registry.go +++ b/cmd/podmanV2/registry/registry.go @@ -21,6 +21,7 @@ var ( imageEngine entities.ImageEngine containerEngine entities.ContainerEngine + cliCtx context.Context EngineOptions entities.EngineOptions @@ -125,3 +126,10 @@ func Options(cmd *cobra.Command) (*entities.EngineOptions, error) { } return nil, nil } + +func GetContext() context.Context { + if cliCtx == nil { + cliCtx = context.TODO() + } + return cliCtx +} diff --git a/cmd/podmanV2/report/templates.go b/cmd/podmanV2/report/templates.go index dc43d4f9b..f3bc06405 100644 --- a/cmd/podmanV2/report/templates.go +++ b/cmd/podmanV2/report/templates.go @@ -4,6 +4,7 @@ import ( "strings" "text/template" "time" + "unicode" "github.com/docker/go-units" ) @@ -15,7 +16,24 @@ var defaultFuncMap = template.FuncMap{ } return s }, - // TODO: Remove on Go 1.14 port + "humanDuration": func(t int64) string { + return units.HumanDuration(time.Since(time.Unix(t, 0))) + " ago" + }, + "humanSize": func(sz int64) string { + s := units.HumanSizeWithPrecision(float64(sz), 3) + i := strings.LastIndexFunc(s, unicode.IsNumber) + return s[:i+1] + " " + s[i+1:] + }, + "join": strings.Join, + "lower": strings.ToLower, + "rfc3339": func(t int64) string { + return time.Unix(t, 0).Format(time.RFC3339) + }, + "replace": strings.Replace, + "split": strings.Split, + "title": strings.Title, + "upper": strings.ToUpper, + // TODO: Remove after Go 1.14 port "slice": func(s string, i, j int) string { if i > j || len(s) < i { return s @@ -25,15 +43,6 @@ var defaultFuncMap = template.FuncMap{ } return s[i:j] }, - "toRFC3339": func(t int64) string { - return time.Unix(t, 0).Format(time.RFC3339) - }, - "toHumanDuration": func(t int64) string { - return units.HumanDuration(time.Since(time.Unix(t, 0))) + " ago" - }, - "toHumanSize": func(sz int64) string { - return units.HumanSize(float64(sz)) - }, } func ReportHeader(columns ...string) []byte { -- cgit v1.2.3-54-g00ecf