Update existing snapshot drivers to register as plugins. Load snapshot driver at containerd startup. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
		
			
				
	
	
		
			359 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			359 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package btrfs
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/sha256"
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/docker/containerd"
 | 
						|
	"github.com/docker/containerd/plugin"
 | 
						|
	"github.com/docker/containerd/snapshot"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"github.com/stevvooe/go-btrfs"
 | 
						|
)
 | 
						|
 | 
						|
type btrfsConfig struct {
 | 
						|
	Device string `toml:"device"`
 | 
						|
}
 | 
						|
 | 
						|
func init() {
 | 
						|
	plugin.Register("snapshot-btrfs", &plugin.Registration{
 | 
						|
		Type:   plugin.SnapshotPlugin,
 | 
						|
		Config: &btrfsConfig{},
 | 
						|
		Init: func(ic *plugin.InitContext) (interface{}, error) {
 | 
						|
			conf := ic.Config.(*btrfsConfig)
 | 
						|
			if conf.Device == "" {
 | 
						|
				return nil, errors.Errorf("btrfs requires \"device\" configuration")
 | 
						|
			}
 | 
						|
			return NewSnapshotter(conf.Device, filepath.Join(ic.Root, "snapshot", "btrfs"))
 | 
						|
		},
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
type Snapshotter struct {
 | 
						|
	device string // maybe we can resolve it with path?
 | 
						|
	root   string // root provides paths for internal storage.
 | 
						|
}
 | 
						|
 | 
						|
func NewSnapshotter(device, root string) (snapshot.Snapshotter, error) {
 | 
						|
	var (
 | 
						|
		active    = filepath.Join(root, "active")
 | 
						|
		snapshots = filepath.Join(root, "snapshots")
 | 
						|
		parents   = filepath.Join(root, "parents")
 | 
						|
		index     = filepath.Join(root, "index")
 | 
						|
		names     = filepath.Join(root, "names")
 | 
						|
	)
 | 
						|
 | 
						|
	for _, path := range []string{
 | 
						|
		active,
 | 
						|
		snapshots,
 | 
						|
		parents,
 | 
						|
		index,
 | 
						|
		names,
 | 
						|
	} {
 | 
						|
		if err := os.MkdirAll(path, 0755); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return &Snapshotter{device: device, root: root}, 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) (snapshot.Info, error) {
 | 
						|
	// resolve the snapshot out of the index.
 | 
						|
	target, err := os.Readlink(filepath.Join(b.root, "index", hash(key)))
 | 
						|
	if err != nil {
 | 
						|
		if !os.IsNotExist(err) {
 | 
						|
			return snapshot.Info{}, err
 | 
						|
		}
 | 
						|
 | 
						|
		return snapshot.Info{}, errors.Errorf("snapshot %v not found", key)
 | 
						|
	}
 | 
						|
 | 
						|
	return b.stat(target)
 | 
						|
}
 | 
						|
 | 
						|
func (b *Snapshotter) stat(target string) (snapshot.Info, error) {
 | 
						|
	var (
 | 
						|
		parents    = filepath.Join(b.root, "parents")
 | 
						|
		names      = filepath.Join(b.root, "names")
 | 
						|
		namep      = filepath.Join(names, filepath.Base(target))
 | 
						|
		parentlink = filepath.Join(parents, filepath.Base(target))
 | 
						|
	)
 | 
						|
 | 
						|
	// grab information about the subvolume
 | 
						|
	info, err := btrfs.SubvolInfo(target)
 | 
						|
	if err != nil {
 | 
						|
		return snapshot.Info{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	// read the name out of the names!
 | 
						|
	nameraw, err := ioutil.ReadFile(namep)
 | 
						|
	if err != nil {
 | 
						|
		return snapshot.Info{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	// resolve the parents path.
 | 
						|
	parentp, err := os.Readlink(parentlink)
 | 
						|
	if err != nil {
 | 
						|
		if !os.IsNotExist(err) {
 | 
						|
			return snapshot.Info{}, err
 | 
						|
		}
 | 
						|
 | 
						|
		// no parent!
 | 
						|
	}
 | 
						|
 | 
						|
	var parent string
 | 
						|
	if parentp != "" {
 | 
						|
		// okay, grab the basename of the parent and look up its name!
 | 
						|
		parentnamep := filepath.Join(names, filepath.Base(parentp))
 | 
						|
 | 
						|
		p, err := ioutil.ReadFile(parentnamep)
 | 
						|
		if err != nil {
 | 
						|
			return snapshot.Info{}, err
 | 
						|
		}
 | 
						|
 | 
						|
		parent = string(p)
 | 
						|
	}
 | 
						|
 | 
						|
	kind := snapshot.KindCommitted
 | 
						|
	if strings.HasPrefix(target, filepath.Join(b.root, "active")) {
 | 
						|
		kind = snapshot.KindActive
 | 
						|
	}
 | 
						|
 | 
						|
	return snapshot.Info{
 | 
						|
		Name:     string(nameraw),
 | 
						|
		Parent:   parent,
 | 
						|
		Readonly: info.Readonly,
 | 
						|
		Kind:     kind,
 | 
						|
	}, nil
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func (b *Snapshotter) Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error) {
 | 
						|
	return b.makeActive(key, parent, false)
 | 
						|
}
 | 
						|
 | 
						|
func (b *Snapshotter) View(ctx context.Context, key, parent string) ([]containerd.Mount, error) {
 | 
						|
	return b.makeActive(key, parent, true)
 | 
						|
}
 | 
						|
 | 
						|
func (b *Snapshotter) makeActive(key, parent string, readonly bool) ([]containerd.Mount, error) {
 | 
						|
	var (
 | 
						|
		active     = filepath.Join(b.root, "active")
 | 
						|
		snapshots  = filepath.Join(b.root, "snapshots")
 | 
						|
		parents    = filepath.Join(b.root, "parents")
 | 
						|
		index      = filepath.Join(b.root, "index")
 | 
						|
		names      = filepath.Join(b.root, "names")
 | 
						|
		keyh       = hash(key)
 | 
						|
		parenth    = hash(parent)
 | 
						|
		target     = filepath.Join(active, keyh)
 | 
						|
		namep      = filepath.Join(names, keyh)
 | 
						|
		indexlink  = filepath.Join(index, keyh)
 | 
						|
		parentlink = filepath.Join(parents, keyh)
 | 
						|
		parentp    = filepath.Join(snapshots, parenth) // parent must be restricted to snaps
 | 
						|
	)
 | 
						|
 | 
						|
	if parent == "" {
 | 
						|
		// create new subvolume
 | 
						|
		// btrfs subvolume create /dir
 | 
						|
		if err := btrfs.SubvolCreate(target); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		// btrfs subvolume snapshot /parent /subvol
 | 
						|
		if err := btrfs.SubvolSnapshot(target, parentp, readonly); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		if err := os.Symlink(parentp, parentlink); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// write in the name
 | 
						|
	if err := ioutil.WriteFile(namep, []byte(key), 0644); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := os.Symlink(target, indexlink); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return b.mounts(target)
 | 
						|
}
 | 
						|
 | 
						|
func (b *Snapshotter) mounts(dir string) ([]containerd.Mount, error) {
 | 
						|
	var options []string
 | 
						|
 | 
						|
	// get the subvolume id back out for the mount
 | 
						|
	info, err := btrfs.SubvolInfo(dir)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	options = append(options, fmt.Sprintf("subvolid=%d", info.ID))
 | 
						|
 | 
						|
	if info.Readonly {
 | 
						|
		options = append(options, "ro")
 | 
						|
	}
 | 
						|
 | 
						|
	return []containerd.Mount{
 | 
						|
		{
 | 
						|
			Type:   "btrfs",
 | 
						|
			Source: b.device, // 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) error {
 | 
						|
	var (
 | 
						|
		active        = filepath.Join(b.root, "active")
 | 
						|
		snapshots     = filepath.Join(b.root, "snapshots")
 | 
						|
		index         = filepath.Join(b.root, "index")
 | 
						|
		parents       = filepath.Join(b.root, "parents")
 | 
						|
		names         = filepath.Join(b.root, "names")
 | 
						|
		keyh          = hash(key)
 | 
						|
		nameh         = hash(name)
 | 
						|
		dir           = filepath.Join(active, keyh)
 | 
						|
		target        = filepath.Join(snapshots, nameh)
 | 
						|
		keynamep      = filepath.Join(names, keyh)
 | 
						|
		namep         = filepath.Join(names, nameh)
 | 
						|
		keyparentlink = filepath.Join(parents, keyh)
 | 
						|
		parentlink    = filepath.Join(parents, nameh)
 | 
						|
		keyindexlink  = filepath.Join(index, keyh)
 | 
						|
		indexlink     = filepath.Join(index, nameh)
 | 
						|
	)
 | 
						|
 | 
						|
	info, err := btrfs.SubvolInfo(dir)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if info.Readonly {
 | 
						|
		return fmt.Errorf("may not commit view snapshot %q", dir)
 | 
						|
	}
 | 
						|
 | 
						|
	// look up the parent information to make sure we have the right parent link.
 | 
						|
	parentp, err := os.Readlink(keyparentlink)
 | 
						|
	if err != nil {
 | 
						|
		if !os.IsNotExist(err) {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// we have no parent!
 | 
						|
	}
 | 
						|
 | 
						|
	if err := os.MkdirAll(snapshots, 0755); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := btrfs.SubvolSnapshot(target, dir, true); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// remove the key name path as we no longer need it.
 | 
						|
	if err := os.Remove(keynamep); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := os.Remove(keyindexlink); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := ioutil.WriteFile(namep, []byte(name), 0755); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if parentp != "" {
 | 
						|
		// move over the parent link into the commit name.
 | 
						|
		if err := os.Rename(keyparentlink, parentlink); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// TODO(stevvooe): For this to not break horribly, we should really
 | 
						|
		// start taking a full lock. We are going to move all this metadata
 | 
						|
		// into common storage, so let's not fret over it for now.
 | 
						|
	}
 | 
						|
 | 
						|
	if err := os.Symlink(target, indexlink); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if err := btrfs.SubvolDelete(dir); err != nil {
 | 
						|
		return errors.Wrapf(err, "delete subvol failed on %v", dir)
 | 
						|
	}
 | 
						|
 | 
						|
	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) ([]containerd.Mount, error) {
 | 
						|
	dir := filepath.Join(b.root, "active", hash(key))
 | 
						|
	return b.mounts(dir)
 | 
						|
}
 | 
						|
 | 
						|
// 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) error {
 | 
						|
	panic("not implemented")
 | 
						|
}
 | 
						|
 | 
						|
// Walk the committed snapshots.
 | 
						|
func (b *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error {
 | 
						|
	// TODO(stevvooe): Copy-pasted almost verbatim from overlay. Really need to
 | 
						|
	// unify the metadata for snapshot implementations.
 | 
						|
	root := filepath.Join(b.root, "index")
 | 
						|
	return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		if path == root {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		if fi.Mode()&os.ModeSymlink == 0 {
 | 
						|
			// only follow links
 | 
						|
			return filepath.SkipDir
 | 
						|
		}
 | 
						|
 | 
						|
		target, err := os.Readlink(path)
 | 
						|
		if err != nil {
 | 
						|
			if !os.IsNotExist(err) {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		si, err := b.stat(target)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		if err := fn(ctx, si); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func hash(k string) string {
 | 
						|
	return fmt.Sprintf("%x", sha256.Sum224([]byte(k)))
 | 
						|
}
 |