Before this change, volume management was relying on the fact that everything the plugin mounts is visible on the host within the plugin's rootfs. In practice this caused some issues with mount leaks, so we changed the behavior such that mounts are not visible on the plugin's rootfs, but available outside of it, which breaks volume management. To fix the issue, allow the plugin to scope the path correctly rather than assuming that everything is visible in `p.Rootfs`. In practice this is just scoping the `PropagatedMount` paths to the correct host path. Signed-off-by: Brian Goff <cpuguy83@gmail.com> Upstream-commit: 0e5eaf8ee32662182147f5f62c1bfebef66f5c47 Component: engine
226 lines
6.2 KiB
Go
226 lines
6.2 KiB
Go
package testutils
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/docker/docker/pkg/plugingetter"
|
|
"github.com/docker/docker/pkg/plugins"
|
|
"github.com/docker/docker/volume"
|
|
)
|
|
|
|
// NoopVolume is a volume that doesn't perform any operation
|
|
type NoopVolume struct{}
|
|
|
|
// Name is the name of the volume
|
|
func (NoopVolume) Name() string { return "noop" }
|
|
|
|
// DriverName is the name of the driver
|
|
func (NoopVolume) DriverName() string { return "noop" }
|
|
|
|
// Path is the filesystem path to the volume
|
|
func (NoopVolume) Path() string { return "noop" }
|
|
|
|
// Mount mounts the volume in the container
|
|
func (NoopVolume) Mount(_ string) (string, error) { return "noop", nil }
|
|
|
|
// Unmount unmounts the volume from the container
|
|
func (NoopVolume) Unmount(_ string) error { return nil }
|
|
|
|
// Status provides low-level details about the volume
|
|
func (NoopVolume) Status() map[string]interface{} { return nil }
|
|
|
|
// CreatedAt provides the time the volume (directory) was created at
|
|
func (NoopVolume) CreatedAt() (time.Time, error) { return time.Now(), nil }
|
|
|
|
// FakeVolume is a fake volume with a random name
|
|
type FakeVolume struct {
|
|
name string
|
|
driverName string
|
|
}
|
|
|
|
// NewFakeVolume creates a new fake volume for testing
|
|
func NewFakeVolume(name string, driverName string) volume.Volume {
|
|
return FakeVolume{name: name, driverName: driverName}
|
|
}
|
|
|
|
// Name is the name of the volume
|
|
func (f FakeVolume) Name() string { return f.name }
|
|
|
|
// DriverName is the name of the driver
|
|
func (f FakeVolume) DriverName() string { return f.driverName }
|
|
|
|
// Path is the filesystem path to the volume
|
|
func (FakeVolume) Path() string { return "fake" }
|
|
|
|
// Mount mounts the volume in the container
|
|
func (FakeVolume) Mount(_ string) (string, error) { return "fake", nil }
|
|
|
|
// Unmount unmounts the volume from the container
|
|
func (FakeVolume) Unmount(_ string) error { return nil }
|
|
|
|
// Status provides low-level details about the volume
|
|
func (FakeVolume) Status() map[string]interface{} { return nil }
|
|
|
|
// CreatedAt provides the time the volume (directory) was created at
|
|
func (FakeVolume) CreatedAt() (time.Time, error) { return time.Now(), nil }
|
|
|
|
// FakeDriver is a driver that generates fake volumes
|
|
type FakeDriver struct {
|
|
name string
|
|
vols map[string]volume.Volume
|
|
}
|
|
|
|
// NewFakeDriver creates a new FakeDriver with the specified name
|
|
func NewFakeDriver(name string) volume.Driver {
|
|
return &FakeDriver{
|
|
name: name,
|
|
vols: make(map[string]volume.Volume),
|
|
}
|
|
}
|
|
|
|
// Name is the name of the driver
|
|
func (d *FakeDriver) Name() string { return d.name }
|
|
|
|
// Create initializes a fake volume.
|
|
// It returns an error if the options include an "error" key with a message
|
|
func (d *FakeDriver) Create(name string, opts map[string]string) (volume.Volume, error) {
|
|
if opts != nil && opts["error"] != "" {
|
|
return nil, fmt.Errorf(opts["error"])
|
|
}
|
|
v := NewFakeVolume(name, d.name)
|
|
d.vols[name] = v
|
|
return v, nil
|
|
}
|
|
|
|
// Remove deletes a volume.
|
|
func (d *FakeDriver) Remove(v volume.Volume) error {
|
|
if _, exists := d.vols[v.Name()]; !exists {
|
|
return fmt.Errorf("no such volume")
|
|
}
|
|
delete(d.vols, v.Name())
|
|
return nil
|
|
}
|
|
|
|
// List lists the volumes
|
|
func (d *FakeDriver) List() ([]volume.Volume, error) {
|
|
var vols []volume.Volume
|
|
for _, v := range d.vols {
|
|
vols = append(vols, v)
|
|
}
|
|
return vols, nil
|
|
}
|
|
|
|
// Get gets the volume
|
|
func (d *FakeDriver) Get(name string) (volume.Volume, error) {
|
|
if v, exists := d.vols[name]; exists {
|
|
return v, nil
|
|
}
|
|
return nil, fmt.Errorf("no such volume")
|
|
}
|
|
|
|
// Scope returns the local scope
|
|
func (*FakeDriver) Scope() string {
|
|
return "local"
|
|
}
|
|
|
|
type fakePlugin struct {
|
|
client *plugins.Client
|
|
name string
|
|
refs int
|
|
}
|
|
|
|
// MakeFakePlugin creates a fake plugin from the passed in driver
|
|
// Note: currently only "Create" is implemented because that's all that's needed
|
|
// so far. If you need it to test something else, add it here, but probably you
|
|
// shouldn't need to use this except for very specific cases with v2 plugin handling.
|
|
func MakeFakePlugin(d volume.Driver, l net.Listener) (plugingetter.CompatPlugin, error) {
|
|
c, err := plugins.NewClient(l.Addr().Network()+"://"+l.Addr().String(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("/VolumeDriver.Create", func(w http.ResponseWriter, r *http.Request) {
|
|
createReq := struct {
|
|
Name string
|
|
Opts map[string]string
|
|
}{}
|
|
if err := json.NewDecoder(r.Body).Decode(&createReq); err != nil {
|
|
fmt.Fprintf(w, `{"Err": "%s"}`, err.Error())
|
|
return
|
|
}
|
|
_, err := d.Create(createReq.Name, createReq.Opts)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `{"Err": "%s"}`, err.Error())
|
|
return
|
|
}
|
|
w.Write([]byte("{}"))
|
|
})
|
|
|
|
go http.Serve(l, mux)
|
|
return &fakePlugin{client: c, name: d.Name()}, nil
|
|
}
|
|
|
|
func (p *fakePlugin) Client() *plugins.Client {
|
|
return p.client
|
|
}
|
|
|
|
func (p *fakePlugin) Name() string {
|
|
return p.name
|
|
}
|
|
|
|
func (p *fakePlugin) IsV1() bool {
|
|
return false
|
|
}
|
|
|
|
func (p *fakePlugin) ScopedPath(s string) string {
|
|
return s
|
|
}
|
|
|
|
type fakePluginGetter struct {
|
|
plugins map[string]plugingetter.CompatPlugin
|
|
}
|
|
|
|
// NewFakePluginGetter returns a plugin getter for fake plugins
|
|
func NewFakePluginGetter(pls ...plugingetter.CompatPlugin) plugingetter.PluginGetter {
|
|
idx := make(map[string]plugingetter.CompatPlugin, len(pls))
|
|
for _, p := range pls {
|
|
idx[p.Name()] = p
|
|
}
|
|
return &fakePluginGetter{plugins: idx}
|
|
}
|
|
|
|
// This ignores the second argument since we only care about volume drivers here,
|
|
// there shouldn't be any other kind of plugin in here
|
|
func (g *fakePluginGetter) Get(name, _ string, mode int) (plugingetter.CompatPlugin, error) {
|
|
p, ok := g.plugins[name]
|
|
if !ok {
|
|
return nil, errors.New("not found")
|
|
}
|
|
p.(*fakePlugin).refs += mode
|
|
return p, nil
|
|
}
|
|
|
|
func (g *fakePluginGetter) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, error) {
|
|
panic("GetAllByCap shouldn't be called")
|
|
}
|
|
|
|
func (g *fakePluginGetter) GetAllManagedPluginsByCap(capability string) []plugingetter.CompatPlugin {
|
|
panic("GetAllManagedPluginsByCap should not be called")
|
|
}
|
|
|
|
func (g *fakePluginGetter) Handle(capability string, callback func(string, *plugins.Client)) {
|
|
panic("Handle should not be called")
|
|
}
|
|
|
|
// FakeRefs checks ref count on a fake plugin.
|
|
func FakeRefs(p plugingetter.CompatPlugin) int {
|
|
// this should panic if something other than a `*fakePlugin` is passed in
|
|
return p.(*fakePlugin).refs
|
|
}
|