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:
parent
7c9b0eab9f
commit
c555df54c0
@ -21,6 +21,7 @@ var imageCommand = cli.Command{
|
|||||||
Usage: "manage images",
|
Usage: "manage images",
|
||||||
Subcommands: cli.Commands{
|
Subcommands: cli.Commands{
|
||||||
imagesListCommand,
|
imagesListCommand,
|
||||||
|
imagesCheckCommand,
|
||||||
imageRemoveCommand,
|
imageRemoveCommand,
|
||||||
imagesSetLabelsCommand,
|
imagesSetLabelsCommand,
|
||||||
imagesImportCommand,
|
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{
|
var imageRemoveCommand = cli.Command{
|
||||||
Name: "remove",
|
Name: "remove",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
|
@ -71,7 +71,12 @@ func (image *Image) Size(ctx context.Context, provider content.Provider, platfor
|
|||||||
}), ChildrenHandler(provider, platform)), image.Target)
|
}), 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) {
|
func Manifest(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform string) (ocispec.Manifest, error) {
|
||||||
var (
|
var (
|
||||||
matcher platforms.Matcher
|
matcher platforms.Matcher
|
||||||
@ -149,7 +154,7 @@ func Manifest(ctx context.Context, provider content.Provider, image ocispec.Desc
|
|||||||
return descs, nil
|
return descs, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("could not resolve manifest")
|
return nil, errors.Wrap(errdefs.ErrNotFound, "could not resolve manifest")
|
||||||
}), image); err != nil {
|
}), image); err != nil {
|
||||||
return ocispec.Manifest{}, err
|
return ocispec.Manifest{}, err
|
||||||
}
|
}
|
||||||
@ -202,6 +207,49 @@ func Platforms(ctx context.Context, provider content.Provider, image ocispec.Des
|
|||||||
}), ChildrenHandler(provider, "")), image)
|
}), 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.
|
// 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) {
|
func Children(ctx context.Context, provider content.Provider, desc ocispec.Descriptor, platform string) ([]ocispec.Descriptor, error) {
|
||||||
var descs []ocispec.Descriptor
|
var descs []ocispec.Descriptor
|
||||||
|
Loading…
Reference in New Issue
Block a user