| package v2 |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "github.com/docker/docker/api/types" |
| "github.com/docker/docker/pkg/plugins" |
| "github.com/docker/docker/pkg/system" |
| specs "github.com/opencontainers/runtime-spec/specs-go" |
| ) |
| |
| // Plugin represents an individual plugin. |
| type Plugin struct { |
| sync.RWMutex |
| PluginObj types.Plugin `json:"plugin"` |
| PClient *plugins.Client `json:"-"` |
| RuntimeSourcePath string `json:"-"` |
| RefCount int `json:"-"` |
| Restart bool `json:"-"` |
| ExitChan chan bool `json:"-"` |
| LibRoot string `json:"-"` |
| } |
| |
| const defaultPluginRuntimeDestination = "/run/docker/plugins" |
| |
| // ErrInadequateCapability indicates that the plugin did not have the requested capability. |
| type ErrInadequateCapability struct { |
| cap string |
| } |
| |
| func (e ErrInadequateCapability) Error() string { |
| return fmt.Sprintf("plugin does not provide %q capability", e.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, libRoot, tag string) *Plugin { |
| return &Plugin{ |
| PluginObj: newPluginObj(name, id, tag), |
| RuntimeSourcePath: filepath.Join(runRoot, id), |
| LibRoot: libRoot, |
| } |
| } |
| |
| // 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.Config.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 config file. |
| func (p *Plugin) InitPlugin() error { |
| dt, err := os.Open(filepath.Join(p.LibRoot, p.PluginObj.ID, "config.json")) |
| if err != nil { |
| return err |
| } |
| err = json.NewDecoder(dt).Decode(&p.PluginObj.Config) |
| dt.Close() |
| if err != nil { |
| return err |
| } |
| |
| p.PluginObj.Settings.Mounts = make([]types.PluginMount, len(p.PluginObj.Config.Mounts)) |
| for i, mount := range p.PluginObj.Config.Mounts { |
| p.PluginObj.Settings.Mounts[i] = mount |
| } |
| p.PluginObj.Settings.Env = make([]string, 0, len(p.PluginObj.Config.Env)) |
| for _, env := range p.PluginObj.Config.Env { |
| if env.Value != nil { |
| p.PluginObj.Settings.Env = append(p.PluginObj.Settings.Env, fmt.Sprintf("%s=%s", env.Name, *env.Value)) |
| } |
| } |
| copy(p.PluginObj.Settings.Args, p.PluginObj.Config.Args.Value) |
| |
| return p.writeSettings() |
| } |
| |
| func (p *Plugin) writeSettings() error { |
| f, err := os.Create(filepath.Join(p.LibRoot, p.PluginObj.ID, "plugin-settings.json")) |
| if err != nil { |
| return err |
| } |
| err = json.NewEncoder(f).Encode(&p.PluginObj.Settings) |
| f.Close() |
| return err |
| } |
| |
| // Set is used to pass arguments to the plugin. |
| func (p *Plugin) Set(args []string) error { |
| p.Lock() |
| defer p.Unlock() |
| |
| if p.PluginObj.Enabled { |
| return fmt.Errorf("cannot set on an active plugin, disable plugin before setting") |
| } |
| |
| sets, err := newSettables(args) |
| if err != nil { |
| return err |
| } |
| |
| // TODO(vieux): lots of code duplication here, needs to be refactored. |
| |
| next: |
| for _, s := range sets { |
| // range over all the envs in the config |
| for _, env := range p.PluginObj.Config.Env { |
| // found the env in the config |
| if env.Name == s.name { |
| // is it settable ? |
| if ok, err := s.isSettable(allowedSettableFieldsEnv, env.Settable); err != nil { |
| return err |
| } else if !ok { |
| return fmt.Errorf("%q is not settable", s.prettyName()) |
| } |
| // is it, so lets update the settings in memory |
| updateSettingsEnv(&p.PluginObj.Settings.Env, &s) |
| continue next |
| } |
| } |
| |
| // range over all the mounts in the config |
| for _, mount := range p.PluginObj.Config.Mounts { |
| // found the mount in the config |
| if mount.Name == s.name { |
| // is it settable ? |
| if ok, err := s.isSettable(allowedSettableFieldsMounts, mount.Settable); err != nil { |
| return err |
| } else if !ok { |
| return fmt.Errorf("%q is not settable", s.prettyName()) |
| } |
| |
| // it is, so lets update the settings in memory |
| *mount.Source = s.value |
| continue next |
| } |
| } |
| |
| // range over all the devices in the config |
| for _, device := range p.PluginObj.Config.Devices { |
| // found the device in the config |
| if device.Name == s.name { |
| // is it settable ? |
| if ok, err := s.isSettable(allowedSettableFieldsDevices, device.Settable); err != nil { |
| return err |
| } else if !ok { |
| return fmt.Errorf("%q is not settable", s.prettyName()) |
| } |
| |
| // it is, so lets update the settings in memory |
| *device.Path = s.value |
| continue next |
| } |
| } |
| |
| // found the name in the config |
| if p.PluginObj.Config.Args.Name == s.name { |
| // is it settable ? |
| if ok, err := s.isSettable(allowedSettableFieldsArgs, p.PluginObj.Config.Args.Settable); err != nil { |
| return err |
| } else if !ok { |
| return fmt.Errorf("%q is not settable", s.prettyName()) |
| } |
| |
| // it is, so lets update the settings in memory |
| p.PluginObj.Settings.Args = strings.Split(s.value, " ") |
| continue next |
| } |
| |
| return fmt.Errorf("setting %q not found in the plugin configuration", s.name) |
| } |
| |
| // update the settings on disk |
| return p.writeSettings() |
| } |
| |
| // ComputePrivileges takes the config file and computes the list of access necessary |
| // for the plugin on the host. |
| func (p *Plugin) ComputePrivileges() types.PluginPrivileges { |
| m := p.PluginObj.Config |
| 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.Config.Interface.Socket |
| } |
| |
| // GetTypes returns the interface types of a plugin. |
| func (p *Plugin) GetTypes() []types.PluginInterfaceType { |
| p.RLock() |
| defer p.RUnlock() |
| |
| return p.PluginObj.Config.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 config? |
| } |
| |
| mounts := append(p.PluginObj.Settings.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.Settings.Env)+1) |
| envs[0] = "PATH=" + system.DefaultPathEnv |
| envs = append(envs, p.PluginObj.Settings.Env...) |
| |
| args := append(p.PluginObj.Config.Entrypoint, p.PluginObj.Settings.Args...) |
| cwd := p.PluginObj.Config.Workdir |
| if len(cwd) == 0 { |
| cwd = "/" |
| } |
| s.Process = specs.Process{ |
| Terminal: false, |
| Args: args, |
| Cwd: cwd, |
| Env: envs, |
| } |
| |
| return &s, nil |
| } |