Move snapshots/overlay to plugins/snapshots/overlay

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2024-01-17 09:53:51 -08:00
parent 9b8c558f9f
commit 57bdbfba6a
11 changed files with 10 additions and 10 deletions

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

View 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\"")
}
}

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

View 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")
}

View 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...)
},
})
}