Make graphdrivers work with pluginv2.

As part of making graphdrivers support pluginv2, a PluginGetter
interface was necessary for cleaner separation and avoiding import
cycles.

This commit creates a PluginGetter interface and makes pluginStore
implement it. Then the pluginStore object is created in the daemon
(rather than by the plugin manager) and passed to plugin init as
well as to the different subsystems (eg. graphdrivers, volumedrivers).
A side effect of this change was that some code was moved out of
experimental. This is good, since plugin support will be stable soon.

Signed-off-by: Anusha Ragunathan <anusha@docker.com>
This commit is contained in:
Anusha Ragunathan 2016-09-07 17:01:10 -07:00
parent beea098b97
commit fefea805e9
24 changed files with 629 additions and 569 deletions

View file

@ -1,5 +1,3 @@
// +build experimental
package types
import (

8
daemon/daemon.go Executable file → Normal file
View file

@ -47,6 +47,7 @@ import (
"github.com/docker/docker/pkg/sysinfo"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/pkg/truncindex"
pluginstore "github.com/docker/docker/plugin/store"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/docker/runconfig"
@ -94,6 +95,7 @@ type Daemon struct {
gidMaps []idtools.IDMap
layerStore layer.Store
imageStore image.Store
pluginStore *pluginstore.Store
nameIndex *registrar.Registrar
linkIndex *linkIndex
containerd libcontainerd.Client
@ -548,6 +550,9 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot
if driverName == "" {
driverName = config.GraphDriver
}
d.pluginStore = pluginstore.NewStore(config.Root)
d.layerStore, err = layer.NewStoreFromOptions(layer.StoreOptions{
StorePath: config.Root,
MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"),
@ -555,6 +560,7 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot
GraphDriverOptions: config.GraphOptions,
UIDMaps: uidMaps,
GIDMaps: gidMaps,
PluginGetter: d.pluginStore,
})
if err != nil {
return nil, err
@ -911,6 +917,8 @@ func (daemon *Daemon) configureVolumes(rootUID, rootGID int) (*store.VolumeStore
return nil, err
}
volumedrivers.RegisterPluginGetter(daemon.pluginStore)
if !volumedrivers.Register(volumesDriver, volumesDriver.Name()) {
return nil, fmt.Errorf("local volume driver could not be registered")
}

View file

@ -13,7 +13,7 @@ func (daemon *Daemon) verifyExperimentalContainerSettings(hostConfig *container.
}
func pluginInit(d *Daemon, cfg *Config, remote libcontainerd.Remote) error {
return plugin.Init(cfg.Root, remote, d.RegistryService, cfg.LiveRestoreEnabled, d.LogPluginEvent)
return plugin.Init(cfg.Root, d.pluginStore, remote, d.RegistryService, cfg.LiveRestoreEnabled, d.LogPluginEvent)
}
func pluginShutdown() {

View file

@ -12,6 +12,7 @@ import (
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/plugin/getter"
)
// FsMagic unsigned id of the filesystem in use.
@ -134,11 +135,11 @@ func Register(name string, initFunc InitFunc) error {
}
// GetDriver initializes and returns the registered driver
func GetDriver(name, home string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) {
func GetDriver(name, home string, options []string, uidMaps, gidMaps []idtools.IDMap, plugingetter getter.PluginGetter) (Driver, error) {
if initFunc, exists := drivers[name]; exists {
return initFunc(filepath.Join(home, name), options, uidMaps, gidMaps)
}
if pluginDriver, err := lookupPlugin(name, home, options); err == nil {
if pluginDriver, err := lookupPlugin(name, home, options, plugingetter); err == nil {
return pluginDriver, nil
}
logrus.Errorf("Failed to GetDriver graph %s %s", name, home)
@ -155,10 +156,10 @@ func getBuiltinDriver(name, home string, options []string, uidMaps, gidMaps []id
}
// New creates the driver and initializes it at the specified root.
func New(root string, name string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) {
func New(root string, name string, options []string, uidMaps, gidMaps []idtools.IDMap, plugingetter getter.PluginGetter) (Driver, error) {
if name != "" {
logrus.Debugf("[graphdriver] trying provided driver: %s", name) // so the logs show specified driver
return GetDriver(name, root, options, uidMaps, gidMaps)
return GetDriver(name, root, options, uidMaps, gidMaps, plugingetter)
}
// Guess for prior driver

View file

@ -41,7 +41,7 @@ func newDriver(t testing.TB, name string, options []string) *Driver {
t.Fatal(err)
}
d, err := graphdriver.GetDriver(name, root, options, nil, nil)
d, err := graphdriver.GetDriver(name, root, options, nil, nil, nil)
if err != nil {
t.Logf("graphdriver: %v\n", err)
if err == graphdriver.ErrNotSupported || err == graphdriver.ErrPrerequisites || err == graphdriver.ErrIncompatibleFS {

View file

@ -6,7 +6,7 @@ import (
"fmt"
"io"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/plugin/getter"
)
type pluginClient interface {
@ -18,8 +18,8 @@ type pluginClient interface {
SendFile(string, io.Reader, interface{}) error
}
func lookupPlugin(name, home string, opts []string) (Driver, error) {
pl, err := plugins.Get(name, "GraphDriver")
func lookupPlugin(name, home string, opts []string, pluginGetter getter.PluginGetter) (Driver, error) {
pl, err := pluginGetter.Get(name, "GraphDriver", getter.LOOKUP)
if err != nil {
return nil, fmt.Errorf("Error looking up graphdriver plugin %s: %v", name, err)
}

View file

@ -2,6 +2,8 @@
package graphdriver
func lookupPlugin(name, home string, opts []string) (Driver, error) {
import "github.com/docker/docker/plugin/getter"
func lookupPlugin(name, home string, opts []string, plugingetter getter.PluginGetter) (Driver, error) {
return nil, ErrNotSupported
}

View file

@ -14,6 +14,7 @@ import (
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/plugin/getter"
"github.com/vbatts/tar-split/tar/asm"
"github.com/vbatts/tar-split/tar/storage"
)
@ -44,6 +45,7 @@ type StoreOptions struct {
GraphDriverOptions []string
UIDMaps []idtools.IDMap
GIDMaps []idtools.IDMap
PluginGetter getter.PluginGetter
}
// NewStoreFromOptions creates a new Store instance
@ -53,7 +55,8 @@ func NewStoreFromOptions(options StoreOptions) (Store, error) {
options.GraphDriver,
options.GraphDriverOptions,
options.UIDMaps,
options.GIDMaps)
options.GIDMaps,
options.PluginGetter)
if err != nil {
return nil, fmt.Errorf("error initializing graphdriver: %v", err)
}

View file

@ -39,7 +39,7 @@ func newVFSGraphDriver(td string) (graphdriver.Driver, error) {
},
}
return graphdriver.GetDriver("vfs", td, nil, uidMap, gidMap)
return graphdriver.GetDriver("vfs", td, nil, uidMap, gidMap, nil)
}
func newTestGraphDriver(t *testing.T) (graphdriver.Driver, func()) {

View file

@ -83,8 +83,8 @@ func (p *Plugin) Client() *Client {
return p.client
}
// IsLegacy returns true for legacy plugins and false otherwise.
func (p *Plugin) IsLegacy() bool {
// IsV1 returns true for V1 plugins and false otherwise.
func (p *Plugin) IsV1() bool {
return true
}

View file

@ -17,7 +17,6 @@ import (
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/plugin/distribution"
"github.com/docker/docker/plugin/v2"
"github.com/docker/docker/reference"
)
// Disable deactivates a plugin, which implies that they cannot be used by containers.
@ -57,9 +56,9 @@ func (pm *Manager) Inspect(name string) (tp types.Plugin, err error) {
// Pull pulls a plugin and computes the privileges required to install it.
func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.AuthConfig) (types.PluginPrivileges, error) {
ref, err := reference.ParseNamed(name)
ref, err := distribution.GetRef(name)
if err != nil {
logrus.Debugf("error in reference.ParseNamed: %v", err)
logrus.Debugf("error in distribution.GetRef: %v", err)
return nil, err
}
name = ref.String()
@ -76,7 +75,7 @@ func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.A
return nil, err
}
pd, err := distribution.Pull(name, pm.registryService, metaHeader, authConfig)
pd, err := distribution.Pull(ref, pm.registryService, metaHeader, authConfig)
if err != nil {
logrus.Debugf("error in distribution.Pull(): %v", err)
return nil, err
@ -87,10 +86,7 @@ func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.A
return nil, err
}
var tag string
if ref, ok := ref.(reference.NamedTagged); ok {
tag = ref.Tag()
}
tag := distribution.GetTag(ref)
p := v2.NewPlugin(ref.Name(), pluginID, pm.runRoot, tag)
if err := p.InitPlugin(pm.libRoot); err != nil {
return nil, err

View file

@ -62,14 +62,26 @@ func (pd *pullData) Layer() (io.ReadCloser, error) {
return rsc, nil
}
// Pull downloads the plugin from Store
func Pull(name string, rs registry.Service, metaheader http.Header, authConfig *types.AuthConfig) (PullData, error) {
// GetRef returns the distribution reference for a given name.
func GetRef(name string) (reference.Named, error) {
ref, err := reference.ParseNamed(name)
if err != nil {
logrus.Debugf("pull.go: error in ParseNamed: %v", err)
return nil, err
}
return ref, nil
}
// GetTag returns the tag associated with the given reference name.
func GetTag(ref reference.Named) string {
tag := DefaultTag
if ref, ok := ref.(reference.NamedTagged); ok {
tag = ref.Tag()
}
return tag
}
// Pull downloads the plugin from Store
func Pull(ref reference.Named, rs registry.Service, metaheader http.Header, authConfig *types.AuthConfig) (PullData, error) {
repoInfo, err := rs.ResolveRepository(ref)
if err != nil {
logrus.Debugf("pull.go: error in ResolveRepository: %v", err)

View file

@ -0,0 +1,25 @@
package getter
import "github.com/docker/docker/pkg/plugins"
const (
// LOOKUP doesn't update RefCount
LOOKUP = 0
// CREATE increments RefCount
CREATE = 1
// REMOVE decrements RefCount
REMOVE = -1
)
// CompatPlugin is a abstraction to handle both v2(new) and v1(legacy) plugins.
type CompatPlugin interface {
Client() *plugins.Client
Name() string
IsV1() bool
}
// PluginGetter is the interface implemented by Store
type PluginGetter interface {
Get(name, capability string, mode int) (CompatPlugin, error)
GetAllByCap(capability string) ([]CompatPlugin, error)
}

View file

@ -35,7 +35,7 @@ type Manager struct {
sync.RWMutex
libRoot string
runRoot string
pluginStore *store.PluginStore
pluginStore *store.Store
containerdClient libcontainerd.Client
registryService registry.Service
liveRestore bool
@ -50,7 +50,7 @@ func GetManager() *Manager {
// Init (was NewManager) instantiates the singleton Manager.
// TODO: revert this to NewManager once we get rid of all the singletons.
func Init(root string, remote libcontainerd.Remote, rs registry.Service, liveRestore bool, evL eventLogger) (err error) {
func Init(root string, ps *store.Store, remote libcontainerd.Remote, rs registry.Service, liveRestore bool, evL eventLogger) (err error) {
if manager != nil {
return nil
}
@ -59,7 +59,7 @@ func Init(root string, remote libcontainerd.Remote, rs registry.Service, liveRes
manager = &Manager{
libRoot: root,
runRoot: "/run/docker",
pluginStore: store.NewPluginStore(root),
pluginStore: ps,
registryService: rs,
liveRestore: liveRestore,
pluginEventLogger: evL,

31
plugin/store/defs.go Normal file
View file

@ -0,0 +1,31 @@
package store
import (
"path/filepath"
"sync"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/plugin/v2"
)
// Store manages the plugin inventory in memory and on-disk
type Store struct {
sync.RWMutex
plugins map[string]*v2.Plugin
/* handlers are necessary for transition path of legacy plugins
* to the new model. Legacy plugins use Handle() for registering an
* activation callback.*/
handlers map[string]func(string, *plugins.Client)
nameToID map[string]string
plugindb string
}
// NewStore creates a Store.
func NewStore(libRoot string) *Store {
return &Store{
plugins: make(map[string]*v2.Plugin),
handlers: make(map[string]func(string, *plugins.Client)),
nameToID: make(map[string]string),
plugindb: filepath.Join(libRoot, "plugins", "plugins.json"),
}
}

View file

@ -1,19 +0,0 @@
package store
import "github.com/docker/docker/pkg/plugins"
const (
// LOOKUP doesn't update RefCount
LOOKUP = 0
// CREATE increments RefCount
CREATE = 1
// REMOVE decrements RefCount
REMOVE = -1
)
// CompatPlugin is an abstraction to handle both new and legacy (v1) plugins.
type CompatPlugin interface {
Client() *plugins.Client
Name() string
IsLegacy() bool
}

View file

@ -1,25 +0,0 @@
// +build !experimental
package store
import (
"github.com/docker/docker/pkg/plugins"
)
// FindWithCapability returns a list of plugins matching the given capability.
func FindWithCapability(capability string) ([]CompatPlugin, error) {
pl, err := plugins.GetAll(capability)
if err != nil {
return nil, err
}
result := make([]CompatPlugin, len(pl))
for i, p := range pl {
result[i] = p
}
return result, nil
}
// LookupWithCapability returns a plugin matching the given name and capability.
func LookupWithCapability(name, capability string, _ int) (CompatPlugin, error) {
return plugins.Get(name, capability)
}

View file

@ -1,257 +1,26 @@
// +build experimental
// +build !experimental
package store
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/plugin/v2"
"github.com/docker/docker/reference"
"github.com/docker/docker/plugin/getter"
)
var (
store *PluginStore
/* allowV1PluginsFallback determines daemon's support for V1 plugins.
* When the time comes to remove support for V1 plugins, flipping
* this bool is all that will be needed.
*/
allowV1PluginsFallback = true
)
// ErrNotFound indicates that a plugin was not found locally.
type ErrNotFound string
func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) }
// PluginStore manages the plugin inventory in memory and on-disk
type PluginStore struct {
sync.RWMutex
plugins map[string]*v2.Plugin
/* handlers are necessary for transition path of legacy plugins
* to the new model. Legacy plugins use Handle() for registering an
* activation callback.*/
handlers map[string]func(string, *plugins.Client)
nameToID map[string]string
plugindb string
}
// NewPluginStore creates a PluginStore.
func NewPluginStore(libRoot string) *PluginStore {
store = &PluginStore{
plugins: make(map[string]*v2.Plugin),
handlers: make(map[string]func(string, *plugins.Client)),
nameToID: make(map[string]string),
plugindb: filepath.Join(libRoot, "plugins.json"),
}
return store
}
// GetByName retreives a plugin by name.
func (ps *PluginStore) GetByName(name string) (*v2.Plugin, error) {
ps.RLock()
defer ps.RUnlock()
id, nameOk := ps.nameToID[name]
if !nameOk {
return nil, ErrNotFound(name)
}
p, idOk := ps.plugins[id]
if !idOk {
return nil, ErrNotFound(id)
}
return p, nil
}
// GetByID retreives a plugin by ID.
func (ps *PluginStore) GetByID(id string) (*v2.Plugin, error) {
ps.RLock()
defer ps.RUnlock()
p, idOk := ps.plugins[id]
if !idOk {
return nil, ErrNotFound(id)
}
return p, nil
}
// GetAll retreives all plugins.
func (ps *PluginStore) GetAll() map[string]*v2.Plugin {
ps.RLock()
defer ps.RUnlock()
return ps.plugins
}
// SetAll initialized plugins during daemon restore.
func (ps *PluginStore) SetAll(plugins map[string]*v2.Plugin) {
ps.Lock()
defer ps.Unlock()
ps.plugins = plugins
}
func (ps *PluginStore) getByCap(name string, capability string) (*v2.Plugin, error) {
ps.RLock()
defer ps.RUnlock()
p, err := ps.GetByName(name)
// GetAllByCap returns a list of plugins matching the given capability.
func (ps Store) GetAllByCap(capability string) ([]getter.CompatPlugin, error) {
pl, err := plugins.GetAll(capability)
if err != nil {
return nil, err
}
return p.FilterByCap(capability)
}
func (ps *PluginStore) getAllByCap(capability string) []CompatPlugin {
ps.RLock()
defer ps.RUnlock()
result := make([]CompatPlugin, 0, 1)
for _, p := range ps.plugins {
if _, err := p.FilterByCap(capability); err == nil {
result = append(result, p)
}
}
return result
}
// SetState sets the active state of the plugin and updates plugindb.
func (ps *PluginStore) SetState(p *v2.Plugin, state bool) {
ps.Lock()
defer ps.Unlock()
p.PluginObj.Enabled = state
ps.updatePluginDB()
}
// Add adds a plugin to memory and plugindb.
func (ps *PluginStore) Add(p *v2.Plugin) {
ps.Lock()
ps.plugins[p.GetID()] = p
ps.nameToID[p.Name()] = p.GetID()
ps.updatePluginDB()
ps.Unlock()
}
// Remove removes a plugin from memory, plugindb and disk.
func (ps *PluginStore) Remove(p *v2.Plugin) {
ps.Lock()
delete(ps.plugins, p.GetID())
delete(ps.nameToID, p.Name())
ps.updatePluginDB()
p.RemoveFromDisk()
ps.Unlock()
}
// Callers are expected to hold the store lock.
func (ps *PluginStore) updatePluginDB() error {
jsonData, err := json.Marshal(ps.plugins)
if err != nil {
logrus.Debugf("Error in json.Marshal: %v", err)
return err
}
ioutils.AtomicWriteFile(ps.plugindb, jsonData, 0600)
return nil
}
// LookupWithCapability returns a plugin matching the given name and capability.
func LookupWithCapability(name, capability string, mode int) (CompatPlugin, error) {
var (
p *v2.Plugin
err error
)
// Lookup using new model.
if store != nil {
fullName := name
if named, err := reference.ParseNamed(fullName); err == nil { // FIXME: validate
if reference.IsNameOnly(named) {
named = reference.WithDefaultTag(named)
}
ref, ok := named.(reference.NamedTagged)
if !ok {
return nil, fmt.Errorf("invalid name: %s", named.String())
}
fullName = ref.String()
}
p, err = store.GetByName(fullName)
if err == nil {
p.Lock()
p.RefCount += mode
p.Unlock()
return p.FilterByCap(capability)
}
if _, ok := err.(ErrNotFound); !ok {
return nil, err
}
}
// Lookup using legacy model.
if allowV1PluginsFallback {
p, err := plugins.Get(name, capability)
if err != nil {
return nil, fmt.Errorf("legacy plugin: %v", err)
}
return p, nil
}
return nil, err
}
// FindWithCapability returns a list of plugins matching the given capability.
func FindWithCapability(capability string) ([]CompatPlugin, error) {
result := make([]CompatPlugin, 0, 1)
/* Daemon start always calls plugin.Init thereby initializing a store.
* So store on experimental builds can never be nil, even while
* handling legacy plugins. However, there are legacy plugin unit
* tests where the volume subsystem directly talks with the plugin,
* bypassing the daemon. For such tests, this check is necessary.
*/
if store != nil {
store.RLock()
result = store.getAllByCap(capability)
store.RUnlock()
}
// Lookup with legacy model
if allowV1PluginsFallback {
pl, err := plugins.GetAll(capability)
if err != nil {
return nil, fmt.Errorf("legacy plugin: %v", err)
}
for _, p := range pl {
result = append(result, p)
}
result := make([]getter.CompatPlugin, len(pl))
for i, p := range pl {
result[i] = p
}
return result, nil
}
// Handle sets a callback for a given capability. It is only used by network
// and ipam drivers during plugin registration. The callback registers the
// driver with the subsystem (network, ipam).
func Handle(capability string, callback func(string, *plugins.Client)) {
pluginType := fmt.Sprintf("docker.%s/1", strings.ToLower(capability))
// Register callback with new plugin model.
store.handlers[pluginType] = callback
// Register callback with legacy plugin model.
if allowV1PluginsFallback {
plugins.Handle(capability, callback)
}
}
// CallHandler calls the registered callback. It is invoked during plugin enable.
func (ps *PluginStore) CallHandler(p *v2.Plugin) {
for _, typ := range p.GetTypes() {
if handler := ps.handlers[typ.String()]; handler != nil {
handler(p.Name(), p.Client())
}
}
// Get returns a plugin matching the given name and capability.
func (ps Store) Get(name, capability string, _ int) (getter.CompatPlugin, error) {
return plugins.Get(name, capability)
}

View file

@ -0,0 +1,234 @@
// +build experimental
package store
import (
"encoding/json"
"fmt"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/plugin/getter"
"github.com/docker/docker/plugin/v2"
"github.com/docker/docker/reference"
)
/* allowV1PluginsFallback determines daemon's support for V1 plugins.
* When the time comes to remove support for V1 plugins, flipping
* this bool is all that will be needed.
*/
var allowV1PluginsFallback = true
// ErrNotFound indicates that a plugin was not found locally.
type ErrNotFound string
func (name ErrNotFound) Error() string { return fmt.Sprintf("plugin %q not found", string(name)) }
// GetByName retreives a plugin by name.
func (ps *Store) GetByName(name string) (*v2.Plugin, error) {
ps.RLock()
defer ps.RUnlock()
id, nameOk := ps.nameToID[name]
if !nameOk {
return nil, ErrNotFound(name)
}
p, idOk := ps.plugins[id]
if !idOk {
return nil, ErrNotFound(id)
}
return p, nil
}
// GetByID retreives a plugin by ID.
func (ps *Store) GetByID(id string) (*v2.Plugin, error) {
ps.RLock()
defer ps.RUnlock()
p, idOk := ps.plugins[id]
if !idOk {
return nil, ErrNotFound(id)
}
return p, nil
}
// GetAll retreives all plugins.
func (ps *Store) GetAll() map[string]*v2.Plugin {
ps.RLock()
defer ps.RUnlock()
return ps.plugins
}
// SetAll initialized plugins during daemon restore.
func (ps *Store) SetAll(plugins map[string]*v2.Plugin) {
ps.Lock()
defer ps.Unlock()
ps.plugins = plugins
}
func (ps *Store) getByCap(name string, capability string) (*v2.Plugin, error) {
ps.RLock()
defer ps.RUnlock()
p, err := ps.GetByName(name)
if err != nil {
return nil, err
}
return p.FilterByCap(capability)
}
func (ps *Store) getAllByCap(capability string) []getter.CompatPlugin {
ps.RLock()
defer ps.RUnlock()
result := make([]getter.CompatPlugin, 0, 1)
for _, p := range ps.plugins {
if _, err := p.FilterByCap(capability); err == nil {
result = append(result, p)
}
}
return result
}
// SetState sets the active state of the plugin and updates plugindb.
func (ps *Store) SetState(p *v2.Plugin, state bool) {
ps.Lock()
defer ps.Unlock()
p.PluginObj.Enabled = state
ps.updatePluginDB()
}
// Add adds a plugin to memory and plugindb.
func (ps *Store) Add(p *v2.Plugin) {
ps.Lock()
ps.plugins[p.GetID()] = p
ps.nameToID[p.Name()] = p.GetID()
ps.updatePluginDB()
ps.Unlock()
}
// Remove removes a plugin from memory, plugindb and disk.
func (ps *Store) Remove(p *v2.Plugin) {
ps.Lock()
delete(ps.plugins, p.GetID())
delete(ps.nameToID, p.Name())
ps.updatePluginDB()
p.RemoveFromDisk()
ps.Unlock()
}
// Callers are expected to hold the store lock.
func (ps *Store) updatePluginDB() error {
jsonData, err := json.Marshal(ps.plugins)
if err != nil {
logrus.Debugf("Error in json.Marshal: %v", err)
return err
}
ioutils.AtomicWriteFile(ps.plugindb, jsonData, 0600)
return nil
}
// Get returns a plugin matching the given name and capability.
func (ps *Store) Get(name, capability string, mode int) (getter.CompatPlugin, error) {
var (
p *v2.Plugin
err error
)
// Lookup using new model.
if ps != nil {
fullName := name
if named, err := reference.ParseNamed(fullName); err == nil { // FIXME: validate
if reference.IsNameOnly(named) {
named = reference.WithDefaultTag(named)
}
ref, ok := named.(reference.NamedTagged)
if !ok {
return nil, fmt.Errorf("invalid name: %s", named.String())
}
fullName = ref.String()
}
p, err = ps.GetByName(fullName)
if err == nil {
p.Lock()
p.RefCount += mode
p.Unlock()
return p.FilterByCap(capability)
}
if _, ok := err.(ErrNotFound); !ok {
return nil, err
}
}
// Lookup using legacy model.
if allowV1PluginsFallback {
p, err := plugins.Get(name, capability)
if err != nil {
return nil, fmt.Errorf("legacy plugin: %v", err)
}
return p, nil
}
return nil, err
}
// GetAllByCap returns a list of plugins matching the given capability.
func (ps *Store) GetAllByCap(capability string) ([]getter.CompatPlugin, error) {
result := make([]getter.CompatPlugin, 0, 1)
/* Daemon start always calls plugin.Init thereby initializing a store.
* So store on experimental builds can never be nil, even while
* handling legacy plugins. However, there are legacy plugin unit
* tests where the volume subsystem directly talks with the plugin,
* bypassing the daemon. For such tests, this check is necessary.
*/
if ps != nil {
ps.RLock()
result = ps.getAllByCap(capability)
ps.RUnlock()
}
// Lookup with legacy model
if allowV1PluginsFallback {
pl, err := plugins.GetAll(capability)
if err != nil {
return nil, fmt.Errorf("legacy plugin: %v", err)
}
for _, p := range pl {
result = append(result, p)
}
}
return result, nil
}
// Handle sets a callback for a given capability. It is only used by network
// and ipam drivers during plugin registration. The callback registers the
// driver with the subsystem (network, ipam).
func (ps Store) Handle(capability string, callback func(string, *plugins.Client)) {
pluginType := fmt.Sprintf("docker.%s/1", strings.ToLower(capability))
store := &ps
// Register callback with new plugin model.
store.Lock()
store.handlers[pluginType] = callback
store.Unlock()
// Register callback with legacy plugin model.
if allowV1PluginsFallback {
plugins.Handle(capability, callback)
}
}
// CallHandler calls the registered callback. It is invoked during plugin enable.
func (ps *Store) CallHandler(p *v2.Plugin) {
for _, typ := range p.GetTypes() {
if handler := ps.handlers[typ.String()]; handler != nil {
handler(p.Name(), p.Client())
}
}
}

View file

@ -1,32 +1,13 @@
// +build experimental
package v2
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/restartmanager"
"github.com/opencontainers/runtime-spec/specs-go"
)
const defaultPluginRuntimeDestination = "/run/docker/plugins"
// ErrInadequateCapability indicates that the plugin did not have the requested capability.
type ErrInadequateCapability string
func (cap ErrInadequateCapability) Error() string {
return fmt.Sprintf("plugin does not provide %q capability", cap)
}
// Plugin represents an individual plugin.
type Plugin struct {
sync.RWMutex
@ -37,226 +18,3 @@ type Plugin struct {
ExitChan chan bool `json:"-"`
RefCount int `json:"-"`
}
func newPluginObj(name, id, tag string) types.Plugin {
return types.Plugin{Name: name, ID: id, Tag: tag}
}
// NewPlugin creates a plugin.
func NewPlugin(name, id, runRoot, tag string) *Plugin {
return &Plugin{
PluginObj: newPluginObj(name, id, tag),
RuntimeSourcePath: filepath.Join(runRoot, id),
}
}
// Client returns the plugin client.
func (p *Plugin) Client() *plugins.Client {
return p.PClient
}
// IsLegacy returns true for legacy plugins and false otherwise.
func (p *Plugin) IsLegacy() bool {
return false
}
// Name returns the plugin name.
func (p *Plugin) Name() string {
name := p.PluginObj.Name
if len(p.PluginObj.Tag) > 0 {
// TODO: this feels hacky, maybe we should be storing the distribution reference rather than splitting these
name += ":" + p.PluginObj.Tag
}
return name
}
// FilterByCap query the plugin for a given capability.
func (p *Plugin) FilterByCap(capability string) (*Plugin, error) {
capability = strings.ToLower(capability)
for _, typ := range p.PluginObj.Manifest.Interface.Types {
if typ.Capability == capability && typ.Prefix == "docker" {
return p, nil
}
}
return nil, ErrInadequateCapability(capability)
}
// RemoveFromDisk deletes the plugin's runtime files from disk.
func (p *Plugin) RemoveFromDisk() error {
return os.RemoveAll(p.RuntimeSourcePath)
}
// InitPlugin populates the plugin object from the plugin manifest file.
func (p *Plugin) InitPlugin(libRoot string) error {
dt, err := os.Open(filepath.Join(libRoot, p.PluginObj.ID, "manifest.json"))
if err != nil {
return err
}
err = json.NewDecoder(dt).Decode(&p.PluginObj.Manifest)
dt.Close()
if err != nil {
return err
}
p.PluginObj.Config.Mounts = make([]types.PluginMount, len(p.PluginObj.Manifest.Mounts))
for i, mount := range p.PluginObj.Manifest.Mounts {
p.PluginObj.Config.Mounts[i] = mount
}
p.PluginObj.Config.Env = make([]string, 0, len(p.PluginObj.Manifest.Env))
for _, env := range p.PluginObj.Manifest.Env {
if env.Value != nil {
p.PluginObj.Config.Env = append(p.PluginObj.Config.Env, fmt.Sprintf("%s=%s", env.Name, *env.Value))
}
}
copy(p.PluginObj.Config.Args, p.PluginObj.Manifest.Args.Value)
f, err := os.Create(filepath.Join(libRoot, p.PluginObj.ID, "plugin-config.json"))
if err != nil {
return err
}
err = json.NewEncoder(f).Encode(&p.PluginObj.Config)
f.Close()
return err
}
// Set is used to pass arguments to the plugin.
func (p *Plugin) Set(args []string) error {
m := make(map[string]string, len(args))
for _, arg := range args {
i := strings.Index(arg, "=")
if i < 0 {
return fmt.Errorf("No equal sign '=' found in %s", arg)
}
m[arg[:i]] = arg[i+1:]
}
return errors.New("not implemented")
}
// ComputePrivileges takes the manifest file and computes the list of access necessary
// for the plugin on the host.
func (p *Plugin) ComputePrivileges() types.PluginPrivileges {
m := p.PluginObj.Manifest
var privileges types.PluginPrivileges
if m.Network.Type != "null" && m.Network.Type != "bridge" {
privileges = append(privileges, types.PluginPrivilege{
Name: "network",
Description: "",
Value: []string{m.Network.Type},
})
}
for _, mount := range m.Mounts {
if mount.Source != nil {
privileges = append(privileges, types.PluginPrivilege{
Name: "mount",
Description: "",
Value: []string{*mount.Source},
})
}
}
for _, device := range m.Devices {
if device.Path != nil {
privileges = append(privileges, types.PluginPrivilege{
Name: "device",
Description: "",
Value: []string{*device.Path},
})
}
}
if len(m.Capabilities) > 0 {
privileges = append(privileges, types.PluginPrivilege{
Name: "capabilities",
Description: "",
Value: m.Capabilities,
})
}
return privileges
}
// IsEnabled returns the active state of the plugin.
func (p *Plugin) IsEnabled() bool {
p.RLock()
defer p.RUnlock()
return p.PluginObj.Enabled
}
// GetID returns the plugin's ID.
func (p *Plugin) GetID() string {
p.RLock()
defer p.RUnlock()
return p.PluginObj.ID
}
// GetSocket returns the plugin socket.
func (p *Plugin) GetSocket() string {
p.RLock()
defer p.RUnlock()
return p.PluginObj.Manifest.Interface.Socket
}
// GetTypes returns the interface types of a plugin.
func (p *Plugin) GetTypes() []types.PluginInterfaceType {
p.RLock()
defer p.RUnlock()
return p.PluginObj.Manifest.Interface.Types
}
// InitSpec creates an OCI spec from the plugin's config.
func (p *Plugin) InitSpec(s specs.Spec, libRoot string) (*specs.Spec, error) {
rootfs := filepath.Join(libRoot, p.PluginObj.ID, "rootfs")
s.Root = specs.Root{
Path: rootfs,
Readonly: false, // TODO: all plugins should be readonly? settable in manifest?
}
mounts := append(p.PluginObj.Config.Mounts, types.PluginMount{
Source: &p.RuntimeSourcePath,
Destination: defaultPluginRuntimeDestination,
Type: "bind",
Options: []string{"rbind", "rshared"},
})
for _, mount := range mounts {
m := specs.Mount{
Destination: mount.Destination,
Type: mount.Type,
Options: mount.Options,
}
// TODO: if nil, then it's required and user didn't set it
if mount.Source != nil {
m.Source = *mount.Source
}
if m.Source != "" && m.Type == "bind" {
fi, err := os.Lstat(filepath.Join(rootfs, string(os.PathSeparator), m.Destination)) // TODO: followsymlinks
if err != nil {
return nil, err
}
if fi.IsDir() {
if err := os.MkdirAll(m.Source, 0700); err != nil {
return nil, err
}
}
}
s.Mounts = append(s.Mounts, m)
}
envs := make([]string, 1, len(p.PluginObj.Config.Env)+1)
envs[0] = "PATH=" + system.DefaultPathEnv
envs = append(envs, p.PluginObj.Config.Env...)
args := append(p.PluginObj.Manifest.Entrypoint, p.PluginObj.Config.Args...)
cwd := p.PluginObj.Manifest.Workdir
if len(cwd) == 0 {
cwd = "/"
}
s.Process = specs.Process{
Terminal: false,
Args: args,
Cwd: cwd,
Env: envs,
}
return &s, nil
}

View file

@ -0,0 +1,249 @@
// +build experimental
package v2
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/pkg/system"
"github.com/opencontainers/runtime-spec/specs-go"
)
const defaultPluginRuntimeDestination = "/run/docker/plugins"
// ErrInadequateCapability indicates that the plugin did not have the requested capability.
type ErrInadequateCapability string
func (cap ErrInadequateCapability) Error() string {
return fmt.Sprintf("plugin does not provide %q capability", cap)
}
func newPluginObj(name, id, tag string) types.Plugin {
return types.Plugin{Name: name, ID: id, Tag: tag}
}
// NewPlugin creates a plugin.
func NewPlugin(name, id, runRoot, tag string) *Plugin {
return &Plugin{
PluginObj: newPluginObj(name, id, tag),
RuntimeSourcePath: filepath.Join(runRoot, id),
}
}
// Client returns the plugin client.
func (p *Plugin) Client() *plugins.Client {
return p.PClient
}
// IsV1 returns true for V1 plugins and false otherwise.
func (p *Plugin) IsV1() bool {
return false
}
// Name returns the plugin name.
func (p *Plugin) Name() string {
name := p.PluginObj.Name
if len(p.PluginObj.Tag) > 0 {
// TODO: this feels hacky, maybe we should be storing the distribution reference rather than splitting these
name += ":" + p.PluginObj.Tag
}
return name
}
// FilterByCap query the plugin for a given capability.
func (p *Plugin) FilterByCap(capability string) (*Plugin, error) {
capability = strings.ToLower(capability)
for _, typ := range p.PluginObj.Manifest.Interface.Types {
if typ.Capability == capability && typ.Prefix == "docker" {
return p, nil
}
}
return nil, ErrInadequateCapability(capability)
}
// RemoveFromDisk deletes the plugin's runtime files from disk.
func (p *Plugin) RemoveFromDisk() error {
return os.RemoveAll(p.RuntimeSourcePath)
}
// InitPlugin populates the plugin object from the plugin manifest file.
func (p *Plugin) InitPlugin(libRoot string) error {
dt, err := os.Open(filepath.Join(libRoot, p.PluginObj.ID, "manifest.json"))
if err != nil {
return err
}
err = json.NewDecoder(dt).Decode(&p.PluginObj.Manifest)
dt.Close()
if err != nil {
return err
}
p.PluginObj.Config.Mounts = make([]types.PluginMount, len(p.PluginObj.Manifest.Mounts))
for i, mount := range p.PluginObj.Manifest.Mounts {
p.PluginObj.Config.Mounts[i] = mount
}
p.PluginObj.Config.Env = make([]string, 0, len(p.PluginObj.Manifest.Env))
for _, env := range p.PluginObj.Manifest.Env {
if env.Value != nil {
p.PluginObj.Config.Env = append(p.PluginObj.Config.Env, fmt.Sprintf("%s=%s", env.Name, *env.Value))
}
}
copy(p.PluginObj.Config.Args, p.PluginObj.Manifest.Args.Value)
f, err := os.Create(filepath.Join(libRoot, p.PluginObj.ID, "plugin-config.json"))
if err != nil {
return err
}
err = json.NewEncoder(f).Encode(&p.PluginObj.Config)
f.Close()
return err
}
// Set is used to pass arguments to the plugin.
func (p *Plugin) Set(args []string) error {
m := make(map[string]string, len(args))
for _, arg := range args {
i := strings.Index(arg, "=")
if i < 0 {
return fmt.Errorf("No equal sign '=' found in %s", arg)
}
m[arg[:i]] = arg[i+1:]
}
return errors.New("not implemented")
}
// ComputePrivileges takes the manifest file and computes the list of access necessary
// for the plugin on the host.
func (p *Plugin) ComputePrivileges() types.PluginPrivileges {
m := p.PluginObj.Manifest
var privileges types.PluginPrivileges
if m.Network.Type != "null" && m.Network.Type != "bridge" {
privileges = append(privileges, types.PluginPrivilege{
Name: "network",
Description: "",
Value: []string{m.Network.Type},
})
}
for _, mount := range m.Mounts {
if mount.Source != nil {
privileges = append(privileges, types.PluginPrivilege{
Name: "mount",
Description: "",
Value: []string{*mount.Source},
})
}
}
for _, device := range m.Devices {
if device.Path != nil {
privileges = append(privileges, types.PluginPrivilege{
Name: "device",
Description: "",
Value: []string{*device.Path},
})
}
}
if len(m.Capabilities) > 0 {
privileges = append(privileges, types.PluginPrivilege{
Name: "capabilities",
Description: "",
Value: m.Capabilities,
})
}
return privileges
}
// IsEnabled returns the active state of the plugin.
func (p *Plugin) IsEnabled() bool {
p.RLock()
defer p.RUnlock()
return p.PluginObj.Enabled
}
// GetID returns the plugin's ID.
func (p *Plugin) GetID() string {
p.RLock()
defer p.RUnlock()
return p.PluginObj.ID
}
// GetSocket returns the plugin socket.
func (p *Plugin) GetSocket() string {
p.RLock()
defer p.RUnlock()
return p.PluginObj.Manifest.Interface.Socket
}
// GetTypes returns the interface types of a plugin.
func (p *Plugin) GetTypes() []types.PluginInterfaceType {
p.RLock()
defer p.RUnlock()
return p.PluginObj.Manifest.Interface.Types
}
// InitSpec creates an OCI spec from the plugin's config.
func (p *Plugin) InitSpec(s specs.Spec, libRoot string) (*specs.Spec, error) {
rootfs := filepath.Join(libRoot, p.PluginObj.ID, "rootfs")
s.Root = specs.Root{
Path: rootfs,
Readonly: false, // TODO: all plugins should be readonly? settable in manifest?
}
mounts := append(p.PluginObj.Config.Mounts, types.PluginMount{
Source: &p.RuntimeSourcePath,
Destination: defaultPluginRuntimeDestination,
Type: "bind",
Options: []string{"rbind", "rshared"},
})
for _, mount := range mounts {
m := specs.Mount{
Destination: mount.Destination,
Type: mount.Type,
Options: mount.Options,
}
// TODO: if nil, then it's required and user didn't set it
if mount.Source != nil {
m.Source = *mount.Source
}
if m.Source != "" && m.Type == "bind" {
fi, err := os.Lstat(filepath.Join(rootfs, m.Destination)) // TODO: followsymlinks
if err != nil {
return nil, err
}
if fi.IsDir() {
if err := os.MkdirAll(m.Source, 0700); err != nil {
return nil, err
}
}
}
s.Mounts = append(s.Mounts, m)
}
envs := make([]string, 1, len(p.PluginObj.Config.Env)+1)
envs[0] = "PATH=" + system.DefaultPathEnv
envs = append(envs, p.PluginObj.Config.Env...)
args := append(p.PluginObj.Manifest.Entrypoint, p.PluginObj.Config.Args...)
cwd := p.PluginObj.Manifest.Workdir
if len(cwd) == 0 {
cwd = "/"
}
s.Process = specs.Process{
Terminal: false,
Args: args,
Cwd: cwd,
Env: envs,
}
return &s, nil
}

View file

@ -7,14 +7,17 @@ import (
"sync"
"github.com/docker/docker/pkg/locker"
pluginStore "github.com/docker/docker/plugin/store"
"github.com/docker/docker/plugin/getter"
"github.com/docker/docker/volume"
)
// currently created by hand. generation tool would generate this like:
// $ extpoint-gen Driver > volume/extpoint.go
var drivers = &driverExtpoint{extensions: make(map[string]volume.Driver), driverLock: &locker.Locker{}}
var drivers = &driverExtpoint{
extensions: make(map[string]volume.Driver),
driverLock: &locker.Locker{},
}
const extName = "VolumeDriver"
@ -49,7 +52,13 @@ type volumeDriver interface {
type driverExtpoint struct {
extensions map[string]volume.Driver
sync.Mutex
driverLock *locker.Locker
driverLock *locker.Locker
plugingetter getter.PluginGetter
}
// RegisterPluginGetter sets the plugingetter
func RegisterPluginGetter(plugingetter getter.PluginGetter) {
drivers.plugingetter = plugingetter
}
// Register associates the given driver to the given name, checking if
@ -72,6 +81,7 @@ func Register(extension volume.Driver, name string) bool {
}
drivers.extensions[name] = extension
return true
}
@ -102,7 +112,7 @@ func lookup(name string, mode int) (volume.Driver, error) {
return ext, nil
}
p, err := pluginStore.LookupWithCapability(name, extName, mode)
p, err := drivers.plugingetter.Get(name, extName, mode)
if err != nil {
return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err)
}
@ -112,7 +122,7 @@ func lookup(name string, mode int) (volume.Driver, error) {
return nil, err
}
if p.IsLegacy() {
if p.IsV1() {
drivers.Lock()
drivers.extensions[name] = d
drivers.Unlock()
@ -134,7 +144,7 @@ func GetDriver(name string) (volume.Driver, error) {
if name == "" {
name = volume.DefaultDriverName
}
return lookup(name, pluginStore.LOOKUP)
return lookup(name, getter.LOOKUP)
}
// CreateDriver returns a volume driver by its name and increments RefCount.
@ -143,7 +153,7 @@ func CreateDriver(name string) (volume.Driver, error) {
if name == "" {
name = volume.DefaultDriverName
}
return lookup(name, pluginStore.CREATE)
return lookup(name, getter.CREATE)
}
// RemoveDriver returns a volume driver by its name and decrements RefCount..
@ -152,7 +162,7 @@ func RemoveDriver(name string) (volume.Driver, error) {
if name == "" {
name = volume.DefaultDriverName
}
return lookup(name, pluginStore.REMOVE)
return lookup(name, getter.REMOVE)
}
// GetDriverList returns list of volume drivers registered.
@ -169,7 +179,7 @@ func GetDriverList() []string {
// GetAllDrivers lists all the registered drivers
func GetAllDrivers() ([]volume.Driver, error) {
plugins, err := pluginStore.FindWithCapability(extName)
plugins, err := drivers.plugingetter.GetAllByCap(extName)
if err != nil {
return nil, fmt.Errorf("error listing plugins: %v", err)
}
@ -190,7 +200,7 @@ func GetAllDrivers() ([]volume.Driver, error) {
}
ext = NewVolumeDriver(name, p.Client())
if p.IsLegacy() {
if p.IsV1() {
drivers.extensions[name] = ext
}
ds = append(ds, ext)

View file

@ -3,16 +3,20 @@ package volumedrivers
import (
"testing"
pluginstore "github.com/docker/docker/plugin/store"
volumetestutils "github.com/docker/docker/volume/testutils"
)
func TestGetDriver(t *testing.T) {
pluginStore := pluginstore.NewStore("/var/lib/docker")
RegisterPluginGetter(pluginStore)
_, err := GetDriver("missing")
if err == nil {
t.Fatal("Expected error, was nil")
}
Register(volumetestutils.NewFakeDriver("fake"), "fake")
d, err := GetDriver("fake")
if err != nil {
t.Fatal(err)

View file

@ -5,11 +5,15 @@ import (
"strings"
"testing"
pluginstore "github.com/docker/docker/plugin/store"
"github.com/docker/docker/volume/drivers"
volumetestutils "github.com/docker/docker/volume/testutils"
)
func TestCreate(t *testing.T) {
pluginStore := pluginstore.NewStore("/var/lib/docker")
volumedrivers.RegisterPluginGetter(pluginStore)
volumedrivers.Register(volumetestutils.NewFakeDriver("fake"), "fake")
defer volumedrivers.Unregister("fake")
s, err := New("")