mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
3816b51438
In some circumstances we were not properly releasing plugin references, leading to failures in removing a plugin with no way to recover other than restarting the daemon. 1. If volume create fails (in the driver) 2. If a driver validation fails (should be rare) 3. If trying to get a plugin that does not match the passed in capability Ideally the test for 1 and 2 would just be a unit test, however the plugin interfaces are too complicated as `plugingetter` relies on github.com/pkg/plugin/Client (a concrete type), which will require spinning up services from within the unit test... it just wouldn't be a unit test at this point. I attempted to refactor this a bit, but since both libnetwork and swarmkit are reliant on `plugingetter` as well, this would not work. This really requires a re-write of the lower-level plugin management to decouple these pieces. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
253 lines
6.6 KiB
Go
253 lines
6.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/docker/docker/pkg/plugingetter"
|
|
"github.com/docker/docker/pkg/plugins"
|
|
"github.com/docker/docker/plugin/v2"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
/* 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.
|
|
*/
|
|
const allowV1PluginsFallback bool = true
|
|
|
|
/* defaultAPIVersion is the version of the plugin API for volume, network,
|
|
IPAM and authz. This is a very stable API. When we update this API, then
|
|
pluginType should include a version. e.g. "networkdriver/2.0".
|
|
*/
|
|
const defaultAPIVersion string = "1.0"
|
|
|
|
// GetV2Plugin retrieves a plugin by name, id or partial ID.
|
|
func (ps *Store) GetV2Plugin(refOrID string) (*v2.Plugin, error) {
|
|
ps.RLock()
|
|
defer ps.RUnlock()
|
|
|
|
id, err := ps.resolvePluginID(refOrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p, idOk := ps.plugins[id]
|
|
if !idOk {
|
|
return nil, errors.WithStack(errNotFound(id))
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// validateName returns error if name is already reserved. always call with lock and full name
|
|
func (ps *Store) validateName(name string) error {
|
|
for _, p := range ps.plugins {
|
|
if p.Name() == name {
|
|
return alreadyExistsError(name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAll retrieves 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) getAllByCap(capability string) []plugingetter.CompatPlugin {
|
|
ps.RLock()
|
|
defer ps.RUnlock()
|
|
|
|
result := make([]plugingetter.CompatPlugin, 0, 1)
|
|
for _, p := range ps.plugins {
|
|
if p.IsEnabled() {
|
|
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
|
|
}
|
|
|
|
// Add adds a plugin to memory and plugindb.
|
|
// An error will be returned if there is a collision.
|
|
func (ps *Store) Add(p *v2.Plugin) error {
|
|
ps.Lock()
|
|
defer ps.Unlock()
|
|
|
|
if v, exist := ps.plugins[p.GetID()]; exist {
|
|
return fmt.Errorf("plugin %q has the same ID %s as %q", p.Name(), p.GetID(), v.Name())
|
|
}
|
|
ps.plugins[p.GetID()] = p
|
|
return nil
|
|
}
|
|
|
|
// Remove removes a plugin from memory and plugindb.
|
|
func (ps *Store) Remove(p *v2.Plugin) {
|
|
ps.Lock()
|
|
delete(ps.plugins, p.GetID())
|
|
ps.Unlock()
|
|
}
|
|
|
|
// Get returns an enabled plugin matching the given name and capability.
|
|
func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlugin, error) {
|
|
// Lookup using new model.
|
|
if ps != nil {
|
|
p, err := ps.GetV2Plugin(name)
|
|
if err == nil {
|
|
if p.IsEnabled() {
|
|
fp, err := p.FilterByCap(capability)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.AddRefCount(mode)
|
|
return fp, nil
|
|
}
|
|
|
|
// Plugin was found but it is disabled, so we should not fall back to legacy plugins
|
|
// but we should error out right away
|
|
return nil, errDisabled(name)
|
|
}
|
|
if _, ok := errors.Cause(err).(errNotFound); !ok {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if !allowV1PluginsFallback {
|
|
return nil, errNotFound(name)
|
|
}
|
|
|
|
p, err := plugins.Get(name, capability)
|
|
if err == nil {
|
|
return p, nil
|
|
}
|
|
if errors.Cause(err) == plugins.ErrNotFound {
|
|
return nil, errNotFound(name)
|
|
}
|
|
return nil, errors.Wrap(systemError{err}, "legacy plugin")
|
|
}
|
|
|
|
// GetAllManagedPluginsByCap returns a list of managed plugins matching the given capability.
|
|
func (ps *Store) GetAllManagedPluginsByCap(capability string) []plugingetter.CompatPlugin {
|
|
return ps.getAllByCap(capability)
|
|
}
|
|
|
|
// GetAllByCap returns a list of enabled plugins matching the given capability.
|
|
func (ps *Store) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, error) {
|
|
result := make([]plugingetter.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, errors.Wrap(systemError{err}, "legacy plugin")
|
|
}
|
|
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/%s", strings.ToLower(capability), defaultAPIVersion)
|
|
|
|
// Register callback with new plugin model.
|
|
ps.Lock()
|
|
handlers, ok := ps.handlers[pluginType]
|
|
if !ok {
|
|
handlers = []func(string, *plugins.Client){}
|
|
}
|
|
handlers = append(handlers, callback)
|
|
ps.handlers[pluginType] = handlers
|
|
ps.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() {
|
|
for _, handler := range ps.handlers[typ.String()] {
|
|
handler(p.Name(), p.Client())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ps *Store) resolvePluginID(idOrName string) (string, error) {
|
|
ps.RLock() // todo: fix
|
|
defer ps.RUnlock()
|
|
|
|
if validFullID.MatchString(idOrName) {
|
|
return idOrName, nil
|
|
}
|
|
|
|
ref, err := reference.ParseNormalizedNamed(idOrName)
|
|
if err != nil {
|
|
return "", errors.WithStack(errNotFound(idOrName))
|
|
}
|
|
if _, ok := ref.(reference.Canonical); ok {
|
|
logrus.Warnf("canonical references cannot be resolved: %v", reference.FamiliarString(ref))
|
|
return "", errors.WithStack(errNotFound(idOrName))
|
|
}
|
|
|
|
ref = reference.TagNameOnly(ref)
|
|
|
|
for _, p := range ps.plugins {
|
|
if p.PluginObj.Name == reference.FamiliarString(ref) {
|
|
return p.PluginObj.ID, nil
|
|
}
|
|
}
|
|
|
|
var found *v2.Plugin
|
|
for id, p := range ps.plugins { // this can be optimized
|
|
if strings.HasPrefix(id, idOrName) {
|
|
if found != nil {
|
|
return "", errors.WithStack(errAmbiguous(idOrName))
|
|
}
|
|
found = p
|
|
}
|
|
}
|
|
if found == nil {
|
|
return "", errors.WithStack(errNotFound(idOrName))
|
|
}
|
|
return found.PluginObj.ID, nil
|
|
}
|