dotcloud/docker

View on GitHub
plugin/store.go

Summary

Maintainability
A
1 hr
Test Coverage
package plugin // import "github.com/docker/docker/plugin"

import (
    "context"
    "fmt"
    "strings"

    "github.com/containerd/log"
    "github.com/distribution/reference"
    "github.com/docker/docker/errdefs"
    "github.com/docker/docker/pkg/plugingetter"
    "github.com/docker/docker/pkg/plugins"
    v2 "github.com/docker/docker/plugin/v2"
    specs "github.com/opencontainers/runtime-spec/specs-go"
    "github.com/pkg/errors"
)

// 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 = 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 = "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)
        }
        var ierr errNotFound
        if !errors.As(err, &ierr) {
            return nil, err
        }
    }

    if !allowV1PluginsFallback {
        return nil, errNotFound(name)
    }

    p, err := plugins.Get(name, capability)
    if err == nil {
        return p, nil
    }
    if errors.Is(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 {
        result = ps.getAllByCap(capability)
    }

    // Lookup with legacy model
    if allowV1PluginsFallback {
        l := plugins.NewLocalRegistry()
        pl, err := l.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()) //nolint:staticcheck // FIXME(thaJeztah): p.Client is deprecated: use p.Addr() and manually create the client
        }
    }
}

// resolvePluginID must be protected by ps.RLock
func (ps *Store) resolvePluginID(idOrName string) (string, error) {
    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 {
        log.G(context.TODO()).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
}