Skip to content

Commit 1a64ee1

Browse files
TBBlegabriel-samfira
authored andcommitted
Implement WCOW parentless active snapshots and view snapshots
The WCOW layer support does not support creating sandboxes with no parent. Instead, parentless scratch layers must be layed out as a directory containing only a directory named 'Files', and all data stored inside 'Files'. At commit-time, this will be converted in-place into a read-only layer suitable for use as a parent layer. The WCOW layer support also does not deal with making read-only layers, i.e. layers that are prepared to be parent layers, visible in a read-only manner. A bind-mount or junction point cannot be made read-only, so a view must instead be a small sandbox layer that we can mount via WCOW, and discard later, to protect the layer against accidental or deliberate modification. Signed-off-by: Paul "TBBle" Hampson <[email protected]>
1 parent 791f042 commit 1a64ee1

2 files changed

Lines changed: 95 additions & 44 deletions

File tree

diff/windows/windows.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,13 @@ func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) {
320320
return "", nil, fmt.Errorf("number of mounts should always be 1 for Windows layers: %w", errdefs.ErrInvalidArgument)
321321
}
322322
mnt := mounts[0]
323-
if mnt.Type != "windows-layer" {
323+
324+
if mnt.Type != "windows-layer" && mnt.Type != "bind" {
324325
// This is a special case error. When this is received the diff service
325326
// will attempt the next differ in the chain which for Windows is the
326327
// lcow differ that we want.
328+
// TODO: Is there any situation where we actually wanted a "bind" mount to
329+
// fall through to the lcow differ?
327330
return "", nil, fmt.Errorf("windowsDiff does not support layer type %s: %w", mnt.Type, errdefs.ErrNotImplemented)
328331
}
329332

@@ -332,6 +335,30 @@ func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) {
332335
return "", nil, err
333336
}
334337

338+
isView := false
339+
for _, o := range mnt.Options {
340+
if o == "ro" {
341+
isView = true
342+
break
343+
}
344+
}
345+
346+
if isView {
347+
if mnt.Type == "bind" && len(parentLayerPaths) != 0 {
348+
return "", nil, fmt.Errorf("unexpected bind-mount View with parents: %w", errdefs.ErrInvalidArgument)
349+
} else if mnt.Type == "bind" {
350+
// rootfs.CreateDiff creates a new, empty View to diff against,
351+
// when diffing something with no parent.
352+
// This makes perfect sense for a walking Diff, but for WCOW,
353+
// we have to recognise this as "diff against nothing"
354+
return "", nil, nil
355+
} else if len(parentLayerPaths) == 0 {
356+
return "", nil, fmt.Errorf("unexpected windows-layer View with no parent: %w", errdefs.ErrInvalidArgument)
357+
}
358+
// Ignore the dummy sandbox.
359+
return parentLayerPaths[0], parentLayerPaths[1:], nil
360+
}
361+
335362
return mnt.Source, parentLayerPaths, nil
336363
}
337364

@@ -346,8 +373,16 @@ func mountPairToLayerStack(lower, upper []mount.Mount) ([]string, error) {
346373
return nil, fmt.Errorf("Upper mount invalid: %w", err)
347374
}
348375

