瀏覽代碼

c8d: Fix `docker diff`

Diffing a container yielded some extra changes that come from the
files/directories that we mount inside the container (/etc/resolv.conf
for example). To avoid that we create an intermediate snapshot that has
these files, with this we can now diff the container fs with its parent
and only get the differences that were made inside the container.

Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
Djordje Lukic 1 年之前
父節點
當前提交
207c4d537c

+ 9 - 42
daemon/containerd/image_changes.go

@@ -2,68 +2,35 @@ package containerd
 
 
 import (
 import (
 	"context"
 	"context"
-	"encoding/json"
 
 
-	"github.com/containerd/containerd/content"
 	"github.com/containerd/containerd/log"
 	"github.com/containerd/containerd/log"
 	"github.com/containerd/containerd/mount"
 	"github.com/containerd/containerd/mount"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/pkg/archive"
 	"github.com/docker/docker/pkg/archive"
-	"github.com/google/uuid"
-	"github.com/opencontainers/image-spec/identity"
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 )
 )
 
 
 func (i *ImageService) Changes(ctx context.Context, container *container.Container) ([]archive.Change, error) {
 func (i *ImageService) Changes(ctx context.Context, container *container.Container) ([]archive.Change, error) {
-	cs := i.client.ContentStore()
-
-	imageManifest, err := getContainerImageManifest(container)
-	if err != nil {
-		return nil, err
-	}
-
-	imageManifestBytes, err := content.ReadBlob(ctx, cs, imageManifest)
-	if err != nil {
-		return nil, err
-	}
-	var manifest ocispec.Manifest
-	if err := json.Unmarshal(imageManifestBytes, &manifest); err != nil {
-		return nil, err
-	}
-
-	imageConfigBytes, err := content.ReadBlob(ctx, cs, manifest.Config)
-	if err != nil {
-		return nil, err
-	}
-	var image ocispec.Image
-	if err := json.Unmarshal(imageConfigBytes, &image); err != nil {
-		return nil, err
-	}
-
-	rnd, err := uuid.NewRandom()
+	snapshotter := i.client.SnapshotService(container.Driver)
+	info, err := snapshotter.Stat(ctx, container.ID)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	snapshotter := i.client.SnapshotService(container.Driver)
+	imageMounts, _ := snapshotter.View(ctx, container.ID+"-parent-view", info.Parent)
 
 
-	diffIDs := image.RootFS.DiffIDs
-	parent, err := snapshotter.View(ctx, rnd.String(), identity.ChainID(diffIDs).String())
-	if err != nil {
-		return nil, err
-	}
 	defer func() {
 	defer func() {
-		if err := snapshotter.Remove(ctx, rnd.String()); err != nil {
-			log.G(ctx).WithError(err).WithField("key", rnd.String()).Warn("remove temporary snapshot")
+		if err := snapshotter.Remove(ctx, container.ID+"-parent-view"); err != nil {
+			log.G(ctx).WithError(err).Warn("error removing the parent view snapshot")
 		}
 		}
 	}()
 	}()
 
 
 	var changes []archive.Change
 	var changes []archive.Change
-	err = i.PerformWithBaseFS(ctx, container, func(containerRootfs string) error {
-		return mount.WithReadonlyTempMount(ctx, parent, func(parentRootfs string) error {
-			changes, err = archive.ChangesDirs(containerRootfs, parentRootfs)
+	err = i.PerformWithBaseFS(ctx, container, func(containerRoot string) error {
+		return mount.WithReadonlyTempMount(ctx, imageMounts, func(imageRoot string) error {
+			changes, err = archive.ChangesDirs(containerRoot, imageRoot)
 			return err
 			return err
 		})
 		})
 	})
 	})
+
 	return changes, err
 	return changes, err
 }
 }

+ 26 - 10
daemon/containerd/image_snapshot.go

