From c555df54c0db5e2b9efa4863a1d747106597af36 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 27 Sep 2017 16:12:12 -0700 Subject: [PATCH] images: support checking status of image content 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 --- cmd/ctr/images.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++ images/image.go | 52 +++++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/cmd/ctr/images.go b/cmd/ctr/images.go index 6b035ea5d..6be8b9ccb 100644 --- a/cmd/ctr/images.go +++ b/cmd/ctr/images.go @@ -21,6 +21,7 @@ var imageCommand = cli.Command{ Usage: "manage images", Subcommands: cli.Commands{ imagesListCommand, + imagesCheckCommand, imageRemoveCommand, imagesSetLabelsCommand, imagesImportCommand, @@ -176,6 +177,90 @@ var imagesSetLabelsCommand = cli.Command{ }, } +var imagesCheckCommand = cli.Command{ + Name: "check", + Usage: "Check that an image has all content available locally.", + ArgsUsage: "[flags] [, ...]", + 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"}, diff --git a/images/image.go b/images/image.go index 1efd0f7c7..8dec06b9b 100644 --- a/images/image.go +++ b/images/image.go @@ -71,7 +71,12 @@ func (image *Image) Size(ctx context.Context, provider content.Provider, platfor }), ChildrenHandler(provider, platform)), image.Target) } -// Manifest returns the manifest for an image. +// Manifest resolves a manifest from the image for the given platform. +// +// TODO(stevvooe): This violates the current platform agnostic approach to this +// package by returning a specific manifest type. We'll need to refactor this +// to return a manifest descriptor or decide that we want to bring the API in +// this direction because this abstraction is not needed.` func Manifest(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform string) (ocispec.Manifest, error) { var ( matcher platforms.Matcher @@ -149,7 +154,7 @@ func Manifest(ctx context.Context, provider content.Provider, image ocispec.Desc return descs, nil } - return nil, errors.New("could not resolve manifest") + return nil, errors.Wrap(errdefs.ErrNotFound, "could not resolve manifest") }), image); err != nil { return ocispec.Manifest{}, err } @@ -202,6 +207,49 @@ func Platforms(ctx context.Context, provider content.Provider, image ocispec.Des }), ChildrenHandler(provider, "")), image) } +// Check returns nil if the all components of an image are available in the +// provider for the specified platform. +// +// If available is true, the caller can assume that required represents the +// complete set of content required for the image. +// +// missing will have the components that are part of required but not avaiiable +// in the provider. +// +// If there is a problem resolving content, an error will be returned. +func Check(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform string) (available bool, required, present, missing []ocispec.Descriptor, err error) { + mfst, err := Manifest(ctx, provider, image, platform) + if err != nil { + if errdefs.IsNotFound(err) { + return false, []ocispec.Descriptor{image}, nil, []ocispec.Descriptor{image}, nil + } + + return false, nil, nil, nil, errors.Wrap(err, "image check failed") + } + + // TODO(stevvooe): It is possible that referenced conponents could have + // children, but this is rare. For now, we ignore this and only verify + // that manfiest components are present. + required = append([]ocispec.Descriptor{mfst.Config}, mfst.Layers...) + + for _, desc := range required { + ra, err := provider.ReaderAt(ctx, desc.Digest) + if err != nil { + if errdefs.IsNotFound(err) { + missing = append(missing, desc) + continue + } else { + return false, nil, nil, nil, err + } + } + ra.Close() + present = append(present, desc) + + } + + return true, required, present, missing, nil +} + // Children returns the immediate children of content described by the descriptor. func Children(ctx context.Context, provider content.Provider, desc ocispec.Descriptor, platform string) ([]ocispec.Descriptor, error) { var descs []ocispec.Descriptor