402 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			402 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"reflect"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 	"unicode"
 | |
| 
 | |
| 	"github.com/containers/buildah/pkg/formats"
 | |
| 	"github.com/containers/libpod/cmd/podman/cliconfig"
 | |
| 	"github.com/containers/libpod/cmd/podman/imagefilters"
 | |
| 	"github.com/containers/libpod/libpod/image"
 | |
| 	"github.com/containers/libpod/pkg/adapter"
 | |
| 	"github.com/docker/go-units"
 | |
| 	"github.com/opencontainers/go-digest"
 | |
| 	"github.com/pkg/errors"
 | |
| 	"github.com/sirupsen/logrus"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| type imagesTemplateParams struct {
 | |
| 	Repository  string
 | |
| 	Tag         string
 | |
| 	ID          string
 | |
| 	Digest      digest.Digest
 | |
| 	Created     string
 | |
| 	CreatedTime time.Time
 | |
| 	Size        string
 | |
| }
 | |
| 
 | |
| type imagesJSONParams struct {
 | |
| 	ID      string        `json:"id"`
 | |
| 	Name    []string      `json:"names"`
 | |
| 	Digest  digest.Digest `json:"digest"`
 | |
| 	Created time.Time     `json:"created"`
 | |
| 	Size    *uint64       `json:"size"`
 | |
| }
 | |
| 
 | |
| type imagesOptions struct {
 | |
| 	quiet        bool
 | |
| 	noHeading    bool
 | |
| 	noTrunc      bool
 | |
| 	digests      bool
 | |
| 	format       string
 | |
| 	outputformat string
 | |
| 	sort         string
 | |
| 	all          bool
 | |
| }
 | |
| 
 | |
| // Type declaration and functions for sorting the images output
 | |
| type imagesSorted []imagesTemplateParams
 | |
| 
 | |
| func (a imagesSorted) Len() int      { return len(a) }
 | |
| func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
 | |
| 
 | |
| type imagesSortedCreated struct{ imagesSorted }
 | |
| 
 | |
| func (a imagesSortedCreated) Less(i, j int) bool {
 | |
| 	return a.imagesSorted[i].CreatedTime.After(a.imagesSorted[j].CreatedTime)
 | |
| }
 | |
| 
 | |
| type imagesSortedID struct{ imagesSorted }
 | |
| 
 | |
| func (a imagesSortedID) Less(i, j int) bool { return a.imagesSorted[i].ID < a.imagesSorted[j].ID }
 | |
| 
 | |
| type imagesSortedTag struct{ imagesSorted }
 | |
| 
 | |
| func (a imagesSortedTag) Less(i, j int) bool { return a.imagesSorted[i].Tag < a.imagesSorted[j].Tag }
 | |
| 
 | |
| type imagesSortedRepository struct{ imagesSorted }
 | |
| 
 | |
| func (a imagesSortedRepository) Less(i, j int) bool {
 | |
| 	return a.imagesSorted[i].Repository < a.imagesSorted[j].Repository
 | |
| }
 | |
| 
 | |
| type imagesSortedSize struct{ imagesSorted }
 | |
| 
 | |
| func (a imagesSortedSize) Less(i, j int) bool {
 | |
| 	size1, _ := units.FromHumanSize(a.imagesSorted[i].Size)
 | |
| 	size2, _ := units.FromHumanSize(a.imagesSorted[j].Size)
 | |
| 	return size1 < size2
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	imagesCommand     cliconfig.ImagesValues
 | |
| 	imagesDescription = "Lists images previously pulled to the system or created on the system."
 | |
| 
 | |
| 	_imagesCommand = cobra.Command{
 | |
| 		Use:   "images [flags] [IMAGE]",
 | |
| 		Short: "List images in local storage",
 | |
| 		Long:  imagesDescription,
 | |
| 		RunE: func(cmd *cobra.Command, args []string) error {
 | |
| 			imagesCommand.InputArgs = args
 | |
| 			imagesCommand.GlobalFlags = MainGlobalOpts
 | |
| 			imagesCommand.Remote = remoteclient
 | |
| 			return imagesCmd(&imagesCommand)
 | |
| 		},
 | |
| 		Example: `podman images --format json
 | |
|   podman images --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}"
 | |
|   podman images --filter dangling=true`,
 | |
| 	}
 | |
| )
 | |
| 
 | |
