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 <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2017-09-27 16:12:12 -07:00
parent 7c9b0eab9f
commit c555df54c0
No known key found for this signature in database
GPG Key ID: 67B3DED84EDC823F
2 changed files with 135 additions and 2 deletions

View File

@ -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] <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"},

View File

@ -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