diff --git a/plugin/backend.go b/plugin/backend.go index b8a35979e3..089efdb4e6 100644 --- a/plugin/backend.go +++ b/plugin/backend.go @@ -15,39 +15,40 @@ import ( "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/plugin/distribution" + "github.com/docker/docker/plugin/v2" "github.com/docker/docker/reference" "github.com/docker/engine-api/types" ) // Disable deactivates a plugin, which implies that they cannot be used by containers. func (pm *Manager) Disable(name string) error { - p, err := pm.get(name) + p, err := pm.pluginStore.GetByName(name) if err != nil { return err } if err := pm.disable(p); err != nil { return err } - pm.pluginEventLogger(p.PluginObj.ID, name, "disable") + pm.pluginEventLogger(p.GetID(), name, "disable") return nil } // Enable activates a plugin, which implies that they are ready to be used by containers. func (pm *Manager) Enable(name string) error { - p, err := pm.get(name) + p, err := pm.pluginStore.GetByName(name) if err != nil { return err } if err := pm.enable(p, false); err != nil { return err } - pm.pluginEventLogger(p.PluginObj.ID, name, "enable") + pm.pluginEventLogger(p.GetID(), name, "enable") return nil } // Inspect examines a plugin manifest func (pm *Manager) Inspect(name string) (tp types.Plugin, err error) { - p, err := pm.get(name) + p, err := pm.pluginStore.GetByName(name) if err != nil { return tp, err } @@ -63,7 +64,7 @@ func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.A } name = ref.String() - if p, _ := pm.get(name); p != nil { + if p, _ := pm.pluginStore.GetByName(name); p != nil { logrus.Debugf("plugin already exists") return nil, fmt.Errorf("%s exists", name) } @@ -86,25 +87,25 @@ func (pm *Manager) Pull(name string, metaHeader http.Header, authConfig *types.A return nil, err } - p := pm.newPlugin(ref, pluginID) - if err := pm.initPlugin(p); err != nil { + var tag string + if ref, ok := ref.(reference.NamedTagged); ok { + tag = ref.Tag() + } + p := v2.NewPlugin(ref.Name(), pluginID, pm.runRoot, tag) + if err := p.InitPlugin(pm.libRoot); err != nil { return nil, err } - - pm.Lock() - pm.plugins[pluginID] = p - pm.nameToID[name] = pluginID - pm.save() - pm.Unlock() + pm.pluginStore.Add(p) pm.pluginEventLogger(pluginID, name, "pull") - return computePrivileges(&p.PluginObj.Manifest), nil + return p.ComputePrivileges(), nil } // List displays the list of plugins and associated metadata. func (pm *Manager) List() ([]types.Plugin, error) { - out := make([]types.Plugin, 0, len(pm.plugins)) - for _, p := range pm.plugins { + plugins := pm.pluginStore.GetAll() + out := make([]types.Plugin, 0, len(plugins)) + for _, p := range plugins { out = append(out, p.PluginObj) } return out, nil @@ -112,11 +113,11 @@ func (pm *Manager) List() ([]types.Plugin, error) { // Push pushes a plugin to the store. func (pm *Manager) Push(name string, metaHeader http.Header, authConfig *types.AuthConfig) error { - p, err := pm.get(name) + p, err := pm.pluginStore.GetByName(name) if err != nil { return err } - dest := filepath.Join(pm.libRoot, p.PluginObj.ID) + dest := filepath.Join(pm.libRoot, p.GetID()) config, err := ioutil.ReadFile(filepath.Join(dest, "manifest.json")) if err != nil { return err @@ -142,22 +143,28 @@ func (pm *Manager) Push(name string, metaHeader http.Header, authConfig *types.A // Remove deletes plugin's root directory. func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error { - p, err := pm.get(name) + p, err := pm.pluginStore.GetByName(name) if err != nil { return err } - if err := pm.remove(p, config.ForceRemove); err != nil { - return err + if p.IsEnabled() { + if !config.ForceRemove { + return fmt.Errorf("plugin %s is enabled", p.Name()) + } + if err := pm.disable(p); err != nil { + logrus.Errorf("failed to disable plugin '%s': %s", p.Name(), err) + } } - pm.pluginEventLogger(p.PluginObj.ID, name, "remove") + pm.pluginStore.Remove(p) + pm.pluginEventLogger(p.GetID(), name, "remove") return nil } // Set sets plugin args func (pm *Manager) Set(name string, args []string) error { - p, err := pm.get(name) + p, err := pm.pluginStore.GetByName(name) if err != nil { return err } - return pm.set(p, args) + return p.Set(args) } diff --git a/plugin/interface.go b/plugin/interface.go deleted file mode 100644 index 80e6b5b8df..0000000000 --- a/plugin/interface.go +++ /dev/null @@ -1,10 +0,0 @@ -package plugin - -import "github.com/docker/docker/pkg/plugins" - -// Plugin represents a plugin. It is used to abstract from an older plugin architecture (in pkg/plugins). -type Plugin interface { - Client() *plugins.Client - Name() string - IsLegacy() bool -} diff --git a/plugin/manager.go b/plugin/manager.go index c710bda09a..a577c95f96 100644 --- a/plugin/manager.go +++ b/plugin/manager.go @@ -4,7 +4,6 @@ package plugin import ( "encoding/json" - "errors" "fmt" "io" "os" @@ -14,16 +13,12 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/libcontainerd" - "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/plugins" - "github.com/docker/docker/reference" + "github.com/docker/docker/plugin/store" + "github.com/docker/docker/plugin/v2" "github.com/docker/docker/registry" - "github.com/docker/docker/restartmanager" - "github.com/docker/engine-api/types" ) -const defaultPluginRuntimeDestination = "/run/docker/plugins" - var ( manager *Manager @@ -34,71 +29,14 @@ 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)) } - -// ErrInadequateCapability indicates that a plugin was found but did not have the requested capability. -type ErrInadequateCapability struct { - name string - capability string -} - -func (e ErrInadequateCapability) Error() string { - return fmt.Sprintf("plugin %q found, but not with %q capability", e.name, e.capability) -} - -type plugin struct { - //sync.RWMutex TODO - PluginObj types.Plugin `json:"plugin"` - client *plugins.Client - restartManager restartmanager.RestartManager - runtimeSourcePath string - exitChan chan bool -} - -func (p *plugin) Client() *plugins.Client { - return p.client -} - -// IsLegacy returns true for legacy plugins and false otherwise. -func (p *plugin) IsLegacy() bool { - return false -} - -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 -} - -func (pm *Manager) newPlugin(ref reference.Named, id string) *plugin { - p := &plugin{ - PluginObj: types.Plugin{ - Name: ref.Name(), - ID: id, - }, - runtimeSourcePath: filepath.Join(pm.runRoot, id), - } - if ref, ok := ref.(reference.NamedTagged); ok { - p.PluginObj.Tag = ref.Tag() - } - return p -} - -func (pm *Manager) restorePlugin(p *plugin) error { - p.runtimeSourcePath = filepath.Join(pm.runRoot, p.PluginObj.ID) - if p.PluginObj.Enabled { +func (pm *Manager) restorePlugin(p *v2.Plugin) error { + p.RuntimeSourcePath = filepath.Join(pm.runRoot, p.GetID()) + if p.IsEnabled() { return pm.restore(p) } return nil } -type pluginMap map[string]*plugin type eventLogger func(id, name, action string) // Manager controls the plugin subsystem. @@ -106,8 +44,7 @@ type Manager struct { sync.RWMutex libRoot string runRoot string - plugins pluginMap // TODO: figure out why save() doesn't json encode *plugin object - nameToID map[string]string + pluginStore *store.PluginStore handlers map[string]func(string, *plugins.Client) containerdClient libcontainerd.Client registryService registry.Service @@ -132,8 +69,7 @@ func Init(root string, remote libcontainerd.Remote, rs registry.Service, liveRes manager = &Manager{ libRoot: root, runRoot: "/run/docker", - plugins: make(map[string]*plugin), - nameToID: make(map[string]string), + pluginStore: store.NewPluginStore(root), handlers: make(map[string]func(string, *plugins.Client)), registryService: rs, liveRestore: liveRestore, @@ -162,105 +98,6 @@ func Handle(capability string, callback func(string, *plugins.Client)) { } } -func (pm *Manager) get(name string) (*plugin, error) { - pm.RLock() - defer pm.RUnlock() - - id, nameOk := pm.nameToID[name] - if !nameOk { - return nil, ErrNotFound(name) - } - - p, idOk := pm.plugins[id] - if !idOk { - return nil, ErrNotFound(name) - } - - return p, nil -} - -// FindWithCapability returns a list of plugins matching the given capability. -func FindWithCapability(capability string) ([]Plugin, error) { - result := make([]Plugin, 0, 1) - - /* Daemon start always calls plugin.Init thereby initializing a manager. - * So manager on experimental builds can never be nil, even while - * handling legacy plugins. However, there are legacy plugin unit - * tests where volume subsystem directly talks with the plugin, - * bypassing the daemon. For such tests, this check is necessary.*/ - if manager != nil { - manager.RLock() - for _, p := range manager.plugins { - for _, typ := range p.PluginObj.Manifest.Interface.Types { - if strings.EqualFold(typ.Capability, capability) && typ.Prefix == "docker" { - result = append(result, p) - break - } - } - } - manager.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 -} - -// LookupWithCapability returns a plugin matching the given name and capability. -func LookupWithCapability(name, capability string) (Plugin, error) { - var ( - p *plugin - err error - ) - - // Lookup using new model. - if manager != 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 = manager.get(fullName) - if err == nil { - 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{name, 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 -} - // StateChanged updates plugin internals using libcontainerd events. func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error { logrus.Debugf("plugin state changed %s %#v", id, e) @@ -272,13 +109,11 @@ func (pm *Manager) StateChanged(id string, e libcontainerd.StateInfo) error { shutdown = pm.shutdown pm.RUnlock() if shutdown { - pm.RLock() - p, idOk := pm.plugins[id] - pm.RUnlock() - if !idOk { - return ErrNotFound(id) + p, err := pm.pluginStore.GetByID(id) + if err != nil { + return err } - close(p.exitChan) + close(p.ExitChan) } } @@ -313,24 +148,24 @@ func (pm *Manager) init() error { } defer dt.Close() - if err := json.NewDecoder(dt).Decode(&pm.plugins); err != nil { + plugins := make(map[string]*v2.Plugin) + if err := json.NewDecoder(dt).Decode(&plugins); err != nil { return err } + pm.pluginStore.SetAll(plugins) var group sync.WaitGroup - group.Add(len(pm.plugins)) - for _, p := range pm.plugins { - go func(p *plugin) { + group.Add(len(plugins)) + for _, p := range plugins { + go func(p *v2.Plugin) { defer group.Done() if err := pm.restorePlugin(p); err != nil { logrus.Errorf("failed to restore plugin '%s': %s", p.Name(), err) return } - pm.Lock() - pm.nameToID[p.Name()] = p.PluginObj.ID - requiresManualRestore := !pm.liveRestore && p.PluginObj.Enabled - pm.Unlock() + pm.pluginStore.Add(p) + requiresManualRestore := !pm.liveRestore && p.IsEnabled() if requiresManualRestore { // if liveRestore is not enabled, the plugin will be stopped now so we should enable it @@ -341,80 +176,6 @@ func (pm *Manager) init() error { }(p) } group.Wait() - return pm.save() -} - -func (pm *Manager) initPlugin(p *plugin) error { - dt, err := os.Open(filepath.Join(pm.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(pm.libRoot, p.PluginObj.ID, "plugin-config.json")) - if err != nil { - return err - } - err = json.NewEncoder(f).Encode(&p.PluginObj.Config) - f.Close() - return err -} - -func (pm *Manager) remove(p *plugin, force bool) error { - if p.PluginObj.Enabled { - if !force { - return fmt.Errorf("plugin %s is enabled", p.Name()) - } - if err := pm.disable(p); err != nil { - logrus.Errorf("failed to disable plugin '%s': %s", p.Name(), err) - } - } - pm.Lock() // fixme: lock single record - defer pm.Unlock() - delete(pm.plugins, p.PluginObj.ID) - delete(pm.nameToID, p.Name()) - pm.save() - return os.RemoveAll(filepath.Join(pm.libRoot, p.PluginObj.ID)) -} - -func (pm *Manager) set(p *plugin, 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") -} - -// fixme: not safe -func (pm *Manager) save() error { - filePath := filepath.Join(pm.libRoot, "plugins.json") - - jsonData, err := json.Marshal(pm.plugins) - if err != nil { - logrus.Debugf("failure in json.Marshal: %v", err) - return err - } - ioutils.AtomicWriteFile(filePath, jsonData, 0600) return nil } @@ -428,40 +189,3 @@ func (l logHook) Fire(entry *logrus.Entry) error { entry.Data = logrus.Fields{"plugin": l.id} return nil } - -func computePrivileges(m *types.PluginManifest) types.PluginPrivileges { - 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 -} diff --git a/plugin/manager_linux.go b/plugin/manager_linux.go index d87fd34173..2b2888d95e 100644 --- a/plugin/manager_linux.go +++ b/plugin/manager_linux.go @@ -4,7 +4,6 @@ package plugin import ( "fmt" - "os" "path/filepath" "syscall" "time" @@ -13,45 +12,38 @@ import ( "github.com/docker/docker/libcontainerd" "github.com/docker/docker/oci" "github.com/docker/docker/pkg/plugins" - "github.com/docker/docker/pkg/system" + "github.com/docker/docker/plugin/v2" "github.com/docker/docker/restartmanager" - "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/container" - "github.com/opencontainers/runtime-spec/specs-go" ) -func (pm *Manager) enable(p *plugin, force bool) error { - if p.PluginObj.Enabled && !force { +func (pm *Manager) enable(p *v2.Plugin, force bool) error { + if p.IsEnabled() && !force { return fmt.Errorf("plugin %s is already enabled", p.Name()) } - spec, err := pm.initSpec(p) + spec, err := p.InitSpec(oci.DefaultSpec(), pm.libRoot) if err != nil { return err } - p.restartManager = restartmanager.New(container.RestartPolicy{Name: "always"}, 0) - if err := pm.containerdClient.Create(p.PluginObj.ID, libcontainerd.Spec(*spec), libcontainerd.WithRestartManager(p.restartManager)); err != nil { // POC-only - if err := p.restartManager.Cancel(); err != nil { + p.RestartManager = restartmanager.New(container.RestartPolicy{Name: "always"}, 0) + if err := pm.containerdClient.Create(p.GetID(), libcontainerd.Spec(*spec), libcontainerd.WithRestartManager(p.RestartManager)); err != nil { + if err := p.RestartManager.Cancel(); err != nil { logrus.Errorf("enable: restartManager.Cancel failed due to %v", err) } return err } - socket := p.PluginObj.Manifest.Interface.Socket - p.client, err = plugins.NewClient("unix://"+filepath.Join(p.runtimeSourcePath, socket), nil) + p.PClient, err = plugins.NewClient("unix://"+filepath.Join(p.RuntimeSourcePath, p.GetSocket()), nil) if err != nil { - if err := p.restartManager.Cancel(); err != nil { + if err := p.RestartManager.Cancel(); err != nil { logrus.Errorf("enable: restartManager.Cancel failed due to %v", err) } return err } - pm.Lock() // fixme: lock single record - p.PluginObj.Enabled = true - pm.save() - pm.Unlock() - - for _, typ := range p.PluginObj.Manifest.Interface.Types { + pm.pluginStore.SetState(p, true) + for _, typ := range p.GetTypes() { if handler := pm.handlers[typ.String()]; handler != nil { handler(p.Name(), p.Client()) } @@ -60,90 +52,25 @@ func (pm *Manager) enable(p *plugin, force bool) error { return nil } -func (pm *Manager) restore(p *plugin) error { - p.restartManager = restartmanager.New(container.RestartPolicy{Name: "always"}, 0) - return pm.containerdClient.Restore(p.PluginObj.ID, libcontainerd.WithRestartManager(p.restartManager)) +func (pm *Manager) restore(p *v2.Plugin) error { + p.RestartManager = restartmanager.New(container.RestartPolicy{Name: "always"}, 0) + return pm.containerdClient.Restore(p.GetID(), libcontainerd.WithRestartManager(p.RestartManager)) } -func (pm *Manager) initSpec(p *plugin) (*specs.Spec, error) { - s := oci.DefaultSpec() - - rootfs := filepath.Join(pm.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" { - /* Debugging issue #25511: Volumes and other content created under the - bind mount should be recursively propagated. rshared, not shared. - This could be the reason for EBUSY during removal. Override options - with rbind, rshared and see if CI errors are fixed. */ - m.Options = []string{"rbind", "rshared"} - 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 -} - -func (pm *Manager) disable(p *plugin) error { - if !p.PluginObj.Enabled { +func (pm *Manager) disable(p *v2.Plugin) error { + if !p.IsEnabled() { return fmt.Errorf("plugin %s is already disabled", p.Name()) } - if err := p.restartManager.Cancel(); err != nil { + if err := p.RestartManager.Cancel(); err != nil { logrus.Error(err) } - if err := pm.containerdClient.Signal(p.PluginObj.ID, int(syscall.SIGKILL)); err != nil { + if err := pm.containerdClient.Signal(p.GetID(), int(syscall.SIGKILL)); err != nil { logrus.Error(err) } - os.RemoveAll(p.runtimeSourcePath) - pm.Lock() // fixme: lock single record - defer pm.Unlock() - p.PluginObj.Enabled = false - pm.save() + if err := p.RemoveFromDisk(); err != nil { + logrus.Error(err) + } + pm.pluginStore.SetState(p, false) return nil } @@ -155,34 +82,36 @@ func (pm *Manager) Shutdown() { pm.RLock() defer pm.RUnlock() - for _, p := range pm.plugins { - if pm.liveRestore && p.PluginObj.Enabled { - logrus.Debug("Plugin enabled when liveRestore is set, skipping shutdown") + plugins := pm.pluginStore.GetAll() + for _, p := range plugins { + if pm.liveRestore && p.IsEnabled() { + logrus.Debug("Plugin active when liveRestore is set, skipping shutdown") continue } - if p.restartManager != nil { - if err := p.restartManager.Cancel(); err != nil { + if p.RestartManager != nil { + if err := p.RestartManager.Cancel(); err != nil { logrus.Error(err) } } - if pm.containerdClient != nil && p.PluginObj.Enabled { - p.exitChan = make(chan bool) + if pm.containerdClient != nil && p.IsEnabled() { + pluginID := p.GetID() + p.ExitChan = make(chan bool) err := pm.containerdClient.Signal(p.PluginObj.ID, int(syscall.SIGTERM)) if err != nil { logrus.Errorf("Sending SIGTERM to plugin failed with error: %v", err) } else { select { - case <-p.exitChan: + case <-p.ExitChan: logrus.Debug("Clean shutdown of plugin") case <-time.After(time.Second * 10): logrus.Debug("Force shutdown plugin") - if err := pm.containerdClient.Signal(p.PluginObj.ID, int(syscall.SIGKILL)); err != nil { + if err := pm.containerdClient.Signal(pluginID, int(syscall.SIGKILL)); err != nil { logrus.Errorf("Sending SIGKILL to plugin failed with error: %v", err) } } } } - if err := os.RemoveAll(p.runtimeSourcePath); err != nil { + if err := p.RemoveFromDisk(); err != nil { logrus.Errorf("Remove plugin runtime failed with error: %v", err) } } diff --git a/plugin/manager_windows.go b/plugin/manager_windows.go index c242fb3bcd..6b8149a0af 100644 --- a/plugin/manager_windows.go +++ b/plugin/manager_windows.go @@ -5,22 +5,23 @@ package plugin import ( "fmt" + "github.com/docker/docker/plugin/v2" "github.com/opencontainers/runtime-spec/specs-go" ) -func (pm *Manager) enable(p *plugin, force bool) error { +func (pm *Manager) enable(p *v2.Plugin, force bool) error { return fmt.Errorf("Not implemented") } -func (pm *Manager) initSpec(p *plugin) (*specs.Spec, error) { +func (pm *Manager) initSpec(p *v2.Plugin) (*specs.Spec, error) { return nil, fmt.Errorf("Not implemented") } -func (pm *Manager) disable(p *plugin) error { +func (pm *Manager) disable(p *v2.Plugin) error { return fmt.Errorf("Not implemented") } -func (pm *Manager) restore(p *plugin) error { +func (pm *Manager) restore(p *v2.Plugin) error { return fmt.Errorf("Not implemented") } diff --git a/plugin/store/interface.go b/plugin/store/interface.go new file mode 100644 index 0000000000..a6f4f883a8 --- /dev/null +++ b/plugin/store/interface.go @@ -0,0 +1,10 @@ +package store + +import "github.com/docker/docker/pkg/plugins" + +// CompatPlugin is a abstraction to handle both new and legacy (v1) plugins. +type CompatPlugin interface { + Client() *plugins.Client + Name() string + IsLegacy() bool +} diff --git a/plugin/legacy.go b/plugin/store/legacy.go similarity index 60% rename from plugin/legacy.go rename to plugin/store/legacy.go index 8ea4c0da96..ecd3f045e4 100644 --- a/plugin/legacy.go +++ b/plugin/store/legacy.go @@ -1,16 +1,18 @@ // +build !experimental -package plugin +package store -import "github.com/docker/docker/pkg/plugins" +import ( + "github.com/docker/docker/pkg/plugins" +) // FindWithCapability returns a list of plugins matching the given capability. -func FindWithCapability(capability string) ([]Plugin, error) { +func FindWithCapability(capability string) ([]CompatPlugin, error) { pl, err := plugins.GetAll(capability) if err != nil { return nil, err } - result := make([]Plugin, len(pl)) + result := make([]CompatPlugin, len(pl)) for i, p := range pl { result[i] = p } @@ -18,6 +20,6 @@ func FindWithCapability(capability string) ([]Plugin, error) { } // LookupWithCapability returns a plugin matching the given name and capability. -func LookupWithCapability(name, capability string) (Plugin, error) { +func LookupWithCapability(name, capability string) (CompatPlugin, error) { return plugins.Get(name, capability) } diff --git a/plugin/store/store.go b/plugin/store/store.go new file mode 100644 index 0000000000..667e62b7ab --- /dev/null +++ b/plugin/store/store.go @@ -0,0 +1,224 @@ +// +build experimental + +package store + +import ( + "encoding/json" + "fmt" + "path/filepath" + "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" +) + +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 + nameToID map[string]string + plugindb string +} + +// NewPluginStore creates a PluginStore. +func NewPluginStore(libRoot string) *PluginStore { + store = &PluginStore{ + plugins: make(map[string]*v2.Plugin), + 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) + 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) (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 { + 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) + } + } + return result, nil +} diff --git a/plugin/v2/plugin.go b/plugin/v2/plugin.go new file mode 100644 index 0000000000..5e6d643136 --- /dev/null +++ b/plugin/v2/plugin.go @@ -0,0 +1,261 @@ +// +build experimental + +package v2 + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/docker/docker/pkg/plugins" + "github.com/docker/docker/pkg/system" + "github.com/docker/docker/restartmanager" + "github.com/docker/engine-api/types" + "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 + PluginObj types.Plugin `json:"plugin"` + PClient *plugins.Client `json:"-"` + RestartManager restartmanager.RestartManager `json:"-"` + RuntimeSourcePath string `json:"-"` + ExitChan chan bool `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 +} diff --git a/volume/drivers/extpoint.go b/volume/drivers/extpoint.go index 98fb06a24c..6048f6e5b9 100644 --- a/volume/drivers/extpoint.go +++ b/volume/drivers/extpoint.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/docker/docker/pkg/locker" - "github.com/docker/docker/plugin" + pluginStore "github.com/docker/docker/plugin/store" "github.com/docker/docker/volume" ) @@ -102,7 +102,7 @@ func lookup(name string) (volume.Driver, error) { return ext, nil } - p, err := plugin.LookupWithCapability(name, extName) + p, err := pluginStore.LookupWithCapability(name, extName) if err != nil { return nil, fmt.Errorf("Error looking up volume plugin %s: %v", name, err) } @@ -151,7 +151,7 @@ func GetDriverList() []string { // GetAllDrivers lists all the registered drivers func GetAllDrivers() ([]volume.Driver, error) { - plugins, err := plugin.FindWithCapability(extName) + plugins, err := pluginStore.FindWithCapability(extName) if err != nil { return nil, fmt.Errorf("error listing plugins: %v", err) }