Currently the metrics plugin uses a really hackish host mount with propagated mounts to get the metrics socket into a plugin after the plugin is alreay running. This approach ends up leaking mounts which requires setting the plugin manager root to private, which causes some other issues. With this change, plugin subsystems can register a set of modifiers to apply to the plugin's runtime spec before the plugin is ever started. This will help to generalize some of the customization work that needs to happen for various plugin subsystems (and future ones). Specifically it lets the metrics plugin subsystem append a mount to the runtime spec to mount the metrics socket in the plugin's mount namespace rather than the host's and prevetns any leaking due to this mount. Signed-off-by: Brian Goff <cpuguy83@gmail.com> Upstream-commit: 426e610e43179d58b29c496bc79a53f410a4b1e1 Component: engine
292 lines
7.5 KiB
Go
292 lines
7.5 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/docker/docker/pkg/plugingetter"
|
|
"github.com/docker/docker/pkg/plugins"
|
|
"github.com/docker/docker/plugin/v2"
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"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()
|
|
|
|
for _, p := range plugins {
|
|
ps.setSpecOpts(p)
|
|
}
|
|
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
|
|
}
|
|
|
|
func (ps *Store) setSpecOpts(p *v2.Plugin) {
|
|
var specOpts []SpecOpt
|
|
for _, typ := range p.GetTypes() {
|
|
opts, ok := ps.specOpts[typ.String()]
|
|
if ok {
|
|
specOpts = append(specOpts, opts...)
|
|
}
|
|
}
|
|
|
|
p.SetSpecOptModifier(func(s *specs.Spec) {
|
|
for _, o := range specOpts {
|
|
o(s)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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.setSpecOpts(p)
|
|
|
|
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(errdefs.System(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(errdefs.System(err), "legacy plugin")
|
|
}
|
|
for _, p := range pl {
|
|
result = append(result, p)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func pluginType(cap string) string {
|
|
return fmt.Sprintf("docker.%s/%s", strings.ToLower(cap), defaultAPIVersion)
|
|
}
|
|
|
|
// 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)) {
|
|
typ := pluginType(capability)
|
|
|
|
// Register callback with new plugin model.
|
|
ps.Lock()
|
|
handlers, ok := ps.handlers[typ]
|
|
if !ok {
|
|
handlers = []func(string, *plugins.Client){}
|
|
}
|
|
handlers = append(handlers, callback)
|
|
ps.handlers[typ] = handlers
|
|
ps.Unlock()
|
|
|
|
// Register callback with legacy plugin model.
|
|
if allowV1PluginsFallback {
|
|
plugins.Handle(capability, callback)
|
|
}
|
|
}
|
|
|
|
// RegisterRuntimeOpt stores a list of SpecOpts for the provided capability.
|
|
// These options are applied to the runtime spec before a plugin is started for the specified capability.
|
|
func (ps *Store) RegisterRuntimeOpt(cap string, opts ...SpecOpt) {
|
|
ps.Lock()
|
|
defer ps.Unlock()
|
|
typ := pluginType(cap)
|
|
ps.specOpts[typ] = append(ps.specOpts[typ], opts...)
|
|
}
|
|
|
|
// 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
|
|
}
|