382 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
| package images
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"sort"
 | |
| 	"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.GetContext(), 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()
 | |
| 	}
 | |
| 
 | |
| 	sort.Slice(imgs, sortFunc(listFlag.sort, imgs))
 | |
| 	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 sortFunc(key string, data []imageReporter) 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].Repository < data[j].Repository
 | |
| 		}
 | |
| 	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].Tag < data[j].Tag
 | |
| 		}
 | |
| 	default:
 | |
| 		// case "created":
 | |
| 		return func(i, j int) bool {
 | |
| 			return data[i].created().After(data[j].created())
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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
 | |
| }
 |