github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/image/list.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package image
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"path"
    26  	"strings"
    27  	"text/tabwriter"
    28  	"text/template"
    29  	"time"
    30  
    31  	"github.com/containerd/containerd"
    32  	"github.com/containerd/containerd/content"
    33  	"github.com/containerd/containerd/images"
    34  	"github.com/containerd/containerd/pkg/progress"
    35  	"github.com/containerd/containerd/snapshots"
    36  	"github.com/containerd/log"
    37  	"github.com/containerd/nerdctl/v2/pkg/api/types"
    38  	"github.com/containerd/nerdctl/v2/pkg/formatter"
    39  	"github.com/containerd/nerdctl/v2/pkg/imgutil"
    40  	"github.com/containerd/platforms"
    41  	v1 "github.com/opencontainers/image-spec/specs-go/v1"
    42  )
    43  
    44  // ListCommandHandler `List` and print images matching filters in `options`.
    45  func ListCommandHandler(ctx context.Context, client *containerd.Client, options types.ImageListOptions) error {
    46  	imageList, err := List(ctx, client, options.Filters, options.NameAndRefFilter)
    47  	if err != nil {
    48  		return err
    49  	}
    50  	return printImages(ctx, client, imageList, options)
    51  }
    52  
    53  // List queries containerd client to get image list and only returns those matching given filters.
    54  //
    55  // Supported filters:
    56  // - before=<image>[:<tag>]: Images created before given image (exclusive)
    57  // - since=<image>[:<tag>]: Images created after given image (exclusive)
    58  // - label=<key>[=<value>]: Matches images based on the presence of a label alone or a label and a value
    59  // - dangling=true: Filter images by dangling
    60  // - reference=<image>[:<tag>]: Filter images by reference (Matches both docker compatible wildcard pattern and regexp
    61  //
    62  // nameAndRefFilter has the format of `name==(<image>[:<tag>])|ID`,
    63  // and they will be used when getting images from containerd,
    64  // while the remaining filters are only applied after getting images from containerd,
    65  // which means that having nameAndRefFilter may speed up the process if there are a lot of images in containerd.
    66  func List(ctx context.Context, client *containerd.Client, filters, nameAndRefFilter []string) ([]images.Image, error) {
    67  	var imageStore = client.ImageService()
    68  	imageList, err := imageStore.List(ctx, nameAndRefFilter...)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	if len(filters) > 0 {
    73  		f, err := imgutil.ParseFilters(filters)
    74  		if err != nil {
    75  			return nil, err
    76  		}
    77  
    78  		if f.Dangling != nil {
    79  			imageList = imgutil.FilterDangling(imageList, *f.Dangling)
    80  		}
    81  
    82  		imageList, err = imgutil.FilterByLabel(ctx, client, imageList, f.Labels)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  
    87  		imageList, err = imgutil.FilterByReference(imageList, f.Reference)
    88  		if err != nil {
    89  			return nil, err
    90  		}
    91  
    92  		var beforeImages []images.Image
    93  		if len(f.Before) > 0 {
    94  			beforeImages, err = imageStore.List(ctx, f.Before...)
    95  			if err != nil {
    96  				return nil, err
    97  			}
    98  		}
    99  		var sinceImages []images.Image
   100  		if len(f.Since) > 0 {
   101  			sinceImages, err = imageStore.List(ctx, f.Since...)
   102  			if err != nil {
   103  				return nil, err
   104  			}
   105  		}
   106  
   107  		imageList = imgutil.FilterImages(imageList, beforeImages, sinceImages)
   108  	}
   109  	return imageList, nil
   110  }
   111  
   112  type imagePrintable struct {
   113  	// TODO: "Containers"
   114  	CreatedAt    string
   115  	CreatedSince string
   116  	Digest       string // "<none>" or image target digest (i.e., index digest or manifest digest)
   117  	ID           string // image target digest (not config digest, unlike Docker), or its short form
   118  	Repository   string
   119  	Tag          string // "<none>" or tag
   120  	Name         string // image name
   121  	Size         string // the size of the unpacked snapshots.
   122  	BlobSize     string // the size of the blobs in the content store (nerdctl extension)
   123  	// TODO: "SharedSize", "UniqueSize"
   124  	Platform string // nerdctl extension
   125  }
   126  
   127  func printImages(ctx context.Context, client *containerd.Client, imageList []images.Image, options types.ImageListOptions) error {
   128  	w := options.Stdout
   129  	digestsFlag := options.Digests
   130  	if options.Format == "wide" {
   131  		digestsFlag = true
   132  	}
   133  	var tmpl *template.Template
   134  	switch options.Format {
   135  	case "", "table", "wide":
   136  		w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
   137  		if !options.Quiet {
   138  			printHeader := ""
   139  			if options.Names {
   140  				printHeader += "NAME\t"
   141  			} else {
   142  				printHeader += "REPOSITORY\tTAG\t"
   143  			}
   144  			if digestsFlag {
   145  				printHeader += "DIGEST\t"
   146  			}
   147  			printHeader += "IMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE"
   148  			fmt.Fprintln(w, printHeader)
   149  		}
   150  	case "raw":
   151  		return errors.New("unsupported format: \"raw\"")
   152  	default:
   153  		if options.Quiet {
   154  			return errors.New("format and quiet must not be specified together")
   155  		}
   156  		var err error
   157  		tmpl, err = formatter.ParseTemplate(options.Format)
   158  		if err != nil {
   159  			return err
   160  		}
   161  	}
   162  
   163  	printer := &imagePrinter{
   164  		w:            w,
   165  		quiet:        options.Quiet,
   166  		noTrunc:      options.NoTrunc,
   167  		digestsFlag:  digestsFlag,
   168  		namesFlag:    options.Names,
   169  		tmpl:         tmpl,
   170  		client:       client,
   171  		contentStore: client.ContentStore(),
   172  		snapshotter:  client.SnapshotService(options.GOptions.Snapshotter),
   173  	}
   174  
   175  	for _, img := range imageList {
   176  		if err := printer.printImage(ctx, img); err != nil {
   177  			log.G(ctx).Warn(err)
   178  		}
   179  	}
   180  	if f, ok := w.(formatter.Flusher); ok {
   181  		return f.Flush()
   182  	}
   183  	return nil
   184  }
   185  
   186  type imagePrinter struct {
   187  	w                                      io.Writer
   188  	quiet, noTrunc, digestsFlag, namesFlag bool
   189  	tmpl                                   *template.Template
   190  	client                                 *containerd.Client
   191  	contentStore                           content.Store
   192  	snapshotter                            snapshots.Snapshotter
   193  }
   194  
   195  func (x *imagePrinter) printImage(ctx context.Context, img images.Image) error {
   196  	ociPlatforms, err := images.Platforms(ctx, x.contentStore, img.Target)
   197  	if err != nil {
   198  		log.G(ctx).WithError(err).Warnf("failed to get the platform list of image %q", img.Name)
   199  		return x.printImageSinglePlatform(ctx, img, platforms.DefaultSpec())
   200  	}
   201  	psm := map[string]struct{}{}
   202  	for _, ociPlatform := range ociPlatforms {
   203  		platformKey := makePlatformKey(ociPlatform)
   204  		if _, done := psm[platformKey]; done {
   205  			continue
   206  		}
   207  		psm[platformKey] = struct{}{}
   208  		if err := x.printImageSinglePlatform(ctx, img, ociPlatform); err != nil {
   209  			log.G(ctx).WithError(err).Warnf("failed to get platform %q of image %q", platforms.Format(ociPlatform), img.Name)
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  func makePlatformKey(platform v1.Platform) string {
   216  	if platform.OS == "" {
   217  		return "unknown"
   218  	}
   219  
   220  	return path.Join(platform.OS, platform.Architecture, platform.OSVersion, platform.Variant)
   221  }
   222  
   223  func (x *imagePrinter) printImageSinglePlatform(ctx context.Context, img images.Image, ociPlatform v1.Platform) error {
   224  	platMC := platforms.OnlyStrict(ociPlatform)
   225  	if avail, _, _, _, availErr := images.Check(ctx, x.contentStore, img.Target, platMC); !avail {
   226  		log.G(ctx).WithError(availErr).Debugf("skipping printing image %q for platform %q", img.Name, platforms.Format(ociPlatform))
   227  		return nil
   228  	}
   229  
   230  	image := containerd.NewImageWithPlatform(x.client, img, platMC)
   231  	desc, err := image.Config(ctx)
   232  	if err != nil {
   233  		log.G(ctx).WithError(err).Warnf("failed to get config of image %q for platform %q", img.Name, platforms.Format(ociPlatform))
   234  	}
   235  	var (
   236  		repository string
   237  		tag        string
   238  	)
   239  	// cri plugin will create an image named digest of image's config, skip parsing.
   240  	if x.namesFlag || desc.Digest.String() != img.Name {
   241  		repository, tag = imgutil.ParseRepoTag(img.Name)
   242  	}
   243  
   244  	blobSize, err := image.Size(ctx)
   245  	if err != nil {
   246  		log.G(ctx).WithError(err).Warnf("failed to get blob size of image %q for platform %q", img.Name, platforms.Format(ociPlatform))
   247  	}
   248  
   249  	size, err := imgutil.UnpackedImageSize(ctx, x.snapshotter, image)
   250  	if err != nil {
   251  		// Warnf is too verbose: https://github.com/containerd/nerdctl/issues/2058
   252  		log.G(ctx).WithError(err).Debugf("failed to get unpacked size of image %q for platform %q", img.Name, platforms.Format(ociPlatform))
   253  	}
   254  
   255  	p := imagePrintable{
   256  		CreatedAt:    img.CreatedAt.Round(time.Second).Local().String(), // format like "2021-08-07 02:19:45 +0900 JST"
   257  		CreatedSince: formatter.TimeSinceInHuman(img.CreatedAt),
   258  		Digest:       img.Target.Digest.String(),
   259  		ID:           img.Target.Digest.String(),
   260  		Repository:   repository,
   261  		Tag:          tag,
   262  		Name:         img.Name,
   263  		Size:         progress.Bytes(size).String(),
   264  		BlobSize:     progress.Bytes(blobSize).String(),
   265  		Platform:     platforms.Format(ociPlatform),
   266  	}
   267  	if p.Repository == "" {
   268  		p.Repository = "<none>"
   269  	}
   270  	if p.Tag == "" {
   271  		p.Tag = "<none>" // for Docker compatibility
   272  	}
   273  	if !x.noTrunc {
   274  		// p.Digest does not need to be truncated
   275  		p.ID = strings.Split(p.ID, ":")[1][:12]
   276  	}
   277  	if x.tmpl != nil {
   278  		var b bytes.Buffer
   279  		if err := x.tmpl.Execute(&b, p); err != nil {
   280  			return err
   281  		}
   282  		if _, err = fmt.Fprintln(x.w, b.String()); err != nil {
   283  			return err
   284  		}
   285  	} else if x.quiet {
   286  		if _, err := fmt.Fprintln(x.w, p.ID); err != nil {
   287  			return err
   288  		}
   289  	} else {
   290  		format := ""
   291  		args := []interface{}{}
   292  		if x.namesFlag {
   293  			format += "%s\t"
   294  			args = append(args, p.Name)
   295  		} else {
   296  			format += "%s\t%s\t"
   297  			args = append(args, p.Repository, p.Tag)
   298  		}
   299  		if x.digestsFlag {
   300  			format += "%s\t"
   301  			args = append(args, p.Digest)
   302  		}
   303  
   304  		format += "%s\t%s\t%s\t%s\t%s\n"
   305  		args = append(args, p.ID, p.CreatedSince, p.Platform, p.Size, p.BlobSize)
   306  		if _, err := fmt.Fprintf(x.w, format, args...); err != nil {
   307  			return err
   308  		}
   309  	}
   310  	return nil
   311  }