@@ -8,6 +8,7 @@ import (
 	cerrdefs "github.com/containerd/containerd/errdefs"
 	cerrdefs "github.com/containerd/containerd/errdefs"
 	containerdimages "github.com/containerd/containerd/images"
 	containerdimages "github.com/containerd/containerd/images"
 	"github.com/containerd/containerd/leases"
 	"github.com/containerd/containerd/leases"
+	"github.com/containerd/containerd/mount"
 	"github.com/containerd/containerd/platforms"
 	"github.com/containerd/containerd/platforms"
 	"github.com/containerd/containerd/snapshots"
 	"github.com/containerd/containerd/snapshots"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/errdefs"
@@ -19,7 +20,7 @@ import (
 const remapSuffix = "-remap"
 const remapSuffix = "-remap"
 
 
 // PrepareSnapshot prepares a snapshot from a parent image for a container
 // PrepareSnapshot prepares a snapshot from a parent image for a container
-func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentImage string, platform *ocispec.Platform) error {
+func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentImage string, platform *ocispec.Platform, setupInit func(string) error) error {
 	var parentSnapshot string
 	var parentSnapshot string
 	if parentImage != "" {
 	if parentImage != "" {
 		img, err := i.resolveImage(ctx, parentImage)
 		img, err := i.resolveImage(ctx, parentImage)
@@ -59,29 +60,44 @@ func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentIma
 		parentSnapshot = identity.ChainID(diffIDs).String()
 		parentSnapshot = identity.ChainID(diffIDs).String()
 	}
 	}
 
 
-	// Add a lease so that containerd doesn't garbage collect our snapshot
 	ls := i.client.LeasesService()
 	ls := i.client.LeasesService()
 	lease, err := ls.Create(ctx, leases.WithID(id))
 	lease, err := ls.Create(ctx, leases.WithID(id))
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if err := ls.AddResource(ctx, lease, leases.Resource{
-		ID:   id,
-		Type: "snapshots/" + i.StorageDriver(),
-	}); err != nil {
-		return err
-	}
+	ctx = leases.WithLease(ctx, lease.ID)
 
 
 	snapshotter := i.client.SnapshotService(i.StorageDriver())
 	snapshotter := i.client.SnapshotService(i.StorageDriver())
 
 
+	if err := i.prepareInitLayer(ctx, id, parentSnapshot, setupInit); err != nil {
+		return err
+	}
+
 	if !i.idMapping.Empty() {
 	if !i.idMapping.Empty() {
-		return i.remapSnapshot(ctx, snapshotter, id, parentSnapshot, lease)
+		return i.remapSnapshot(ctx, snapshotter, id, id+"-init")
 	}
 	}
 
 
-	_, err = snapshotter.Prepare(ctx, id, parentSnapshot)
+	_, err = snapshotter.Prepare(ctx, id, id+"-init")
 	return err
 	return err
 }
 }
 
 
+func (i *ImageService) prepareInitLayer(ctx context.Context, id string, parent string, setupInit func(string) error) error {
+	snapshotter := i.client.SnapshotService(i.StorageDriver())
+
+	mounts, err := snapshotter.Prepare(ctx, id+"-init-key", parent)
+	if err != nil {
+		return err
+	}
+
+	if err := mount.WithTempMount(ctx, mounts, func(root string) error {
+		return setupInit(root)
+	}); err != nil {
+		return err
+	}
+
+	return snapshotter.Commit(ctx, id+"-init", id+"-init-key")
+}
+
 // calculateSnapshotParentUsage returns the usage of all ancestors of the
 // calculateSnapshotParentUsage returns the usage of all ancestors of the
 // provided snapshot. It doesn't include the size of the snapshot itself.
 // 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) {
 func calculateSnapshotParentUsage(ctx context.Context, snapshotter snapshots.Snapshotter, snapshotID string) (snapshots.Usage, error) {

+ 1 - 16
daemon/containerd/image_snapshot_unix.go

@@ -9,15 +9,13 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"syscall"
 	"syscall"
 
 
-	"github.com/containerd/containerd/leases"
 	"github.com/containerd/containerd/log"
 	"github.com/containerd/containerd/log"
 	"github.com/containerd/containerd/mount"
 	"github.com/containerd/containerd/mount"
 	"github.com/containerd/containerd/snapshots"
 	"github.com/containerd/containerd/snapshots"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/pkg/idtools"
 )
 )
 
 
