Move snapshots/overlay to plugins/snapshots/overlay
Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
657
plugins/snapshots/overlay/overlay.go
Normal file
657
plugins/snapshots/overlay/overlay.go
Normal file
@@ -0,0 +1,657 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/plugins/snapshots/overlay/overlayutils"
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
"github.com/containerd/containerd/v2/snapshots/storage"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/containerd/log"
|
||||
)
|
||||
|
||||
// upperdirKey is a key of an optional label to each snapshot.
|
||||
// This optional label of a snapshot contains the location of "upperdir" where
|
||||
// the change set between this snapshot and its parent is stored.
|
||||
const upperdirKey = "containerd.io/snapshot/overlay.upperdir"
|
||||
|
||||
// SnapshotterConfig is used to configure the overlay snapshotter instance
|
||||
type SnapshotterConfig struct {
|
||||
asyncRemove bool
|
||||
upperdirLabel bool
|
||||
ms MetaStore
|
||||
mountOptions []string
|
||||
remapIds bool
|
||||
slowChown bool
|
||||
}
|
||||
|
||||
// Opt is an option to configure the overlay snapshotter
|
||||
type Opt func(config *SnapshotterConfig) error
|
||||
|
||||
// AsynchronousRemove defers removal of filesystem content until
|
||||
// the Cleanup method is called. Removals will make the snapshot
|
||||
// referred to by the key unavailable and make the key immediately
|
||||
// available for re-use.
|
||||
func AsynchronousRemove(config *SnapshotterConfig) error {
|
||||
config.asyncRemove = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithUpperdirLabel adds as an optional label
|
||||
// "containerd.io/snapshot/overlay.upperdir". This stores the location
|
||||
// of the upperdir that contains the changeset between the labelled
|
||||
// snapshot and its parent.
|
||||
func WithUpperdirLabel(config *SnapshotterConfig) error {
|
||||
config.upperdirLabel = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithMountOptions defines the default mount options used for the overlay mount.
|
||||
// NOTE: Options are not applied to bind mounts.
|
||||
func WithMountOptions(options []string) Opt {
|
||||
return func(config *SnapshotterConfig) error {
|
||||
config.mountOptions = append(config.mountOptions, options...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type MetaStore interface {
|
||||
TransactionContext(ctx context.Context, writable bool) (context.Context, storage.Transactor, error)
|
||||
WithTransaction(ctx context.Context, writable bool, fn storage.TransactionCallback) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// WithMetaStore allows the MetaStore to be created outside the snapshotter
|
||||
// and passed in.
|
||||
func WithMetaStore(ms MetaStore) Opt {
|
||||
return func(config *SnapshotterConfig) error {
|
||||
config.ms = ms
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithRemapIds(config *SnapshotterConfig) error {
|
||||
config.remapIds = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithSlowChown(config *SnapshotterConfig) error {
|
||||
config.slowChown = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type snapshotter struct {
|
||||
root string
|
||||
ms MetaStore
|
||||
asyncRemove bool
|
||||
upperdirLabel bool
|
||||
options []string
|
||||
remapIds bool
|
||||
slowChown bool
|
||||
}
|
||||
|
||||
// NewSnapshotter returns a Snapshotter which uses overlayfs. The overlayfs
|
||||
// diffs are stored under the provided root. A metadata file is stored under
|
||||
// the root.
|
||||
func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
|
||||
var config SnapshotterConfig
|
||||
for _, opt := range opts {
|
||||
if err := opt(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(root, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
supportsDType, err := fs.SupportsDType(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !supportsDType {
|
||||
return nil, fmt.Errorf("%s does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support", root)
|
||||
}
|
||||
if config.ms == nil {
|
||||
config.ms, err = storage.NewMetaStore(filepath.Join(root, "metadata.db"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !hasOption(config.mountOptions, "userxattr", false) {
|
||||
// figure out whether "userxattr" option is recognized by the kernel && needed
|
||||
userxattr, err := overlayutils.NeedsUserXAttr(root)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Warnf("cannot detect whether \"userxattr\" option needs to be used, assuming to be %v", userxattr)
|
||||
}
|
||||
if userxattr {
|
||||
config.mountOptions = append(config.mountOptions, "userxattr")
|
||||
}
|
||||
}
|
||||
|
||||
if !hasOption(config.mountOptions, "index", false) && supportsIndex() {
|
||||
config.mountOptions = append(config.mountOptions, "index=off")
|
||||
}
|
||||
|
||||
return &snapshotter{
|
||||
root: root,
|
||||
ms: config.ms,
|
||||
asyncRemove: config.asyncRemove,
|
||||
upperdirLabel: config.upperdirLabel,
|
||||
options: config.mountOptions,
|
||||
remapIds: config.remapIds,
|
||||
slowChown: config.slowChown,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func hasOption(options []string, key string, hasValue bool) bool {
|
||||
for _, option := range options {
|
||||
if hasValue {
|
||||
if strings.HasPrefix(option, key) && len(option) > len(key) && option[len(key)] == '=' {
|
||||
return true
|
||||
}
|
||||
} else if option == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Stat returns the info for an active or committed snapshot by name or
|
||||
// key.
|
||||
//
|
||||
// Should be used for parent resolution, existence checks and to discern
|
||||
// the kind of snapshot.
|
||||
func (o *snapshotter) Stat(ctx context.Context, key string) (info snapshots.Info, err error) {
|
||||
var id string
|
||||
if err := o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
|
||||
id, info, _, err = storage.GetInfo(ctx, key)
|
||||
return err
|
||||
}); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
if o.upperdirLabel {
|
||||
if info.Labels == nil {
|
||||
info.Labels = make(map[string]string)
|
||||
}
|
||||
info.Labels[upperdirKey] = o.upperPath(id)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (newInfo snapshots.Info, err error) {
|
||||
err = o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
|
||||
newInfo, err = storage.UpdateInfo(ctx, info, fieldpaths...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.upperdirLabel {
|
||||
id, _, _, err := storage.GetInfo(ctx, newInfo.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newInfo.Labels == nil {
|
||||
newInfo.Labels = make(map[string]string)
|
||||
}
|
||||
newInfo.Labels[upperdirKey] = o.upperPath(id)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return newInfo, err
|
||||
}
|
||||
|
||||
// Usage returns the resources taken by the snapshot identified by key.
|
||||
//
|
||||
// For active snapshots, this will scan the usage of the overlay "diff" (aka
|
||||
// "upper") directory and may take some time.
|
||||
//
|
||||
// For committed snapshots, the value is returned from the metadata database.
|
||||
func (o *snapshotter) Usage(ctx context.Context, key string) (_ snapshots.Usage, err error) {
|
||||
var (
|
||||
usage snapshots.Usage
|
||||
info snapshots.Info
|
||||
id string
|
||||
)
|
||||
if err := o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
|
||||
id, info, usage, err = storage.GetInfo(ctx, key)
|
||||
return err
|
||||
}); err != nil {
|
||||
return usage, err
|
||||
}
|
||||
|
||||
if info.Kind == snapshots.KindActive {
|
||||
upperPath := o.upperPath(id)
|
||||
du, err := fs.DiskUsage(ctx, upperPath)
|
||||
if err != nil {
|
||||
// TODO(stevvooe): Consider not reporting an error in this case.
|
||||
return snapshots.Usage{}, err
|
||||
}
|
||||
usage = snapshots.Usage(du)
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
|
||||
return o.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
|
||||
}
|
||||
|
||||
func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
|
||||
return o.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
|
||||
}
|
||||
|
||||
// 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 (o *snapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
|
||||
var s storage.Snapshot
|
||||
var info snapshots.Info
|
||||
if err := o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
|
||||
s, err = storage.GetSnapshot(ctx, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get active mount: %w", err)
|
||||
}
|
||||
|
||||
_, info, _, err = storage.GetInfo(ctx, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get snapshot info: %w", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return o.mounts(s, info), nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
|
||||
return o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
|
||||
// grab the existing id
|
||||
id, _, _, err := storage.GetInfo(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usage, err := fs.DiskUsage(ctx, o.upperPath(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil {
|
||||
return fmt.Errorf("failed to commit snapshot %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Remove abandons the snapshot identified by key. The snapshot will
|
||||
// immediately become unavailable and unrecoverable. Disk space will
|
||||
// be freed up on the next call to `Cleanup`.
|
||||
func (o *snapshotter) Remove(ctx context.Context, key string) (err error) {
|
||||
var removals []string
|
||||
// Remove directories after the transaction is closed, failures must not
|
||||
// return error since the transaction is committed with the removal
|
||||
// key no longer available.
|
||||
defer func() {
|
||||
if err == nil {
|
||||
for _, dir := range removals {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
|
||||
_, _, err = storage.Remove(ctx, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove snapshot %s: %w", key, err)
|
||||
}
|
||||
|
||||
if !o.asyncRemove {
|
||||
removals, err = o.getCleanupDirectories(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get directories for removal: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Walk the snapshots.
|
||||
func (o *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
|
||||
return o.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
|
||||
if o.upperdirLabel {
|
||||
return storage.WalkInfo(ctx, func(ctx context.Context, info snapshots.Info) error {
|
||||
id, _, _, err := storage.GetInfo(ctx, info.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Labels == nil {
|
||||
info.Labels = make(map[string]string)
|
||||
}
|
||||
info.Labels[upperdirKey] = o.upperPath(id)
|
||||
return fn(ctx, info)
|
||||
}, fs...)
|
||||
}
|
||||
return storage.WalkInfo(ctx, fn, fs...)
|
||||
})
|
||||
}
|
||||
|
||||
// Cleanup cleans up disk resources from removed or abandoned snapshots
|
||||
func (o *snapshotter) Cleanup(ctx context.Context) error {
|
||||
cleanup, err := o.cleanupDirectories(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dir := range cleanup {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) cleanupDirectories(ctx context.Context) (_ []string, err error) {
|
||||
var cleanupDirs []string
|
||||
// Get a write transaction to ensure no other write transaction can be entered
|
||||
// while the cleanup is scanning.
|
||||
if err := o.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
|
||||
cleanupDirs, err = o.getCleanupDirectories(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cleanupDirs, nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) getCleanupDirectories(ctx context.Context) ([]string, error) {
|
||||
ids, err := storage.IDMap(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshotDir := filepath.Join(o.root, "snapshots")
|
||||
fd, err := os.Open(snapshotDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
dirs, err := fd.Readdirnames(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cleanup := []string{}
|
||||
for _, d := range dirs {
|
||||
if _, ok := ids[d]; ok {
|
||||
continue
|
||||
}
|
||||
cleanup = append(cleanup, filepath.Join(snapshotDir, d))
|
||||
}
|
||||
|
||||
return cleanup, nil
|
||||
}
|
||||
|
||||
func validateIDMapping(mapping string) error {
|
||||
var (
|
||||
hostID int
|
||||
ctrID int
|
||||
length int
|
||||
)
|
||||
|
||||
if _, err := fmt.Sscanf(mapping, "%d:%d:%d", &ctrID, &hostID, &length); err != nil {
|
||||
return err
|
||||
}
|
||||
// Almost impossible, but snapshots.WithLabels doesn't check it
|
||||
if ctrID < 0 || hostID < 0 || length < 0 {
|
||||
return fmt.Errorf("invalid mapping \"%d:%d:%d\"", ctrID, hostID, length)
|
||||
}
|
||||
if ctrID != 0 {
|
||||
return fmt.Errorf("container mapping of 0 is only supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostID(mapping string) (int, error) {
|
||||
var (
|
||||
hostID int
|
||||
ctrID int
|
||||
length int
|
||||
)
|
||||
if err := validateIDMapping(mapping); err != nil {
|
||||
return -1, fmt.Errorf("invalid mapping: %w", err)
|
||||
}
|
||||
if _, err := fmt.Sscanf(mapping, "%d:%d:%d", &ctrID, &hostID, &length); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return hostID, nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
|
||||
var (
|
||||
s storage.Snapshot
|
||||
td, path string
|
||||
info snapshots.Info
|
||||
)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if td != "" {
|
||||
if err1 := os.RemoveAll(td); err1 != nil {
|
||||
log.G(ctx).WithError(err1).Warn("failed to cleanup temp snapshot directory")
|
||||
}
|
||||
}
|
||||
if path != "" {
|
||||
if err1 := os.RemoveAll(path); err1 != nil {
|
||||
log.G(ctx).WithError(err1).WithField("path", path).Error("failed to reclaim snapshot directory, directory may need removal")
|
||||
err = fmt.Errorf("failed to remove path: %v: %w", err1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := o.ms.WithTransaction(ctx, true, func(ctx context.Context) (err error) {
|
||||
snapshotDir := filepath.Join(o.root, "snapshots")
|
||||
td, err = o.prepareDirectory(ctx, snapshotDir, kind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create prepare snapshot dir: %w", err)
|
||||
}
|
||||
|
||||
s, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create snapshot: %w", err)
|
||||
}
|
||||
|
||||
_, info, _, err = storage.GetInfo(ctx, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get snapshot info: %w", err)
|
||||
}
|
||||
|
||||
mappedUID := -1
|
||||
mappedGID := -1
|
||||
// NOTE: if idmapped mounts' supported by hosted kernel there may be
|
||||
// no parents at all, so overlayfs will not work and snapshotter
|
||||
// will use bind mount. To be able to create file objects inside the
|
||||
// rootfs -- just chown this only bound directory according to provided
|
||||
// {uid,gid}map. In case of one/multiple parents -- chown upperdir.
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotUIDMapping]; ok {
|
||||
if mappedUID, err = hostID(v); err != nil {
|
||||
return fmt.Errorf("failed to parse UID mapping: %w", err)
|
||||
}
|
||||
}
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotGIDMapping]; ok {
|
||||
if mappedGID, err = hostID(v); err != nil {
|
||||
return fmt.Errorf("failed to parse GID mapping: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if mappedUID == -1 || mappedGID == -1 {
|
||||
if len(s.ParentIDs) > 0 {
|
||||
st, err := os.Stat(o.upperPath(s.ParentIDs[0]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat parent: %w", err)
|
||||
}
|
||||
stat, ok := st.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return fmt.Errorf("incompatible types after stat call: *syscall.Stat_t expected")
|
||||
}
|
||||
mappedUID = int(stat.Uid)
|
||||
mappedGID = int(stat.Gid)
|
||||
}
|
||||
}
|
||||
|
||||
if mappedUID != -1 && mappedGID != -1 {
|
||||
if err := os.Lchown(filepath.Join(td, "fs"), mappedUID, mappedGID); err != nil {
|
||||
return fmt.Errorf("failed to chown: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
path = filepath.Join(snapshotDir, s.ID)
|
||||
if err = os.Rename(td, path); err != nil {
|
||||
return fmt.Errorf("failed to rename: %w", err)
|
||||
}
|
||||
td = ""
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return o.mounts(s, info), nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) prepareDirectory(ctx context.Context, snapshotDir string, kind snapshots.Kind) (string, error) {
|
||||
td, err := os.MkdirTemp(snapshotDir, "new-")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(filepath.Join(td, "fs"), 0755); err != nil {
|
||||
return td, err
|
||||
}
|
||||
|
||||
if kind == snapshots.KindActive {
|
||||
if err := os.Mkdir(filepath.Join(td, "work"), 0711); err != nil {
|
||||
return td, err
|
||||
}
|
||||
}
|
||||
|
||||
return td, nil
|
||||
}
|
||||
|
||||
func (o *snapshotter) mounts(s storage.Snapshot, info snapshots.Info) []mount.Mount {
|
||||
var options []string
|
||||
|
||||
if o.remapIds {
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotUIDMapping]; ok {
|
||||
options = append(options, fmt.Sprintf("uidmap=%s", v))
|
||||
}
|
||||
if v, ok := info.Labels[snapshots.LabelSnapshotGIDMapping]; ok {
|
||||
options = append(options, fmt.Sprintf("gidmap=%s", v))
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.ParentIDs) == 0 {
|
||||
// if we only have one layer/no parents then just return a bind mount as overlay
|
||||
// will not work
|
||||
roFlag := "rw"
|
||||
if s.Kind == snapshots.KindView {
|
||||
roFlag = "ro"
|
||||
}
|
||||
return []mount.Mount{
|
||||
{
|
||||
Source: o.upperPath(s.ID),
|
||||
Type: "bind",
|
||||
Options: append(options,
|
||||
roFlag,
|
||||
"rbind",
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if s.Kind == snapshots.KindActive {
|
||||
options = append(options,
|
||||
fmt.Sprintf("workdir=%s", o.workPath(s.ID)),
|
||||
fmt.Sprintf("upperdir=%s", o.upperPath(s.ID)),
|
||||
)
|
||||
} else if len(s.ParentIDs) == 1 {
|
||||
return []mount.Mount{
|
||||
{
|
||||
Source: o.upperPath(s.ParentIDs[0]),
|
||||
Type: "bind",
|
||||
Options: append(options,
|
||||
"ro",
|
||||
"rbind",
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
parentPaths := make([]string, len(s.ParentIDs))
|
||||
for i := range s.ParentIDs {
|
||||
parentPaths[i] = o.upperPath(s.ParentIDs[i])
|
||||
}
|
||||
options = append(options, fmt.Sprintf("lowerdir=%s", strings.Join(parentPaths, ":")))
|
||||
options = append(options, o.options...)
|
||||
|
||||
return []mount.Mount{
|
||||
{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: options,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (o *snapshotter) upperPath(id string) string {
|
||||
return filepath.Join(o.root, "snapshots", id, "fs")
|
||||
}
|
||||
|
||||
func (o *snapshotter) workPath(id string) string {
|
||||
return filepath.Join(o.root, "snapshots", id, "work")
|
||||
}
|
||||
|
||||
// Close closes the snapshotter
|
||||
func (o *snapshotter) Close() error {
|
||||
return o.ms.Close()
|
||||
}
|
||||
|
||||
// supportsIndex checks whether the "index=off" option is supported by the kernel.
|
||||
func supportsIndex() bool {
|
||||
if _, err := os.Stat("/sys/module/overlay/parameters/index"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
606
plugins/snapshots/overlay/overlay_test.go
Normal file
606
plugins/snapshots/overlay/overlay_test.go
Normal file
@@ -0,0 +1,606 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
containerd "github.com/containerd/containerd/v2/client"
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
"github.com/containerd/containerd/v2/plugins/snapshots/overlay/overlayutils"
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
"github.com/containerd/containerd/v2/snapshots/storage"
|
||||
"github.com/containerd/containerd/v2/snapshots/testsuite"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
func newSnapshotterWithOpts(opts ...Opt) testsuite.SnapshotterFunc {
|
||||
return func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
|
||||
snapshotter, err := NewSnapshotter(root, opts...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return snapshotter, func() error { return snapshotter.Close() }, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlay(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
optTestCases := map[string][]Opt{
|
||||
"no opt": nil,
|
||||
// default in init()
|
||||
"AsynchronousRemove": {AsynchronousRemove},
|
||||
// idmapped mounts enabled
|
||||
"WithRemapIds": {WithRemapIds},
|
||||
}
|
||||
|
||||
for optsName, opts := range optTestCases {
|
||||
t.Run(optsName, func(t *testing.T) {
|
||||
newSnapshotter := newSnapshotterWithOpts(opts...)
|
||||
testsuite.SnapshotterSuite(t, "overlayfs", newSnapshotter)
|
||||
t.Run("TestOverlayRemappedBind", func(t *testing.T) {
|
||||
testOverlayRemappedBind(t, newSnapshotter)
|
||||
})
|
||||
t.Run("TestOverlayRemappedActive", func(t *testing.T) {
|
||||
testOverlayRemappedActive(t, newSnapshotter)
|
||||
})
|
||||
t.Run("TestOverlayRemappedInvalidMappings", func(t *testing.T) {
|
||||
testOverlayRemappedInvalidMapping(t, newSnapshotter)
|
||||
})
|
||||
t.Run("TestOverlayMounts", func(t *testing.T) {
|
||||
testOverlayMounts(t, newSnapshotter)
|
||||
})
|
||||
t.Run("TestOverlayCommit", func(t *testing.T) {
|
||||
testOverlayCommit(t, newSnapshotter)
|
||||
})
|
||||
t.Run("TestOverlayOverlayMount", func(t *testing.T) {
|
||||
testOverlayOverlayMount(t, newSnapshotter)
|
||||
})
|
||||
t.Run("TestOverlayOverlayRead", func(t *testing.T) {
|
||||
testOverlayOverlayRead(t, newSnapshotter)
|
||||
})
|
||||
t.Run("TestOverlayView", func(t *testing.T) {
|
||||
testOverlayView(t, newSnapshotterWithOpts(append(opts, WithMountOptions([]string{"volatile"}))...))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testOverlayMounts(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mounts, err := o.Prepare(ctx, "/tmp/test", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(mounts) != 1 {
|
||||
t.Errorf("should only have 1 mount but received %d", len(mounts))
|
||||
}
|
||||
m := mounts[0]
|
||||
if m.Type != "bind" {
|
||||
t.Errorf("mount type should be bind but received %q", m.Type)
|
||||
}
|
||||
expected := filepath.Join(root, "snapshots", "1", "fs")
|
||||
if m.Source != expected {
|
||||
t.Errorf("expected source %q but received %q", expected, m.Source)
|
||||
}
|
||||
if m.Options[0] != "rw" {
|
||||
t.Errorf("expected mount option rw but received %q", m.Options[0])
|
||||
}
|
||||
if m.Options[1] != "rbind" {
|
||||
t.Errorf("expected mount option rbind but received %q", m.Options[1])
|
||||
}
|
||||
}
|
||||
|
||||
func testOverlayCommit(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := "/tmp/test"
|
||||
mounts, err := o.Prepare(ctx, key, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := mounts[0]
|
||||
if err := os.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := o.Commit(ctx, "base", key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testOverlayOverlayMount(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := "/tmp/test"
|
||||
if _, err = o.Prepare(ctx, key, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := o.Commit(ctx, "base", key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var mounts []mount.Mount
|
||||
if mounts, err = o.Prepare(ctx, "/tmp/layer2", "base"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(mounts) != 1 {
|
||||
t.Errorf("should only have 1 mount but received %d", len(mounts))
|
||||
}
|
||||
m := mounts[0]
|
||||
if m.Type != "overlay" {
|
||||
t.Errorf("mount type should be overlay but received %q", m.Type)
|
||||
}
|
||||
if m.Source != "overlay" {
|
||||
t.Errorf("expected source %q but received %q", "overlay", m.Source)
|
||||
}
|
||||
var (
|
||||
expected []string
|
||||
bp = getBasePath(ctx, o, root, "/tmp/layer2")
|
||||
work = "workdir=" + filepath.Join(bp, "work")
|
||||
upper = "upperdir=" + filepath.Join(bp, "fs")
|
||||
lower = "lowerdir=" + getParents(ctx, o, root, "/tmp/layer2")[0]
|
||||
)
|
||||
|
||||
expected = append(expected, []string{
|
||||
work,
|
||||
upper,
|
||||
lower,
|
||||
}...)
|
||||
|
||||
if supportsIndex() {
|
||||
expected = append(expected, "index=off")
|
||||
}
|
||||
if userxattr, err := overlayutils.NeedsUserXAttr(root); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if userxattr {
|
||||
expected = append(expected, "userxattr")
|
||||
}
|
||||
|
||||
for i, v := range expected {
|
||||
if m.Options[i] != v {
|
||||
t.Errorf("expected %q but received %q", v, m.Options[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testOverlayRemappedBind(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
var (
|
||||
opts []snapshots.Opt
|
||||
mounts []mount.Mount
|
||||
)
|
||||
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if sn, ok := o.(*snapshotter); !ok || !sn.remapIds {
|
||||
t.Skip("overlayfs doesn't support idmapped mounts")
|
||||
}
|
||||
|
||||
hostID := uint32(666)
|
||||
contID := uint32(0)
|
||||
length := uint32(65536)
|
||||
|
||||
uidMap := specs.LinuxIDMapping{
|
||||
ContainerID: contID,
|
||||
HostID: hostID,
|
||||
Size: length,
|
||||
}
|
||||
gidMap := specs.LinuxIDMapping{
|
||||
ContainerID: contID,
|
||||
HostID: hostID,
|
||||
Size: length,
|
||||
}
|
||||
opts = append(opts, containerd.WithRemapperLabels(
|
||||
uidMap.ContainerID, uidMap.HostID,
|
||||
gidMap.ContainerID, gidMap.HostID,
|
||||
length),
|
||||
)
|
||||
|
||||
key := "/tmp/test"
|
||||
if mounts, err = o.Prepare(ctx, key, "", opts...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bp := getBasePath(ctx, o, root, key)
|
||||
expected := []string{
|
||||
fmt.Sprintf("uidmap=%d:%d:%d", uidMap.ContainerID, uidMap.HostID, uidMap.Size),
|
||||
fmt.Sprintf("gidmap=%d:%d:%d", gidMap.ContainerID, gidMap.HostID, gidMap.Size),
|
||||
"rw",
|
||||
"rbind",
|
||||
}
|
||||
|
||||
checkMountOpts := func() {
|
||||
if len(mounts) != 1 {
|
||||
t.Errorf("should only have 1 mount but received %d", len(mounts))
|
||||
}
|
||||
|
||||
if len(mounts[0].Options) != len(expected) {
|
||||
t.Errorf("expected %d options, but received %d", len(expected), len(mounts[0].Options))
|
||||
}
|
||||
|
||||
m := mounts[0]
|
||||
for i, v := range expected {
|
||||
if m.Options[i] != v {
|
||||
t.Errorf("mount option %q is not valid, expected %q", m.Options[i], v)
|
||||
}
|
||||
}
|
||||
|
||||
st, err := os.Stat(filepath.Join(bp, "fs"))
|
||||
if err != nil {
|
||||
t.Errorf("failed to stat %s", filepath.Join(bp, "fs"))
|
||||
}
|
||||
|
||||
if stat, ok := st.Sys().(*syscall.Stat_t); !ok {
|
||||
t.Errorf("incompatible types after stat call: *syscall.Stat_t expected")
|
||||
} else if stat.Uid != uidMap.HostID || stat.Gid != gidMap.HostID {
|
||||
t.Errorf("bad mapping: expected {uid: %d, gid: %d}; real {uid: %d, gid: %d}", uidMap.HostID, gidMap.HostID, int(stat.Uid), int(stat.Gid))
|
||||
}
|
||||
}
|
||||
checkMountOpts()
|
||||
|
||||
expected[2] = "ro"
|
||||
if err = o.Commit(ctx, "base", key, opts...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mounts, err = o.View(ctx, key, "base", opts...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bp = getBasePath(ctx, o, root, key)
|
||||
checkMountOpts()
|
||||
|
||||
key = "/tmp/test1"
|
||||
if mounts, err = o.Prepare(ctx, key, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bp = getBasePath(ctx, o, root, key)
|
||||
|
||||
expected = expected[2:]
|
||||
expected[0] = "rw"
|
||||
|
||||
uidMap.HostID = 0
|
||||
gidMap.HostID = 0
|
||||
|
||||
checkMountOpts()
|
||||
}
|
||||
|
||||
func testOverlayRemappedActive(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
var (
|
||||
opts []snapshots.Opt
|
||||
mounts []mount.Mount
|
||||
)
|
||||
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if sn, ok := o.(*snapshotter); !ok || !sn.remapIds {
|
||||
t.Skip("overlayfs doesn't support idmapped mounts")
|
||||
}
|
||||
|
||||
hostID := uint32(666)
|
||||
contID := uint32(0)
|
||||
length := uint32(65536)
|
||||
|
||||
uidMap := specs.LinuxIDMapping{
|
||||
ContainerID: contID,
|
||||
HostID: hostID,
|
||||
Size: length,
|
||||
}
|
||||
gidMap := specs.LinuxIDMapping{
|
||||
ContainerID: contID,
|
||||
HostID: hostID,
|
||||
Size: length,
|
||||
}
|
||||
opts = append(opts, containerd.WithRemapperLabels(
|
||||
uidMap.ContainerID, uidMap.HostID,
|
||||
gidMap.ContainerID, gidMap.HostID,
|
||||
length),
|
||||
)
|
||||
|
||||
key := "/tmp/test"
|
||||
if _, err = o.Prepare(ctx, key, "", opts...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = o.Commit(ctx, "base", key, opts...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mounts, err = o.Prepare(ctx, key, "base", opts...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(mounts) != 1 {
|
||||
t.Errorf("should only have 1 mount but received %d", len(mounts))
|
||||
}
|
||||
|
||||
bp := getBasePath(ctx, o, root, key)
|
||||
expected := []string{
|
||||
fmt.Sprintf("uidmap=%d:%d:%d", uidMap.ContainerID, uidMap.HostID, uidMap.Size),
|
||||
fmt.Sprintf("gidmap=%d:%d:%d", gidMap.ContainerID, gidMap.HostID, gidMap.Size),
|
||||
fmt.Sprintf("workdir=%s", filepath.Join(bp, "work")),
|
||||
fmt.Sprintf("upperdir=%s", filepath.Join(bp, "fs")),
|
||||
fmt.Sprintf("lowerdir=%s", getParents(ctx, o, root, key)[0]),
|
||||
}
|
||||
|
||||
m := mounts[0]
|
||||
for i, v := range expected {
|
||||
if m.Options[i] != v {
|
||||
t.Errorf("mount option %q is invalid, expected %q", m.Options[i], v)
|
||||
}
|
||||
}
|
||||
|
||||
st, err := os.Stat(filepath.Join(bp, "fs"))
|
||||
if err != nil {
|
||||
t.Errorf("failed to stat %s", filepath.Join(bp, "fs"))
|
||||
}
|
||||
if stat, ok := st.Sys().(*syscall.Stat_t); !ok {
|
||||
t.Errorf("incompatible types after stat call: *syscall.Stat_t expected")
|
||||
} else if stat.Uid != uidMap.HostID || stat.Gid != gidMap.HostID {
|
||||
t.Errorf("bad mapping: expected {uid: %d, gid: %d}; received {uid: %d, gid: %d}", uidMap.HostID, gidMap.HostID, int(stat.Uid), int(stat.Gid))
|
||||
}
|
||||
}
|
||||
|
||||
func testOverlayRemappedInvalidMapping(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if sn, ok := o.(*snapshotter); !ok || !sn.remapIds {
|
||||
t.Skip("overlayfs doesn't support idmapped mounts")
|
||||
}
|
||||
|
||||
key := "/tmp/test"
|
||||
for desc, opts := range map[string][]snapshots.Opt{
|
||||
"WithLabels: negative UID mapping must fail": {
|
||||
snapshots.WithLabels(map[string]string{
|
||||
snapshots.LabelSnapshotUIDMapping: "-1:-1:-2",
|
||||
snapshots.LabelSnapshotGIDMapping: "0:0:66666",
|
||||
}),
|
||||
},
|
||||
"WithLabels: negative GID mapping must fail": {
|
||||
snapshots.WithLabels(map[string]string{
|
||||
snapshots.LabelSnapshotUIDMapping: "0:0:66666",
|
||||
snapshots.LabelSnapshotGIDMapping: "-1:-1:-2",
|
||||
}),
|
||||
},
|
||||
"WithLabels: negative GID/UID mappings must fail": {
|
||||
snapshots.WithLabels(map[string]string{
|
||||
snapshots.LabelSnapshotUIDMapping: "-666:-666:-666",
|
||||
snapshots.LabelSnapshotGIDMapping: "-666:-666:-666",
|
||||
}),
|
||||
},
|
||||
"WithRemapperLabels: container ID (GID/UID) other than 0 must fail": {
|
||||
containerd.WithRemapperLabels(666, 666, 666, 666, 666),
|
||||
},
|
||||
"WithRemapperLabels: container ID (UID) other than 0 must fail": {
|
||||
containerd.WithRemapperLabels(666, 0, 0, 0, 65536),
|
||||
},
|
||||
"WithRemapperLabels: container ID (GID) other than 0 must fail": {
|
||||
containerd.WithRemapperLabels(0, 0, 666, 0, 4294967295),
|
||||
},
|
||||
} {
|
||||
t.Log(desc)
|
||||
if _, err = o.Prepare(ctx, key, "", opts...); err == nil {
|
||||
t.Fatalf("snapshots with invalid mappings must fail")
|
||||
}
|
||||
// remove may fail, but it doesn't matter
|
||||
_ = o.Remove(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
func getBasePath(ctx context.Context, sn snapshots.Snapshotter, root, key string) string {
|
||||
o := sn.(*snapshotter)
|
||||
ctx, t, err := o.ms.TransactionContext(ctx, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer t.Rollback()
|
||||
|
||||
s, err := storage.GetSnapshot(ctx, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return filepath.Join(root, "snapshots", s.ID)
|
||||
}
|
||||
|
||||
func getParents(ctx context.Context, sn snapshots.Snapshotter, root, key string) []string {
|
||||
o := sn.(*snapshotter)
|
||||
ctx, t, err := o.ms.TransactionContext(ctx, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer t.Rollback()
|
||||
s, err := storage.GetSnapshot(ctx, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
parents := make([]string, len(s.ParentIDs))
|
||||
for i := range s.ParentIDs {
|
||||
parents[i] = filepath.Join(root, "snapshots", s.ParentIDs[i], "fs")
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
func testOverlayOverlayRead(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
testutil.RequiresRoot(t)
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := "/tmp/test"
|
||||
mounts, err := o.Prepare(ctx, key, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := mounts[0]
|
||||
if err := os.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := o.Commit(ctx, "base", key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mounts, err = o.Prepare(ctx, "/tmp/layer2", "base"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dest := filepath.Join(root, "dest")
|
||||
if err := os.Mkdir(dest, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := mount.All(mounts, dest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer syscall.Unmount(dest, 0)
|
||||
data, err := os.ReadFile(filepath.Join(dest, "foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e := string(data); e != "hi" {
|
||||
t.Fatalf("expected file contents hi but got %q", e)
|
||||
}
|
||||
}
|
||||
|
||||
func testOverlayView(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) {
|
||||
ctx := context.TODO()
|
||||
root := t.TempDir()
|
||||
o, _, err := newSnapshotter(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := "/tmp/base"
|
||||
mounts, err := o.Prepare(ctx, key, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := mounts[0]
|
||||
if err := os.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := o.Commit(ctx, "base", key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key = "/tmp/top"
|
||||
_, err = o.Prepare(ctx, key, "base")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(getParents(ctx, o, root, "/tmp/top")[0], "foo"), []byte("hi, again"), 0660); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := o.Commit(ctx, "top", key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mounts, err = o.View(ctx, "/tmp/view1", "base")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(mounts) != 1 {
|
||||
t.Fatalf("should only have 1 mount but received %d", len(mounts))
|
||||
}
|
||||
m = mounts[0]
|
||||
if m.Type != "bind" {
|
||||
t.Errorf("mount type should be bind but received %q", m.Type)
|
||||
}
|
||||
expected := getParents(ctx, o, root, "/tmp/view1")[0]
|
||||
if m.Source != expected {
|
||||
t.Errorf("expected source %q but received %q", expected, m.Source)
|
||||
}
|
||||
|
||||
if m.Options[0] != "ro" {
|
||||
t.Errorf("expected mount option ro but received %q", m.Options[0])
|
||||
}
|
||||
if m.Options[1] != "rbind" {
|
||||
t.Errorf("expected mount option rbind but received %q", m.Options[1])
|
||||
}
|
||||
|
||||
mounts, err = o.View(ctx, "/tmp/view2", "top")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(mounts) != 1 {
|
||||
t.Fatalf("should only have 1 mount but received %d", len(mounts))
|
||||
}
|
||||
m = mounts[0]
|
||||
if m.Type != "overlay" {
|
||||
t.Errorf("mount type should be overlay but received %q", m.Type)
|
||||
}
|
||||
if m.Source != "overlay" {
|
||||
t.Errorf("mount source should be overlay but received %q", m.Source)
|
||||
}
|
||||
|
||||
supportsIndex := supportsIndex()
|
||||
expectedOptions := 3
|
||||
if !supportsIndex {
|
||||
expectedOptions--
|
||||
}
|
||||
userxattr, err := overlayutils.NeedsUserXAttr(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if userxattr {
|
||||
expectedOptions++
|
||||
}
|
||||
|
||||
if len(m.Options) != expectedOptions {
|
||||
t.Errorf("expected %d additional mount option but got %d", expectedOptions, len(m.Options))
|
||||
}
|
||||
lowers := getParents(ctx, o, root, "/tmp/view2")
|
||||
|
||||
expected = fmt.Sprintf("lowerdir=%s:%s", lowers[0], lowers[1])
|
||||
if m.Options[0] != expected {
|
||||
t.Errorf("expected option %q but received %q", expected, m.Options[0])
|
||||
}
|
||||
|
||||
if m.Options[1] != "volatile" {
|
||||
t.Error("expected option first option to be provided option \"volatile\"")
|
||||
}
|
||||
}
|
||||
297
plugins/snapshots/overlay/overlayutils/check.go
Normal file
297
plugins/snapshots/overlay/overlayutils/check.go
Normal file
@@ -0,0 +1,297 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package overlayutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
kernel "github.com/containerd/containerd/v2/contrib/seccomp/kernelversion"
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/pkg/userns"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/containerd/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// see https://man7.org/linux/man-pages/man2/statfs.2.html
|
||||
tmpfsMagic = 0x01021994
|
||||
)
|
||||
|
||||
// SupportsMultipleLowerDir checks if the system supports multiple lowerdirs,
|
||||
// which is required for the overlay snapshotter. On 4.x kernels, multiple lowerdirs
|
||||
// are always available (so this check isn't needed), and backported to RHEL and
|
||||
// CentOS 3.x kernels (3.10.0-693.el7.x86_64 and up). This function is to detect
|
||||
// support on those kernels, without doing a kernel version compare.
|
||||
//
|
||||
// Ported from moby overlay2.
|
||||
func SupportsMultipleLowerDir(d string) error {
|
||||
td, err := os.MkdirTemp(d, "multiple-lowerdir-check")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(td); err != nil {
|
||||
log.L.WithError(err).Warnf("Failed to remove check directory %v", td)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, dir := range []string{"lower1", "lower2", "upper", "work", "merged"} {
|
||||
if err := os.Mkdir(filepath.Join(td, dir), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts := fmt.Sprintf("lowerdir=%s:%s,upperdir=%s,workdir=%s", filepath.Join(td, "lower2"), filepath.Join(td, "lower1"), filepath.Join(td, "upper"), filepath.Join(td, "work"))
|
||||
m := mount.Mount{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: []string{opts},
|
||||
}
|
||||
dest := filepath.Join(td, "merged")
|
||||
if err := m.Mount(dest); err != nil {
|
||||
return fmt.Errorf("failed to mount overlay: %w", err)
|
||||
}
|
||||
if err := mount.UnmountAll(dest, 0); err != nil {
|
||||
log.L.WithError(err).Warnf("Failed to unmount check directory %v", dest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Supported returns nil when the overlayfs is functional on the system with the root directory.
|
||||
// Supported is not called during plugin initialization, but exposed for downstream projects which uses
|
||||
// this snapshotter as a library.
|
||||
func Supported(root string) error {
|
||||
if err := os.MkdirAll(root, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
supportsDType, err := fs.SupportsDType(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !supportsDType {
|
||||
return fmt.Errorf("%s does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support", root)
|
||||
}
|
||||
return SupportsMultipleLowerDir(root)
|
||||
}
|
||||
|
||||
// IsPathOnTmpfs returns whether the path is on a tmpfs or not.
|
||||
//
|
||||
// It uses statfs to check if the fs type is TMPFS_MAGIC (0x01021994)
|
||||
// see https://man7.org/linux/man-pages/man2/statfs.2.html
|
||||
func IsPathOnTmpfs(d string) bool {
|
||||
stat := syscall.Statfs_t{}
|
||||
err := syscall.Statfs(d, &stat)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Warnf("Could not retrieve statfs for %v", d)
|
||||
return false
|
||||
}
|
||||
|
||||
return stat.Type == tmpfsMagic
|
||||
}
|
||||
|
||||
// NeedsUserXAttr returns whether overlayfs should be mounted with the "userxattr" mount option.
|
||||
//
|
||||
// The "userxattr" option is needed for mounting overlayfs inside a user namespace with kernel >= 5.11.
|
||||
//
|
||||
// The "userxattr" option is NOT needed for the initial user namespace (aka "the host").
|
||||
//
|
||||
// Also, Ubuntu (since circa 2015) and Debian (since 10) with kernel < 5.11 can mount
|
||||
// the overlayfs in a user namespace without the "userxattr" option.
|
||||
//
|
||||
// The corresponding kernel commit: https://github.com/torvalds/linux/commit/2d2f2d7322ff43e0fe92bf8cccdc0b09449bf2e1
|
||||
// > ovl: user xattr
|
||||
// >
|
||||
// > Optionally allow using "user.overlay." namespace instead of "trusted.overlay."
|
||||
// > ...
|
||||
// > Disable redirect_dir and metacopy options, because these would allow privilege escalation through direct manipulation of the
|
||||
// > "user.overlay.redirect" or "user.overlay.metacopy" xattrs.
|
||||
// > ...
|
||||
//
|
||||
// The "userxattr" support is not exposed in "/sys/module/overlay/parameters".
|
||||
func NeedsUserXAttr(d string) (bool, error) {
|
||||
if !userns.RunningInUserNS() {
|
||||
// we are the real root (i.e., the root in the initial user NS),
|
||||
// so we do never need "userxattr" opt.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// userxattr not permitted on tmpfs https://man7.org/linux/man-pages/man5/tmpfs.5.html
|
||||
if IsPathOnTmpfs(d) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Fast path on kernels >= 5.11
|
||||
//
|
||||
// Keep in mind that distro vendors might be going to backport the patch to older kernels
|
||||
// so we can't completely remove the "slow path".
|
||||
fiveDotEleven := kernel.KernelVersion{Kernel: 5, Major: 11}
|
||||
if ok, err := kernel.GreaterEqualThan(fiveDotEleven); err == nil && ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
tdRoot := filepath.Join(d, "userxattr-check")
|
||||
if err := os.RemoveAll(tdRoot); err != nil {
|
||||
log.L.WithError(err).Warnf("Failed to remove check directory %v", tdRoot)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(tdRoot, 0700); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tdRoot); err != nil {
|
||||
log.L.WithError(err).Warnf("Failed to remove check directory %v", tdRoot)
|
||||
}
|
||||
}()
|
||||
|
||||
td, err := os.MkdirTemp(tdRoot, "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, dir := range []string{"lower1", "lower2", "upper", "work", "merged"} {
|
||||
if err := os.Mkdir(filepath.Join(td, dir), 0755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
opts := []string{
|
||||
"ro",
|
||||
fmt.Sprintf("lowerdir=%s:%s,upperdir=%s,workdir=%s", filepath.Join(td, "lower2"), filepath.Join(td, "lower1"), filepath.Join(td, "upper"), filepath.Join(td, "work")),
|
||||
"userxattr",
|
||||
}
|
||||
|
||||
m := mount.Mount{
|
||||
Type: "overlay",
|
||||
Source: "overlay",
|
||||
Options: opts,
|
||||
}
|
||||
|
||||
dest := filepath.Join(td, "merged")
|
||||
if err := m.Mount(dest); err != nil {
|
||||
// Probably the host is running Ubuntu/Debian kernel (< 5.11) with the userns patch but without the userxattr patch.
|
||||
// Return false without error.
|
||||
log.L.WithError(err).Debugf("cannot mount overlay with \"userxattr\", probably the kernel does not support userxattr")
|
||||
return false, nil
|
||||
}
|
||||
if err := mount.UnmountAll(dest, 0); err != nil {
|
||||
log.L.WithError(err).Warnf("Failed to unmount check directory %v", dest)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SupportsIDMappedMounts tells if this kernel supports idmapped mounts for overlayfs
|
||||
// or not.
|
||||
//
|
||||
// This function returns error whether the kernel supports idmapped mounts
|
||||
// for overlayfs or not, i.e. if e.g. -ENOSYS may be returned as well as -EPERM.
|
||||
// So, caller should check for (true, err == nil), otherwise treat it as there's
|
||||
// no support from the kernel side.
|
||||
func SupportsIDMappedMounts() (bool, error) {
|
||||
// Fast path
|
||||
fiveDotNineteen := kernel.KernelVersion{Kernel: 5, Major: 19}
|
||||
if ok, err := kernel.GreaterEqualThan(fiveDotNineteen); err == nil && ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Do slow path, because idmapped mounts may be backported to older kernels.
|
||||
uidMap := syscall.SysProcIDMap{
|
||||
ContainerID: 0,
|
||||
HostID: 666,
|
||||
Size: 1,
|
||||
}
|
||||
gidMap := syscall.SysProcIDMap{
|
||||
ContainerID: 0,
|
||||
HostID: 666,
|
||||
Size: 1,
|
||||
}
|
||||
td, err := os.MkdirTemp("", "ovl-idmapped-check")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create check directory: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(td); err != nil {
|
||||
log.L.WithError(err).Warnf("failed to remove check directory %s", td)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, dir := range []string{"lower", "upper", "work", "merged"} {
|
||||
if err = os.Mkdir(filepath.Join(td, dir), 0755); err != nil {
|
||||
return false, fmt.Errorf("failed to create %s directory: %w", dir, err)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if err = os.RemoveAll(td); err != nil {
|
||||
log.L.WithError(err).Warnf("failed remove overlay check directory %s", td)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = os.Lchown(filepath.Join(td, "upper"), uidMap.HostID, gidMap.HostID); err != nil {
|
||||
return false, fmt.Errorf("failed to chown upper directory %s: %w", filepath.Join(td, "upper"), err)
|
||||
}
|
||||
|
||||
lowerDir := filepath.Join(td, "lower")
|
||||
uidmap := fmt.Sprintf("%d:%d:%d", uidMap.ContainerID, uidMap.HostID, uidMap.Size)
|
||||
gidmap := fmt.Sprintf("%d:%d:%d", gidMap.ContainerID, gidMap.HostID, gidMap.Size)
|
||||
|
||||
usernsFd, err := mount.GetUsernsFD(uidmap, gidmap)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer usernsFd.Close()
|
||||
|
||||
if err = mount.IDMapMount(lowerDir, lowerDir, int(usernsFd.Fd())); err != nil {
|
||||
return false, fmt.Errorf("failed to remap lowerdir %s: %w", lowerDir, err)
|
||||
}
|
||||
defer func() {
|
||||
if err = unix.Unmount(lowerDir, 0); err != nil {
|
||||
log.L.WithError(err).Warnf("failed to unmount lowerdir %s", lowerDir)
|
||||
}
|
||||
}()
|
||||
|
||||
opts := fmt.Sprintf("index=off,lowerdir=%s,upperdir=%s,workdir=%s", lowerDir, filepath.Join(td, "upper"), filepath.Join(td, "work"))
|
||||
if err = unix.Mount("", filepath.Join(td, "merged"), "overlay", uintptr(unix.MS_RDONLY), opts); err != nil {
|
||||
return false, fmt.Errorf("failed to mount idmapped overlay to %s: %w", filepath.Join(td, "merged"), err)
|
||||
}
|
||||
defer func() {
|
||||
if err = unix.Unmount(filepath.Join(td, "merged"), 0); err != nil {
|
||||
log.L.WithError(err).Warnf("failed to unmount overlay check directory %s", filepath.Join(td, "merged"))
|
||||
}
|
||||
}()
|
||||
|
||||
// NOTE: we can't just return true if mount didn't fail since overlay supports
|
||||
// idmappings for {lower,upper}dir. That means we need to check merged directory
|
||||
// to make sure it completely supports idmapped mounts.
|
||||
st, err := os.Stat(filepath.Join(td, "merged"))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to stat %s: %w", filepath.Join(td, "merged"), err)
|
||||
}
|
||||
if stat, ok := st.Sys().(*syscall.Stat_t); !ok {
|
||||
return false, fmt.Errorf("incompatible types after stat call: *syscall.Stat_t expected")
|
||||
} else if int(stat.Uid) != uidMap.HostID || int(stat.Gid) != gidMap.HostID {
|
||||
return false, fmt.Errorf("bad mapping: expected {uid: %d, gid: %d}; real {uid: %d, gid: %d}", uidMap.HostID, gidMap.HostID, int(stat.Uid), int(stat.Gid))
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
86
plugins/snapshots/overlay/overlayutils/check_test.go
Normal file
86
plugins/snapshots/overlay/overlayutils/check_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package overlayutils
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
"github.com/containerd/continuity/testutil/loopback"
|
||||
)
|
||||
|
||||
func testOverlaySupported(t testing.TB, expected bool, mkfs ...string) {
|
||||
testutil.RequiresRoot(t)
|
||||
mnt := t.TempDir()
|
||||
|
||||
loop, err := loopback.New(100 << 20) // 100 MB
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out, err := exec.Command(mkfs[0], append(mkfs[1:], loop.Device)...).CombinedOutput(); err != nil {
|
||||
// not fatal
|
||||
loop.Close()
|
||||
t.Skipf("could not mkfs (%v) %s: %v (out: %q)", mkfs, loop.Device, err, string(out))
|
||||
}
|
||||
if out, err := exec.Command("mount", loop.Device, mnt).CombinedOutput(); err != nil {
|
||||
// not fatal
|
||||
loop.Close()
|
||||
t.Skipf("could not mount %s: %v (out: %q)", loop.Device, err, string(out))
|
||||
}
|
||||
defer func() {
|
||||
testutil.Unmount(t, mnt)
|
||||
loop.Close()
|
||||
}()
|
||||
workload := func() {
|
||||
err = Supported(mnt)
|
||||
if expected && err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !expected && err == nil {
|
||||
t.Fatal("error is expected")
|
||||
}
|
||||
}
|
||||
b, ok := t.(*testing.B)
|
||||
if ok {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
workload()
|
||||
}
|
||||
b.StopTimer()
|
||||
} else {
|
||||
workload()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOverlaySupportedOnExt4(b *testing.B) {
|
||||
testOverlaySupported(b, true, "mkfs.ext4", "-F")
|
||||
}
|
||||
|
||||
func BenchmarkOverlayUnsupportedOnFType0XFS(b *testing.B) {
|
||||
testOverlaySupported(b, false, "mkfs.xfs", "-m", "crc=0", "-n", "ftype=0")
|
||||
}
|
||||
|
||||
func BenchmarkOverlaySupportedOnFType1XFS(b *testing.B) {
|
||||
testOverlaySupported(b, true, "mkfs.xfs", "-m", "crc=0", "-n", "ftype=1")
|
||||
}
|
||||
|
||||
func BenchmarkOverlayUnsupportedOnFAT(b *testing.B) {
|
||||
testOverlaySupported(b, false, "mkfs.fat")
|
||||
}
|
||||
99
plugins/snapshots/overlay/plugin/plugin.go
Normal file
99
plugins/snapshots/overlay/plugin/plugin.go
Normal file
@@ -0,0 +1,99 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/containerd/containerd/v2/platforms"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
"github.com/containerd/containerd/v2/plugins/snapshots/overlay"
|
||||
"github.com/containerd/containerd/v2/plugins/snapshots/overlay/overlayutils"
|
||||
"github.com/containerd/plugin"
|
||||
"github.com/containerd/plugin/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
capaRemapIds = "remap-ids"
|
||||
capaOnlyRemapIds = "only-remap-ids"
|
||||
)
|
||||
|
||||
// Config represents configuration for the overlay plugin.
|
||||
type Config struct {
|
||||
// Root directory for the plugin
|
||||
RootPath string `toml:"root_path"`
|
||||
UpperdirLabel bool `toml:"upperdir_label"`
|
||||
SyncRemove bool `toml:"sync_remove"`
|
||||
|
||||
// slowChown allows the plugin to fallback to a recursive chown if fast options (like
|
||||
// idmap mounts) are not available. See more info about the overhead this can have in
|
||||
// github.com/containerd/containerd/docs/user-namespaces/.
|
||||
SlowChown bool `toml:"slow_chown"`
|
||||
|
||||
// MountOptions are options used for the overlay mount (not used on bind mounts)
|
||||
MountOptions []string `toml:"mount_options"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Register(&plugin.Registration{
|
||||
Type: plugins.SnapshotPlugin,
|
||||
ID: "overlayfs",
|
||||
Config: &Config{},
|
||||
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
|
||||
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
|
||||
|
||||
config, ok := ic.Config.(*Config)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid overlay configuration")
|
||||
}
|
||||
|
||||
root := ic.Properties[plugins.PropertyRootDir]
|
||||
if config.RootPath != "" {
|
||||
root = config.RootPath
|
||||
}
|
||||
|
||||
var oOpts []overlay.Opt
|
||||
if config.UpperdirLabel {
|
||||
oOpts = append(oOpts, overlay.WithUpperdirLabel)
|
||||
}
|
||||
if !config.SyncRemove {
|
||||
oOpts = append(oOpts, overlay.AsynchronousRemove)
|
||||
}
|
||||
|
||||
if len(config.MountOptions) > 0 {
|
||||
oOpts = append(oOpts, overlay.WithMountOptions(config.MountOptions))
|
||||
}
|
||||
if ok, err := overlayutils.SupportsIDMappedMounts(); err == nil && ok {
|
||||
oOpts = append(oOpts, overlay.WithRemapIds)
|
||||
ic.Meta.Capabilities = append(ic.Meta.Capabilities, capaRemapIds)
|
||||
}
|
||||
|
||||
if config.SlowChown {
|
||||
oOpts = append(oOpts, overlay.WithSlowChown)
|
||||
} else {
|
||||
// If slowChown is false, we use capaOnlyRemapIds to signal we only
|
||||
// allow idmap mounts.
|
||||
ic.Meta.Capabilities = append(ic.Meta.Capabilities, capaOnlyRemapIds)
|
||||
}
|
||||
|
||||
ic.Meta.Exports["root"] = root
|
||||
return overlay.NewSnapshotter(root, oOpts...)
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user