diff --git a/cmd/ctr/commands/images/images.go b/cmd/ctr/commands/images/images.go index 074e118d3..3af336189 100644 --- a/cmd/ctr/commands/images/images.go +++ b/cmd/ctr/commands/images/images.go @@ -177,8 +177,9 @@ var setLabelsCommand = cli.Command{ var checkCommand = cli.Command{ Name: "check", Usage: "check that an image has all content available locally", - ArgsUsage: " [, ...]", + ArgsUsage: "[flags] [, ...]", Description: "check that an image has all content available locally", + Flags: commands.SnapshotterFlags, Action: func(context *cli.Context) error { var ( exitErr error @@ -189,14 +190,13 @@ var checkCommand = cli.Command{ } defer cancel() var ( - imageStore = client.ImageService() contentStore = client.ContentStore() tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0) ) - fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSTATUS\tSIZE\t") + fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSTATUS\tSIZE\tUNPACKED\t") args := []string(context.Args()) - imageList, err := imageStore.List(ctx, args...) + imageList, err := client.ListImages(ctx, args...) if err != nil { return errors.Wrap(err, "failed listing images") } @@ -209,12 +209,12 @@ var checkCommand = cli.Command{ presentSize int64 ) - available, required, present, missing, err := images.Check(ctx, contentStore, image.Target, platforms.Default()) + 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) + exitErr = errors.Wrapf(err, "unable to check %v", image.Name()) } - log.G(ctx).WithError(err).Errorf("unable to check %v", image.Name) + log.G(ctx).WithError(err).Errorf("unable to check %v", image.Name()) status = "error" } @@ -242,12 +242,21 @@ var checkCommand = cli.Command{ size = "-" } - fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t\n", - image.Name, - image.Target.MediaType, - image.Target.Digest, + unpacked, err := image.IsUnpacked(ctx, context.String("snapshotter")) + if err != nil { + if exitErr == nil { + exitErr = errors.Wrapf(err, "unable to check unpack for %v", image.Name()) + } + log.G(ctx).WithError(err).Errorf("unable to check unpack for %v", image.Name()) + } + + fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%t\n", + image.Name(), + image.Target().MediaType, + image.Target().Digest, status, - size) + size, + unpacked) } tw.Flush() diff --git a/image.go b/image.go index b41037a16..426e15817 100644 --- a/image.go +++ b/image.go @@ -6,6 +6,7 @@ import ( "time" "github.com/containerd/containerd/content" + "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/images" "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/rootfs" @@ -30,6 +31,8 @@ type Image interface { Size(ctx context.Context) (int64, error) // Config descriptor for the image. Config(ctx context.Context) (ocispec.Descriptor, error) + // IsUnpacked returns whether or not an image is unpacked. + IsUnpacked(context.Context, string) (bool, error) } var _ = (Image)(&image{}) @@ -63,6 +66,26 @@ func (i *image) Config(ctx context.Context) (ocispec.Descriptor, error) { return i.i.Config(ctx, provider, platforms.Default()) } +func (i *image) IsUnpacked(ctx context.Context, snapshotterName string) (bool, error) { + sn := i.client.SnapshotService(snapshotterName) + cs := i.client.ContentStore() + + diffs, err := i.i.RootFS(ctx, cs, platforms.Default()) + if err != nil { + return false, err + } + + chainID := identity.ChainID(diffs) + _, err = sn.Stat(ctx, chainID.String()) + if err == nil { + return true, nil + } else if !errdefs.IsNotFound(err) { + return false, err + } + + return false, nil +} + func (i *image) Unpack(ctx context.Context, snapshotterName string) error { layers, err := i.getLayers(ctx, platforms.Default()) if err != nil { diff --git a/image_test.go b/image_test.go new file mode 100644 index 000000000..8f933a5cd --- /dev/null +++ b/image_test.go @@ -0,0 +1,58 @@ +package containerd + +import ( + "runtime" + "testing" + + "github.com/containerd/containerd/errdefs" +) + +func TestImageIsUnpacked(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } + + const imageName = "docker.io/library/busybox:latest" + ctx, cancel := testContext() + defer cancel() + + client, err := newClient(t, address) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + // Cleanup + err = client.ImageService().Delete(ctx, imageName) + if err != nil && !errdefs.IsNotFound(err) { + t.Fatal(err) + } + + // By default pull does not unpack an image + image, err := client.Pull(ctx, imageName) + if err != nil { + t.Fatal(err) + } + + // Check that image is not unpacked + unpacked, err := image.IsUnpacked(ctx, DefaultSnapshotter) + if err != nil { + t.Fatal(err) + } + if unpacked { + t.Fatalf("image should not be unpacked") + } + + // Check that image is unpacked + err = image.Unpack(ctx, DefaultSnapshotter) + if err != nil { + t.Fatal(err) + } + unpacked, err = image.IsUnpacked(ctx, DefaultSnapshotter) + if err != nil { + t.Fatal(err) + } + if !unpacked { + t.Fatalf("image should be unpacked") + } +}