diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go index e8afa43945a46..069a63ee56f38 100644 --- a/daemon/containerd/image_list.go +++ b/daemon/containerd/image_list.go @@ -10,6 +10,7 @@ import ( cerrdefs "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/images" "github.com/containerd/containerd/labels" + "github.com/containerd/containerd/snapshots" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -142,41 +143,32 @@ func (i *ImageService) Images(ctx context.Context, opts types.ImageListOptions) func (i *ImageService) singlePlatformImage(ctx context.Context, contentStore content.Store, image *ImageManifest) (*types.ImageSummary, []digest.Digest, error) { diffIDs, err := image.RootFS(ctx) if err != nil { - return nil, nil, err - } - chainIDs := identity.ChainIDs(diffIDs) - - size, err := image.Size(ctx) - if err != nil { - return nil, nil, err + return nil, nil, errors.Wrapf(err, "failed to get rootfs of image %s", image.Name()) } // TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273 snapshotter := i.client.SnapshotService(i.snapshotter) - sizeCache := make(map[digest.Digest]int64) - snapshotSizeFn := func(d digest.Digest) (int64, error) { - if s, ok := sizeCache[d]; ok { - return s, nil - } - usage, err := snapshotter.Usage(ctx, d.String()) - if err != nil { - if cerrdefs.IsNotFound(err) { - return 0, nil - } - return 0, err + imageSnapshotID := identity.ChainID(diffIDs).String() + unpackedUsage, err := calculateSnapshotTotalUsage(ctx, snapshotter, imageSnapshotID) + if err != nil { + if !cerrdefs.IsNotFound(err) { + logrus.WithError(err).WithFields(logrus.Fields{ + "image": image.Name(), + "snapshotID": imageSnapshotID, + }).Warn("failed to calculate unpacked size of image") } - sizeCache[d] = usage.Size - return usage.Size, nil + unpackedUsage = snapshots.Usage{Size: 0} } - snapshotSize, err := computeSnapshotSize(chainIDs, snapshotSizeFn) + + contentSize, err := image.Size(ctx) if err != nil { return nil, nil, err } // totalSize is the size of the image's packed layers and snapshots // (unpacked layers) combined. - totalSize := size + snapshotSize + totalSize := contentSize + unpackedUsage.Size var repoTags, repoDigests []string rawImg := image.Metadata() @@ -225,7 +217,7 @@ func (i *ImageService) singlePlatformImage(ctx context.Context, contentStore con Containers: -1, } - return summary, chainIDs, nil + return summary, identity.ChainIDs(diffIDs), nil } type imageFilterFunc func(image images.Image) bool @@ -446,20 +438,6 @@ func setupLabelFilter(store content.Store, fltrs filters.Args) (func(image image }, nil } -// computeSnapshotSize calculates the total size consumed by the snapshots -// for the given chainIDs. -func computeSnapshotSize(chainIDs []digest.Digest, sizeFn func(d digest.Digest) (int64, error)) (int64, error) { - var totalSize int64 - for _, chainID := range chainIDs { - size, err := sizeFn(chainID) - if err != nil { - return totalSize, err - } - totalSize += size - } - return totalSize, nil -} - func computeSharedSize(chainIDs []digest.Digest, layers map[digest.Digest]int, sizeFn func(d digest.Digest) (int64, error)) (int64, error) { var sharedSize int64 for _, chainID := range chainIDs { diff --git a/daemon/containerd/image_snapshot.go b/daemon/containerd/image_snapshot.go index a2152505c15c7..962c2100dc223 100644 --- a/daemon/containerd/image_snapshot.go +++ b/daemon/containerd/image_snapshot.go @@ -2,13 +2,18 @@ package containerd import ( "context" + "fmt" "github.com/containerd/containerd" + cerrdefs "github.com/containerd/containerd/errdefs" containerdimages "github.com/containerd/containerd/images" "github.com/containerd/containerd/leases" "github.com/containerd/containerd/platforms" + "github.com/containerd/containerd/snapshots" + "github.com/docker/docker/errdefs" "github.com/opencontainers/image-spec/identity" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" ) // PrepareSnapshot prepares a snapshot from a parent image for a container @@ -67,3 +72,46 @@ func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentIma _, err = s.Prepare(ctx, id, parent) return err } + +// calculateSnapshotParentUsage returns the usage of all ancestors of the +// provided snapshot. It doesn't include the size of the snapshot itself. +func calculateSnapshotParentUsage(ctx context.Context, snapshotter snapshots.Snapshotter, snapshotID string) (snapshots.Usage, error) { + info, err := snapshotter.Stat(ctx, snapshotID) + if err != nil { + if cerrdefs.IsNotFound(err) { + return snapshots.Usage{}, errdefs.NotFound(err) + } + return snapshots.Usage{}, errdefs.System(errors.Wrapf(err, "snapshotter.Stat failed for %s", snapshotID)) + } + if info.Parent == "" { + return snapshots.Usage{}, errdefs.NotFound(fmt.Errorf("snapshot %s has no parent", snapshotID)) + } + + return calculateSnapshotTotalUsage(ctx, snapshotter, info.Parent) +} + +// calculateSnapshotTotalUsage returns the total usage of that snapshot +// including all of its ancestors. +func calculateSnapshotTotalUsage(ctx context.Context, snapshotter snapshots.Snapshotter, snapshotID string) (snapshots.Usage, error) { + var total snapshots.Usage + next := snapshotID + + for next != "" { + usage, err := snapshotter.Usage(ctx, next) + if err != nil { + if cerrdefs.IsNotFound(err) { + return total, errdefs.NotFound(errors.Wrapf(err, "non-existing ancestor of %s", snapshotID)) + } + return total, errdefs.System(errors.Wrapf(err, "snapshotter.Usage failed for %s", next)) + } + total.Size += usage.Size + total.Inodes += usage.Inodes + + info, err := snapshotter.Stat(ctx, next) + if err != nil { + return total, errdefs.System(errors.Wrapf(err, "snapshotter.Stat failed for %s", next)) + } + next = info.Parent + } + return total, nil +} diff --git a/daemon/containerd/service.go b/daemon/containerd/service.go index fce32665a845c..d68bcef91d77c 100644 --- a/daemon/containerd/service.go +++ b/daemon/containerd/service.go @@ -2,16 +2,15 @@ package containerd import ( "context" - "encoding/json" + "fmt" "sync/atomic" "github.com/containerd/containerd" - "github.com/containerd/containerd/content" + cerrdefs "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/remotes/docker" "github.com/containerd/containerd/snapshots" "github.com/docker/distribution/reference" - imagetypes "github.com/docker/docker/api/types/image" "github.com/docker/docker/container" daemonevents "github.com/docker/docker/daemon/events" "github.com/docker/docker/daemon/images" @@ -20,8 +19,6 @@ import ( "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/docker/docker/registry" - "github.com/opencontainers/go-digest" - "github.com/opencontainers/image-spec/identity" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -165,72 +162,29 @@ func (i *ImageService) GetContainerLayerSize(ctx context.Context, containerID st } snapshotter := i.client.SnapshotService(ctr.Driver) - - usage, err := snapshotter.Usage(ctx, containerID) + rwLayerUsage, err := snapshotter.Usage(ctx, containerID) if err != nil { - return 0, 0, err - } - - imageManifest, err := getContainerImageManifest(ctr) - if err != nil { - // Best efforts attempt to pick an image. - // We don't have platform information at this point, so we can only - // assume that the platform matches host. - // Otherwise this will give a wrong base image size (different - // platform), but should be close enough. - mfst, err := i.GetImageManifest(ctx, ctr.Config.Image, imagetypes.GetImageOpts{}) - if err != nil { - // Log error, don't error out whole operation. - logrus.WithFields(logrus.Fields{ - logrus.ErrorKey: err, - "container": containerID, - }).Warn("empty ImageManifest, can't calculate base image size") - return usage.Size, 0, nil + if cerrdefs.IsNotFound(err) { + return 0, 0, errdefs.NotFound(fmt.Errorf("rw layer snapshot not found for container %s", containerID)) } - imageManifest = *mfst - } - cs := i.client.ContentStore() - - imageManifestBytes, err := content.ReadBlob(ctx, cs, imageManifest) - if err != nil { - return 0, 0, err - } - - var manifest ocispec.Manifest - if err := json.Unmarshal(imageManifestBytes, &manifest); err != nil { - return 0, 0, err + return 0, 0, errdefs.System(errors.Wrapf(err, "snapshotter.Usage failed for %s", containerID)) } - imageConfigBytes, err := content.ReadBlob(ctx, cs, manifest.Config) + unpackedUsage, err := calculateSnapshotParentUsage(ctx, snapshotter, containerID) if err != nil { - return 0, 0, err - } - var img ocispec.Image - if err := json.Unmarshal(imageConfigBytes, &img); err != nil { - return 0, 0, err - } - - sizeCache := make(map[digest.Digest]int64) - snapshotSizeFn := func(d digest.Digest) (int64, error) { - if s, ok := sizeCache[d]; ok { - return s, nil + if cerrdefs.IsNotFound(err) { + logrus.WithField("ctr", containerID).Warn("parent of container snapshot no longer present") + } else { + logrus.WithError(err).WithField("ctr", containerID).Warn("unexpected error when calculating usage of the parent snapshots") } - u, err := snapshotter.Usage(ctx, d.String()) - if err != nil { - return 0, err - } - sizeCache[d] = u.Size - return u.Size, nil - } - - chainIDs := identity.ChainIDs(img.RootFS.DiffIDs) - snapShotSize, err := computeSnapshotSize(chainIDs, snapshotSizeFn) - if err != nil { - return 0, 0, err } + logrus.WithFields(logrus.Fields{ + "rwLayerUsage": rwLayerUsage.Size, + "unpacked": unpackedUsage.Size, + }).Debug("GetContainerLayerSize") // TODO(thaJeztah): include content-store size for the image (similar to "GET /images/json") - return usage.Size, usage.Size + snapShotSize, nil + return rwLayerUsage.Size, rwLayerUsage.Size + unpackedUsage.Size, nil } // getContainerImageManifest safely dereferences ImageManifest.