Browse Source

c8d: Implement Children by comparing diff ids

Implement Children method for containerd image store which makes the
`ancestor` filter work for `docker ps`. Checking if image is a children
of other image is implemented by comparing their rootfs diffids because
containerd image store doesn't have a concept of image parentship like
the graphdriver store. The child is expected to have more layers than
the parent and should start with all parent layers.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Gronowski 2 years ago
parent
commit
bea751beb7

+ 129 - 0
daemon/containerd/image_children.go

@@ -0,0 +1,129 @@
+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(id image.ID) []image.ID {
+	ctx := context.TODO()
+
+	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)
 }
 
-// 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.
 // called from create.go
 // TODO: accept an opt struct instead of container?