| package v2 |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "github.com/docker/docker/api/types" |
| "github.com/docker/docker/oci" |
| "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:"-"` |
| TimeoutInSecs int `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)) |
| p.PluginObj.Settings.Devices = make([]types.PluginDevice, 0, len(p.PluginObj.Config.Linux.Devices)) |
| copy(p.PluginObj.Settings.Devices, p.PluginObj.Config.Linux.Devices) |
| 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.Linux.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 { |
| c := p.PluginObj.Config |
| var privileges types.PluginPrivileges |
| if c.Network.Type != "null" && c.Network.Type != "bridge" { |
| privileges = append(privileges, types.PluginPrivilege{ |
| Name: "network", |
| Description: "permissions to access a network", |
| Value: []string{c.Network.Type}, |
| }) |
| } |
| for _, mount := range c.Mounts { |
| if mount.Source != nil { |
| privileges = append(privileges, types.PluginPrivilege{ |
| Name: "mount", |
| Description: "host path to mount", |
| Value: []string{*mount.Source}, |
| }) |
| } |
| } |
| for _, device := range c.Linux.Devices { |
| if device.Path != nil { |
| privileges = append(privileges, types.PluginPrivilege{ |
| Name: "device", |
| Description: "host device to access", |
| Value: []string{*device.Path}, |
| }) |
| } |
| } |
| if c.Linux.DeviceCreation { |
| privileges = append(privileges, types.PluginPrivilege{ |
| Name: "device-creation", |
| Description: "allow creating devices inside plugin", |
| Value: []string{"true"}, |
| }) |
| } |
| if len(c.Linux.Capabilities) > 0 { |
| privileges = append(privileges, types.PluginPrivilege{ |
| Name: "capabilities", |
| Description: "list of additional capabilities required", |
| Value: c.Linux.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? |
| } |
| |
| userMounts := make(map[string]struct{}, len(p.PluginObj.Config.Mounts)) |
| for _, m := range p.PluginObj.Config.Mounts { |
| userMounts[m.Destination] = struct{}{} |
| } |
| |
| mounts := append(p.PluginObj.Config.Mounts, types.PluginMount{ |
| Source: &p.RuntimeSourcePath, |
| Destination: defaultPluginRuntimeDestination, |
| Type: "bind", |
| Options: []string{"rbind", "rshared"}, |
| }) |
| |
| if p.PluginObj.Config.Network.Type != "" { |
| // TODO: if net == bridge, use libnetwork controller to create a new plugin-specific bridge, bind mount /etc/hosts and /etc/resolv.conf look at the docker code (allocateNetwork, initialize) |
| if p.PluginObj.Config.Network.Type == "host" { |
| oci.RemoveNamespace(&s, specs.NamespaceType("network")) |
| } |
| etcHosts := "/etc/hosts" |
| resolvConf := "/etc/resolv.conf" |
| mounts = append(mounts, |
| types.PluginMount{ |
| Source: &etcHosts, |
| Destination: etcHosts, |
| Type: "bind", |
| Options: []string{"rbind", "ro"}, |
| }, |
| types.PluginMount{ |
| Source: &resolvConf, |
| Destination: resolvConf, |
| Type: "bind", |
| Options: []string{"rbind", "ro"}, |
| }) |
| } |
| |
| 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) |
| } |
| |
| for i, m := range s.Mounts { |
| if strings.HasPrefix(m.Destination, "/dev/") { |
| if _, ok := userMounts[m.Destination]; ok { |
| s.Mounts = append(s.Mounts[:i], s.Mounts[i+1:]...) |
| } |
| } |
| } |
| |
| if p.PluginObj.Config.Linux.DeviceCreation { |
| rwm := "rwm" |
| s.Linux.Resources.Devices = []specs.DeviceCgroup{{Allow: true, Access: &rwm}} |
| } |
| for _, dev := range p.PluginObj.Config.Linux.Devices { |
| path := *dev.Path |
| d, dPermissions, err := oci.DevicesFromPath(path, path, "rwm") |
| if err != nil { |
| return nil, err |
| } |
| s.Linux.Devices = append(s.Linux.Devices, d...) |
| s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, dPermissions...) |
| } |
| |
| 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.Terminal = false |
| s.Process.Args = args |
| s.Process.Cwd = cwd |
| s.Process.Env = envs |
| |
| s.Process.Capabilities = append(s.Process.Capabilities, p.PluginObj.Config.Linux.Capabilities...) |
| |
| return &s, nil |
| } |