-func (i *ImageService) remapSnapshot(ctx context.Context, snapshotter snapshots.Snapshotter, id string, parentSnapshot string, lease leases.Lease) error {
-	ls := i.client.LeasesService()
+func (i *ImageService) remapSnapshot(ctx context.Context, snapshotter snapshots.Snapshotter, id string, parentSnapshot string) error {
 	rootPair := i.idMapping.RootPair()
 	rootPair := i.idMapping.RootPair()
 	usernsID := fmt.Sprintf("%s-%d-%d", parentSnapshot, rootPair.UID, rootPair.GID)
 	usernsID := fmt.Sprintf("%s-%d-%d", parentSnapshot, rootPair.UID, rootPair.GID)
 	remappedID := usernsID + remapSuffix
 	remappedID := usernsID + remapSuffix
@@ -28,19 +26,6 @@ func (i *ImageService) remapSnapshot(ctx context.Context, snapshotter snapshots.
 		return err
 		return err
 	}
 	}
 
 
-	if err := ls.AddResource(ctx, lease, leases.Resource{
-		ID:   remappedID,
-		Type: "snapshots/" + i.StorageDriver(),
-	}); err != nil {
-		return err
-	}
-	if err := ls.AddResource(ctx, lease, leases.Resource{
-		ID:   usernsID,
-		Type: "snapshots/" + i.StorageDriver(),
-	}); err != nil {
-		return err
-	}
-
 	mounts, err := snapshotter.Prepare(ctx, remappedID, parentSnapshot)
 	mounts, err := snapshotter.Prepare(ctx, remappedID, parentSnapshot)
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 1 - 2
daemon/containerd/image_snapshot_windows.go

@@ -3,10 +3,9 @@ package containerd
 import (
 import (
 	"context"
 	"context"
 
 
-	"github.com/containerd/containerd/leases"
 	"github.com/containerd/containerd/snapshots"
 	"github.com/containerd/containerd/snapshots"
 )
 )
 
 
-func (i *ImageService) remapSnapshot(ctx context.Context, snapshotter snapshots.Snapshotter, id string, parentSnapshot string, lease leases.Lease) error {
+func (i *ImageService) remapSnapshot(ctx context.Context, snapshotter snapshots.Snapshotter, id string, parentSnapshot string) error {
 	return nil
 	return nil
 }
 }

+ 0 - 12
daemon/containerd/service.go

@@ -21,7 +21,6 @@ import (
 	"github.com/docker/docker/layer"
 	"github.com/docker/docker/layer"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/registry"
 	"github.com/docker/docker/registry"
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 	"github.com/pkg/errors"
 )
 )
 
 
@@ -186,14 +185,3 @@ func (i *ImageService) GetContainerLayerSize(ctx context.Context, containerID st
 	// TODO(thaJeztah): include content-store size for the image (similar to "GET /images/json")
 	// TODO(thaJeztah): include content-store size for the image (similar to "GET /images/json")
 	return rwLayerUsage.Size, rwLayerUsage.Size + unpackedUsage.Size, nil
 	return rwLayerUsage.Size, rwLayerUsage.Size + unpackedUsage.Size, nil
 }
 }
-
-// getContainerImageManifest safely dereferences ImageManifest.
-// ImageManifest can be nil for containers created with Docker Desktop with old
-// containerd image store integration enabled which didn't set this field.
-func getContainerImageManifest(ctr *container.Container) (ocispec.Descriptor, error) {
-	if ctr.ImageManifest == nil {
-		return ocispec.Descriptor{}, errdefs.InvalidParameter(errors.New("container is missing ImageManifest (probably created on old version), please recreate it"))
-	}
-
-	return *ctr.ImageManifest, nil
-}

+ 1 - 1
daemon/create.go

