github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/cmd/podman/images.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  	"unicode"
    11  
    12  	"github.com/containers/buildah/pkg/formats"
    13  	"github.com/containers/libpod/cmd/podman/cliconfig"
    14  	"github.com/containers/libpod/libpod/image"
    15  	"github.com/containers/libpod/pkg/adapter"
    16  	units "github.com/docker/go-units"
    17  	digest "github.com/opencontainers/go-digest"
    18  	"github.com/pkg/errors"
    19  	"github.com/sirupsen/logrus"
    20  	"github.com/spf13/cobra"
    21  )
    22  
    23  type imagesTemplateParams struct {
    24  	Repository   string
    25  	Tag          string
    26  	ID           string
    27  	Digest       digest.Digest
    28  	Digests      []digest.Digest
    29  	CreatedAt    time.Time
    30  	CreatedSince string
    31  	Size         string
    32  	ReadOnly     bool
    33  	History      string
    34  }
    35  
    36  type imagesJSONParams struct {
    37  	ID        string          `json:"ID"`
    38  	Name      []string        `json:"Names"`
    39  	Created   string          `json:"Created"`
    40  	Digest    digest.Digest   `json:"Digest"`
    41  	Digests   []digest.Digest `json:"Digests"`
    42  	CreatedAt time.Time       `json:"CreatedAt"`
    43  	Size      *uint64         `json:"Size"`
    44  	ReadOnly  bool            `json:"ReadOnly"`
    45  	History   []string        `json:"History"`
    46  }
    47  
    48  type imagesOptions struct {
    49  	quiet        bool
    50  	noHeading    bool
    51  	noTrunc      bool
    52  	digests      bool
    53  	format       string
    54  	outputformat string
    55  	sort         string
    56  	all          bool
    57  	history      bool
    58  }
    59  
    60  // Type declaration and functions for sorting the images output
    61  type imagesSorted []imagesTemplateParams
    62  
    63  func (a imagesSorted) Len() int      { return len(a) }
    64  func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
    65  
    66  type imagesSortedCreated struct{ imagesSorted }
    67  
    68  func (a imagesSortedCreated) Less(i, j int) bool {
    69  	return a.imagesSorted[i].CreatedAt.After(a.imagesSorted[j].CreatedAt)
    70  }
    71  
    72  type imagesSortedID struct{ imagesSorted }
    73  
    74  func (a imagesSortedID) Less(i, j int) bool { return a.imagesSorted[i].ID < a.imagesSorted[j].ID }
    75  
    76  type imagesSortedTag struct{ imagesSorted }
    77  
    78  func (a imagesSortedTag) Less(i, j int) bool { return a.imagesSorted[i].Tag < a.imagesSorted[j].Tag }
    79  
    80  type imagesSortedRepository struct{ imagesSorted }
    81  
    82  func (a imagesSortedRepository) Less(i, j int) bool {
    83  	return a.imagesSorted[i].Repository < a.imagesSorted[j].Repository
    84  }
    85  
    86  type imagesSortedSize struct{ imagesSorted }
    87  
    88  func (a imagesSortedSize) Less(i, j int) bool {
    89  	size1, _ := units.FromHumanSize(a.imagesSorted[i].Size)
    90  	size2, _ := units.FromHumanSize(a.imagesSorted[j].Size)
    91  	return size1 < size2
    92  }
    93  
    94  var (
    95  	imagesCommand     cliconfig.ImagesValues
    96  	imagesDescription = "Lists images previously pulled to the system or created on the system."
    97  
    98  	_imagesCommand = cobra.Command{
    99  		Use:   "images [flags] [IMAGE]",
   100  		Short: "List images in local storage",
   101  		Long:  imagesDescription,
   102  		RunE: func(cmd *cobra.Command, args []string) error {
   103  			imagesCommand.InputArgs = args
   104  			imagesCommand.GlobalFlags = MainGlobalOpts
   105  			imagesCommand.Remote = remoteclient
   106  			return imagesCmd(&imagesCommand)
   107  		},
   108  		Example: `podman images --format json
   109    podman images --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}"
   110    podman images --filter dangling=true`,
   111  	}
   112  )
   113  
   114  func imagesInit(command *cliconfig.ImagesValues) {
   115  	command.SetHelpTemplate(HelpTemplate())
   116  	command.SetUsageTemplate(UsageTemplate())
   117  
   118  	flags := command.Flags()
   119  	flags.BoolVarP(&command.All, "all", "a", false, "Show all images (default hides intermediate images)")
   120  	flags.BoolVar(&command.Digests, "digests", false, "Show digests")
   121  	flags.StringSliceVarP(&command.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])")
   122  	flags.StringVar(&command.Format, "format", "", "Change the output format to JSON or a Go template")
   123  	flags.BoolVarP(&command.Noheading, "noheading", "n", false, "Do not print column headings")
   124  	// TODO Need to learn how to deal with second name being a string instead of a char.
   125  	// This needs to be "no-trunc, notruncate"
   126  	flags.BoolVar(&command.NoTrunc, "no-trunc", false, "Do not truncate output")
   127  	flags.BoolVarP(&command.Quiet, "quiet", "q", false, "Display only image IDs")
   128  	flags.StringVar(&command.Sort, "sort", "created", "Sort by created, id, repository, size, or tag")
   129  	flags.BoolVarP(&command.History, "history", "", false, "Display the image name history")
   130  
   131  }
   132  
   133  func init() {
   134  	imagesCommand.Command = &_imagesCommand
   135  	imagesInit(&imagesCommand)
   136  }
   137  
   138  func imagesCmd(c *cliconfig.ImagesValues) error {
   139  	var (
   140  		image string
   141  	)
   142  
   143  	ctx := getContext()
   144  	runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand)
   145  	if err != nil {
   146  		return errors.Wrapf(err, "Could not get runtime")
   147  	}
   148  	defer runtime.DeferredShutdown(false)
   149  	if len(c.InputArgs) == 1 {
   150  		image = c.InputArgs[0]
   151  	}
   152  	if len(c.InputArgs) > 1 {
   153  		return errors.New("'podman images' requires at most 1 argument")
   154  	}
   155  	if len(c.Filter) > 0 && image != "" {
   156  		return errors.New("can not specify an image and a filter")
   157  	}
   158  	filters := c.Filter
   159  	if len(filters) < 1 && len(image) > 0 {
   160  		filters = append(filters, fmt.Sprintf("reference=%s", image))
   161  	}
   162  
   163  	var sortValues = map[string]bool{
   164  		"created":    true,
   165  		"id":         true,
   166  		"repository": true,
   167  		"size":       true,
   168  		"tag":        true,
   169  	}
   170  	if !sortValues[c.Sort] {
   171  		keys := make([]string, 0, len(sortValues))
   172  		for k := range sortValues {
   173  			keys = append(keys, k)
   174  		}
   175  		return errors.Errorf("invalid sort value %q,  required values: %s", c.Sort, strings.Join(keys, ", "))
   176  	}
   177  
   178  	opts := imagesOptions{
   179  		quiet:     c.Quiet,
   180  		noHeading: c.Noheading,
   181  		noTrunc:   c.NoTrunc,
   182  		digests:   c.Digests,
   183  		format:    c.Format,
   184  		sort:      c.Sort,
   185  		all:       c.All,
   186  		history:   c.History,
   187  	}
   188  
   189  	outputformat := opts.setOutputFormat()
   190  	// These fields were renamed, so we need to provide backward compat for
   191  	// the old names.
   192  	if strings.Contains(outputformat, "{{.Created}}") {
   193  		outputformat = strings.Replace(outputformat, "{{.Created}}", "{{.CreatedSince}}", -1)
   194  	}
   195  	if strings.Contains(outputformat, "{{.CreatedTime}}") {
   196  		outputformat = strings.Replace(outputformat, "{{.CreatedTime}}", "{{.CreatedAt}}", -1)
   197  	}
   198  	opts.outputformat = outputformat
   199  
   200  	filteredImages, err := runtime.GetFilteredImages(filters, false)
   201  	if err != nil {
   202  		return errors.Wrapf(err, "unable to get images")
   203  	}
   204  
   205  	for _, image := range filteredImages {
   206  		if image.IsReadOnly() {
   207  			opts.outputformat += "{{.ReadOnly}}\t"
   208  			break
   209  		}
   210  	}
   211  	return generateImagesOutput(ctx, filteredImages, opts)
   212  }
   213  
   214  func (i imagesOptions) setOutputFormat() string {
   215  	if i.format != "" {
   216  		// "\t" from the command line is not being recognized as a tab
   217  		// replacing the string "\t" to a tab character if the user passes in "\t"
   218  		return strings.Replace(i.format, `\t`, "\t", -1)
   219  	}
   220  	if i.quiet {
   221  		return formats.IDString
   222  	}
   223  	format := "table {{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t"
   224  	if i.noHeading {
   225  		format = "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t"
   226  	}
   227  	if i.digests {
   228  		format += "{{.Digest}}\t"
   229  	}
   230  	format += "{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t"
   231  	if i.history {
   232  		format += "{{if .History}}{{.History}}{{else}}<none>{{end}}\t"
   233  	}
   234  	return format
   235  }
   236  
   237  // imagesToGeneric creates an empty array of interfaces for output
   238  func imagesToGeneric(templParams []imagesTemplateParams, jsonParams []imagesJSONParams) []interface{} {
   239  	genericParams := []interface{}{}
   240  	if len(templParams) > 0 {
   241  		for _, v := range templParams {
   242  			genericParams = append(genericParams, interface{}(v))
   243  		}
   244  		return genericParams
   245  	}
   246  	for _, v := range jsonParams {
   247  		genericParams = append(genericParams, interface{}(v))
   248  	}
   249  	return genericParams
   250  }
   251  
   252  func sortImagesOutput(sortBy string, imagesOutput imagesSorted) imagesSorted {
   253  	switch sortBy {
   254  	case "id":
   255  		sort.Sort(imagesSortedID{imagesOutput})
   256  	case "size":
   257  		sort.Sort(imagesSortedSize{imagesOutput})
   258  	case "tag":
   259  		sort.Sort(imagesSortedTag{imagesOutput})
   260  	case "repository":
   261  		sort.Sort(imagesSortedRepository{imagesOutput})
   262  	default:
   263  		// default is created time
   264  		sort.Sort(imagesSortedCreated{imagesOutput})
   265  	}
   266  	return imagesOutput
   267  }
   268  
   269  // getImagesTemplateOutput returns the images information to be printed in human readable format
   270  func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) imagesSorted {
   271  	var imagesOutput imagesSorted
   272  	for _, img := range images {
   273  		// If all is false and the image doesn't have a name, check to see if the top layer of the image is a parent
   274  		// to another image's top layer. If it is, then it is an intermediate image so don't print out if the --all flag
   275  		// is not set.
   276  		isParent, err := img.IsParent(ctx)
   277  		if err != nil {
   278  			logrus.Errorf("error checking if image is a parent %q: %v", img.ID(), err)
   279  		}
   280  		if !opts.all && len(img.Names()) == 0 && isParent {
   281  			continue
   282  		}
   283  		createdTime := img.Created()
   284  
   285  		imageID := "sha256:" + img.ID()
   286  		if !opts.noTrunc {
   287  			imageID = shortID(img.ID())
   288  		}
   289  
   290  		// get all specified repo:tag and repo@digest pairs and print them separately
   291  		repopairs, err := image.ReposToMap(img.Names())
   292  		if err != nil {
   293  			logrus.Errorf("error finding tag/digest for %s", img.ID())
   294  		}
   295  	outer:
   296  		for repo, tags := range repopairs {
   297  			for _, tag := range tags {
   298  				size, err := img.Size(ctx)
   299  				var sizeStr string
   300  				if err != nil {
   301  					sizeStr = err.Error()
   302  				} else {
   303  					sizeStr = units.HumanSizeWithPrecision(float64(*size), 3)
   304  					lastNumIdx := strings.LastIndexFunc(sizeStr, unicode.IsNumber)
   305  					sizeStr = sizeStr[:lastNumIdx+1] + " " + sizeStr[lastNumIdx+1:]
   306  				}
   307  				var imageDigest digest.Digest
   308  				if len(tag) == 71 && strings.HasPrefix(tag, "sha256:") {
   309  					imageDigest = digest.Digest(tag)
   310  					tag = ""
   311  				} else if img.Digest() != "" {
   312  					imageDigest = img.Digest()
   313  				}
   314  				params := imagesTemplateParams{
   315  					Repository:   repo,
   316  					Tag:          tag,
   317  					ID:           imageID,
   318  					Digest:       imageDigest,
   319  					Digests:      img.Digests(),
   320  					CreatedAt:    createdTime,
   321  					CreatedSince: units.HumanDuration(time.Since(createdTime)) + " ago",
   322  					Size:         sizeStr,
   323  					ReadOnly:     img.IsReadOnly(),
   324  					History:      strings.Join(img.NamesHistory(), ", "),
   325  				}
   326  				imagesOutput = append(imagesOutput, params)
   327  				if opts.quiet { // Show only one image ID when quiet
   328  					break outer
   329  				}
   330  			}
   331  		}
   332  	}
   333  
   334  	// Sort images by created time
   335  	sortImagesOutput(opts.sort, imagesOutput)
   336  	return imagesOutput
   337  }
   338  
   339  // getImagesJSONOutput returns the images information in its raw form
   340  func getImagesJSONOutput(ctx context.Context, images []*adapter.ContainerImage) []imagesJSONParams {
   341  	imagesOutput := []imagesJSONParams{}
   342  	for _, img := range images {
   343  		size, err := img.Size(ctx)
   344  		if err != nil {
   345  			size = nil
   346  		}
   347  		params := imagesJSONParams{
   348  			ID:        img.ID(),
   349  			Name:      img.Names(),
   350  			Digest:    img.Digest(),
   351  			Digests:   img.Digests(),
   352  			Created:   units.HumanDuration(time.Since(img.Created())) + " ago",
   353  			CreatedAt: img.Created(),
   354  			Size:      size,
   355  			ReadOnly:  img.IsReadOnly(),
   356  			History:   img.NamesHistory(),
   357  		}
   358  		imagesOutput = append(imagesOutput, params)
   359  	}
   360  	return imagesOutput
   361  }
   362  
   363  // generateImagesOutput generates the images based on the format provided
   364  
   365  func generateImagesOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) error {
   366  	templateMap := GenImageOutputMap()
   367  	var out formats.Writer
   368  
   369  	switch opts.format {
   370  	case formats.JSONString:
   371  		imagesOutput := getImagesJSONOutput(ctx, images)
   372  		out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)}
   373  	default:
   374  		imagesOutput := getImagesTemplateOutput(ctx, images, opts)
   375  		out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.outputformat, Fields: templateMap}
   376  	}
   377  	return out.Out()
   378  }
   379  
   380  // GenImageOutputMap generates the map used for outputting the images header
   381  // without requiring a populated image. This replaces the previous HeaderMap
   382  // call.
   383  func GenImageOutputMap() map[string]string {
   384  	io := imagesTemplateParams{}
   385  	v := reflect.Indirect(reflect.ValueOf(io))
   386  	values := make(map[string]string)
   387  
   388  	for i := 0; i < v.NumField(); i++ {
   389  		key := v.Type().Field(i).Name
   390  		value := key
   391  		if value == "ID" {
   392  			value = "Image" + value
   393  		}
   394  
   395  		if value == "ReadOnly" {
   396  			values[key] = "R/O"
   397  			continue
   398  		}
   399  		if value == "CreatedSince" {
   400  			value = "created"
   401  		}
   402  		values[key] = strings.ToUpper(splitCamelCase(value))
   403  	}
   404  	return values
   405  }