Browse code

Add containerd migration to daemon startup

Add layer migration on startup
Use image size threshold rather than image count
Add daemon integration test
Add test for migrating to containerd snapshotters
Add vfs migration
Add tar export for containerd migration
Add containerd migration test with save and load

Signed-off-by: Derek McGowan <derek@mcg.dev>

Derek McGowan authored on 2024/10/10 07:19:32
Showing 4 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,316 @@
0
+package migration
1
+
2
+import (
3
+	"bytes"
4
+	"context"
5
+	"encoding/json"
6
+	"fmt"
7
+	"io"
8
+	"strings"
9
+	"sync"
10
+	"time"
11
+
12
+	"github.com/containerd/containerd/v2/core/content"
13
+	"github.com/containerd/containerd/v2/core/images"
14
+	"github.com/containerd/containerd/v2/core/leases"
15
+	"github.com/containerd/containerd/v2/core/mount"
16
+	"github.com/containerd/containerd/v2/core/snapshots"
17
+	"github.com/containerd/containerd/v2/pkg/archive/compression"
18
+	"github.com/containerd/continuity/fs"
19
+	cerrdefs "github.com/containerd/errdefs"
20
+	"github.com/containerd/log"
21
+	"github.com/moby/moby/v2/daemon/internal/image"
22
+	"github.com/moby/moby/v2/daemon/internal/layer"
23
+	refstore "github.com/moby/moby/v2/daemon/internal/refstore"
24
+	"github.com/opencontainers/go-digest"
25
+	"github.com/opencontainers/image-spec/identity"
26
+	"github.com/opencontainers/image-spec/specs-go"
27
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
28
+	"golang.org/x/sync/errgroup"
29
+)
30
+
31
+type LayerMigrator struct {
32
+	layers  layer.Store
33
+	refs    refstore.Store
34
+	dis     image.Store
35
+	leases  leases.Manager
36
+	content content.Store
37
+	cis     images.Store
38
+}
39
+
40
+type Config struct {
41
+	LayerStore       layer.Store
42
+	ReferenceStore   refstore.Store
43
+	DockerImageStore image.Store
44
+	Leases           leases.Manager
45
+	Content          content.Store
46
+	ImageStore       images.Store
47
+}
48
+
49
+func NewLayerMigrator(config Config) *LayerMigrator {
50
+	return &LayerMigrator{
51
+		layers:  config.LayerStore,
52
+		refs:    config.ReferenceStore,
53
+		dis:     config.DockerImageStore,
54
+		leases:  config.Leases,
55
+		content: config.Content,
56
+		cis:     config.ImageStore,
57
+	}
58
+}
59
+
60
+// MigrateTocontainerd migrates containers from overlay2 to overlayfs or vfs to native
61
+func (lm *LayerMigrator) MigrateTocontainerd(ctx context.Context, snKey string, sn snapshots.Snapshotter) error {
62
+	if sn == nil {
63
+		return fmt.Errorf("no snapshotter to migrate to: %w", cerrdefs.ErrNotImplemented)
64
+	}
65
+
66
+	switch driver := lm.layers.DriverName(); driver {
67
+	case "overlay2":
68
+	case "vfs":
69
+	default:
70
+		return fmt.Errorf("%q not supported for migration: %w", driver, cerrdefs.ErrNotImplemented)
71
+	}
72
+
73
+	var (
74
+		// Zstd makes migration 10x faster
75
+		// TODO: make configurable
76
+		layerMediaType   = ocispec.MediaTypeImageLayerZstd
77
+		layerCompression = compression.Zstd
78
+	)
79
+
80
+	l, err := lm.leases.Create(ctx, leases.WithRandomID(), leases.WithExpiration(24*time.Hour))
81
+	if err != nil {
82
+		return err
83
+	}
84
+	defer func() {
85
+		lm.leases.Delete(ctx, l)
86
+	}()
87
+	ctx = leases.WithLease(ctx, l.ID)
88
+
89
+	for imgID, img := range lm.dis.Heads() {
90
+		diffids := img.RootFS.DiffIDs
91
+		if len(diffids) == 0 {
92
+			continue
93
+		}
94
+		var (
95
+			parent   string
96
+			manifest = ocispec.Manifest{
97
+				MediaType: ocispec.MediaTypeImageManifest,
98
+				Versioned: specs.Versioned{
99
+					SchemaVersion: 2,
100
+				},
101
+				Layers: make([]ocispec.Descriptor, len(diffids)),
102
+			}
103
+			ml        sync.Mutex
104
+			eg, egctx = errgroup.WithContext(ctx)
105
+		)
106
+		for i := range diffids {
107
+			chainID := identity.ChainID(diffids[:i+1])
108
+			l, err := lm.layers.Get(chainID)
109
+			if err != nil {
110
+				return fmt.Errorf("failed to get layer [%d] %q: %w", i, chainID, err)
111
+			}
112
+			layerIndex := i
113
+			eg.Go(func() error {
114
+				ctx := egctx
115
+				t1 := time.Now()
116
+				ts, err := l.TarStream()
117
+				if err != nil {
118
+					return err
119
+				}
120
+
121
+				desc := ocispec.Descriptor{
122
+					MediaType: layerMediaType,
123
+				}
124
+
125
+				cw, err := lm.content.Writer(ctx,
126
+					content.WithRef(fmt.Sprintf("ingest-%s", chainID)),
127
+					content.WithDescriptor(desc))
128
+				if err != nil {
129
+					return fmt.Errorf("failed to get content writer: %w", err)
130
+				}
131
+
132
+				dgstr := digest.Canonical.Digester()
133
+				cs, _ := compression.CompressStream(io.MultiWriter(cw, dgstr.Hash()), layerCompression)
134
+				_, err = io.Copy(cs, ts)
135
+				if err != nil {
136
+					return fmt.Errorf("failed to copy to compressed stream: %w", err)
137
+				}
138
+				cs.Close()
139
+
140
+				status, err := cw.Status()
141
+				if err != nil {
142
+					return err
143
+				}
144
+
145
+				desc.Size = status.Offset
146
+				desc.Digest = dgstr.Digest()
147
+
148
+				if err := cw.Commit(ctx, desc.Size, desc.Digest); err != nil && !cerrdefs.IsAlreadyExists(err) {
149
+					return err
150
+				}
151
+
152
+				log.G(ctx).WithFields(log.Fields{
153
+					"t":      time.Since(t1),
154
+					"size":   desc.Size,
155
+					"digest": desc.Digest,
156
+				}).Debug("Converted layer to content tar")
157
+
158
+				ml.Lock()
159
+				manifest.Layers[layerIndex] = desc
160
+				ml.Unlock()
161
+				return nil
162
+			})
163
+
164
+			metadata, err := l.Metadata()
165
+			if err != nil {
166
+				return err
167
+			}
168
+			src, ok := metadata["UpperDir"]
169
+			if !ok {
170
+				src, ok = metadata["SourceDir"]
171
+				if !ok {
172
+					log.G(ctx).WithField("metadata", metadata).WithField("driver", lm.layers.DriverName()).Debug("no source directory metadata")
173
+					return fmt.Errorf("graphdriver not supported: %w", cerrdefs.ErrNotImplemented)
174
+				}
175
+			}
176
+			log.G(ctx).WithField("metadata", metadata).Debugf("migrating %s from %s", chainID, src)
177
+
178
+			active := fmt.Sprintf("migration-%s", chainID)
179
+
180
+			key := chainID.String()
181
+
182
+			snapshotLabels := map[string]string{
183
+				"containerd.io/snapshot.ref": key,
184
+			}
185
+			mounts, err := sn.Prepare(ctx, active, parent, snapshots.WithLabels(snapshotLabels))
186
+			parent = key
187
+			if err != nil {
188
+				if cerrdefs.IsAlreadyExists(err) {
189
+					continue
190
+				}
191
+				return err
192
+			}
193
+
194
+			dst, err := extractSource(mounts)
195
+			if err != nil {
196
+				return err
197
+			}
198
+
199
+			t1 := time.Now()
200
+			if err := fs.CopyDir(dst, src); err != nil {
201
+				return err
202
+			}
203
+			log.G(ctx).WithFields(log.Fields{
204
+				"t":   time.Since(t1),
205
+				"key": key,
206
+			}).Debug("Copied layer to snapshot")
207
+
208
+			if err := sn.Commit(ctx, key, active); err != nil && !cerrdefs.IsAlreadyExists(err) {
209
+				return err
210
+			}
211
+		}
212
+
213
+		configBytes := img.RawJSON()
214
+		digest.FromBytes(configBytes)
215
+		manifest.Config = ocispec.Descriptor{
216
+			MediaType: ocispec.MediaTypeImageConfig,
217
+			Digest:    digest.FromBytes(configBytes),
218
+			Size:      int64(len(configBytes)),
219
+		}
220
+
221
+		configLabels := map[string]string{
222
+			fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snKey): parent,
223
+		}
224
+		if err = content.WriteBlob(ctx, lm.content, "config"+manifest.Config.Digest.String(), bytes.NewReader(configBytes), manifest.Config, content.WithLabels(configLabels)); err != nil && !cerrdefs.IsAlreadyExists(err) {
225
+			return err
226
+		}
227
+
228
+		if err := eg.Wait(); err != nil {
229
+			return err
230
+		}
231
+
232
+		manifestBytes, err := json.MarshalIndent(manifest, "", "   ")
233
+		if err != nil {
234
+			return err
235
+		}
236
+
237
+		manifestDesc := ocispec.Descriptor{
238
+			MediaType: manifest.MediaType,
239
+			Digest:    digest.FromBytes(manifestBytes),
240
+			Size:      int64(len(manifestBytes)),
241
+		}
242
+
243
+		manifestLabels := map[string]string{
244
+			"containerd.io/gc.ref.content.config": manifest.Config.Digest.String(),
245
+		}
246
+		for i := range manifest.Layers {
247
+			manifestLabels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = manifest.Layers[i].Digest.String()
248
+		}
249
+
250
+		if err = content.WriteBlob(ctx, lm.content, "manifest"+manifestDesc.Digest.String(), bytes.NewReader(manifestBytes), manifestDesc, content.WithLabels(manifestLabels)); err != nil && !cerrdefs.IsAlreadyExists(err) {
251
+			return err
252
+		}
253
+
254
+		childrenHandler := images.ChildrenHandler(lm.content)
255
+		childrenHandler = images.SetChildrenMappedLabels(lm.content, childrenHandler, nil)
256
+		if err = images.Walk(ctx, childrenHandler, manifestDesc); err != nil {
257
+			return err
258
+		}
259
+
260
+		var added bool
261
+		for _, named := range lm.refs.References(digest.Digest(imgID)) {
262
+			img := images.Image{
263
+				Name:   named.String(),
264
+				Target: manifestDesc,
265
+				// TODO: Any labels?
266
+			}
267
+			img, err = lm.cis.Create(ctx, img)
268
+			if err != nil && !cerrdefs.IsAlreadyExists(err) {
269
+				return err
270
+			} else if err != nil {
271
+				log.G(ctx).Infof("Tag already exists: %s", named)
272
+				continue
273
+			}
274
+
275
+			log.G(ctx).Infof("Migrated image %s to %s", img.Name, img.Target.Digest)
276
+			added = true
277
+		}
278
+
279
+		if !added {
280
+			img := images.Image{
281
+				Name:   "moby-dangling@" + manifestDesc.Digest.String(),
282
+				Target: manifestDesc,
283
+				// TODO: Any labels?
284
+			}
285
+			img, err = lm.cis.Create(ctx, img)
286
+			if err != nil && !cerrdefs.IsAlreadyExists(err) {
287
+				return err
288
+			} else if err == nil {
289
+				log.G(ctx).Infof("Migrated image %s to %s", img.Name, img.Target.Digest)
290
+			}
291
+		}
292
+	}
293
+
294
+	return nil
295
+}
296
+
297
+func extractSource(mounts []mount.Mount) (string, error) {
298
+	if len(mounts) != 1 {
299
+		return "", fmt.Errorf("cannot support snapshotters with multiple mount sources: %w", cerrdefs.ErrNotImplemented)
300
+	}
301
+	switch mounts[0].Type {
302
+	case "bind":
303
+		return mounts[0].Source, nil
304
+	case "overlay":
305
+		for _, option := range mounts[0].Options {
306
+			if strings.HasPrefix(option, "upperdir=") {
307
+				return option[9:], nil
308
+			}
309
+		}
310
+	default:
311
+		return "", fmt.Errorf("mount type %q not supported: %w", mounts[0].Type, cerrdefs.ErrNotImplemented)
312
+	}
313
+
314
+	return "", fmt.Errorf("mount is missing upper option: %w", cerrdefs.ErrNotImplemented)
315
+}
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"crypto/sha256"
11 11
 	"encoding/binary"
