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 855be1a0d..8179dc22c 100644 --- a/images/image.go +++ b/images/image.go @@ -87,7 +87,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 @@ -165,7 +170,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 } @@ -218,6 +223,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