| func imagesInit(command *cliconfig.ImagesValues) {
 | |
| 	command.SetHelpTemplate(HelpTemplate())
 | |
| 	command.SetUsageTemplate(UsageTemplate())
 | |
| 
 | |
| 	flags := command.Flags()
 | |
| 	flags.BoolVarP(&command.All, "all", "a", false, "Show all images (default hides intermediate images)")
 | |
| 	flags.BoolVar(&command.Digests, "digests", false, "Show digests")
 | |
| 	flags.StringSliceVarP(&command.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])")
 | |
| 	flags.StringVar(&command.Format, "format", "", "Change the output format to JSON or a Go template")
 | |
| 	flags.BoolVarP(&command.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(&command.NoTrunc, "no-trunc", false, "Do not truncate output")
 | |
| 	flags.BoolVarP(&command.Quiet, "quiet", "q", false, "Display only image IDs")
 | |
| 	flags.StringVar(&command.Sort, "sort", "created", "Sort by created, id, repository, size, or tag")
 | |
| 
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	imagesCommand.Command = &_imagesCommand
 | |
| 	imagesInit(&imagesCommand)
 | |
| }
 | |
| 
 | |
| func imagesCmd(c *cliconfig.ImagesValues) error {
 | |
| 	var (
 | |
| 		filterFuncs []imagefilters.ResultFilter
 | |
| 		image       string
 | |
| 	)
 | |
| 
 | |
| 	runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand)
 | |
| 	if err != nil {
 | |
| 		return errors.Wrapf(err, "Could not get runtime")
 | |
| 	}
 | |
| 	defer runtime.DeferredShutdown(false)
 | |
| 	if len(c.InputArgs) == 1 {
 | |
| 		image = c.InputArgs[0]
 | |
| 	}
 | |
| 	if len(c.InputArgs) > 1 {
 | |
| 		return errors.New("'podman images' requires at most 1 argument")
 | |
| 	}
 | |
| 	if len(c.Filter) > 0 && image != "" {
 | |
| 		return errors.New("can not specify an image and a filter")
 | |
| 	}
 | |
| 	ctx := getContext()
 | |
| 
 | |
| 	if len(c.Filter) > 0 {
 | |
| 		filterFuncs, err = CreateFilterFuncs(ctx, runtime, c.Filter, nil)
 | |
| 	} else {
 | |
| 		filterFuncs, err = CreateFilterFuncs(ctx, runtime, []string{fmt.Sprintf("reference=%s", image)}, nil)
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	opts := imagesOptions{
 | |
| 		quiet:     c.Quiet,
 | |
| 		noHeading: c.Noheading,
 | |
| 		noTrunc:   c.NoTrunc,
 | |
| 		digests:   c.Digests,
 | |
| 		format:    c.Format,
 | |
| 		sort:      c.Sort,
 | |
| 		all:       c.All,
 | |
| 	}
 | |
| 
 | |
| 	opts.outputformat = opts.setOutputFormat()
 | |
| 	images, err := runtime.GetImages()
 | |
| 	if err != nil {
 | |
| 		return errors.Wrapf(err, "unable to get images")
 | |
| 	}
 | |
| 
 | |
| 	var filteredImages []*adapter.ContainerImage
 | |
| 	//filter the images
 | |
| 	if len(c.Filter) > 0 || len(c.InputArgs) == 1 {
 | |
| 		filteredImages = imagefilters.FilterImages(images, filterFuncs)
 | |
| 	} else {
 | |
| 		filteredImages = images
 | |
| 	}
 | |
| 
 | |
| 	return generateImagesOutput(ctx, filteredImages, opts)
 | |
| }
 | |
| 
 | |
| func (i imagesOptions) setOutputFormat() string {
 | |
| 	if i.format != "" {
 | |
| 		// "\t" from the command line is not being recognized as a tab
 | |
| 		// replacing the string "\t" to a tab character if the user passes in "\t"
 | |
| 		return strings.Replace(i.format, `\t`, "\t", -1)
 | |
| 	}
 | |
| 	if i.quiet {
 | |
| 		return formats.IDString
 | |
| 	}
 | |
| 	format := "table {{.Repository}}\t{{.Tag}}\t"
 | |
| 	if i.noHeading {
 | |
| 		format = "{{.Repository}}\t{{.Tag}}\t"
 | |
| 	}
 | |
| 	if i.digests {
 | |
| 		format += "{{.Digest}}\t"
 | |
| 	}
 | |
| 	format += "{{.ID}}\t{{.Created}}\t{{.Size}}\t"
 | |
| 	return format
 | |
| }
 | |
| 
 | |
| // imagesToGeneric creates an empty array of interfaces for output
 | |
