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>
| 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 |
+} |