diff --git a/api/swagger.yaml b/api/swagger.yaml index a8daf61f02..8d46ecec56 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1329,9 +1329,8 @@ definitions: - Entrypoint - Workdir - Network - - Capabilities + - Linux - Mounts - - Devices - Env - Args properties: @@ -1379,18 +1378,26 @@ definitions: Type: x-nullable: false type: "string" - Capabilities: - type: "array" - items: - type: "string" + Linux: + type: "object" + x-nullable: false + required: [Capabilities, DeviceCreation, Devices] + properties: + Capabilities: + type: "array" + items: + type: "string" + DeviceCreation: + type: "boolean" + x-nullable: false + Devices: + type: "array" + items: + $ref: "#/definitions/PluginDevice" Mounts: type: "array" items: $ref: "#/definitions/PluginMount" - Devices: - type: "array" - items: - $ref: "#/definitions/PluginDevice" Env: type: "array" items: diff --git a/api/types/plugin.go b/api/types/plugin.go index 2c3222ae2a..2bd2a6eeac 100644 --- a/api/types/plugin.go +++ b/api/types/plugin.go @@ -39,18 +39,10 @@ type PluginConfig struct { // Required: true Args PluginConfigArgs `json:"Args"` - // capabilities - // Required: true - Capabilities []string `json:"Capabilities"` - // description // Required: true Description string `json:"Description"` - // devices - // Required: true - Devices []PluginDevice `json:"Devices"` - // documentation // Required: true Documentation string `json:"Documentation"` @@ -67,6 +59,10 @@ type PluginConfig struct { // Required: true Interface PluginConfigInterface `json:"Interface"` + // linux + // Required: true + Linux PluginConfigLinux `json:"Linux"` + // mounts // Required: true Mounts []PluginMount `json:"Mounts"` @@ -117,6 +113,23 @@ type PluginConfigInterface struct { Types []PluginInterfaceType `json:"Types"` } +// PluginConfigLinux plugin config linux +// swagger:model PluginConfigLinux +type PluginConfigLinux struct { + + // capabilities + // Required: true + Capabilities []string `json:"Capabilities"` + + // device creation + // Required: true + DeviceCreation bool `json:"DeviceCreation"` + + // devices + // Required: true + Devices []PluginDevice `json:"Devices"` +} + // PluginConfigNetwork plugin config network // swagger:model PluginConfigNetwork type PluginConfigNetwork struct { diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go index a55c688334..2296045765 100644 --- a/daemon/container_operations_unix.go +++ b/daemon/container_operations_unix.go @@ -8,13 +8,11 @@ import ( "os" "path/filepath" "strconv" - "strings" "syscall" "time" "github.com/Sirupsen/logrus" "github.com/cloudflare/cfssl/log" - containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/container" "github.com/docker/docker/daemon/links" "github.com/docker/docker/pkg/idtools" @@ -22,16 +20,10 @@ import ( "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/runconfig" "github.com/docker/libnetwork" - "github.com/opencontainers/runc/libcontainer/configs" - "github.com/opencontainers/runc/libcontainer/devices" "github.com/opencontainers/runc/libcontainer/label" - "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) -func u32Ptr(i int64) *uint32 { u := uint32(i); return &u } -func fmPtr(i int64) *os.FileMode { fm := os.FileMode(i); return &fm } - func (daemon *Daemon) setupLinkedContainers(container *container.Container) ([]string, error) { var env []string children := daemon.children(container) @@ -247,78 +239,6 @@ func killProcessDirectly(container *container.Container) error { return nil } -func specDevice(d *configs.Device) specs.Device { - return specs.Device{ - Type: string(d.Type), - Path: d.Path, - Major: d.Major, - Minor: d.Minor, - FileMode: fmPtr(int64(d.FileMode)), - UID: u32Ptr(int64(d.Uid)), - GID: u32Ptr(int64(d.Gid)), - } -} - -func specDeviceCgroup(d *configs.Device) specs.DeviceCgroup { - t := string(d.Type) - return specs.DeviceCgroup{ - Allow: true, - Type: &t, - Major: &d.Major, - Minor: &d.Minor, - Access: &d.Permissions, - } -} - -func getDevicesFromPath(deviceMapping containertypes.DeviceMapping) (devs []specs.Device, devPermissions []specs.DeviceCgroup, err error) { - resolvedPathOnHost := deviceMapping.PathOnHost - - // check if it is a symbolic link - if src, e := os.Lstat(deviceMapping.PathOnHost); e == nil && src.Mode()&os.ModeSymlink == os.ModeSymlink { - if linkedPathOnHost, e := filepath.EvalSymlinks(deviceMapping.PathOnHost); e == nil { - resolvedPathOnHost = linkedPathOnHost - } - } - - device, err := devices.DeviceFromPath(resolvedPathOnHost, deviceMapping.CgroupPermissions) - // if there was no error, return the device - if err == nil { - device.Path = deviceMapping.PathInContainer - return append(devs, specDevice(device)), append(devPermissions, specDeviceCgroup(device)), nil - } - - // if the device is not a device node - // try to see if it's a directory holding many devices - if err == devices.ErrNotADevice { - - // check if it is a directory - if src, e := os.Stat(resolvedPathOnHost); e == nil && src.IsDir() { - - // mount the internal devices recursively - filepath.Walk(resolvedPathOnHost, func(dpath string, f os.FileInfo, e error) error { - childDevice, e := devices.DeviceFromPath(dpath, deviceMapping.CgroupPermissions) - if e != nil { - // ignore the device - return nil - } - - // add the device to userSpecified devices - childDevice.Path = strings.Replace(dpath, resolvedPathOnHost, deviceMapping.PathInContainer, 1) - devs = append(devs, specDevice(childDevice)) - devPermissions = append(devPermissions, specDeviceCgroup(childDevice)) - - return nil - }) - } - } - - if len(devs) > 0 { - return devs, devPermissions, nil - } - - return devs, devPermissions, fmt.Errorf("error gathering device information while adding custom device %q: %s", deviceMapping.PathOnHost, err) -} - func detachMounted(path string) error { return syscall.Unmount(path, syscall.MNT_DETACH) } diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index 5a2158c222..bf21df86c4 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -88,7 +88,7 @@ func setDevices(s *specs.Spec, c *container.Container) error { return err } for _, d := range hostDevices { - devs = append(devs, specDevice(d)) + devs = append(devs, oci.Device(d)) } rwm := "rwm" devPermissions = []specs.DeviceCgroup{ @@ -99,7 +99,7 @@ func setDevices(s *specs.Spec, c *container.Container) error { } } else { for _, deviceMapping := range c.HostConfig.Devices { - d, dPermissions, err := getDevicesFromPath(deviceMapping) + d, dPermissions, err := oci.DevicesFromPath(deviceMapping.PathOnHost, deviceMapping.PathInContainer, deviceMapping.CgroupPermissions) if err != nil { return err } @@ -221,18 +221,6 @@ func setCapabilities(s *specs.Spec, c *container.Container) error { return nil } -func delNamespace(s *specs.Spec, nsType specs.NamespaceType) { - idx := -1 - for i, n := range s.Linux.Namespaces { - if n.Type == nsType { - idx = i - } - } - if idx >= 0 { - s.Linux.Namespaces = append(s.Linux.Namespaces[:idx], s.Linux.Namespaces[idx+1:]...) - } -} - func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error { userNS := false // user @@ -283,7 +271,7 @@ func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error setNamespace(s, nsUser) } } else if c.HostConfig.IpcMode.IsHost() { - delNamespace(s, specs.NamespaceType("ipc")) + oci.RemoveNamespace(s, specs.NamespaceType("ipc")) } else { ns := specs.Namespace{Type: "ipc"} setNamespace(s, ns) @@ -304,14 +292,14 @@ func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error setNamespace(s, nsUser) } } else if c.HostConfig.PidMode.IsHost() { - delNamespace(s, specs.NamespaceType("pid")) + oci.RemoveNamespace(s, specs.NamespaceType("pid")) } else { ns := specs.Namespace{Type: "pid"} setNamespace(s, ns) } // uts if c.HostConfig.UTSMode.IsHost() { - delNamespace(s, specs.NamespaceType("uts")) + oci.RemoveNamespace(s, specs.NamespaceType("uts")) s.Hostname = "" } diff --git a/docs/extend/config.md b/docs/extend/config.md index 329af250eb..53f52f7b66 100644 --- a/docs/extend/config.md +++ b/docs/extend/config.md @@ -16,6 +16,7 @@ keywords: "API, Usage, plugins, documentation, developer" will be rejected. --> + # Plugin Config Version 0 of Plugin V2 This document outlines the format of the V0 plugin configuration. The plugin @@ -85,10 +86,6 @@ Config provides the base accessible fields for working with V0 plugin format - **host** - **none** -- **`capabilities`** *array* - - capabilities of the plugin (*Linux only*), see list [`here`](https://github.com/opencontainers/runc/blob/master/libcontainer/SPEC.md#security) - - **`mounts`** *PluginMount array* mount of the plugin, struct consisting of the following fields, see [`MOUNTS`](https://github.com/opencontainers/runtime-spec/blob/master/config.md#mounts) @@ -117,22 +114,6 @@ Config provides the base accessible fields for working with V0 plugin format options of the mount. -- **`devices`** *PluginDevice array* - - device of the plugin, (*Linux only*), struct consisting of the following fields, see [`DEVICES`](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#devices) - - - **`name`** *string* - - name of the device. - - - **`description`** *string* - - description of the device. - - - **`path`** *string* - - path of the device. - - **`env`** *PluginEnv array* env of the plugin, struct consisting of the following fields @@ -165,6 +146,27 @@ Config provides the base accessible fields for working with V0 plugin format values of the args. +- **`linux`** *PluginLinux* + + - **`capabilities`** *string array* + + capabilities of the plugin (*Linux only*), see list [`here`](https://github.com/opencontainers/runc/blob/master/libcontainer/SPEC.md#security) + + - **`devices`** *PluginDevice array* + + device of the plugin, (*Linux only*), struct consisting of the following fields, see [`DEVICES`](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#devices) + + - **`name`** *string* + + name of the device. + + - **`description`** *string* + + description of the device. + + - **`path`** *string* + + path of the device. ## Example Config diff --git a/integration-cli/docker_cli_daemon_plugins_test.go b/integration-cli/docker_cli_daemon_plugins_test.go index 2f55724cd2..8edc3e66fc 100644 --- a/integration-cli/docker_cli_daemon_plugins_test.go +++ b/integration-cli/docker_cli_daemon_plugins_test.go @@ -13,8 +13,6 @@ import ( "github.com/go-check/check" ) -var pluginName = "tiborvass/no-remove" - // TestDaemonRestartWithPluginEnabled tests state restore for an enabled plugin func (s *DockerDaemonSuite) TestDaemonRestartWithPluginEnabled(c *check.C) { testRequires(c, Network) @@ -23,15 +21,15 @@ func (s *DockerDaemonSuite) TestDaemonRestartWithPluginEnabled(c *check.C) { c.Fatalf("Could not start daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pName); err != nil { c.Fatalf("Could not install plugin: %v %s", err, out) } defer func() { - if out, err := s.d.Cmd("plugin", "disable", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "disable", pName); err != nil { c.Fatalf("Could not disable plugin: %v %s", err, out) } - if out, err := s.d.Cmd("plugin", "remove", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "remove", pName); err != nil { c.Fatalf("Could not remove plugin: %v %s", err, out) } }() @@ -44,7 +42,7 @@ func (s *DockerDaemonSuite) TestDaemonRestartWithPluginEnabled(c *check.C) { if err != nil { c.Fatalf("Could not list plugins: %v %s", err, out) } - c.Assert(out, checker.Contains, pluginName) + c.Assert(out, checker.Contains, pName) c.Assert(out, checker.Contains, "true") } @@ -56,12 +54,12 @@ func (s *DockerDaemonSuite) TestDaemonRestartWithPluginDisabled(c *check.C) { c.Fatalf("Could not start daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pluginName, "--disable"); err != nil { + if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pName, "--disable"); err != nil { c.Fatalf("Could not install plugin: %v %s", err, out) } defer func() { - if out, err := s.d.Cmd("plugin", "remove", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "remove", pName); err != nil { c.Fatalf("Could not remove plugin: %v %s", err, out) } }() @@ -74,7 +72,7 @@ func (s *DockerDaemonSuite) TestDaemonRestartWithPluginDisabled(c *check.C) { if err != nil { c.Fatalf("Could not list plugins: %v %s", err, out) } - c.Assert(out, checker.Contains, pluginName) + c.Assert(out, checker.Contains, pName) c.Assert(out, checker.Contains, "false") } @@ -86,17 +84,17 @@ func (s *DockerDaemonSuite) TestDaemonKillLiveRestoreWithPlugins(c *check.C) { if err := s.d.Start("--live-restore"); err != nil { c.Fatalf("Could not start daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pName); err != nil { c.Fatalf("Could not install plugin: %v %s", err, out) } defer func() { if err := s.d.Restart("--live-restore"); err != nil { c.Fatalf("Could not restart daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "disable", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "disable", pName); err != nil { c.Fatalf("Could not disable plugin: %v %s", err, out) } - if out, err := s.d.Cmd("plugin", "remove", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "remove", pName); err != nil { c.Fatalf("Could not remove plugin: %v %s", err, out) } }() @@ -105,7 +103,7 @@ func (s *DockerDaemonSuite) TestDaemonKillLiveRestoreWithPlugins(c *check.C) { c.Fatalf("Could not kill daemon: %v", err) } - cmd := exec.Command("pgrep", "-f", "plugin-no-remove") + cmd := exec.Command("pgrep", "-f", pluginProcessName) if out, ec, err := runCommandWithOutput(cmd); ec != 0 { c.Fatalf("Expected exit code '0', got %d err: %v output: %s ", ec, err, out) } @@ -119,17 +117,17 @@ func (s *DockerDaemonSuite) TestDaemonShutdownLiveRestoreWithPlugins(c *check.C) if err := s.d.Start("--live-restore"); err != nil { c.Fatalf("Could not start daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pName); err != nil { c.Fatalf("Could not install plugin: %v %s", err, out) } defer func() { if err := s.d.Restart("--live-restore"); err != nil { c.Fatalf("Could not restart daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "disable", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "disable", pName); err != nil { c.Fatalf("Could not disable plugin: %v %s", err, out) } - if out, err := s.d.Cmd("plugin", "remove", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "remove", pName); err != nil { c.Fatalf("Could not remove plugin: %v %s", err, out) } }() @@ -138,7 +136,7 @@ func (s *DockerDaemonSuite) TestDaemonShutdownLiveRestoreWithPlugins(c *check.C) c.Fatalf("Could not kill daemon: %v", err) } - cmd := exec.Command("pgrep", "-f", "plugin-no-remove") + cmd := exec.Command("pgrep", "-f", pluginProcessName) if out, ec, err := runCommandWithOutput(cmd); ec != 0 { c.Fatalf("Expected exit code '0', got %d err: %v output: %s ", ec, err, out) } @@ -151,7 +149,7 @@ func (s *DockerDaemonSuite) TestDaemonShutdownWithPlugins(c *check.C) { if err := s.d.Start(); err != nil { c.Fatalf("Could not start daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", pName); err != nil { c.Fatalf("Could not install plugin: %v %s", err, out) } @@ -159,10 +157,10 @@ func (s *DockerDaemonSuite) TestDaemonShutdownWithPlugins(c *check.C) { if err := s.d.Restart(); err != nil { c.Fatalf("Could not restart daemon: %v", err) } - if out, err := s.d.Cmd("plugin", "disable", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "disable", pName); err != nil { c.Fatalf("Could not disable plugin: %v %s", err, out) } - if out, err := s.d.Cmd("plugin", "remove", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "remove", pName); err != nil { c.Fatalf("Could not remove plugin: %v %s", err, out) } }() @@ -177,7 +175,7 @@ func (s *DockerDaemonSuite) TestDaemonShutdownWithPlugins(c *check.C) { } } - cmd := exec.Command("pgrep", "-f", "plugin-no-remove") + cmd := exec.Command("pgrep", "-f", pluginProcessName) if out, ec, err := runCommandWithOutput(cmd); ec != 1 { c.Fatalf("Expected exit code '1', got %d err: %v output: %s ", ec, err, out) } @@ -195,20 +193,20 @@ func (s *DockerDaemonSuite) TestVolumePlugin(c *check.C) { if err := s.d.Start(); err != nil { c.Fatalf("Could not start daemon: %v", err) } - out, err := s.d.Cmd("plugin", "install", pluginName, "--grant-all-permissions") + out, err := s.d.Cmd("plugin", "install", pName, "--grant-all-permissions") if err != nil { c.Fatalf("Could not install plugin: %v %s", err, out) } defer func() { - if out, err := s.d.Cmd("plugin", "disable", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "disable", pName); err != nil { c.Fatalf("Could not disable plugin: %v %s", err, out) } - if out, err := s.d.Cmd("plugin", "remove", pluginName); err != nil { + if out, err := s.d.Cmd("plugin", "remove", pName); err != nil { c.Fatalf("Could not remove plugin: %v %s", err, out) } }() - out, err = s.d.Cmd("volume", "create", "-d", pluginName, volName) + out, err = s.d.Cmd("volume", "create", "-d", pName, volName) if err != nil { c.Fatalf("Could not create volume: %v %s", err, out) } @@ -223,7 +221,7 @@ func (s *DockerDaemonSuite) TestVolumePlugin(c *check.C) { c.Fatalf("Could not list volume: %v %s", err, out) } c.Assert(out, checker.Contains, volName) - c.Assert(out, checker.Contains, pluginName) + c.Assert(out, checker.Contains, pName) mountPoint, err := s.d.Cmd("volume", "inspect", volName, "--format", "{{.Mountpoint}}") if err != nil { diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index 5353c98d5e..7191376f1a 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -278,12 +278,11 @@ func (s *DockerSuite) TestEventsImageLoad(c *check.C) { func (s *DockerSuite) TestEventsPluginOps(c *check.C) { testRequires(c, DaemonIsLinux) - pluginName := "tiborvass/no-remove:latest" since := daemonUnixTime(c) - dockerCmd(c, "plugin", "install", pluginName, "--grant-all-permissions") - dockerCmd(c, "plugin", "disable", pluginName) - dockerCmd(c, "plugin", "remove", pluginName) + dockerCmd(c, "plugin", "install", pNameWithTag, "--grant-all-permissions") + dockerCmd(c, "plugin", "disable", pNameWithTag) + dockerCmd(c, "plugin", "remove", pNameWithTag) out, _ := dockerCmd(c, "events", "--since", since, "--until", daemonUnixTime(c)) events := strings.Split(out, "\n") @@ -292,7 +291,7 @@ func (s *DockerSuite) TestEventsPluginOps(c *check.C) { nEvents := len(events) c.Assert(nEvents, checker.GreaterOrEqualThan, 4) - pluginEvents := eventActionsByIDAndType(c, events, pluginName, "plugin") + pluginEvents := eventActionsByIDAndType(c, events, pNameWithTag, "plugin") c.Assert(pluginEvents, checker.HasLen, 4, check.Commentf("events: %v", events)) c.Assert(pluginEvents[0], checker.Equals, "pull", check.Commentf(out)) diff --git a/integration-cli/docker_cli_network_unix_test.go b/integration-cli/docker_cli_network_unix_test.go index e5565da208..0e3b88cad1 100644 --- a/integration-cli/docker_cli_network_unix_test.go +++ b/integration-cli/docker_cli_network_unix_test.go @@ -774,7 +774,7 @@ func (s *DockerNetworkSuite) TestDockerPluginV2NetworkDriver(c *check.C) { testRequires(c, DaemonIsLinux, Network, IsAmd64) var ( - npName = "mavenugo/test-docker-netplugin" + npName = "tiborvass/test-docker-netplugin" npTag = "latest" npNameWithTag = npName + ":" + npTag ) diff --git a/integration-cli/docker_cli_plugins_test.go b/integration-cli/docker_cli_plugins_test.go index 613f1e3496..b57ef8c53b 100644 --- a/integration-cli/docker_cli_plugins_test.go +++ b/integration-cli/docker_cli_plugins_test.go @@ -10,9 +10,10 @@ import ( ) var ( - pName = "tiborvass/no-remove" - pTag = "latest" - pNameWithTag = pName + ":" + pTag + pluginProcessName = "no-remove" + pName = "tiborvass/no-remove" + pTag = "latest" + pNameWithTag = pName + ":" + pTag ) func (s *DockerSuite) TestPluginBasicOps(c *check.C) { diff --git a/oci/devices_linux.go b/oci/devices_linux.go new file mode 100644 index 0000000000..2840d2586a --- /dev/null +++ b/oci/devices_linux.go @@ -0,0 +1,86 @@ +package oci + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/opencontainers/runc/libcontainer/configs" + "github.com/opencontainers/runc/libcontainer/devices" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// Device transforms a libcontainer configs.Device to a specs.Device object. +func Device(d *configs.Device) specs.Device { + return specs.Device{ + Type: string(d.Type), + Path: d.Path, + Major: d.Major, + Minor: d.Minor, + FileMode: fmPtr(int64(d.FileMode)), + UID: u32Ptr(int64(d.Uid)), + GID: u32Ptr(int64(d.Gid)), + } +} + +func deviceCgroup(d *configs.Device) specs.DeviceCgroup { + t := string(d.Type) + return specs.DeviceCgroup{ + Allow: true, + Type: &t, + Major: &d.Major, + Minor: &d.Minor, + Access: &d.Permissions, + } +} + +// DevicesFromPath computes a list of devices and device permissions from paths (pathOnHost and pathInContainer) and cgroup permissions. +func DevicesFromPath(pathOnHost, pathInContainer, cgroupPermissions string) (devs []specs.Device, devPermissions []specs.DeviceCgroup, err error) { + resolvedPathOnHost := pathOnHost + + // check if it is a symbolic link + if src, e := os.Lstat(pathOnHost); e == nil && src.Mode()&os.ModeSymlink == os.ModeSymlink { + if linkedPathOnHost, e := filepath.EvalSymlinks(pathOnHost); e == nil { + resolvedPathOnHost = linkedPathOnHost + } + } + + device, err := devices.DeviceFromPath(resolvedPathOnHost, cgroupPermissions) + // if there was no error, return the device + if err == nil { + device.Path = pathInContainer + return append(devs, Device(device)), append(devPermissions, deviceCgroup(device)), nil + } + + // if the device is not a device node + // try to see if it's a directory holding many devices + if err == devices.ErrNotADevice { + + // check if it is a directory + if src, e := os.Stat(resolvedPathOnHost); e == nil && src.IsDir() { + + // mount the internal devices recursively + filepath.Walk(resolvedPathOnHost, func(dpath string, f os.FileInfo, e error) error { + childDevice, e := devices.DeviceFromPath(dpath, cgroupPermissions) + if e != nil { + // ignore the device + return nil + } + + // add the device to userSpecified devices + childDevice.Path = strings.Replace(dpath, resolvedPathOnHost, pathInContainer, 1) + devs = append(devs, Device(childDevice)) + devPermissions = append(devPermissions, deviceCgroup(childDevice)) + + return nil + }) + } + } + + if len(devs) > 0 { + return devs, devPermissions, nil + } + + return devs, devPermissions, fmt.Errorf("error gathering device information while adding custom device %q: %s", pathOnHost, err) +} diff --git a/oci/devices_unsupported.go b/oci/devices_unsupported.go new file mode 100644 index 0000000000..6252cab536 --- /dev/null +++ b/oci/devices_unsupported.go @@ -0,0 +1,20 @@ +// +build !linux + +package oci + +import ( + "errors" + + "github.com/opencontainers/runc/libcontainer/configs" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// Device transforms a libcontainer configs.Device to a specs.Device object. +// Not implemented +func Device(d *configs.Device) specs.Device { return specs.Device{} } + +// DevicesFromPath computes a list of devices and device permissions from paths (pathOnHost and pathInContainer) and cgroup permissions. +// Not implemented +func DevicesFromPath(pathOnHost, pathInContainer, cgroupPermissions string) (devs []specs.Device, devPermissions []specs.DeviceCgroup, err error) { + return nil, nil, errors.New("oci/devices: unsupported platform") +} diff --git a/oci/namespaces.go b/oci/namespaces.go new file mode 100644 index 0000000000..4902482498 --- /dev/null +++ b/oci/namespaces.go @@ -0,0 +1,16 @@ +package oci + +import specs "github.com/opencontainers/runtime-spec/specs-go" + +// RemoveNamespace removes the `nsType` namespace from OCI spec `s` +func RemoveNamespace(s *specs.Spec, nsType specs.NamespaceType) { + idx := -1 + for i, n := range s.Linux.Namespaces { + if n.Type == nsType { + idx = i + } + } + if idx >= 0 { + s.Linux.Namespaces = append(s.Linux.Namespaces[:idx], s.Linux.Namespaces[idx+1:]...) + } +} diff --git a/plugin/store/store.go b/plugin/store/store.go index b2cec91afe..d764060204 100644 --- a/plugin/store/store.go +++ b/plugin/store/store.go @@ -126,7 +126,7 @@ func (ps *Store) updatePluginDB() error { return nil } -// Get returns a plugin matching the given name and capability. +// Get returns an enabled plugin matching the given name and capability. func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlugin, error) { var ( p *v2.Plugin @@ -151,7 +151,12 @@ func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlug p.Lock() p.RefCount += mode p.Unlock() - return p.FilterByCap(capability) + if p.IsEnabled() { + return p.FilterByCap(capability) + } + // 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, ErrNotFound(fullName) } if _, ok := err.(ErrNotFound); !ok { return nil, err @@ -170,7 +175,7 @@ func (ps *Store) Get(name, capability string, mode int) (plugingetter.CompatPlug return nil, err } -// GetAllByCap returns a list of plugins matching the given 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) diff --git a/plugin/v2/plugin.go b/plugin/v2/plugin.go index 56775fce62..4679498ee6 100644 --- a/plugin/v2/plugin.go +++ b/plugin/v2/plugin.go @@ -9,6 +9,7 @@ import ( "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" @@ -104,6 +105,8 @@ func (p *Plugin) InitPlugin() error { 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)) @@ -176,7 +179,7 @@ next: } // range over all the devices in the config - for _, device := range p.PluginObj.Config.Devices { + for _, device := range p.PluginObj.Config.Linux.Devices { // found the device in the config if device.Name == s.name { // is it settable ? @@ -216,38 +219,45 @@ next: // 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 + c := p.PluginObj.Config var privileges types.PluginPrivileges - if m.Network.Type != "null" && m.Network.Type != "bridge" { + if c.Network.Type != "null" && c.Network.Type != "bridge" { privileges = append(privileges, types.PluginPrivilege{ Name: "network", - Description: "", - Value: []string{m.Network.Type}, + Description: "permissions to access a network", + Value: []string{c.Network.Type}, }) } - for _, mount := range m.Mounts { + for _, mount := range c.Mounts { if mount.Source != nil { privileges = append(privileges, types.PluginPrivilege{ Name: "mount", - Description: "", + Description: "host path to mount", Value: []string{*mount.Source}, }) } } - for _, device := range m.Devices { + for _, device := range c.Linux.Devices { if device.Path != nil { privileges = append(privileges, types.PluginPrivilege{ Name: "device", - Description: "", + Description: "host device to access", Value: []string{*device.Path}, }) } } - if len(m.Capabilities) > 0 { + 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: "", - Value: m.Capabilities, + Description: "list of additional capabilities required", + Value: c.Linux.Capabilities, }) } return privileges @@ -293,12 +303,40 @@ func (p *Plugin) InitSpec(s specs.Spec, libRoot string) (*specs.Spec, error) { Readonly: false, // TODO: all plugins should be readonly? settable in config? } - mounts := append(p.PluginObj.Settings.Mounts, types.PluginMount{ + 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, @@ -323,6 +361,28 @@ func (p *Plugin) InitSpec(s specs.Spec, libRoot string) (*specs.Spec, error) { 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...) @@ -332,12 +392,12 @@ func (p *Plugin) InitSpec(s specs.Spec, libRoot string) (*specs.Spec, error) { if len(cwd) == 0 { cwd = "/" } - s.Process = specs.Process{ - Terminal: false, - Args: args, - Cwd: cwd, - Env: envs, - } + 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 } diff --git a/vendor.conf b/vendor.conf index 7e57ef4c30..55d0982056 100644 --- a/vendor.conf +++ b/vendor.conf @@ -17,7 +17,7 @@ github.com/vdemeester/shakers 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 golang.org/x/net 2beffdc2e92c8a3027590f898fe88f69af48a3f8 https://github.com/tonistiigi/net.git golang.org/x/sys 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 github.com/docker/go-units 8a7beacffa3009a9ac66bad506b18ffdd110cf97 -github.com/docker/go-connections f512407a188ecb16f31a33dbc9c4e4814afc1b03 +github.com/docker/go-connections 4ccf312bf1d35e5dbda654e57a9be4c3f3cd0366 github.com/RackSec/srslog 456df3a81436d29ba874f3590eeeee25d666f8a5 github.com/imdario/mergo 0.2.1 diff --git a/vendor/github.com/docker/go-connections/sockets/sockets.go b/vendor/github.com/docker/go-connections/sockets/sockets.go index 1739cecf2a..a1d7beb4d8 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets.go @@ -2,6 +2,7 @@ package sockets import ( + "errors" "net" "net/http" "time" @@ -10,6 +11,9 @@ import ( // Why 32? See https://github.com/docker/docker/pull/8035. const defaultTimeout = 32 * time.Second +// ErrProtocolNotAvailable is returned when a given transport protocol is not provided by the operating system. +var ErrProtocolNotAvailable = errors.New("protocol not available") + // ConfigureTransport configures the specified Transport according to the // specified proto and addr. // If the proto is unix (using a unix socket to communicate) or npipe the @@ -17,17 +21,9 @@ const defaultTimeout = 32 * time.Second func ConfigureTransport(tr *http.Transport, proto, addr string) error { switch proto { case "unix": - // No need for compression in local communications. - tr.DisableCompression = true - tr.Dial = func(_, _ string) (net.Conn, error) { - return net.DialTimeout(proto, addr, defaultTimeout) - } + return configureUnixTransport(tr, proto, addr) case "npipe": - // No need for compression in local communications. - tr.DisableCompression = true - tr.Dial = func(_, _ string) (net.Conn, error) { - return DialPipe(addr, defaultTimeout) - } + return configureNpipeTransport(tr, proto, addr) default: tr.Proxy = http.ProxyFromEnvironment dialer, err := DialerFromEnvironment(&net.Dialer{ diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go index b255ac9ac7..386cf0dbbd 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go @@ -3,11 +3,31 @@ package sockets import ( + "fmt" "net" + "net/http" "syscall" "time" ) +const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) + +func configureUnixTransport(tr *http.Transport, proto, addr string) error { + if len(addr) > maxUnixSocketPathSize { + return fmt.Errorf("Unix socket path %q is too long", addr) + } + // No need for compression in local communications. + tr.DisableCompression = true + tr.Dial = func(_, _ string) (net.Conn, error) { + return net.DialTimeout(proto, addr, defaultTimeout) + } + return nil +} + +func configureNpipeTransport(tr *http.Transport, proto, addr string) error { + return ErrProtocolNotAvailable +} + // DialPipe connects to a Windows named pipe. // This is not supported on other OSes. func DialPipe(_ string, _ time.Duration) (net.Conn, error) { diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go index 1f3540b2fe..5c21644e1f 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go @@ -2,11 +2,25 @@ package sockets import ( "net" + "net/http" "time" "github.com/Microsoft/go-winio" ) +func configureUnixTransport(tr *http.Transport, proto, addr string) error { + return ErrProtocolNotAvailable +} + +func configureNpipeTransport(tr *http.Transport, proto, addr string) error { + // No need for compression in local communications. + tr.DisableCompression = true + tr.Dial = func(_, _ string) (net.Conn, error) { + return DialPipe(addr, defaultTimeout) + } + return nil +} + // DialPipe connects to a Windows named pipe. func DialPipe(addr string, timeout time.Duration) (net.Conn, error) { return winio.DialPipe(addr, &timeout)