spiegel_podman/cmd/podman/images/list.go
Paul Holzinger 34de0feda5
cmd/podman: refactor Context handling
The PodmanOptionsKey is never used anywhere so it is pointless to add
this. Second having several functions to return the same context makes
no sense so fold them all into one. Lastly create the context once and
always return the same one instead of having to nil check each time.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2025-02-12 14:01:10 +01:00

384 lines
9.3 KiB
Go

package images
import (
"cmp"
"errors"
"fmt"
"os"
"slices"
"strings"
"time"
"unicode"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/docker/go-units"
"github.com/spf13/cobra"
)
type listFlagType struct {
format string
history bool
noHeading bool
noTrunc bool
quiet bool
sort string
readOnly bool
digests bool
}
var (
imageListCmd = &cobra.Command{
Use: "list [options] [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,
ValidArgsFunction: common.AutocompleteImages,
Example: `podman image list --format json
podman image list --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}"
podman image list --filter dangling=true`,
}
imagesCmd = &cobra.Command{
Use: "images [options] [IMAGE]",
Args: imageListCmd.Args,
Short: imageListCmd.Short,
Long: imageListCmd.Long,
RunE: imageListCmd.RunE,
ValidArgsFunction: imageListCmd.ValidArgsFunction,
Example: `podman images --format json
podman images --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}"
podman images --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() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: imageListCmd,
Parent: imageCmd,
})
imageListFlagSet(imageListCmd)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: imagesCmd,
})
imageListFlagSet(imagesCmd)
}
func imageListFlagSet(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&listOptions.All, "all", "a", false, "Show all images (default hides intermediate images)")
filterFlagName := "filter"
flags.StringArrayVarP(&listOptions.Filter, filterFlagName, "f", []string{}, "Filter output based on conditions provided (default [])")
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteImageFilters)
formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, "", "Change the output format to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&imageReporter{}))
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.BoolVarP(&listFlag.quiet, "quiet", "q", false, "Display only image IDs")
sortFlagName := "sort"
flags.StringVar(&listFlag.sort, sortFlagName, "created", "Sort by "+sortFields.String())
_ = cmd.RegisterFlagCompletionFunc(sortFlagName, completion.AutocompleteNone)
flags.BoolVarP(&listFlag.history, "history", "", false, "Display the image name history")
}
func images(cmd *cobra.Command, args []string) error {
if len(listOptions.Filter) > 0 && len(args) > 0 {
return errors.New("cannot specify an image and a filter(s)")
}
if len(args) > 0 {
listOptions.Filter = append(listOptions.Filter, "reference="+args[0])
}
if cmd.Flags().Changed("sort") && !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.Context(), listOptions)
if err != nil {
return err
}
imgs, err := sortImages(summaries)
if err != nil {
return err
}
switch {
case report.IsJSON(listFlag.format):
return writeJSON(imgs)
case listFlag.quiet:
return writeID(imgs)
default:
if cmd.Flags().Changed("format") && !report.HasTable(listFlag.format) {
listFlag.noHeading = true
}
return writeTemplate(cmd, imgs)
}
}
func writeID(imgs []imageReporter) error {
lookup := make(map[string]struct{}, len(imgs))
ids := make([]string, 0)
for _, e := range imgs {
if _, found := lookup[e.ID()]; !found {
lookup[e.ID()] = struct{}{}
ids = append(ids, e.ID())
}
}
for _, k := range ids {
fmt.Println(k)
}
return nil
}
func writeJSON(images []imageReporter) error {
type image struct {
entities.ImageSummary
Created int64
CreatedAt string
}
imgs := make([]image, 0, len(images))
for _, e := range images {
var h image
h.ImageSummary = e.ImageSummary
h.Created = e.ImageSummary.Created
h.CreatedAt = e.created().Format(time.RFC3339Nano)
h.RepoTags = nil
imgs = append(imgs, h)
}
prettyJSON, err := json.MarshalIndent(imgs, "", " ")
if err != nil {
return err
}
fmt.Println(string(prettyJSON))
return nil
}
func writeTemplate(cmd *cobra.Command, imgs []imageReporter) error {
hdrs := report.Headers(imageReporter{}, map[string]string{
"ID": "IMAGE ID",
"ReadOnly": "R/O",
})
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
var err error
if cmd.Flags().Changed("format") {
rpt, err = rpt.Parse(report.OriginUser, cmd.Flag("format").Value.String())
} else {
rpt, err = rpt.Parse(report.OriginPodman, lsFormatFromFlags(listFlag))
}
if err != nil {
return err
}
if rpt.RenderHeaders && !listFlag.noHeading {
if err := rpt.Execute(hdrs); err != nil {
return err
}
}
return rpt.Execute(imgs)
}
func sortImages(imageS []*entities.ImageSummary) ([]imageReporter, error) {
imgs := make([]imageReporter, 0, len(imageS))
var err error
for _, e := range imageS {
var h imageReporter
if len(e.RepoTags) > 0 {
tagged := []imageReporter{}
untagged := []imageReporter{}
for _, tag := range e.RepoTags {
h.ImageSummary = *e
h.Repository, h.Tag, err = tokenRepoTag(tag)
if err != nil {
return nil, fmt.Errorf("parsing repository tag: %q: %w", tag, err)
}
if h.Tag == "<none>" {
untagged = append(untagged, h)
} else {
tagged = append(tagged, h)
}
}
// Note: we only want to display "<none>" if we
// couldn't find any tagged name in RepoTags.
if len(tagged) > 0 {
imgs = append(imgs, tagged...)
} else {
imgs = append(imgs, untagged[0])
}
} else {
h.ImageSummary = *e
h.Repository = "<none>"
h.Tag = "<none>"
imgs = append(imgs, h)
}
listFlag.readOnly = e.IsReadOnly()
}
slices.SortFunc(imgs, func(a, b imageReporter) int {
switch listFlag.sort {
case "id":
return cmp.Compare(a.ID(), b.ID())
case "repository":
r := cmp.Compare(a.Repository, b.Repository)
if r == 0 {
// if repository is the same imply tag sorting
return cmp.Compare(a.Tag, b.Tag)
}
return r
case "size":
return cmp.Compare(a.size(), b.size())
case "tag":
return cmp.Compare(a.Tag, b.Tag)
case "created":
if a.created().After(b.created()) {
return -1
}
if a.created().Equal(b.created()) {
return 0
}
return 1
default:
return 0
}
})
return imgs, err
}
func tokenRepoTag(ref string) (string, string, error) {
if ref == "<none>:<none>" {
return "<none>", "<none>", nil
}
repo, err := reference.Parse(ref)
if err != nil {
return "<none>", "<none>", err
}
named, ok := repo.(reference.Named)
if !ok {
return ref, "<none>", nil
}
name := named.Name()
if name == "" {
name = "<none>"
}
tagged, ok := repo.(reference.Tagged)
if !ok {
return name, "<none>", nil
}
tag := tagged.Tag()
if tag == "" {
tag = "<none>"
}
return name, tag, nil
}
func lsFormatFromFlags(flags listFlagType) string {
row := []string{
"{{if .Repository}}{{.Repository}}{{else}}<none>{{end}}",
"{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}",
}
if flags.digests {
row = append(row, "{{.Digest}}")
}
row = append(row, "{{.ID}}", "{{.Created}}", "{{.Size}}")
if flags.history {
row = append(row, "{{if .History}}{{.History}}{{else}}<none>{{end}}")
}
if flags.readOnly {
row = append(row, "{{.ReadOnly}}")
}
return "{{range . }}" + strings.Join(row, "\t") + "\n{{end -}}"
}
type imageReporter struct {
Repository string `json:"repository,omitempty"`
Tag string `json:"tag,omitempty"`
entities.ImageSummary
}
func (i imageReporter) ID() string {
if !listFlag.noTrunc && len(i.ImageSummary.ID) >= 12 {
return i.ImageSummary.ID[0:12]
}
return "sha256:" + i.ImageSummary.ID
}
func (i imageReporter) Created() string {
return units.HumanDuration(time.Since(i.created())) + " ago"
}
func (i imageReporter) created() time.Time {
return time.Unix(i.ImageSummary.Created, 0).UTC()
}
func (i imageReporter) Size() string {
s := units.HumanSizeWithPrecision(float64(i.ImageSummary.Size), 3)
j := strings.LastIndexFunc(s, unicode.IsNumber)
return s[:j+1] + " " + s[j+1:]
}
func (i imageReporter) History() string {
return strings.Join(i.ImageSummary.History, ", ")
}
func (i imageReporter) CreatedAt() string {
return i.created().String()
}
func (i imageReporter) CreatedSince() string {
return i.Created()
}
func (i imageReporter) CreatedTime() string {
return i.CreatedAt()
}
func (i imageReporter) size() int64 {
return i.ImageSummary.Size
}