diff --git a/client/driver/docker.go b/client/driver/docker.go index 4dc6ae08f..a2243d3dc 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -142,6 +142,12 @@ type DockerMount struct { VolumeOptions []*DockerVolumeOptions `mapstructure:"volume_options"` } +type DockerDevice struct { + HostPath string `mapstructure:"host_path"` + ContainerPath string `mapstructure:"container_path"` + CgroupPermissions string `mapstructure:"cgroup_permissions"` +} + type DockerVolumeOptions struct { NoCopy bool `mapstructure:"no_copy"` Labels []map[string]string `mapstructure:"labels"` @@ -190,6 +196,7 @@ type DockerDriverConfig struct { ForcePull bool `mapstructure:"force_pull"` // Always force pull before running image, useful if your tags are mutable MacAddress string `mapstructure:"mac_address"` // Pin mac address to container SecurityOpt []string `mapstructure:"security_opt"` // Flags to pass directly to security-opt + Devices []DockerDevice `mapstructure:"devices"` // To allow mounting USB or other serial control devices } // Validate validates a docker driver config @@ -197,6 +204,22 @@ func (c *DockerDriverConfig) Validate() error { if c.ImageName == "" { return fmt.Errorf("Docker Driver needs an image name") } + if len(c.Devices) > 0 { + for _, dev := range c.Devices { + if dev.HostPath == "" { + return fmt.Errorf("host path must be set in configuration for devices") + } + + if dev.CgroupPermissions != "" { + for _, c := range dev.CgroupPermissions { + ch := string(c) + if ch != "r" && ch != "w" && ch != "m" { + return fmt.Errorf("invalid cgroup permission string: %q", dev.CgroupPermissions) + } + } + } + } + } return nil } @@ -537,6 +560,9 @@ func (d *DockerDriver) Validate(config map[string]interface{}) error { "security_opt": { Type: fields.TypeArray, }, + "devices": { + Type: fields.TypeArray, + }, }, } @@ -1020,6 +1046,20 @@ func (d *DockerDriver) createContainerConfig(ctx *ExecContext, task *structs.Tas } } + if len(driverConfig.Devices) > 0 { + var devices []docker.Device + for _, device := range driverConfig.Devices { + if device.HostPath != "" { + dev := docker.Device{ + PathOnHost: device.HostPath, + PathInContainer: device.ContainerPath, + CgroupPermissions: device.CgroupPermissions} + devices = append(devices, dev) + } + } + hostConfig.Devices = devices + } + // Setup mounts for _, m := range driverConfig.Mounts { hm := docker.HostMount{ diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index b93807763..2cd26a125 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -1803,3 +1803,44 @@ func TestDockerDriver_OOMKilled(t *testing.T) { t.Fatalf("timeout") } } + +func TestDockerDriver_Devices_IsInvalidConfig(t *testing.T) { + if !tu.IsTravis() { + t.Parallel() + } + if !testutil.DockerIsConnected(t) { + t.Skip("Docker not connected") + } + + brokenConfigs := []interface{}{ + map[string]interface{}{ + "host_path": "", + }, + map[string]interface{}{ + "host_path": "/dev/sda1", + "cgroup_permissions": "rxb", + }, + } + + test_cases := []struct { + deviceConfig interface{} + err error + }{ + {[]interface{}{brokenConfigs[0]}, fmt.Errorf("host path must be set in configuration for devices")}, + {[]interface{}{brokenConfigs[1]}, fmt.Errorf("invalid cgroup permission string: \"rxb\"")}, + } + + for _, tc := range test_cases { + task, _, _ := dockerTask(t) + task.Config["devices"] = tc.deviceConfig + + ctx := testDockerDriverContexts(t, task) + driver := NewDockerDriver(ctx.DriverCtx) + copyImage(t, ctx.ExecCtx.TaskDir, "busybox.tar") + defer ctx.AllocDir.Destroy() + + if _, err := driver.Prestart(ctx.ExecCtx, task); err == nil || err.Error() != tc.err.Error() { + t.Fatalf("error expected in prestart, got %v, expected %v", err, tc.err) + } + } +} diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index 62a1528e5..c1f3eae57 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -280,7 +280,26 @@ The `docker` driver supports the following configuration in the job spec. Only ] } ``` +* `devices` - (Optional) A list of + [devices](https://docs.docker.com/engine/reference/commandline/run/#add-host-device-to-container-device) + to be exposed the container. `host_path` is the only required field. By default, the container will be able to + `read`, `write` and `mknod` these devices. Use the optional `cgroup_permissions` field to restrict permissions. + ```hcl + config { + devices = [ + { + host_path = "/dev/sda1" + container_path = "/dev/xvdc" + cgroup_permissions = "r" + }, + { + host_path = "/dev/sda2" + container_path = "/dev/xvdd" + } + ] + } + ``` ### Container Name Nomad creates a container after pulling an image. Containers are named