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 }