
The `Check` function returns information about an image's content components over a content provider. From this information, one can tell which content is required, present or missing to run an image. The utility can be demonstrated with the `check` command: ```console $ ctr images check REF TYPE DIGEST STATUS SIZE docker.io/library/alpine:latest application/vnd.docker.distribution.manifest.list.v2+json sha256:f006ecbb824d87947d0b51ab8488634bf69fe4094959d935c0c103f4820a417d incomplete (1/2) 1.5 KiB/1.9 MiB docker.io/library/postgres:latest application/vnd.docker.distribution.manifest.v2+json sha256:2f8080b9910a8b4f38ff5a55a82e77cb43d88bdbb16d723c71d18493590832e9 complete (13/13) 99.3 MiB/99.3 MiB docker.io/library/redis:alpine application/vnd.docker.distribution.manifest.v2+json sha256:e633cded055a94202e4ccccb8125b7f383cd6ee56527ab890db643383a2647dd incomplete (6/7) 8.1 MiB/10.0 MiB docker.io/library/ubuntu:latest application/vnd.docker.distribution.manifest.list.v2+json sha256:60f835698ea19e8d9d3a59e68fb96fb35bc43e745941cb2ea9eaf4ba3029ed8a unavailable (0/?) 0.0 B/? docker.io/trollin/busybox:latest application/vnd.docker.distribution.manifest.list.v2+json sha256:54a6424f7a2d5f4f27b3d69e5f9f2bc25fe9087f0449d3cb4215db349f77feae complete (2/2) 699.9 KiB/699.9 KiB ``` The above shows us that we have two incomplete images and one that is unavailable. The incomplete images are those that we know the complete size of all content but some are missing. "Unavailable" means that the check could not get enough information about the image to get its full size. Signed-off-by: Stephen J Day <stephen.day@docker.com>
302 lines
6.9 KiB
Go
302 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/images"
|
|
"github.com/containerd/containerd/log"
|
|
"github.com/containerd/containerd/platforms"
|
|
"github.com/containerd/containerd/progress"
|
|
"github.com/pkg/errors"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
var imageCommand = cli.Command{
|
|
Name: "images",
|
|
Usage: "manage images",
|
|
Subcommands: cli.Commands{
|
|
imagesListCommand,
|
|
imagesCheckCommand,
|
|
imageRemoveCommand,
|
|
imagesSetLabelsCommand,
|
|
imagesImportCommand,
|
|
imagesExportCommand,
|
|
},
|
|
}
|
|
|
|
var imagesListCommand = cli.Command{
|
|
Name: "list",
|
|
Aliases: []string{"ls"},
|
|
Usage: "list images known to containerd",
|
|
ArgsUsage: "[flags] <ref>",
|
|
Description: `List images registered with containerd.`,
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "quiet, q",
|
|
Usage: "print only the image refs",
|
|
},
|
|
},
|
|
Action: func(clicontext *cli.Context) error {
|
|
var (
|
|
filters = clicontext.Args()
|
|
quiet = clicontext.Bool("quiet")
|
|
ctx, cancel = appContext(clicontext)
|
|
)
|
|
defer cancel()
|
|
|
|
client, err := newClient(clicontext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
imageStore := client.ImageService()
|
|
cs := client.ContentStore()
|
|
|
|
imageList, err := imageStore.List(ctx, filters...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to list images")
|
|
}
|
|
if quiet {
|
|
for _, image := range imageList {
|
|
fmt.Println(image.Name)
|
|
}
|
|
return nil
|
|
}
|
|
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
|
|
fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSIZE\tPLATFORM\tLABELS\t")
|
|
for _, image := range imageList {
|
|
size, err := image.Size(ctx, cs, platforms.Default())
|
|
if err != nil {
|
|
log.G(ctx).WithError(err).Errorf("failed calculating size for image %s", image.Name)
|
|
}
|
|
|
|
platformColumn := "-"
|
|
specs, err := images.Platforms(ctx, cs, image.Target)
|
|
if err != nil {
|
|
log.G(ctx).WithError(err).Errorf("failed resolving platform for image %s", image.Name)
|
|
} else if len(specs) > 0 {
|
|
psm := map[string]struct{}{}
|
|
for _, p := range specs {
|
|
psm[platforms.Format(p)] = struct{}{}
|
|
}
|
|
var ps []string
|
|
for p := range psm {
|
|
ps = append(ps, p)
|
|
}
|
|
sort.Stable(sort.StringSlice(ps))
|
|
platformColumn = strings.Join(ps, ",")
|
|
}
|
|
|
|
labels := "-"
|
|
if len(image.Labels) > 0 {
|
|
var pairs []string
|
|
for k, v := range image.Labels {
|
|
pairs = append(pairs, fmt.Sprintf("%v=%v", k, v))
|
|
}
|
|
sort.Strings(pairs)
|
|
labels = strings.Join(pairs, ",")
|
|
}
|
|
|
|
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%s\t\n",
|
|
image.Name,
|
|
image.Target.MediaType,
|
|
image.Target.Digest,
|
|
progress.Bytes(size),
|
|
platformColumn,
|
|
labels)
|
|
}
|
|
|
|
return tw.Flush()
|
|
},
|
|
}
|
|
|
|
var imagesSetLabelsCommand = cli.Command{
|
|
Name: "label",
|
|
Usage: "set and clear labels for an image.",
|
|
ArgsUsage: "[flags] <name> [<key>=<value>, ...]",
|
|
Description: "Set and clear labels for an image.",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "replace-all, r",
|
|
Usage: "replace all labels",
|
|
},
|
|
},
|
|
Action: func(clicontext *cli.Context) error {
|
|
var (
|
|
ctx, cancel = appContext(clicontext)
|
|
replaceAll = clicontext.Bool("replace-all")
|
|
name, labels = objectWithLabelArgs(clicontext)
|
|
)
|
|
defer cancel()
|
|
|
|
client, err := newClient(clicontext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if name == "" {
|
|
return errors.New("please specify an image")
|
|
}
|
|
|
|
var (
|
|
is = client.ImageService()
|
|
fieldpaths []string
|
|
)
|
|
|
|
for k := range labels {
|
|
if replaceAll {
|
|
fieldpaths = append(fieldpaths, "labels")
|
|
} else {
|
|
fieldpaths = append(fieldpaths, strings.Join([]string{"labels", k}, "."))
|
|
}
|
|
}
|
|
|
|
image := images.Image{
|
|
Name: name,
|
|
Labels: labels,
|
|
}
|
|
|
|
updated, err := is.Update(ctx, image, fieldpaths...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var labelStrings []string
|
|
for k, v := range updated.Labels {
|
|
labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
|
|
fmt.Println(strings.Join(labelStrings, ","))
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var imagesCheckCommand = cli.Command{
|
|
Name: "check",
|
|
Usage: "Check that an image has all content available locally.",
|
|
ArgsUsage: "[flags] <ref> [<ref>, ...]",
|
|
Description: "Check that an image has all content available locally.",
|
|
Flags: []cli.Flag{},
|
|
Action: func(clicontext *cli.Context) error {
|
|
var (
|
|
exitErr error
|
|
)
|
|
ctx, cancel := appContext(clicontext)
|
|
defer cancel()
|
|
|
|
client, err := newClient(clicontext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
imageStore := client.ImageService()
|
|
contentStore := client.ContentStore()
|
|
|
|
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
|
|
fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSTATUS\tSIZE\t")
|
|
|
|
args := []string(clicontext.Args())
|
|
imageList, err := imageStore.List(ctx, args...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed listing images")
|
|
}
|
|
|
|
for _, image := range imageList {
|
|
var (
|
|
status string = "complete"
|
|
size string
|
|
requiredSize int64
|
|
presentSize int64
|
|
)
|
|
|
|
available, required, present, missing, err := images.Check(ctx, contentStore, image.Target, platforms.Default())
|
|
if err != nil {
|
|
if exitErr == nil {
|
|
exitErr = errors.Wrapf(err, "unable to check %v", image.Name)
|
|
}
|
|
log.G(ctx).WithError(err).Errorf("unable to check %v", image.Name)
|
|
status = "error"
|
|
}
|
|
|
|
if status != "error" {
|
|
for _, d := range required {
|
|
requiredSize += d.Size
|
|
}
|
|
|
|
for _, d := range present {
|
|
presentSize += d.Size
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
status = "incomplete"
|
|
}
|
|
|
|
if available {
|
|
status += fmt.Sprintf(" (%v/%v)", len(present), len(required))
|
|
size = fmt.Sprintf("%v/%v", progress.Bytes(presentSize), progress.Bytes(requiredSize))
|
|
} else {
|
|
status = fmt.Sprintf("unavailable (%v/?)", len(present))
|
|
size = fmt.Sprintf("%v/?", progress.Bytes(presentSize))
|
|
}
|
|
} else {
|
|
size = "-"
|
|
}
|
|
|
|
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t\n",
|
|
image.Name,
|
|
image.Target.MediaType,
|
|
image.Target.Digest,
|
|
status,
|
|
size)
|
|
}
|
|
tw.Flush()
|
|
|
|
return exitErr
|
|
},
|
|
}
|
|
|
|
var imageRemoveCommand = cli.Command{
|
|
Name: "remove",
|
|
Aliases: []string{"rm"},
|
|
Usage: "Remove one or more images by reference.",
|
|
ArgsUsage: "[flags] <ref> [<ref>, ...]",
|
|
Description: `Remove one or more images by reference.`,
|
|
Flags: []cli.Flag{},
|
|
Action: func(clicontext *cli.Context) error {
|
|
var (
|
|
exitErr error
|
|
)
|
|
ctx, cancel := appContext(clicontext)
|
|
defer cancel()
|
|
|
|
client, err := newClient(clicontext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
imageStore := client.ImageService()
|
|
|
|
for _, target := range clicontext.Args() {
|
|
if err := imageStore.Delete(ctx, target); err != nil {
|
|
if !errdefs.IsNotFound(err) {
|
|
if exitErr == nil {
|
|
exitErr = errors.Wrapf(err, "unable to delete %v", target)
|
|
}
|
|
log.G(ctx).WithError(err).Errorf("unable to delete %v", target)
|
|
continue
|
|
}
|
|
}
|
|
|
|
fmt.Println(target)
|
|
}
|
|
|
|
return exitErr
|
|
},
|
|
}
|