Browse Source

Merge pull request #45265 from vvoland/c8d-children-upstream

c8d: Implement Children by comparing diff ids
Sebastiaan van Stijn 2 years ago
parent
commit
74a0fdf961

+ 127 - 0
daemon/containerd/image_children.go

@@ -0,0 +1,127 @@
+package containerd
+
+import (
+	"context"
+
+	"github.com/containerd/containerd/content"
+	cerrdefs "github.com/containerd/containerd/errdefs"
+	containerdimages "github.com/containerd/containerd/images"
+	"github.com/containerd/containerd/platforms"
+	"github.com/docker/docker/image"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/sirupsen/logrus"
+)
+
+// Children returns a slice of image ID which rootfs is a superset of the
+// rootfs of the given image ID, excluding images with exactly the same rootfs.
+// Called from list.go to filter containers.
+func (i *ImageService) Children(ctx context.Context, id image.ID) []image.ID {
+	target, err := i.resolveDescriptor(ctx, id.String())
+	if err != nil {
+		logrus.WithError(err).Error("failed to get parent image")
+		return []image.ID{}
+	}
+
+	is := i.client.ImageService()
+	cs := i.client.ContentStore()
+
+	log := logrus.WithField("id", id)
+
+	allPlatforms, err := containerdimages.Platforms(ctx, cs, target)
+	if err != nil {
+		log.WithError(err).Error("failed to list supported platorms of image")
+		return []image.ID{}
+	}
+
+	parentRootFS := []ocispec.RootFS{}
+	for _, platform := range allPlatforms {
+		rootfs, err := platformRootfs(ctx, cs, target, platform)
+		if err != nil {
+			continue
+		}
+
+		parentRootFS = append(parentRootFS, rootfs)
+	}
+
+	imgs, err := is.List(ctx)
+	if err != nil {
+		log.WithError(err).Error("failed to list all images")
+		return []image.ID{}
+	}
+
+	children := []image.ID{}
+	for _, img := range imgs {
+	nextImage:
+		for _, platform := range allPlatforms {
+			rootfs, err := platformRootfs(ctx, cs, img.Target, platform)
+			if err != nil {
+				continue
+			}
+
+			for _, parentRoot := range parentRootFS {
+				if isRootfsChildOf(rootfs, parentRoot) {
+					children = append(children, image.ID(img.Target.Digest))
+					break nextImage
+				}
+			}
+		}
+
+	}
+
+	return children
+}
+
+// platformRootfs returns a rootfs for a specified platform.
+func platformRootfs(ctx context.Context, store content.Store, desc ocispec.Descriptor, platform ocispec.Platform) (ocispec.RootFS, error) {
+	empty := ocispec.RootFS{}
+
+	log := logrus.WithField("desc", desc.Digest).WithField("platform", platforms.Format(platform))
+	configDesc, err := containerdimages.Config(ctx, store, desc, platforms.OnlyStrict(platform))
+	if err != nil {
+		if !cerrdefs.IsNotFound(err) {
+			log.WithError(err).Warning("failed to get parent image config")
+		}
+		return empty, err
+	}
+
+	log = log.WithField("configDesc", configDesc)
+	diffs, err := containerdimages.RootFS(ctx, store, configDesc)
+	if err != nil {
+		if !cerrdefs.IsNotFound(err) {
+			log.WithError(err).Warning("failed to get parent image rootfs")
+		}
+		return empty, err
+	}
+
+	return ocispec.RootFS{
+		Type:    "layers",
+		DiffIDs: diffs,
+	}, nil
+}
+
+// isRootfsChildOf checks if all layers from parent rootfs are child's first layers
+// and child has at least one more layer (to make it not commutative).
+// Example:
+// A with layers [X, Y],
+// B with layers [X, Y, Z]
+// C with layers [Y, Z]
+//
+// Only isRootfsChildOf(B, A) is true.
+// Which means that B is considered a children of A. B and C has no children.
+// See more examples in TestIsRootfsChildOf.
+func isRootfsChildOf(child ocispec.RootFS, parent ocispec.RootFS) bool {
+	childLen := len(child.DiffIDs)
+	parentLen := len(parent.DiffIDs)
+
+	if childLen <= parentLen {
+		return false
+	}
+
+	for i := 0; i < parentLen; i++ {
+		if child.DiffIDs[i] != parent.DiffIDs[i] {
+			return false
+		}
+	}
+
+	return true
+}

+ 55 - 0
daemon/containerd/image_children_test.go