@@ -195,7 +195,7 @@ func (daemon *Daemon) create(ctx context.Context, daemonCfg *config.Config, opts
 	ctr.ImageManifest = imgManifest
 	ctr.ImageManifest = imgManifest
 
 
 	if daemon.UsesSnapshotter() {
 	if daemon.UsesSnapshotter() {
-		if err := daemon.imageService.PrepareSnapshot(ctx, ctr.ID, opts.params.Config.Image, opts.params.Platform); err != nil {
+		if err := daemon.imageService.PrepareSnapshot(ctx, ctr.ID, opts.params.Config.Image, opts.params.Platform, setupInitLayer(daemon.idMapping)); err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 	} else {
 	} else {

+ 1 - 1
daemon/image_service.go

@@ -47,7 +47,7 @@ type ImageService interface {
 
 
 	// Containerd related methods
 	// Containerd related methods
 
 
-	PrepareSnapshot(ctx context.Context, id string, image string, platform *ocispec.Platform) error
+	PrepareSnapshot(ctx context.Context, id string, parentImage string, platform *ocispec.Platform, setupInit func(string) error) error
 	GetImageManifest(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*ocispec.Descriptor, error)
 	GetImageManifest(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*ocispec.Descriptor, error)
 
 
 	// Layers
 	// Layers

+ 1 - 1
daemon/images/image.go

@@ -46,7 +46,7 @@ type manifest struct {
 	Config ocispec.Descriptor `json:"config"`
 	Config ocispec.Descriptor `json:"config"`
 }
 }
 
 
-func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, image string, platform *ocispec.Platform) error {
+func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentImage string, platform *ocispec.Platform, setupInit func(string) error) error {
 	// Only makes sense when conatinerd image store is used
 	// Only makes sense when conatinerd image store is used
 	panic("not implemented")
 	panic("not implemented")
 }
 }

+ 21 - 5
integration/container/diff_test.go

@@ -12,22 +12,38 @@ import (
 )
 )
 
 
 func TestDiff(t *testing.T) {
 func TestDiff(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "cannot diff a running container on Windows")
+	ctx := setupTest(t)
+	apiClient := testEnv.APIClient()
+
+	cID := container.Run(ctx, t, apiClient, container.WithCmd("sh", "-c", `mkdir /foo; echo xyzzy > /foo/bar`))
+
+	expected := []containertypes.FilesystemChange{
+		{Kind: containertypes.ChangeAdd, Path: "/foo"},
+		{Kind: containertypes.ChangeAdd, Path: "/foo/bar"},
+	}
+
+	items, err := apiClient.ContainerDiff(ctx, cID)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expected, items)
+}
+
+func TestDiffStoppedContainer(t *testing.T) {
+	// There's no way in Windows to differentiate between an Add or a Modify,
+	// and all files are under a "Files/" prefix.
 	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
 	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
 	ctx := setupTest(t)
 	ctx := setupTest(t)
 	apiClient := testEnv.APIClient()
 	apiClient := testEnv.APIClient()
 
 
 	cID := container.Run(ctx, t, apiClient, container.WithCmd("sh", "-c", `mkdir /foo; echo xyzzy > /foo/bar`))
 	cID := container.Run(ctx, t, apiClient, container.WithCmd("sh", "-c", `mkdir /foo; echo xyzzy > /foo/bar`))
 
 
-	// Wait for it to exit as cannot diff a running container on Windows, and
-	// it will take a few seconds to exit. Also there's no way in Windows to
-	// differentiate between an Add or a Modify, and all files are under
-	// a "Files/" prefix.
+	poll.WaitOn(t, container.IsInState(ctx, apiClient, cID, "exited"), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(60*time.Second))
+
 	expected := []containertypes.FilesystemChange{
 	expected := []containertypes.FilesystemChange{
 		{Kind: containertypes.ChangeAdd, Path: "/foo"},
 		{Kind: containertypes.ChangeAdd, Path: "/foo"},
 		{Kind: containertypes.ChangeAdd, Path: "/foo/bar"},
 		{Kind: containertypes.ChangeAdd, Path: "/foo/bar"},
 	}
 	}
 	if testEnv.DaemonInfo.OSType == "windows" {
 	if testEnv.DaemonInfo.OSType == "windows" {
-		poll.WaitOn(t, container.IsInState(ctx, apiClient, cID, "exited"), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(60*time.Second))
 		expected = []containertypes.FilesystemChange{
 		expected = []containertypes.FilesystemChange{
 			{Kind: containertypes.ChangeModify, Path: "Files/foo"},
 			{Kind: containertypes.ChangeModify, Path: "Files/foo"},
 			{Kind: containertypes.ChangeModify, Path: "Files/foo/bar"},
 			{Kind: containertypes.ChangeModify, Path: "Files/foo/bar"},