automation-tests/cmd/podman/images/list.go

385 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
}