@@ -0,0 +1,55 @@
+package containerd
+
+import (
+	"testing"
+
+	"github.com/opencontainers/go-digest"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"gotest.tools/v3/assert"
+	is "gotest.tools/v3/assert/cmp"
+)
+
+func TestIsRootfsChildOf(t *testing.T) {
+	// Each unique letter is one distinct DiffID
+	ab := toRootfs("AB")
+	abc := toRootfs("ABC")
+	abd := toRootfs("ABD")
+	xyz := toRootfs("XYZ")
+	xyzab := toRootfs("XYZAB")
+
+	for _, tc := range []struct {
+		name   string
+		parent ocispec.RootFS
+		child  ocispec.RootFS
+		out    bool
+	}{
+		{parent: ab, child: abc, out: true, name: "one additional layer"},
+		{parent: xyz, child: xyzab, out: true, name: "two additional layers"},
+		{parent: xyz, child: xyz, out: false, name: "parent is not a child of itself"},
+		{parent: abc, child: abd, out: false, name: "sibling"},
+		{parent: abc, child: xyz, out: false, name: "completely different rootfs, but same length"},
+		{parent: abc, child: ab, out: false, name: "child can't be shorter than parent"},
+		{parent: ab, child: xyzab, out: false, name: "parent layers appended"},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			out := isRootfsChildOf(tc.child, tc.parent)
+
+			assert.Check(t, is.Equal(out, tc.out))
+		})
+	}
+}
+
+func toRootfs(values string) ocispec.RootFS {
+	dgsts := []digest.Digest{}
+
+	for _, v := range values {
+		vd := digest.FromString(string(v))
+		dgsts = append(dgsts, vd)
+	}
+
+	return ocispec.RootFS{
+		Type:    "layers",
+		DiffIDs: dgsts,
+	}
+}

+ 0 - 7
daemon/containerd/service.go

@@ -76,13 +76,6 @@ func (i *ImageService) CountImages() int {
 	return len(imgs)
 	return len(imgs)
 }
 }
 
 
-// Children returns the children image.IDs for a parent image.
-// called from list.go to filter containers
-// TODO: refactor to expose an ancestry for image.ID?
-func (i *ImageService) Children(id image.ID) []image.ID {
-	panic("not implemented")
-}
-
 // CreateLayer creates a filesystem layer for a container.
 // CreateLayer creates a filesystem layer for a container.
 // called from create.go
 // called from create.go
 // TODO: accept an opt struct instead of container?
 // TODO: accept an opt struct instead of container?

+ 1 - 1
daemon/image_service.go

@@ -73,7 +73,7 @@ type ImageService interface {
 	// Other
 	// Other
 
 
 	DistributionServices() images.DistributionServices
 	DistributionServices() images.DistributionServices
-	Children(id image.ID) []image.ID
+	Children(ctx context.Context, id image.ID) []image.ID
 	Cleanup() error
 	Cleanup() error
 	StorageDriver() string
 	StorageDriver() string
 	UpdateConfig(maxDownloads, maxUploads int)
 	UpdateConfig(maxDownloads, maxUploads int)

+ 1 - 1
daemon/images/service.go

@@ -109,7 +109,7 @@ func (i *ImageService) CountImages() int {
 // Children returns the children image.IDs for a parent image.
 // Children returns the children image.IDs for a parent image.
 // called from list.go to filter containers
 // called from list.go to filter containers
 // TODO: refactor to expose an ancestry for image.ID?
 // TODO: refactor to expose an ancestry for image.ID?
-func (i *ImageService) Children(id image.ID) []image.ID {
+func (i *ImageService) Children(_ context.Context, id image.ID) []image.ID {
 	return i.imageStore.Children(id)
 	return i.imageStore.Children(id)
 }
 }
 
 

+ 4 - 4
daemon/list.go

@@ -328,7 +328,7 @@ func (daemon *Daemon) foldFilter(ctx context.Context, view *container.View, conf
 				return nil
 				return nil
 			}
 			}
 			// Then walk down the graph and put the imageIds in imagesFilter
 			// Then walk down the graph and put the imageIds in imagesFilter
-			populateImageFilterByParents(imagesFilter, img.ID(), daemon.imageService.Children)
+			populateImageFilterByParents(ctx, imagesFilter, img.ID(), daemon.imageService.Children)
 			return nil
 			return nil
 		})
 		})
 	}
 	}
@@ -594,10 +594,10 @@ func (daemon *Daemon) refreshImage(ctx context.Context, s *container.Snapshot, f
 	return &c, nil
 	return &c, nil
 }
 }
 
 
-func populateImageFilterByParents(ancestorMap map[image.ID]bool, imageID image.ID, getChildren func(image.ID) []image.ID) {
+func populateImageFilterByParents(ctx context.Context, ancestorMap map[image.ID]bool, imageID image.ID, getChildren func(context.Context, image.ID) []image.ID) {
 	if !ancestorMap[imageID] {
 	if !ancestorMap[imageID] {
-		for _, id := range getChildren(imageID) {
-			populateImageFilterByParents(ancestorMap, id, getChildren)
+		for _, id := range getChildren(ctx, imageID) {
+			populateImageFilterByParents(ctx, ancestorMap, id, getChildren)
 		}
 		}
 		ancestorMap[imageID] = true
 		ancestorMap[imageID] = true
 	}
 	}