Move snapshots/btrfs to plugins/snapshots/btrfs

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2024-01-17 09:53:15 -08:00
parent 2909f07f85
commit 7dd96fe346
4 changed files with 2 additions and 2 deletions

View File

@@ -0,0 +1,383 @@
//go:build linux && !no_btrfs && cgo
/*
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 btrfs
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containerd/btrfs/v2"
"github.com/containerd/continuity/fs"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/snapshots"
"github.com/containerd/containerd/v2/snapshots/storage"
"github.com/containerd/log"
"github.com/containerd/plugin"
)
type snapshotter struct {
device string // device of the root
root string // root provides paths for internal storage.
ms *storage.MetaStore
}
// NewSnapshotter returns a Snapshotter using btrfs. Uses the provided
// root directory for snapshots and stores the metadata in
// a file in the provided root.
// root needs to be a mount point of btrfs.
func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
// If directory does not exist, create it
if st, err := os.Stat(root); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err := os.Mkdir(root, 0700); err != nil {
return nil, err
}
} else if st.Mode()&os.ModePerm != 0700 {
if err := os.Chmod(root, 0700); err != nil {
return nil, err
}
}
mnt, err := mount.Lookup(root)
if err != nil {
return nil, err
}
if mnt.FSType != "btrfs" {
return nil, fmt.Errorf("path %s (%s) must be a btrfs filesystem to be used with the btrfs snapshotter: %w", root, mnt.FSType, plugin.ErrSkipPlugin)
}
var (
active = filepath.Join(root, "active")
view = filepath.Join(root, "view")
snapshots = filepath.Join(root, "snapshots")
)
for _, path := range []string{
active,
view,
snapshots,
} {
if err := os.Mkdir(path, 0755); err != nil && !os.IsExist(err) {
return nil, err
}
}
ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
if err != nil {
return nil, err
}
return &snapshotter{
device: mnt.Source,
root: root,
ms: ms,
}, nil
}
// 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 (b *snapshotter) Stat(ctx context.Context, key string) (info snapshots.Info, err error) {
err = b.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
_, info, _, err = storage.GetInfo(ctx, key)
return err
})
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (b *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (_ snapshots.Info, err error) {
err = b.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
return err
})
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
// Usage retrieves the disk usage of the top-level snapshot.
func (b *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
return b.usage(ctx, key)
}
func (b *snapshotter) usage(ctx context.Context, key string) (usage snapshots.Usage, err error) {
var (
id, parentID string
info snapshots.Info
)
err = b.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
id, info, usage, err = storage.GetInfo(ctx, key)
if err == nil && info.Kind == snapshots.KindActive && info.Parent != "" {
parentID, _, _, err = storage.GetInfo(ctx, info.Parent)
}
return err
})
if err != nil {
return snapshots.Usage{}, err
}
if info.Kind == snapshots.KindActive {
var du fs.Usage
p := filepath.Join(b.root, "active", id)
if parentID != "" {
du, err = fs.DiffUsage(ctx, filepath.Join(b.root, "snapshots", parentID), p)
} else {
du, err = fs.DiskUsage(ctx, p)
}
if err != nil {
// TODO(stevvooe): Consider not reporting an error in this case.
return snapshots.Usage{}, err
}
usage = snapshots.Usage(du)
}
return usage, nil
}
// Walk the committed snapshots.
func (b *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) (err error) {
return b.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
return storage.WalkInfo(ctx, fn, fs...)
})
}
func (b *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return b.makeSnapshot(ctx, snapshots.KindActive, key, parent, opts)
}
func (b *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return b.makeSnapshot(ctx, snapshots.KindView, key, parent, opts)
}
func (b *snapshotter) makeSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ []mount.Mount, err error) {
var (
target string
s storage.Snapshot
)
err = b.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
s, err = storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
return err
}
target = filepath.Join(b.root, strings.ToLower(s.Kind.String()), s.ID)
if len(s.ParentIDs) == 0 {
// create new subvolume
// btrfs subvolume create /dir
return btrfs.SubvolCreate(target)
}
parentp := filepath.Join(b.root, "snapshots", s.ParentIDs[0])
// btrfs subvolume snapshot /parent /subvol
readOnly := kind == snapshots.KindView
return btrfs.SubvolSnapshot(target, parentp, readOnly)
})
if err != nil {
if target != "" {
if derr := btrfs.SubvolDelete(target); derr != nil {
log.G(ctx).WithError(derr).WithField("subvolume", target).Error("failed to delete subvolume")
}
}
return nil, err
}
return b.mounts(target, s)
}
func (b *snapshotter) mounts(dir string, s storage.Snapshot) ([]mount.Mount, error) {
var options []string
// get the subvolume id back out for the mount
sid, err := btrfs.SubvolID(dir)
if err != nil {
return nil, err
}
options = append(options, fmt.Sprintf("subvolid=%d", sid))
if s.Kind != snapshots.KindActive {
options = append(options, "ro")
}
return []mount.Mount{
{
Type: "btrfs",
Source: b.device,
// NOTE(stevvooe): While it would be nice to use to uuids for
// mounts, they don't work reliably if the uuids are missing.
Options: options,
},
}, nil
}
func (b *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) (err error) {
var usage snapshots.Usage
usage, err = b.usage(ctx, key)
if err != nil {
return fmt.Errorf("failed to compute usage: %w", err)
}
var source, target string
err = b.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
id, err := storage.CommitActive(ctx, key, name, usage, opts...) // TODO(stevvooe): Resolve a usage value for btrfs
if err != nil {
return fmt.Errorf("failed to commit: %w", err)
}
source = filepath.Join(b.root, "active", id)
target = filepath.Join(b.root, "snapshots", id)
return btrfs.SubvolSnapshot(target, source, true)
})
if err != nil {
if target != "" {
if derr := btrfs.SubvolDelete(target); derr != nil {
log.G(ctx).WithError(derr).WithField("subvolume", target).Error("failed to delete subvolume")
}
}
return err
}
if source != "" {
if derr := btrfs.SubvolDelete(source); derr != nil {
// Log as warning, only needed for cleanup, will not cause name collision
log.G(ctx).WithError(derr).WithField("subvolume", source).Warn("failed to delete subvolume")
}
}
return nil
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (b *snapshotter) Mounts(ctx context.Context, key string) (_ []mount.Mount, err error) {
var s storage.Snapshot
err = b.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
s, err = storage.GetSnapshot(ctx, key)
return err
})
if err != nil {
return nil, fmt.Errorf("failed to get active snapshot: %w", err)
}
dir := filepath.Join(b.root, strings.ToLower(s.Kind.String()), s.ID)
return b.mounts(dir, s)
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
func (b *snapshotter) Remove(ctx context.Context, key string) (err error) {
var (
source, removed string
readonly, restore bool
)
defer func() {
if removed != "" {
if derr := btrfs.SubvolDelete(removed); derr != nil {
log.G(ctx).WithError(derr).WithField("subvolume", removed).Warn("failed to delete subvolume")
}
}
}()
err = b.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
id, k, err := storage.Remove(ctx, key)
if err != nil {
return fmt.Errorf("failed to remove snapshot: %w", err)
}
switch k {
case snapshots.KindView:
source = filepath.Join(b.root, "view", id)
removed = filepath.Join(b.root, "view", "rm-"+id)
readonly = true
case snapshots.KindActive:
source = filepath.Join(b.root, "active", id)
removed = filepath.Join(b.root, "active", "rm-"+id)
case snapshots.KindCommitted:
source = filepath.Join(b.root, "snapshots", id)
removed = filepath.Join(b.root, "snapshots", "rm-"+id)
readonly = true
}
if err = btrfs.SubvolSnapshot(removed, source, readonly); err != nil {
removed = ""
return err
}
if err = btrfs.SubvolDelete(source); err != nil {
return fmt.Errorf("failed to remove snapshot %v: %w", source, err)
}
restore = true
return nil
})
if err != nil {
if restore { // means failed to commit transaction
// Attempt to restore source
if err1 := btrfs.SubvolSnapshot(source, removed, readonly); err1 != nil {
log.G(ctx).WithFields(log.Fields{
"error": err1,
"subvolume": source,
"renamed": removed,
}).Error("failed to restore subvolume from renamed")
// Keep removed to allow for manual restore
removed = ""
}
}
return err
}
return nil
}
// Close closes the snapshotter
func (b *snapshotter) Close() error {
return b.ms.Close()
}

View File

@@ -0,0 +1,200 @@
//go:build linux && !no_btrfs && cgo
/*
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 btrfs
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/pkg/testutil"
"github.com/containerd/containerd/v2/snapshots"
"github.com/containerd/containerd/v2/snapshots/testsuite"
"github.com/containerd/continuity/testutil/loopback"
"github.com/containerd/plugin"
"golang.org/x/sys/unix"
)
func boltSnapshotter(t *testing.T) func(context.Context, string) (snapshots.Snapshotter, func() error, error) {
mkbtrfs, err := exec.LookPath("mkfs.btrfs")
if err != nil {
t.Skipf("could not find mkfs.btrfs: %v", err)
}
procModules, err := os.ReadFile("/proc/modules")
if err == nil && !bytes.Contains(procModules, []byte("btrfs")) {
t.Skip("check for btrfs kernel module failed, skipping test")
}
return func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
loopbackSize := int64(128 << 20) // 128 MB
// mkfs.btrfs creates a fs which has a blocksize equal to the system default pagesize. If that pagesize
// is > 4KB, mounting the fs will fail unless we increase the size of the file used by mkfs.btrfs
if os.Getpagesize() > 4096 {
loopbackSize = int64(650 << 20) // 650 MB
}
loop, err := loopback.New(loopbackSize)
if err != nil {
return nil, nil, err
}
if out, err := exec.Command(mkbtrfs, loop.Device).CombinedOutput(); err != nil {
loop.Close()
return nil, nil, fmt.Errorf("failed to make btrfs filesystem (out: %q): %w", out, err)
}
// sync after a mkfs on the loopback before trying to mount the device
unix.Sync()
var snapshotter snapshots.Snapshotter
for i := 0; i < 5; i++ {
if out, err := exec.Command("mount", loop.Device, root).CombinedOutput(); err != nil {
loop.Close()
return nil, nil, fmt.Errorf("failed to mount device %s (out: %q): %w", loop.Device, out, err)
}
if i > 0 {
time.Sleep(10 * time.Duration(i) * time.Millisecond)
}
snapshotter, err = NewSnapshotter(root)
if err == nil {
break
} else if !errors.Is(err, plugin.ErrSkipPlugin) {
return nil, nil, err
}
t.Logf("Attempt %d to create btrfs snapshotter failed: %#v", i+1, err)
// unmount and try again
unix.Unmount(root, 0)
}
if snapshotter == nil {
return nil, nil, fmt.Errorf("failed to successfully create snapshotter after 5 attempts: %w", err)
}
return snapshotter, func() error {
if err := snapshotter.Close(); err != nil {
return err
}
err := mount.UnmountAll(root, 0)
if cerr := loop.Close(); cerr != nil {
err = fmt.Errorf("device cleanup failed: %w", cerr)
}
return err
}, nil
}
}
func TestBtrfs(t *testing.T) {
testutil.RequiresRoot(t)
testsuite.SnapshotterSuite(t, "Btrfs", boltSnapshotter(t))
}
func TestBtrfsMounts(t *testing.T) {
testutil.RequiresRoot(t)
ctx := context.Background()
root := t.TempDir()
b, c, err := boltSnapshotter(t)(ctx, root)
if err != nil {
t.Fatal(err)
}
defer c()
target := filepath.Join(root, "test")
mounts, err := b.Prepare(ctx, target, "")
if err != nil {
t.Fatal(err)
}
t.Log(mounts)
for _, mount := range mounts {
if mount.Type != "btrfs" {
t.Fatalf("wrong mount type: %v != btrfs", mount.Type)
}
// assumes the first, maybe incorrect in the future
if !strings.HasPrefix(mount.Options[0], "subvolid=") {
t.Fatalf("no subvolid option in %v", mount.Options)
}
}
if err := os.MkdirAll(target, 0o755); err != nil {
t.Fatal(err)
}
if err := mount.All(mounts, target); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, target)
// write in some data
if err := os.WriteFile(filepath.Join(target, "foo"), []byte("content"), 0o777); err != nil {
t.Fatal(err)
}
// TODO(stevvooe): We don't really make this with the driver, but that
// might prove annoying in practice.
if err := os.MkdirAll(filepath.Join(root, "snapshots"), 0o755); err != nil {
t.Fatal(err)
}
if err := b.Commit(ctx, filepath.Join(root, "snapshots/committed"), filepath.Join(root, "test")); err != nil {
t.Fatal(err)
}
target = filepath.Join(root, "test2")
mounts, err = b.Prepare(ctx, target, filepath.Join(root, "snapshots/committed"))
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(target, 0o755); err != nil {
t.Fatal(err)
}
if err := mount.All(mounts, target); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, target)
bs, err := os.ReadFile(filepath.Join(target, "foo"))
if err != nil {
t.Fatal(err)
}
if string(bs) != "content" {
t.Fatalf("wrong content in foo want: content, got: %s", bs)
}
if err := os.WriteFile(filepath.Join(target, "bar"), []byte("content"), 0o777); err != nil {
t.Fatal(err)
}
if err := b.Commit(ctx, filepath.Join(root, "snapshots/committed2"), target); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,61 @@
//go:build linux && !no_btrfs && cgo
/*
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 plugin
import (
"errors"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/containerd/containerd/v2/platforms"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/snapshots/btrfs"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
// Config represents configuration for the btrfs plugin.
type Config struct {
// Root directory for the plugin
RootPath string `toml:"root_path"`
}
func init() {
registry.Register(&plugin.Registration{
ID: "btrfs",
Type: plugins.SnapshotPlugin,
Config: &Config{},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = []ocispec.Platform{platforms.DefaultSpec()}
config, ok := ic.Config.(*Config)
if !ok {
return nil, errors.New("invalid btrfs configuration")
}
root := ic.Properties[plugins.PropertyRootDir]
if len(config.RootPath) != 0 {
root = config.RootPath
}
ic.Meta.Exports = map[string]string{"root": root}
return btrfs.NewSnapshotter(root)
},
})
}