Merge pull request #9659 from ambarve/cimfs_unionfs

Don't create new scratch VHD per image for CimFS
This commit is contained in:
Fu Wei 2024-03-07 04:51:03 +00:00 committed by GitHub
commit 1afc6ca3be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 409 additions and 184 deletions

View File

@ -21,12 +21,17 @@ package windows
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/Microsoft/go-winio/pkg/security"
"github.com/Microsoft/go-winio/vhd"
"github.com/Microsoft/hcsshim"
"github.com/Microsoft/hcsshim/computestorage"
"github.com/Microsoft/hcsshim/pkg/cimfs"
cimlayer "github.com/Microsoft/hcsshim/pkg/ociwclayer/cim"
"github.com/containerd/containerd/v2/core/mount"
@ -39,6 +44,14 @@ import (
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sys/windows"
)
const (
baseVHDName = "blank-base.vhdx"
templateVHDName = "blank.vhdx"
vhdMaxSizeInBytes uint64 = 10 * 1024 * 1024 * 1024 // 10 GB
vhdBlockSizeInBytes uint32 = 1 * 1024 * 1024 // 1 MB
)
// Composite image FileSystem (CimFS) is a new read-only filesystem (similar to overlayFS on Linux) created
@ -78,6 +91,10 @@ func NewCimFSSnapshotter(root string) (snapshots.Snapshotter, error) {
return nil, err
}
if err = createScratchVHDs(context.Background(), baseSn.root); err != nil {
return nil, fmt.Errorf("failed to init base scratch VHD: %w", err)
}
return &cimFSSnapshotter{
windowsBaseSnapshotter: baseSn,
cimDir: filepath.Join(baseSn.info.HomeDir, "cim-layers"),
@ -139,30 +156,28 @@ func (s *cimFSSnapshotter) Usage(ctx context.Context, key string) (snapshots.Usa
}
func (s *cimFSSnapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
m, err := s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
if err != nil {
return m, err
}
m[0].Type = "CimFS"
return m, nil
return s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
}
func (s *cimFSSnapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
m, err := s.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
if err != nil {
return m, err
}
m[0].Type = "CimFS"
return m, nil
return s.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
}
func (s *cimFSSnapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
mounts, err := s.windowsBaseSnapshotter.Mounts(ctx, key)
func (s *cimFSSnapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
var snapshot storage.Snapshot
err = s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
snapshot, err = storage.GetSnapshot(ctx, key)
if err != nil {
return fmt.Errorf("failed to get snapshot mount: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
mounts[0].Type = "CimFS"
return mounts, nil
return s.mounts(snapshot, key), nil
}
func (s *cimFSSnapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
@ -215,3 +230,199 @@ func (s *cimFSSnapshotter) Remove(ctx context.Context, key string) error {
}
return nil
}
func (s *cimFSSnapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
var newSnapshot storage.Snapshot
err = s.ms.WithTransaction(ctx, true, func(ctx context.Context) (retErr error) {
newSnapshot, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
return fmt.Errorf("failed to create snapshot: %w", err)
}
log.G(ctx).Debug("createSnapshot")
// Create the new snapshot dir
snDir := s.getSnapshotDir(newSnapshot.ID)
if err = os.MkdirAll(snDir, 0700); err != nil {
return fmt.Errorf("failed to create snapshot dir %s: %w", snDir, err)
}
defer func() {
if retErr != nil {
os.RemoveAll(snDir)
}
}()
if strings.Contains(key, snapshots.UnpackKeyPrefix) {
// IO/disk space optimization: Do nothing
//
// We only need one sandbox.vhdx for the container. Skip making one for this
// snapshot if this isn't the snapshot that just houses the final sandbox.vhd
// that will be mounted as the containers scratch. Currently the key for a snapshot
// where a layer will be extracted to will have the string `extract-` in it.
return nil
}
if len(newSnapshot.ParentIDs) == 0 {
return fmt.Errorf("scratch snapshot without any parents isn't supported")
}
parentLayerPaths := s.parentIDsToParentPaths(newSnapshot.ParentIDs)
var snapshotInfo snapshots.Info
for _, o := range opts {
o(&snapshotInfo)
}
sizeInBytes, err := getRequestedScratchSize(ctx, snapshotInfo)
if err != nil {
return err
}
var makeUVMScratch bool
if _, ok := snapshotInfo.Labels[uvmScratchLabel]; ok {
makeUVMScratch = true
}
// This has to be run first to avoid clashing with the containers sandbox.vhdx.
if makeUVMScratch {
if err = s.createUVMScratchLayer(ctx, snDir, parentLayerPaths); err != nil {
return fmt.Errorf("failed to make UVM's scratch layer: %w", err)
}
}
if err = s.createScratchLayer(ctx, snDir, sizeInBytes); err != nil {
return fmt.Errorf("failed to create scratch layer: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return s.mounts(newSnapshot, key), nil
}
// In case of CimFS layers, the scratch VHDs are fully empty (WCIFS layers have reparse points in scratch VHDs, hence those VHDs are unique per image), so we create only one scratch VHD and then copy & expand it for every scratch layer creation.
func (s *cimFSSnapshotter) createScratchLayer(ctx context.Context, snDir string, sizeInBytes uint64) error {
dest := filepath.Join(snDir, "sandbox.vhdx")
if err := copyScratchDisk(filepath.Join(s.root, templateVHDName), dest); err != nil {
return err
}
if sizeInBytes != 0 {
if err := hcsshim.ExpandSandboxSize(s.info, filepath.Base(snDir), sizeInBytes); err != nil {
return fmt.Errorf("failed to expand sandbox vhdx size to %d bytes: %w", sizeInBytes, err)
}
}
return nil
}
func (s *cimFSSnapshotter) mounts(sn storage.Snapshot, key string) []mount.Mount {
var (
roFlag string
)
if sn.Kind == snapshots.KindView {
roFlag = "ro"
} else {
roFlag = "rw"
}
source := s.getSnapshotDir(sn.ID)
parentLayerPaths := s.parentIDsToParentPaths(sn.ParentIDs)
mountType := "CimFS"
// error is not checked here, as a string array will never fail to Marshal
parentLayersJSON, _ := json.Marshal(parentLayerPaths)
parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON)
options := []string{
roFlag,
}
if len(sn.ParentIDs) != 0 {
options = append(options, parentLayersOption)
}
mounts := []mount.Mount{
{
Source: source,
Type: mountType,
Options: options,
},
}
return mounts
}
// creates a base scratch VHD and a differencing VHD from that base VHD inside the given `path`
// directory. Once these VHDs are created, every scratch snapshot will make a copy of the differencing VHD to
// be used as the scratch for that snapshot. We could ideally just have a base VHD and no differencing VHD and
// copy the base VHD for every scratch snapshot. However, base VHDs are slightly bigger in size and so take
// longer to copy so we keep a differencing VHD and copy that.
func createScratchVHDs(ctx context.Context, path string) (err error) {
baseVHDPath := filepath.Join(path, baseVHDName)
diffVHDPath := filepath.Join(path, templateVHDName)
baseVHDExists := false
diffVHDExists := false
if _, err = os.Stat(baseVHDPath); err == nil {
baseVHDExists = true
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to stat base VHD: %w", err)
}
_, err = os.Stat(diffVHDPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to stat diff VHD: %w", err)
} else if baseVHDExists && err == nil {
diffVHDExists = true
} else {
// remove this diff VHD, it must be recreated with the new base VHD.
os.RemoveAll(diffVHDPath)
}
defer func() {
if err != nil {
os.RemoveAll(baseVHDPath)
os.RemoveAll(diffVHDPath)
}
}()
if !baseVHDExists {
var baseVHDHandle syscall.Handle
createParams := &vhd.CreateVirtualDiskParameters{
Version: 2,
Version2: vhd.CreateVersion2{
MaximumSize: vhdMaxSizeInBytes,
BlockSizeInBytes: vhdBlockSizeInBytes,
},
}
baseVHDHandle, err = vhd.CreateVirtualDisk(baseVHDPath, vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone, createParams)
if err != nil {
return fmt.Errorf("failed to create base vhd: %w", err)
}
err = computestorage.FormatWritableLayerVhd(ctx, windows.Handle(baseVHDHandle))
// we always wanna close the handle whether format succeeds for not.
closeErr := syscall.CloseHandle(baseVHDHandle)
if err != nil {
return err
} else if closeErr != nil {
return fmt.Errorf("failed to close vhdx handle: %w", closeErr)
}
}
if !diffVHDExists {
// Create the differencing disk that will be what's copied for the final rw layer
// for a container.
if err = vhd.CreateDiffVhd(diffVHDPath, baseVHDPath, vhdBlockSizeInBytes); err != nil {
return fmt.Errorf("failed to create differencing disk: %w", err)
}
}
// re assigning group access even if we didn't create the VHD shouldn't throw an error
if err = security.GrantVmGroupAccess(baseVHDPath); err != nil {
return fmt.Errorf("failed to grant vm group access to %s: %w", baseVHDPath, err)
}
if err = security.GrantVmGroupAccess(diffVHDPath); err != nil {
return fmt.Errorf("failed to grant vm group access to %s: %w", diffVHDPath, err)
}
return nil
}

View File

@ -21,17 +21,14 @@ package windows
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/Microsoft/hcsshim"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/core/snapshots/storage"
"github.com/containerd/continuity/fs"
@ -134,64 +131,6 @@ func (w *windowsBaseSnapshotter) Usage(ctx context.Context, key string) (usage s
return usage, nil
}
func (w *windowsBaseSnapshotter) mounts(sn storage.Snapshot, key string) []mount.Mount {
var (
roFlag string
)
if sn.Kind == snapshots.KindView {
roFlag = "ro"
} else {
roFlag = "rw"
}
source := w.getSnapshotDir(sn.ID)
parentLayerPaths := w.parentIDsToParentPaths(sn.ParentIDs)
mountType := "windows-layer"
// error is not checked here, as a string array will never fail to Marshal
parentLayersJSON, _ := json.Marshal(parentLayerPaths)
parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON)
options := []string{
roFlag,
}
if len(sn.ParentIDs) != 0 {
options = append(options, parentLayersOption)
}
mounts := []mount.Mount{
{
Source: source,
Type: mountType,
Options: options,
},
}
return mounts
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (w *windowsBaseSnapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
var snapshot storage.Snapshot
err = w.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
snapshot, err = storage.GetSnapshot(ctx, key)
if err != nil {
return fmt.Errorf("failed to get snapshot mount: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return w.mounts(snapshot, key), nil
}
// Walk the committed snapshots.
func (w *windowsBaseSnapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
return w.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
@ -260,113 +199,6 @@ func (w *windowsBaseSnapshotter) Close() error {
return w.ms.Close()
}
func (w *windowsBaseSnapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
var newSnapshot storage.Snapshot
err = w.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
newSnapshot, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
return fmt.Errorf("failed to create snapshot: %w", err)
}
log.G(ctx).Debug("createSnapshot")
// Create the new snapshot dir
snDir := w.getSnapshotDir(newSnapshot.ID)
if err = os.MkdirAll(snDir, 0700); err != nil {
return fmt.Errorf("failed to create snapshot dir %s: %w", snDir, err)
}
if strings.Contains(key, snapshots.UnpackKeyPrefix) {
// IO/disk space optimization: Do nothing
//
// We only need one sandbox.vhdx for the container. Skip making one for this
// snapshot if this isn't the snapshot that just houses the final sandbox.vhd
// that will be mounted as the containers scratch. Currently the key for a snapshot
// where a layer will be extracted to will have the string `extract-` in it.
return nil
}
if len(newSnapshot.ParentIDs) == 0 {
// A parentless snapshot a new base layer. Valid base layers must have a "Files" folder.
// When committed, there'll be some post-processing to fill in the rest
// of the metadata.
filesDir := filepath.Join(snDir, "Files")
if err := os.MkdirAll(filesDir, 0700); err != nil {
return fmt.Errorf("creating Files dir: %w", err)
}
return nil
}
parentLayerPaths := w.parentIDsToParentPaths(newSnapshot.ParentIDs)
var snapshotInfo snapshots.Info
for _, o := range opts {
o(&snapshotInfo)
}
var sizeInBytes uint64
if sizeGBstr, ok := snapshotInfo.Labels[rootfsSizeInGBLabel]; ok {
log.G(ctx).Warnf("%q label is deprecated, please use %q instead.", rootfsSizeInGBLabel, rootfsSizeInBytesLabel)
sizeInGB, err := strconv.ParseUint(sizeGBstr, 10, 32)
if err != nil {
return fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInGBLabel, sizeGBstr, err)
}
sizeInBytes = sizeInGB * 1024 * 1024 * 1024
}
// Prefer the newer label in bytes over the deprecated Windows specific GB variant.
if sizeBytesStr, ok := snapshotInfo.Labels[rootfsSizeInBytesLabel]; ok {
sizeInBytes, err = strconv.ParseUint(sizeBytesStr, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInBytesLabel, sizeBytesStr, err)
}
}
var makeUVMScratch bool
if _, ok := snapshotInfo.Labels[uvmScratchLabel]; ok {
makeUVMScratch = true
}
// This has to be run first to avoid clashing with the containers sandbox.vhdx.
if makeUVMScratch {
if err = w.createUVMScratchLayer(ctx, snDir, parentLayerPaths); err != nil {
return fmt.Errorf("failed to make UVM's scratch layer: %w", err)
}
}
if err = w.createScratchLayer(ctx, snDir, parentLayerPaths, sizeInBytes); err != nil {
return fmt.Errorf("failed to create scratch layer: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return w.mounts(newSnapshot, key), nil
}
// This is essentially a recreation of what HCS' CreateSandboxLayer does with some extra bells and
// whistles like expanding the volume if a size is specified.
func (w *windowsBaseSnapshotter) createScratchLayer(ctx context.Context, snDir string, parentLayers []string, sizeInBytes uint64) error {
parentLen := len(parentLayers)
if parentLen == 0 {
return errors.New("no parent layers present")
}
baseLayer := parentLayers[parentLen-1]
templateDiffDisk := filepath.Join(baseLayer, "blank.vhdx")
dest := filepath.Join(snDir, "sandbox.vhdx")
if err := copyScratchDisk(templateDiffDisk, dest); err != nil {
return err
}
if sizeInBytes != 0 {
if err := hcsshim.ExpandSandboxSize(w.info, filepath.Base(snDir), sizeInBytes); err != nil {
return fmt.Errorf("failed to expand sandbox vhdx size to %d bytes: %w", sizeInBytes, err)
}
}
return nil
}
// This handles creating the UVMs scratch layer.
func (w *windowsBaseSnapshotter) createUVMScratchLayer(ctx context.Context, snDir string, parentLayers []string) error {
parentLen := len(parentLayers)
@ -417,3 +249,26 @@ func copyScratchDisk(source, dest string) error {
}
return nil
}
func getRequestedScratchSize(ctx context.Context, snapshotInfo snapshots.Info) (uint64, error) {
var sizeInBytes uint64
var err error
if sizeGBstr, ok := snapshotInfo.Labels[rootfsSizeInGBLabel]; ok {
log.G(ctx).Warnf("%q label is deprecated, please use %q instead.", rootfsSizeInGBLabel, rootfsSizeInBytesLabel)
sizeInGB, err := strconv.ParseUint(sizeGBstr, 10, 32)
if err != nil {
return 0, fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInGBLabel, sizeGBstr, err)
}
sizeInBytes = sizeInGB * 1024 * 1024 * 1024
}
// Prefer the newer label in bytes over the deprecated Windows specific GB variant.
if sizeBytesStr, ok := snapshotInfo.Labels[rootfsSizeInBytesLabel]; ok {
sizeInBytes, err = strconv.ParseUint(sizeBytesStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInBytesLabel, sizeBytesStr, err)
}
}
return sizeInBytes, nil
}

View File

@ -21,8 +21,11 @@ package windows
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio"
@ -138,6 +141,81 @@ func (s *wcowSnapshotter) Commit(ctx context.Context, name, key string, opts ...
})
}
func (s *wcowSnapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
var newSnapshot storage.Snapshot
err = s.ms.WithTransaction(ctx, true, func(ctx context.Context) (retErr error) {
newSnapshot, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
return fmt.Errorf("failed to create snapshot: %w", err)
}
log.G(ctx).Debug("createSnapshot")
// Create the new snapshot dir
snDir := s.getSnapshotDir(newSnapshot.ID)
if err = os.MkdirAll(snDir, 0700); err != nil {
return fmt.Errorf("failed to create snapshot dir %s: %w", snDir, err)
}
defer func() {
if retErr != nil {
os.RemoveAll(snDir)
}
}()
if strings.Contains(key, snapshots.UnpackKeyPrefix) {
// IO/disk space optimization: Do nothing
//
// We only need one sandbox.vhdx for the container. Skip making one for this
// snapshot if this isn't the snapshot that just houses the final sandbox.vhd
// that will be mounted as the containers scratch. Currently the key for a snapshot
// where a layer will be extracted to will have the string `extract-` in it.
return nil
}
if len(newSnapshot.ParentIDs) == 0 {
// A parentless snapshot a new base layer. Valid base layers must have a "Files" folder.
// When committed, there'll be some post-processing to fill in the rest
// of the metadata.
filesDir := filepath.Join(snDir, "Files")
if err := os.MkdirAll(filesDir, 0700); err != nil {
return fmt.Errorf("creating Files dir: %w", err)
}
return nil
}
parentLayerPaths := s.parentIDsToParentPaths(newSnapshot.ParentIDs)
var snapshotInfo snapshots.Info
for _, o := range opts {
o(&snapshotInfo)
}
sizeInBytes, err := getRequestedScratchSize(ctx, snapshotInfo)
if err != nil {
return err
}
var makeUVMScratch bool
if _, ok := snapshotInfo.Labels[uvmScratchLabel]; ok {
makeUVMScratch = true
}
// This has to be run first to avoid clashing with the containers sandbox.vhdx.
if makeUVMScratch {
if err = s.createUVMScratchLayer(ctx, snDir, parentLayerPaths); err != nil {
return fmt.Errorf("failed to make UVM's scratch layer: %w", err)
}
}
if err = s.createScratchLayer(ctx, snDir, parentLayerPaths, sizeInBytes); err != nil {
return fmt.Errorf("failed to create scratch layer: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return s.mounts(newSnapshot, key), nil
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
func (s *wcowSnapshotter) Remove(ctx context.Context, key string) error {
@ -155,6 +233,50 @@ func (s *wcowSnapshotter) Remove(ctx context.Context, key string) error {
return nil
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (s *wcowSnapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
var snapshot storage.Snapshot
err = s.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
snapshot, err = storage.GetSnapshot(ctx, key)
if err != nil {
return fmt.Errorf("failed to get snapshot mount: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return s.mounts(snapshot, key), nil
}
// This is essentially a recreation of what HCS' CreateSandboxLayer does with some extra bells and
// whistles like expanding the volume if a size is specified.
func (s *wcowSnapshotter) createScratchLayer(ctx context.Context, snDir string, parentLayers []string, sizeInBytes uint64) error {
parentLen := len(parentLayers)
if parentLen == 0 {
return fmt.Errorf("no parent layers present")
}
baseLayer := parentLayers[parentLen-1]
templateDiffDisk := filepath.Join(baseLayer, "blank.vhdx")
dest := filepath.Join(snDir, "sandbox.vhdx")
if err := copyScratchDisk(templateDiffDisk, dest); err != nil {
return err
}
if sizeInBytes != 0 {
if err := hcsshim.ExpandSandboxSize(s.info, filepath.Base(snDir), sizeInBytes); err != nil {
return fmt.Errorf("failed to expand sandbox vhdx size to %d bytes: %w", sizeInBytes, err)
}
}
return nil
}
// convertScratchToReadOnlyLayer reimports the layer over itself, to transfer the files from the sandbox.vhdx to the on-disk storage.
func (s *wcowSnapshotter) convertScratchToReadOnlyLayer(ctx context.Context, snapshot storage.Snapshot, path string) (retErr error) {
@ -197,3 +319,40 @@ func (s *wcowSnapshotter) convertScratchToReadOnlyLayer(ctx context.Context, sna
return nil
}
func (s *wcowSnapshotter) mounts(sn storage.Snapshot, key string) []mount.Mount {
var (
roFlag string
)
if sn.Kind == snapshots.KindView {
roFlag = "ro"
} else {
roFlag = "rw"
}
source := s.getSnapshotDir(sn.ID)
parentLayerPaths := s.parentIDsToParentPaths(sn.ParentIDs)
mountType := "windows-layer"
// error is not checked here, as a string array will never fail to Marshal
parentLayersJSON, _ := json.Marshal(parentLayerPaths)
parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON)
options := []string{
roFlag,
}
if len(sn.ParentIDs) != 0 {
options = append(options, parentLayersOption)
}
mounts := []mount.Mount{
{
Source: source,
Type: mountType,
Options: options,
},
}
return mounts
}