| func imagesToGeneric(templParams []imagesTemplateParams, JSONParams []imagesJSONParams) (genericParams []interface{}) {
 | |
| 	if len(templParams) > 0 {
 | |
| 		for _, v := range templParams {
 | |
| 			genericParams = append(genericParams, interface{}(v))
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 	for _, v := range JSONParams {
 | |
| 		genericParams = append(genericParams, interface{}(v))
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func sortImagesOutput(sortBy string, imagesOutput imagesSorted) imagesSorted {
 | |
| 	switch sortBy {
 | |
| 	case "id":
 | |
| 		sort.Sort(imagesSortedID{imagesOutput})
 | |
| 	case "size":
 | |
| 		sort.Sort(imagesSortedSize{imagesOutput})
 | |
| 	case "tag":
 | |
| 		sort.Sort(imagesSortedTag{imagesOutput})
 | |
| 	case "repository":
 | |
| 		sort.Sort(imagesSortedRepository{imagesOutput})
 | |
| 	default:
 | |
| 		// default is created time
 | |
| 		sort.Sort(imagesSortedCreated{imagesOutput})
 | |
| 	}
 | |
| 	return imagesOutput
 | |
| }
 | |
| 
 | |
| // getImagesTemplateOutput returns the images information to be printed in human readable format
 | |
| func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) (imagesOutput imagesSorted) {
 | |
| 	for _, img := range images {
 | |
| 		// 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
 | |
| 		// to another image's top layer. If it is, then it is an intermediate image so don't print out if the --all flag
 | |
| 		// is not set.
 | |
| 		isParent, err := img.IsParent(ctx)
 | |
| 		if err != nil {
 | |
| 			logrus.Errorf("error checking if image is a parent %q: %v", img.ID(), err)
 | |
| 		}
 | |
| 		if !opts.all && len(img.Names()) == 0 && isParent {
 | |
| 			continue
 | |
| 		}
 | |
| 		createdTime := img.Created()
 | |
| 
 | |
| 		imageID := "sha256:" + img.ID()
 | |
| 		if !opts.noTrunc {
 | |
| 			imageID = shortID(img.ID())
 | |
| 		}
 | |
| 
 | |
| 		// get all specified repo:tag pairs and print them separately
 | |
| 		repopairs, err := image.ReposToMap(img.Names())
 | |
| 		if err != nil {
 | |
| 			logrus.Errorf("error finding tag/digest for %s", img.ID())
 | |
| 		}
 | |
| 	outer:
 | |
| 		for repo, tags := range repopairs {
 | |
| 			for _, tag := range tags {
 | |
| 				size, err := img.Size(ctx)
 | |
| 				var sizeStr string
 | |
| 				if err != nil {
 | |
| 					sizeStr = err.Error()
 | |
| 				} else {
 | |
| 					sizeStr = units.HumanSizeWithPrecision(float64(*size), 3)
 | |
| 					lastNumIdx := strings.LastIndexFunc(sizeStr, unicode.IsNumber)
 | |
| 					sizeStr = sizeStr[:lastNumIdx+1] + " " + sizeStr[lastNumIdx+1:]
 | |
| 				}
 | |
| 				params := imagesTemplateParams{
 | |
| 					Repository:  repo,
 | |
| 					Tag:         tag,
 | |
| 					ID:          imageID,
 | |
| 					Digest:      img.Digest(),
 | |
| 					CreatedTime: createdTime,
 | |
| 					Created:     units.HumanDuration(time.Since(createdTime)) + " ago",
 | |
| 					Size:        sizeStr,
 | |
| 				}
 | |
| 				imagesOutput = append(imagesOutput, params)
 | |
| 				if opts.quiet { // Show only one image ID when quiet
 | |
| 					break outer
 | |
| 				}
 | |
| 
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Sort images by created time
 | |
| 	sortImagesOutput(opts.sort, imagesOutput)
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // getImagesJSONOutput returns the images information in its raw form
 | |
| func getImagesJSONOutput(ctx context.Context, images []*adapter.ContainerImage) (imagesOutput []imagesJSONParams) {
 | |
| 	for _, img := range images {
 | |
| 		size, err := img.Size(ctx)
 | |
| 		if err != nil {
 | |
| 			size = nil
 | |
| 		}
 | |
| 		params := imagesJSONParams{
 | |
| 			ID:      img.ID(),
 | |
| 			Name:    img.Names(),
 | |
| 			Digest:  img.Digest(),
 | |
| 			Created: img.Created(),
 | |
| 			Size:    size,
 | |
| 		}
 | |
| 		imagesOutput = append(imagesOutput, params)
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // generateImagesOutput generates the images based on the format provided
 | |
| 
 | |
| func generateImagesOutput(ctx context.Context, images []*adapter.ContainerImage, opts imagesOptions) error {
 | |
| 	templateMap := GenImageOutputMap()
 | |
| 	var out formats.Writer
 | |
| 
 | |
| 	switch opts.format {
 | |
| 	case formats.JSONString:
 | |
| 		// If 0 images are present, print nothing for JSON
 | |
| 		if len(images) == 0 {
 | |
| 			return nil
 | |
| 		}
 | |
| 		imagesOutput := getImagesJSONOutput(ctx, images)
 | |
| 		out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)}
 | |
| 	default:
 | |
| 		imagesOutput := getImagesTemplateOutput(ctx, images, opts)
 | |
| 		out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.outputformat, Fields: templateMap}
 | |
| 	}
 | |
| 	return formats.Writer(out).Out()
 | |
| }
 | |
| 
 | |
| // GenImageOutputMap generates the map used for outputting the images header
 | |
| // without requiring a populated image. This replaces the previous HeaderMap
 | |
| // call.
 | |
| func GenImageOutputMap() map[string]string {
 | |
| 	io := imagesTemplateParams{}
 | |
| 	v := reflect.Indirect(reflect.ValueOf(io))
 | |
| 	values := make(map[string]string)
 | |
| 
 | |
| 	for i := 0; i < v.NumField(); i++ {
 | |
| 		key := v.Type().Field(i).Name
 | |
| 		value := key
 | |
| 		if value == "ID" {
 | |
| 			value = "Image" + value
 | |
| 		}
 | |
| 		values[key] = strings.ToUpper(splitCamelCase(value))
 | |
| 	}
 | |
| 	return values
 | |
| }
 | |
| 
 | |
| // CreateFilterFuncs returns an array of filter functions based on the user inputs
 | |
| // and is later used to filter images for output
 | |
| func CreateFilterFuncs(ctx context.Context, r *adapter.LocalRuntime, filters []string, img *adapter.ContainerImage) ([]imagefilters.ResultFilter, error) {
 | |
| 	var filterFuncs []imagefilters.ResultFilter
 | |
| 	for _, filter := range filters {
 | |
| 		splitFilter := strings.Split(filter, "=")
 | |
| 		if len(splitFilter) < 2 {
 | |
| 			return nil, errors.Errorf("invalid filter syntax %s", filter)
 | |
| 		}
 | |
| 		switch splitFilter[0] {
 | |
| 		case "before":
 | |
| 			before, err := r.NewImageFromLocal(splitFilter[1])
 | |
| 			if err != nil {
 | |
| 				return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1])
 | |
| 			}
 | |
| 			filterFuncs = append(filterFuncs, imagefilters.CreatedBeforeFilter(before.Created()))
 | |
| 		case "after":
 | |
| 			after, err := r.NewImageFromLocal(splitFilter[1])
 | |
| 			if err != nil {
 | |
| 				return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1])
 | |
| 			}
 | |
| 			filterFuncs = append(filterFuncs, imagefilters.CreatedAfterFilter(after.Created()))
 | |
| 		case "dangling":
 | |
| 			danglingImages, err := strconv.ParseBool(splitFilter[1])
 | |
| 			if err != nil {
 | |
| 				return nil, errors.Wrapf(err, "invalid filter dangling=%s", splitFilter[1])
 | |
| 			}
 | |
| 			filterFuncs = append(filterFuncs, imagefilters.DanglingFilter(danglingImages))
 | |
| 		case "label":
 | |
| 			labelFilter := strings.Join(splitFilter[1:], "=")
 | |
| 			filterFuncs = append(filterFuncs, imagefilters.LabelFilter(ctx, labelFilter))
 | |
| 		case "reference":
 | |
| 			referenceFilter := strings.Join(splitFilter[1:], "=")
 | |
| 			filterFuncs = append(filterFuncs, imagefilters.ReferenceFilter(ctx, referenceFilter))
 | |
| 		default:
 | |
| 			return nil, errors.Errorf("invalid filter %s ", splitFilter[0])
 | |
| 		}
 | |
| 	}
 | |
| 	if img != nil {
 | |
| 		filterFuncs = append(filterFuncs, imagefilters.OutputImageFilter(img))
 | |
| 	}
 | |
| 	return filterFuncs, nil
 | |
| }
 |