12 12
 	"fmt"
13
+	"math"
13 14
 	"net"
14 15
 	"net/netip"
15 16
 	"os"
... ...
@@ -42,6 +43,7 @@ import (
42 42
 	"github.com/moby/moby/v2/daemon/config"
43 43
 	"github.com/moby/moby/v2/daemon/container"
44 44
 	ctrd "github.com/moby/moby/v2/daemon/containerd"
45
+	"github.com/moby/moby/v2/daemon/containerd/migration"
45 46
 	"github.com/moby/moby/v2/daemon/events"
46 47
 	_ "github.com/moby/moby/v2/daemon/graphdriver/register" // register graph drivers
47 48
 	"github.com/moby/moby/v2/daemon/images"
... ...
@@ -202,15 +204,15 @@ func (daemon *Daemon) UsesSnapshotter() bool {
202 202
 	return daemon.usesSnapshotter
203 203
 }
204 204
 
205
-func (daemon *Daemon) restore(cfg *configStore) error {
205
+func (daemon *Daemon) loadContainers() (map[string]map[string]*container.Container, error) {
206 206
 	var mapLock sync.Mutex
207
-	containers := make(map[string]*container.Container)
207
+	driverContainers := make(map[string]map[string]*container.Container)
208 208
 
209 209
 	log.G(context.TODO()).Info("Loading containers: start.")
210 210
 
211 211
 	dir, err := os.ReadDir(daemon.repository)
212 212
 	if err != nil {
213
-		return err
213
+		return nil, err
214 214
 	}
215 215
 
216 216
 	// parallelLimit is the maximum number of parallel startup jobs that we
... ...
@@ -238,29 +240,39 @@ func (daemon *Daemon) restore(cfg *configStore) error {
238 238
 				logger.WithError(err).Error("failed to load container")
239 239
 				return
240 240
 			}
241
-			if c.Driver != daemon.imageService.StorageDriver() {
242
-				// Ignore the container if it wasn't created with the current storage-driver
243
-				logger.Debugf("not restoring container because it was created with another storage driver (%s)", c.Driver)
244
-				return
245
-			}
246
-			rwlayer, err := daemon.imageService.GetLayerByID(c.ID)
247
-			if err != nil {
248
-				logger.WithError(err).Error("failed to load container mount")
249
-				return
250
-			}
251
-			c.RWLayer = rwlayer
252
-			logger.WithFields(log.Fields{
253
-				"running": c.IsRunning(),
254
-				"paused":  c.IsPaused(),
255
-			}).Debug("loaded container")
256 241
 
257 242
 			mapLock.Lock()
258
-			containers[c.ID] = c
243
+			if containers, ok := driverContainers[c.Driver]; !ok {
244
+				driverContainers[c.Driver] = map[string]*container.Container{
245
+					c.ID: c,
246
+				}
247
+			} else {
248
+				containers[c.ID] = c
249
+			}
259 250
 			mapLock.Unlock()
260 251
 		}(v.Name())
261 252
 	}
262 253
 	group.Wait()
263 254
 
255
+	return driverContainers, nil
256
+}
257
+
258
+func (daemon *Daemon) restore(cfg *configStore, containers map[string]*container.Container) error {
259
+	var mapLock sync.Mutex
260
+
261
+	log.G(context.TODO()).Info("Restoring containers: start.")
262
+
263
+	// parallelLimit is the maximum number of parallel startup jobs that we
264
+	// allow (this is the limited used for all startup semaphores). The multipler
265
+	// (128) was chosen after some fairly significant benchmarking -- don't change
266
+	// it unless you've tested it significantly (this value is adjusted if
267
+	// RLIMIT_NOFILE is small to avoid EMFILE).
268
+	parallelLimit := adjustParallelLimit(len(containers), 128*runtime.NumCPU())
269
+
270
+	// Re-used for all parallel startup jobs.
271
+	var group sync.WaitGroup
272
+	sem := semaphore.NewWeighted(int64(parallelLimit))
273
+
264 274
 	removeContainers := make(map[string]*container.Container)
265 275
 	restartContainers := make(map[*container.Container]chan struct{})
266 276
 	activeSandboxes := make(map[string]any)
... ...
@@ -274,6 +286,17 @@ func (daemon *Daemon) restore(cfg *configStore) error {
274 274
 
275 275
 			logger := log.G(context.TODO()).WithField("container", c.ID)
276 276
 
277
+			rwlayer, err := daemon.imageService.GetLayerByID(c.ID)
278
+			if err != nil {
279
+				logger.WithError(err).Error("failed to load container mount")
280
+				return
281
+			}
282
+			c.RWLayer = rwlayer
283
+			logger.WithFields(log.Fields{
284
+				"running": c.IsRunning(),
285
+				"paused":  c.IsPaused(),
286
+			}).Debug("loaded container")
287
+
277 288
 			if err := daemon.registerName(c); err != nil {
278 289
 				logger.WithError(err).Errorf("failed to register container name: %s", c.Name)
279 290
 				mapLock.Lock()
... ...
@@ -523,7 +546,7 @@ func (daemon *Daemon) restore(cfg *configStore) error {
523 523
 	//
524 524
 	// Note that we cannot initialize the network controller earlier, as it
525 525
 	// needs to know if there's active sandboxes (running containers).
526
-	if err = daemon.initNetworkController(&cfg.Config, activeSandboxes); err != nil {
526
+	if err := daemon.initNetworkController(&cfg.Config, activeSandboxes); err != nil {
527 527
 		return fmt.Errorf("Error initializing network controller: %v", err)
528 528
 	}
529 529
 
... ...
@@ -833,10 +856,17 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
833 833
 	d.configStore.Store(cfgStore)
834 834
 
835 835
 	// TEST_INTEGRATION_USE_SNAPSHOTTER is used for integration tests only.
836
+	migrationThreshold := int64(-1)
836 837
 	if os.Getenv("TEST_INTEGRATION_USE_SNAPSHOTTER") != "" {
837 838
 		d.usesSnapshotter = true
838 839
 	} else {
839
-		d.usesSnapshotter = config.Features["containerd-snapshotter"]
840
+		log.G(ctx).WithField("features", config.Features).Debug("Checking features for migration")
841
+		if config.Features["containerd-migration"] {
842
+			// TODO: Allow setting the threshold
843
+			migrationThreshold = math.MaxInt64
844
+		} else {
845
+			d.usesSnapshotter = config.Features["containerd-snapshotter"]
846
+		}
840 847
 	}
841 848
 
842 849
 	// Ensure the daemon is properly shutdown if there is a failure during
... ...
@@ -1033,12 +1063,17 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1033 1033
 
1034 1034
 	d.linkIndex = newLinkIndex()
1035 1035
 
1036
+	containers, err := d.loadContainers()
1037
+	if err != nil {
1038
+		return nil, err
1039
+	}
1040
+
1036 1041
 	// On Windows we don't support the environment variable, or a user supplied graphdriver
1037 1042
 	// Unix platforms however run a single graphdriver for all containers, and it can
1038 1043
 	// be set through an environment variable, a daemon start parameter, or chosen through
1039 1044
 	// initialization of the layerstore through driver priority order for example.
1040 1045
 	driverName := os.Getenv("DOCKER_DRIVER")
1041
-	if isWindows && d.UsesSnapshotter() {
1046
+	if isWindows && d.usesSnapshotter {
1042 1047
 		// Containerd WCOW snapshotter
1043 1048
 		driverName = "windows"
1044 1049
 	} else if isWindows {
... ...
@@ -1050,33 +1085,8 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1050 1050
 		driverName = cfgStore.GraphDriver
1051 1051
 	}
1052 1052
 
1053
-	if d.UsesSnapshotter() {
1054
-		if os.Getenv("TEST_INTEGRATION_USE_SNAPSHOTTER") != "" {
1055
-			log.G(ctx).Warn("Enabling containerd snapshotter through the $TEST_INTEGRATION_USE_SNAPSHOTTER environment variable. This should only be used for testing.")
1056
-		}
1057
-		log.G(ctx).Info("Starting daemon with containerd snapshotter integration enabled")
1058
-
1059
-		// FIXME(thaJeztah): implement automatic snapshotter-selection similar to graph-driver selection; see https://github.com/moby/moby/issues/44076
1060
-		if driverName == "" {
1061
-			driverName = defaults.DefaultSnapshotter
1062
-		}
1063
-
1064
-		// Configure and validate the kernels security support. Note this is a Linux/FreeBSD
1065
-		// operation only, so it is safe to pass *just* the runtime OS graphdriver.
1066
-		if err := configureKernelSecuritySupport(&cfgStore.Config, driverName); err != nil {
1067
-			return nil, err
1068
-		}
1069
-		d.imageService = ctrd.NewService(ctrd.ImageServiceConfig{
1070
-			Client:          d.containerdClient,
1071
-			Containers:      d.containers,
1072
-			Snapshotter:     driverName,
1073
-			RegistryHosts:   d.RegistryHosts,
1074
-			Registry:        d.registryService,
1075
-			EventsService:   d.EventsService,
1076
-			IDMapping:       idMapping,
1077
-			RefCountMounter: snapshotter.NewMounter(config.Root, driverName, idMapping),
1078
-		})
1079
-	} else {
1053
+	var migrationConfig migration.Config
1054
+	if !d.usesSnapshotter {
1080 1055
 		layerStore, err := layer.NewStoreFromOptions(layer.StoreOptions{
1081 1056
 			Root:               cfgStore.Root,
1082 1057
 			GraphDriver:        driverName,
... ...
@@ -1161,6 +1171,93 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1161 1161
 		log.G(ctx).Debugf("Max Concurrent Downloads: %d", imgSvcConfig.MaxConcurrentDownloads)
1162 1162
 		log.G(ctx).Debugf("Max Concurrent Uploads: %d", imgSvcConfig.MaxConcurrentUploads)
1163 1163
 		log.G(ctx).Debugf("Max Download Attempts: %d", imgSvcConfig.MaxDownloadAttempts)
1164
+
1165
+		// If no containers are running, check whether can migrate image service
1166
+		if drv := d.imageService.StorageDriver(); len(containers[drv]) == 0 && migrationThreshold >= 0 {
1167
+			switch drv {
1168
+			case "overlay2":
1169
+				driverName = "overlayfs"
1170
+			case "windowsfilter":
1171
+				driverName = "windows"
1172
+				migrationThreshold = 0
1173
+			case "vfs":
1174
+				driverName = "native"
1175
+			default:
1176
+				migrationThreshold = -1
1177
+				log.G(ctx).Infof("Not migrating to containerd snapshotter, no migration defined for graph driver %q", drv)
1178
+			}
1179
+
1180
+			var totalSize int64
1181
+			ic := d.imageService.CountImages(ctx)
1182
+			if migrationThreshold >= 0 && ic > 0 {
1183
+				sum, err := d.imageService.Images(ctx, imagetypes.ListOptions{All: true})
1184
+				if err != nil {
1185
+					return nil, err
1186
+				}
1187
+				for _, s := range sum {
1188
+					// Just add the size, don't consider shared size since this
1189
+					// represents a maximum size
1190
+					totalSize += s.Size
1191
+				}
1192
+
1193
+			}
1194
+
1195
+			if totalSize <= migrationThreshold {
1196
+				log.G(ctx).WithField("total", totalSize).Infof("Enabling containerd snapshotter because migration set with no containers and %d images in graph driver", ic)
1197
+				d.usesSnapshotter = true
1198
+				migrationConfig.LayerStore = imgSvcConfig.LayerStore
1199
+				migrationConfig.DockerImageStore = imgSvcConfig.ImageStore
1200
+				migrationConfig.ReferenceStore = imgSvcConfig.ReferenceStore
1201
+			} else if migrationThreshold >= 0 {
1202
+				log.G(ctx).Warnf("Not migrating to containerd snapshotter because still have %d images in graph driver", ic)
1203
+			}
1204
+		} else {
1205
+			log.G(ctx).Debugf("Not attempting migration with %d containers and %d image threshold", len(containers[d.imageService.StorageDriver()]), migrationThreshold)
1206
+		}
1207
+	}
1208
+
1209
+	if d.usesSnapshotter {
1210
+		if os.Getenv("TEST_INTEGRATION_USE_SNAPSHOTTER") != "" {
1211
+			log.G(ctx).Warn("Enabling containerd snapshotter through the $TEST_INTEGRATION_USE_SNAPSHOTTER environment variable. This should only be used for testing.")
1212
+		}
1213
+		log.G(ctx).Info("Starting daemon with containerd snapshotter integration enabled")
1214
+
1215
+		// FIXME(thaJeztah): implement automatic snapshotter-selection similar to graph-driver selection; see https://github.com/moby/moby/issues/44076
1216
+		if driverName == "" {
1217
+			driverName = defaults.DefaultSnapshotter
1218
+		}
1219
+
1220
+		// Configure and validate the kernels security support. Note this is a Linux/FreeBSD
1221
+		// operation only, so it is safe to pass *just* the runtime OS graphdriver.
1222
+		if err := configureKernelSecuritySupport(&cfgStore.Config, driverName); err != nil {
1223
+			return nil, err
1224
+		}
1225
+		oldImageService := d.imageService
1226
+		d.imageService = ctrd.NewService(ctrd.ImageServiceConfig{
1227
+			Client:          d.containerdClient,
1228
+			Containers:      d.containers,
1229
+			Snapshotter:     driverName,
1230
+			RegistryHosts:   d.RegistryHosts,
1231
+			Registry:        d.registryService,
1232
+			EventsService:   d.EventsService,
1233
+			IDMapping:       idMapping,
1234
+			RefCountMounter: snapshotter.NewMounter(config.Root, driverName, idMapping),
1235
+		})
1236
+
1237
+		if oldImageService != nil {
1238
+			if count := oldImageService.CountImages(ctx); count > 0 {
1239
+				migrationConfig.Leases = d.containerdClient.LeasesService()
1240
+				migrationConfig.Content = d.containerdClient.ContentStore()
1241
+				migrationConfig.ImageStore = d.containerdClient.ImageService()
1242
+				m := migration.NewLayerMigrator(migrationConfig)
1243
+				err := m.MigrateTocontainerd(ctx, driverName, d.containerdClient.SnapshotService(driverName))
1244
+				if err != nil {
1245
+					log.G(ctx).WithError(err).Errorf("Failed to migrate images to containerd, images in graph driver %q are no longer visible", oldImageService.StorageDriver())
1246
+				} else {
1247
+					log.G(ctx).WithField("image_count", count).Infof("Successfully migrated images from %q to containerd", oldImageService.StorageDriver())
1248
+				}
1249
+			}
1250
+		}
1164 1251
 	}
1165 1252
 
1166 1253
 	go d.execCommandGC()
... ...
@@ -1169,9 +1266,22 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1169 1169
 		return nil, err
1170 1170
 	}
1171 1171
 
1172
-	if err := d.restore(cfgStore); err != nil {
1172
+	driverContainers, ok := containers[driverName]
1173
+	// Log containers which are not loaded with current driver
1174
+	if (!ok && len(containers) > 0) || len(containers) > 1 {
1175
+		for driver, all := range containers {
1176
+			if driver == driverName {
1177
+				continue
1178
+			}
1179
+			for id := range all {
1180
+				log.G(ctx).WithField("container", id).Debugf("not restoring container because it was created with another storage driver (%s)", driver)
1181
+			}
1182
+		}
1183
+	}
1184
+	if err := d.restore(cfgStore, driverContainers); err != nil {
1173 1185
 		return nil, err
1174 1186
 	}
1187
+	// Wait for migration to complete
1175 1188
 	close(d.startupDone)
1176 1189
 
1177 1190
 	info, err := d.SystemInfo(ctx)
... ...
@@ -86,7 +86,16 @@ func (d *Driver) Status() [][2]string {
86 86
 
87 87
 // GetMetadata is used for implementing the graphdriver.ProtoDriver interface. VFS does not currently have any meta data.
88 88
 func (d *Driver) GetMetadata(id string) (map[string]string, error) {
89
-	return nil, nil
89
+	dir := d.dir(id)
90
+	if _, err := os.Stat(dir); err != nil {
91
+		return nil, err
92
+	}
93
+
94
+	metadata := map[string]string{
95
+		"SourceDir": dir,
96
+	}
97
+
98
+	return metadata, nil
90 99
 }
91 100
 
92 101
 // Cleanup is used to implement graphdriver.ProtoDriver. There is no cleanup required for this driver.
93 102
new file mode 100644
... ...
@@ -0,0 +1,159 @@
0
+package daemon // import "github.com/docker/docker/integration/daemon"
1
+
2
+import (
3
+	"bytes"
4
+	"io"
5
+	"os"
6
+	"runtime"
7
+	"testing"
8
+
9
+	containertypes "github.com/moby/moby/api/types/container"
10
+	"github.com/moby/moby/api/types/image"
11
+	"github.com/moby/moby/v2/integration/internal/container"
12
+	"github.com/moby/moby/v2/testutil"
13
+	"github.com/moby/moby/v2/testutil/daemon"
14
+	"github.com/moby/moby/v2/testutil/fixtures/load"
15
+	"gotest.tools/v3/assert"
16
+	"gotest.tools/v3/skip"
17
+)
18
+
19
+func TestMigrateOverlaySnapshotter(t *testing.T) {
20
+	testMigrateSnapshotter(t, "overlay2", "overlayfs")
21
+}
22
+
23
+func TestMigrateNativeSnapshotter(t *testing.T) {
24
+	testMigrateSnapshotter(t, "vfs", "native")
25
+}
26
+
27
+func testMigrateSnapshotter(t *testing.T, graphdriver, snapshotter string) {
28
+	skip.If(t, runtime.GOOS != "linux")
29
+	skip.If(t, os.Getenv("TEST_INTEGRATION_USE_SNAPSHOTTER") != "")
30
+
31
+	ctx := testutil.StartSpan(baseContext, t)
32
+
33
+	d := daemon.New(t)
34
+	defer d.Stop(t)
35
+
36
+	d.Start(t, "--iptables=false", "--ip6tables=false", "-s", graphdriver)
37
+	info := d.Info(t)
38
+	id := info.ID
39
+	assert.Check(t, id != "")
40
+	assert.Equal(t, info.Containers, 0)
41
+	assert.Equal(t, info.Images, 0)
42
+	assert.Equal(t, info.Driver, graphdriver)
43
+
44
+	load.FrozenImagesLinux(ctx, d.NewClientT(t), "busybox:latest")
45
+
46
+	info = d.Info(t)
47
+	allImages := info.Images
48
+	assert.Check(t, allImages > 0)
49
+
50
+	apiClient := d.NewClientT(t)
51
+
52
+	containerID := container.Run(ctx, t, apiClient, func(c *container.TestContainerConfig) {
53
+		c.Name = "Migration-1-" + snapshotter
54
+		c.Config.Image = "busybox:latest"
55
+		c.Config.Cmd = []string{"top"}
56
+	})
57
+
58
+	d.Stop(t)
59
+
60
+	// Start with migration feature but with a container which will prevent migration
61
+	d.Start(t, "--iptables=false", "--ip6tables=false", "-s", graphdriver, "--feature", "containerd-migration")
62
+	info = d.Info(t)
63
+	assert.Equal(t, info.ID, id)
64
+	assert.Equal(t, info.Driver, graphdriver)
65
+	assert.Equal(t, info.Containers, 1)
66
+	assert.Equal(t, info.Images, allImages)
67
+	container.Remove(ctx, t, apiClient, containerID, containertypes.RemoveOptions{
68
+		Force: true,
69
+	})
70
+
71
+	d.Stop(t)
72
+
73
+	d.Start(t, "--iptables=false", "--ip6tables=false", "-s", graphdriver, "--feature", "containerd-migration")
74
+	info = d.Info(t)
75
+	assert.Equal(t, info.ID, id)
76
+	assert.Equal(t, info.Containers, 0)
77
+	assert.Equal(t, info.Driver, snapshotter, "expected migrate to switch from %s to %s", graphdriver, snapshotter)
78
+	assert.Equal(t, info.Images, allImages)
79
+
80
+	result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) {
81
+		c.Name = "Migration-2-" + snapshotter
82
+		c.Config.Image = "busybox:latest"
83
+		c.Config.Cmd = []string{"echo", "hello"}
84
+	})
85
+	assert.Equal(t, result.ExitCode, 0)
86
+	container.Remove(ctx, t, apiClient, result.ContainerID, containertypes.RemoveOptions{})
87
+}
88
+
89
+func TestMigrateSaveLoad(t *testing.T) {
90
+	skip.If(t, runtime.GOOS != "linux")
91
+	skip.If(t, os.Getenv("TEST_INTEGRATION_USE_SNAPSHOTTER") != "")
92
+
93
+	var (
94
+		ctx         = testutil.StartSpan(baseContext, t)
95
+		d           = daemon.New(t)
96
+		graphdriver = "overlay2"
97
+		snapshotter = "overlayfs"
98
+	)
99
+	defer d.Stop(t)
100
+
101
+	d.Start(t, "--iptables=false", "--ip6tables=false", "-s", graphdriver)
102
+	info := d.Info(t)
103
+	id := info.ID
104
+	assert.Check(t, id != "")
105
+	assert.Equal(t, info.Containers, 0)
106
+	assert.Equal(t, info.Images, 0)
107
+	assert.Equal(t, info.Driver, graphdriver)
108
+
109
+	load.FrozenImagesLinux(ctx, d.NewClientT(t), "busybox:latest")
110
+
111
+	info = d.Info(t)
112
+	allImages := info.Images
113
+	assert.Check(t, allImages > 0)
114
+
115
+	d.Stop(t)
116
+
117
+	d.Start(t, "--iptables=false", "--ip6tables=false", "-s", graphdriver, "--feature", "containerd-migration")
118
+	info = d.Info(t)
119
+	assert.Equal(t, info.ID, id)
120
+	assert.Equal(t, info.Containers, 0)
121
+	assert.Equal(t, info.Driver, snapshotter, "expected migrate to switch from %s to %s", graphdriver, snapshotter)
122
+	assert.Equal(t, info.Images, allImages)
123
+
124
+	apiClient := d.NewClientT(t)
125
+
126
+	// Save image to buffer
127
+	rdr, err := apiClient.ImageSave(ctx, []string{"busybox:latest"}, image.SaveOptions{})
128
+	assert.NilError(t, err)
129
+	buf := bytes.NewBuffer(nil)
130
+	io.Copy(buf, rdr)
131
+	rdr.Close()
132
+
133
+	// Delete all images
134
+	list, err := apiClient.ImageList(ctx, image.ListOptions{})
135
+	assert.NilError(t, err)
136
+	for _, i := range list {
137
+		_, err = apiClient.ImageRemove(ctx, i.ID, image.RemoveOptions{})
138
+		assert.NilError(t, err)
139
+	}
140
+
141
+	// Check zero images
142
+	info = d.Info(t)
143
+	assert.Equal(t, info.Images, 0)
144
+
145
+	// Import
146
+	lr, err := apiClient.ImageLoad(ctx, bytes.NewReader(buf.Bytes()), image.LoadOptions{Quiet: true})
147
+	assert.NilError(t, err)
148
+	io.Copy(io.Discard, lr.Body)
149
+	lr.Body.Close()
150
+
151
+	result := container.RunAttach(ctx, t, apiClient, func(c *container.TestContainerConfig) {
152
+		c.Name = "Migration-save-load-" + snapshotter
153
+		c.Config.Image = "busybox:latest"
154
+		c.Config.Cmd = []string{"echo", "hello"}
155
+	})
156
+	assert.Equal(t, result.ExitCode, 0)
157
+	container.Remove(ctx, t, apiClient, result.ContainerID, containertypes.RemoveOptions{})
158
+}