376+
lowerLayer, lowerParentLayerPaths, err := mountsToLayerAndParents(lower)
377+
if errdefs.IsNotImplemented(err) {
378+
// Upper was a windows-layer or bind, lower is not. We can't handle that.
379+
return nil, fmt.Errorf("windowsDiff cannot diff a windows-layer against a non-windows-layer: %w", errdefs.ErrInvalidArgument)
380+
} else if err != nil {
381+
return nil, fmt.Errorf("Lower mount invalid: %w", err)
382+
}
383+
349384
// Trivial case, diff-against-nothing
350-
if len(lower) == 0 {
385+
if lowerLayer == "" {
351386
if len(upperParentLayerPaths) != 0 {
352387
return nil, fmt.Errorf("windowsDiff cannot diff a layer with parents against a null layer: %w", errdefs.ErrInvalidArgument)
353388
}
@@ -358,14 +393,6 @@ func mountPairToLayerStack(lower, upper []mount.Mount) ([]string, error) {
358393
return nil, fmt.Errorf("windowsDiff cannot diff a layer with no parents against another layer: %w", errdefs.ErrInvalidArgument)
359394
}
360395

361-
lowerLayer, lowerParentLayerPaths, err := mountsToLayerAndParents(lower)
362-
if errdefs.IsNotImplemented(err) {
363-
// Upper was a windows-layer, lower is not. We can't handle that.
364-
return nil, fmt.Errorf("windowsDiff cannot diff a windows-layer against a non-windows-layer: %w", errdefs.ErrInvalidArgument)
365-
} else if err != nil {
366-
return nil, fmt.Errorf("Lower mount invalid: %w", err)
367-
}
368-
369396
if upperParentLayerPaths[0] != lowerLayer {
370397
return nil, fmt.Errorf("windowsDiff cannot diff a layer against a layer other than its own parent: %w", errdefs.ErrInvalidArgument)
371398
}

snapshots/windows/windows.go

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func (s *snapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount,
187187
return nil, err
188188
}
189189

190-
return s.mounts(snapshot), nil
190+
return s.mounts(snapshot, key), nil
191191
}
192192

193193
func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) (retErr error) {
@@ -208,7 +208,11 @@ func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snap
208208
// If (windowsDiff).Apply was used to populate this layer, then it's already in the 'committed' state.
209209
// See createSnapshot below for more details
210210
if !strings.Contains(key, snapshots.UnpackKeyPrefix) {
211-
if err := s.convertScratchToReadOnlyLayer(ctx, snapshot, path); err != nil {
211+
if len(snapshot.ParentIDs) == 0 {
212+
if err = hcsshim.ConvertToBaseLayer(path); err != nil {
213+
return err
214+
}
215+
} else if err := s.convertScratchToReadOnlyLayer(ctx, snapshot, path); err != nil {
212216
return err
213217
}
214218
}
@@ -299,11 +303,9 @@ func (s *snapshotter) Close() error {
299303
return s.ms.Close()
300304
}
301305

302-
func (s *snapshotter) mounts(sn storage.Snapshot) []mount.Mount {
306+
func (s *snapshotter) mounts(sn storage.Snapshot, key string) []mount.Mount {
303307
var (
304-
roFlag string
305-
source string
306-
parentLayerPaths []string
308+
roFlag string
307309
)
308310

309311
if sn.Kind == snapshots.KindView {
@@ -312,12 +314,19 @@ func (s *snapshotter) mounts(sn storage.Snapshot) []mount.Mount {
312314
roFlag = "rw"
313315
}
314316

315-
if len(sn.ParentIDs) == 0 || sn.Kind == snapshots.KindActive {
316-
source = s.getSnapshotDir(sn.ID)
317-
parentLayerPaths = s.parentIDsToParentPaths(sn.ParentIDs)
318-
} else {
319-
source = s.getSnapshotDir(sn.ParentIDs[0])
320-
parentLayerPaths = s.parentIDsToParentPaths(sn.ParentIDs[1:])
317+
source := s.getSnapshotDir(sn.ID)
318+
parentLayerPaths := s.parentIDsToParentPaths(sn.ParentIDs)
319+
320+
mountType := "windows-layer"
321+
322+
if len(sn.ParentIDs) == 0 {
323+
// A mount of a parentless snapshot is a bind-mount.
324+
mountType = "bind"
325+
// If not being extracted into, then the bind-target is the
326+
// "Files" subdirectory.
327+
if !strings.Contains(key, snapshots.UnpackKeyPrefix) {
328+
source = filepath.Join(source, "Files")
329+
}
321330
}
322331

323332
// error is not checked here, as a string array will never fail to Marshal
@@ -327,7 +336,7 @@ func (s *snapshotter) mounts(sn storage.Snapshot) []mount.Mount {
327336
var mounts []mount.Mount
328337
mounts = append(mounts, mount.Mount{
329338
Source: source,
330-
Type: "windows-layer",
339+
Type: mountType,
331340
Options: []string{
332341
roFlag,
333342
parentLayersOption,
@@ -360,13 +369,22 @@ func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
360369
return err
361370
}
362371

363-
// IO/disk space optimization
364-
//
365-
// We only need one sandbox.vhdx for the container. Skip making one for this
366-
// snapshot if this isn't the snapshot that just houses the final sandbox.vhd
367-
// that will be mounted as the containers scratch. Currently the key for a snapshot
368-
// where a layer will be extracted to will have the string `extract-` in it.
369-
if !strings.Contains(key, snapshots.UnpackKeyPrefix) {
372+
if strings.Contains(key, snapshots.UnpackKeyPrefix) {
373+
// IO/disk space optimization: Do nothing
374+
//
375+
// We only need one sandbox.vhdx for the container. Skip making one for this
376+
// snapshot if this isn't the snapshot that just houses the final sandbox.vhd
377+
// that will be mounted as the containers scratch. Currently the key for a snapshot
378+
// where a layer will be extracted to will have the string `extract-` in it.
379+
} else if len(newSnapshot.ParentIDs) == 0 {
380+
// A parentless snapshot is just a bind-mount to a directory named
381+
// "Files". When committed, there'll be some post-processing to fill in the rest
382+
// of the metadata.
383+
filesDir := filepath.Join(snDir, "Files")
384+
if err := os.MkdirAll(filesDir, 0700); err != nil {
385+
return err
386+
}
387+
} else {
370388
parentLayerPaths := s.parentIDsToParentPaths(newSnapshot.ParentIDs)
371389

372390
var snapshotInfo snapshots.Info
@@ -375,22 +393,28 @@ func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
375393
}
376394

377395
var sizeInBytes uint64
378-
if sizeGBstr, ok := snapshotInfo.Labels[rootfsSizeInGBLabel]; ok {
379-
log.G(ctx).Warnf("%q label is deprecated, please use %q instead.", rootfsSizeInGBLabel, rootfsSizeInBytesLabel)
380-
381-
sizeInGB, err := strconv.ParseUint(sizeGBstr, 10, 32)
382-
if err != nil {
383-
return fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInGBLabel, sizeGBstr, err)
396+
if kind == snapshots.KindActive {
397+
if sizeGBstr, ok := snapshotInfo.Labels[rootfsSizeInGBLabel]; ok {
398+
log.G(ctx).Warnf("%q label is deprecated, please use %q instead.", rootfsSizeInGBLabel, rootfsSizeInBytesLabel)
399+
400+
sizeInGB, err := strconv.ParseUint(sizeGBstr, 10, 32)
401+
if err != nil {
402+
return fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInGBLabel, sizeGBstr, err)
403+
}
404+
sizeInBytes = sizeInGB * 1024 * 1024 * 1024
384405
}
385-
sizeInBytes = sizeInGB * 1024 * 1024 * 1024
386-
}
387406

388-
// Prefer the newer label in bytes over the deprecated Windows specific GB variant.
389-
if sizeBytesStr, ok := snapshotInfo.Labels[rootfsSizeInBytesLabel]; ok {
390-
sizeInBytes, err = strconv.ParseUint(sizeBytesStr, 10, 64)
391-
if err != nil {
392-
return fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInBytesLabel, sizeBytesStr, err)
407+
// Prefer the newer label in bytes over the deprecated Windows specific GB variant.
408+
if sizeBytesStr, ok := snapshotInfo.Labels[rootfsSizeInBytesLabel]; ok {
409+
sizeInBytes, err = strconv.ParseUint(sizeBytesStr, 10, 64)
410+
if err != nil {
411+
return fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInBytesLabel, sizeBytesStr, err)
412+
}
393413
}
414+
} else {
415+
// A view is just a read-write snapshot with a _really_ small sandbox, since we cannot actually
416+
// make a read-only mount or junction point. https://superuser.com/q/881544/112473
417+
sizeInBytes = 1024 * 1024 * 1024
394418
}
395419

396420
var makeUVMScratch bool
@@ -415,7 +439,7 @@ func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
415439
return nil, err
416440
}
417441

418-
return s.mounts(newSnapshot), nil
442+
return s.mounts(newSnapshot, key), nil
419443
}
420444

421445
func (s *snapshotter) parentIDsToParentPaths(parentIDs []string) []string {

0 commit comments

Comments
 (0)