diff --git a/drivers/docker/config.go b/drivers/docker/config.go index 5d63cbaac993..342a12a352bf 100644 --- a/drivers/docker/config.go +++ b/drivers/docker/config.go @@ -331,6 +331,29 @@ var ( })), "mac_address": hclspec.NewAttr("mac_address", "string", false), "memory_hard_limit": hclspec.NewAttr("memory_hard_limit", "number", false), + "mount": hclspec.NewBlockMap("mount", []string{"target"}, hclspec.NewObject(map[string]*hclspec.Spec{ + "type": hclspec.NewDefault( + hclspec.NewAttr("type", "string", false), + hclspec.NewLiteral("\"volume\""), + ), + "source": hclspec.NewAttr("source", "string", false), + "readonly": hclspec.NewAttr("readonly", "bool", false), + "bind_options": hclspec.NewBlock("bind_options", false, hclspec.NewObject(map[string]*hclspec.Spec{ + "propagation": hclspec.NewAttr("propagation", "string", false), + })), + "tmpfs_options": hclspec.NewBlock("tmpfs_options", false, hclspec.NewObject(map[string]*hclspec.Spec{ + "size": hclspec.NewAttr("size", "number", false), + "mode": hclspec.NewAttr("mode", "number", false), + })), + "volume_options": hclspec.NewBlock("volume_options", false, hclspec.NewObject(map[string]*hclspec.Spec{ + "no_copy": hclspec.NewAttr("no_copy", "bool", false), + "labels": hclspec.NewAttr("labels", "list(map(string))", false), + "driver_config": hclspec.NewBlock("driver_config", false, hclspec.NewObject(map[string]*hclspec.Spec{ + "name": hclspec.NewAttr("name", "string", false), + "options": hclspec.NewAttr("options", "list(map(string))", false), + })), + })), + })), "mounts": hclspec.NewBlockList("mounts", hclspec.NewObject(map[string]*hclspec.Spec{ "type": hclspec.NewDefault( hclspec.NewAttr("type", "string", false), @@ -398,56 +421,57 @@ var ( ) type TaskConfig struct { - Image string `codec:"image"` - AdvertiseIPv6Addr bool `codec:"advertise_ipv6_address"` - Args []string `codec:"args"` - Auth DockerAuth `codec:"auth"` - AuthSoftFail bool `codec:"auth_soft_fail"` - CapAdd []string `codec:"cap_add"` - CapDrop []string `codec:"cap_drop"` - Command string `codec:"command"` - CPUCFSPeriod int64 `codec:"cpu_cfs_period"` - CPUHardLimit bool `codec:"cpu_hard_limit"` - CPUSetCPUs string `codec:"cpuset_cpus"` - Devices []DockerDevice `codec:"devices"` - DNSSearchDomains []string `codec:"dns_search_domains"` - DNSOptions []string `codec:"dns_options"` - DNSServers []string `codec:"dns_servers"` - Entrypoint []string `codec:"entrypoint"` - ExtraHosts []string `codec:"extra_hosts"` - ForcePull bool `codec:"force_pull"` - Hostname string `codec:"hostname"` - Interactive bool `codec:"interactive"` - IPCMode string `codec:"ipc_mode"` - IPv4Address string `codec:"ipv4_address"` - IPv6Address string `codec:"ipv6_address"` - Labels hclutils.MapStrStr `codec:"labels"` - LoadImage string `codec:"load"` - Logging DockerLogging `codec:"logging"` - MacAddress string `codec:"mac_address"` - MemoryHardLimit int64 `codec:"memory_hard_limit"` - Mounts []DockerMount `codec:"mounts"` - NetworkAliases []string `codec:"network_aliases"` - NetworkMode string `codec:"network_mode"` - Runtime string `codec:"runtime"` - PidsLimit int64 `codec:"pids_limit"` - PidMode string `codec:"pid_mode"` - Ports []string `codec:"ports"` - PortMap hclutils.MapStrInt `codec:"port_map"` - Privileged bool `codec:"privileged"` - ImagePullTimeout string `codec:"image_pull_timeout"` - ReadonlyRootfs bool `codec:"readonly_rootfs"` - SecurityOpt []string `codec:"security_opt"` - ShmSize int64 `codec:"shm_size"` - StorageOpt map[string]string `codec:"storage_opt"` - Sysctl hclutils.MapStrStr `codec:"sysctl"` - TTY bool `codec:"tty"` - Ulimit hclutils.MapStrStr `codec:"ulimit"` - UTSMode string `codec:"uts_mode"` - UsernsMode string `codec:"userns_mode"` - Volumes []string `codec:"volumes"` - VolumeDriver string `codec:"volume_driver"` - WorkDir string `codec:"work_dir"` + Image string `codec:"image"` + AdvertiseIPv6Addr bool `codec:"advertise_ipv6_address"` + Args []string `codec:"args"` + Auth DockerAuth `codec:"auth"` + AuthSoftFail bool `codec:"auth_soft_fail"` + CapAdd []string `codec:"cap_add"` + CapDrop []string `codec:"cap_drop"` + Command string `codec:"command"` + CPUCFSPeriod int64 `codec:"cpu_cfs_period"` + CPUHardLimit bool `codec:"cpu_hard_limit"` + CPUSetCPUs string `codec:"cpuset_cpus"` + Devices []DockerDevice `codec:"devices"` + DNSSearchDomains []string `codec:"dns_search_domains"` + DNSOptions []string `codec:"dns_options"` + DNSServers []string `codec:"dns_servers"` + Entrypoint []string `codec:"entrypoint"` + ExtraHosts []string `codec:"extra_hosts"` + ForcePull bool `codec:"force_pull"` + Hostname string `codec:"hostname"` + Interactive bool `codec:"interactive"` + IPCMode string `codec:"ipc_mode"` + IPv4Address string `codec:"ipv4_address"` + IPv6Address string `codec:"ipv6_address"` + Labels hclutils.MapStrStr `codec:"labels"` + LoadImage string `codec:"load"` + Logging DockerLogging `codec:"logging"` + MacAddress string `codec:"mac_address"` + MemoryHardLimit int64 `codec:"memory_hard_limit"` + Mounts []DockerMount `codec:"mounts"` + MountMap map[string]DockerMount `codec:"mount"` + NetworkAliases []string `codec:"network_aliases"` + NetworkMode string `codec:"network_mode"` + Runtime string `codec:"runtime"` + PidsLimit int64 `codec:"pids_limit"` + PidMode string `codec:"pid_mode"` + Ports []string `codec:"ports"` + PortMap hclutils.MapStrInt `codec:"port_map"` + Privileged bool `codec:"privileged"` + ImagePullTimeout string `codec:"image_pull_timeout"` + ReadonlyRootfs bool `codec:"readonly_rootfs"` + SecurityOpt []string `codec:"security_opt"` + ShmSize int64 `codec:"shm_size"` + StorageOpt map[string]string `codec:"storage_opt"` + Sysctl hclutils.MapStrStr `codec:"sysctl"` + TTY bool `codec:"tty"` + Ulimit hclutils.MapStrStr `codec:"ulimit"` + UTSMode string `codec:"uts_mode"` + UsernsMode string `codec:"userns_mode"` + Volumes []string `codec:"volumes"` + VolumeDriver string `codec:"volume_driver"` + WorkDir string `codec:"work_dir"` } type DockerAuth struct { diff --git a/drivers/docker/config_test.go b/drivers/docker/config_test.go index 9fb749a2622b..a2a37170e172 100644 --- a/drivers/docker/config_test.go +++ b/drivers/docker/config_test.go @@ -24,6 +24,7 @@ func TestConfig_ParseHCL(t *testing.T) { Image: "redis:3.2", Devices: []DockerDevice{}, Mounts: []DockerMount{}, + MountMap: map[string]DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -56,6 +57,7 @@ func TestConfig_ParseJSON(t *testing.T) { expected: TaskConfig{ Image: "bash:3", Mounts: []DockerMount{}, + MountMap: map[string]DockerMount{}, Devices: []DockerDevice{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", @@ -67,6 +69,7 @@ func TestConfig_ParseJSON(t *testing.T) { expected: TaskConfig{ Image: "bash:3", Mounts: []DockerMount{}, + MountMap: map[string]DockerMount{}, Devices: []DockerDevice{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", @@ -78,6 +81,7 @@ func TestConfig_ParseJSON(t *testing.T) { expected: TaskConfig{ Image: "bash:3", Mounts: []DockerMount{}, + MountMap: map[string]DockerMount{}, Devices: []DockerDevice{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", @@ -89,6 +93,7 @@ func TestConfig_ParseJSON(t *testing.T) { expected: TaskConfig{ Image: "bash:3", Mounts: []DockerMount{}, + MountMap: map[string]DockerMount{}, Devices: []DockerDevice{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", @@ -229,6 +234,25 @@ config { } mac_address = "02:42:ac:11:00:02" memory_hard_limit = 512 + + mount "/mount-bind-target" { + type = "bind" + source = "/bind-source-mount" + readonly = true + bind_options { + propagation = "rshared" + } + } + + mount "/mount-tmpfs-target" { + type = "tmpfs" + readonly = true + tmpfs_options { + size = 30000 + mode = 0777 + } + } + mounts = [ { type = "bind" @@ -360,6 +384,25 @@ config { }}, MacAddress: "02:42:ac:11:00:02", MemoryHardLimit: 512, + MountMap: map[string]DockerMount{ + "/mount-bind-target": { + Type: "bind", + Source: "/bind-source-mount", + ReadOnly: true, + BindOptions: DockerBindOptions{ + Propagation: "rshared", + }, + }, + "/mount-tmpfs-target": { + Type: "tmpfs", + Source: "", + ReadOnly: true, + TmpfsOptions: DockerTmpfsOptions{ + SizeBytes: 30000, + Mode: 511, + }, + }, + }, Mounts: []DockerMount{ { Type: "bind", diff --git a/drivers/docker/driver.go b/drivers/docker/driver.go index 301ba35a6020..847653ce27ca 100644 --- a/drivers/docker/driver.go +++ b/drivers/docker/driver.go @@ -947,32 +947,21 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T // Setup mounts for _, m := range driverConfig.Mounts { - hm, err := m.toDockerHostMount() + hm, err := d.toDockerMount(&m, task) if err != nil { return c, err } - - switch hm.Type { - case "bind": - hm.Source = expandPath(task.TaskDir().Dir, hm.Source) - - // paths inside alloc dir are always allowed as they mount within - // a container, and treated as relative to task dir - if !d.config.Volumes.Enabled && !isParentPath(task.AllocDir, hm.Source) { - return c, fmt.Errorf( - "volumes are not enabled; cannot mount host path: %q %q", - hm.Source, task.AllocDir) - } - case "tmpfs": - // no source, so no sandbox check required - default: // "volume", but also any new thing that comes along - if !d.config.Volumes.Enabled { - return c, fmt.Errorf( - "volumes are not enabled; cannot mount volume: %q", hm.Source) - } + hostConfig.Mounts = append(hostConfig.Mounts, *hm) + } + for t, m := range driverConfig.MountMap { + if m.Target == "" { + m.Target = t } - - hostConfig.Mounts = append(hostConfig.Mounts, hm) + hm, err := d.toDockerMount(&m, task) + if err != nil { + return c, err + } + hostConfig.Mounts = append(hostConfig.Mounts, *hm) } // Setup DNS @@ -1177,6 +1166,35 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T }, nil } +func (d *Driver) toDockerMount(m *DockerMount, task *drivers.TaskConfig) (*docker.HostMount, error) { + hm, err := m.toDockerHostMount() + if err != nil { + return nil, err + } + + switch hm.Type { + case "bind": + hm.Source = expandPath(task.TaskDir().Dir, hm.Source) + + // paths inside alloc dir are always allowed as they mount within + // a container, and treated as relative to task dir + if !d.config.Volumes.Enabled && !isParentPath(task.AllocDir, hm.Source) { + return nil, fmt.Errorf( + "volumes are not enabled; cannot mount host path: %q %q", + hm.Source, task.AllocDir) + } + case "tmpfs": + // no source, so no sandbox check required + default: // "volume", but also any new thing that comes along + if !d.config.Volumes.Enabled { + return nil, fmt.Errorf( + "volumes are not enabled; cannot mount volume: %q", hm.Source) + } + } + + return &hm, nil +} + // detectIP of Docker container. Returns the first IP found as well as true if // the IP should be advertised (bridge network IPs return false). Returns an // empty string and false if no IP could be found. diff --git a/drivers/docker/driver_test.go b/drivers/docker/driver_test.go index 5bfb8d6eec68..ac879ca46762 100644 --- a/drivers/docker/driver_test.go +++ b/drivers/docker/driver_test.go @@ -9,6 +9,7 @@ import ( "reflect" "runtime" "runtime/debug" + "sort" "strings" "syscall" "testing" @@ -1150,6 +1151,82 @@ func TestDockerDriver_CreateContainerConfig_Logging(t *testing.T) { } } +func TestDockerDriver_CreateContainerConfig_Mounts(t *testing.T) { + t.Parallel() + + task, cfg, ports := dockerTask(t) + defer freeport.Return(ports) + + cfg.MountMap = map[string]DockerMount{ + "/map-bind-target": DockerMount{ + Type: "bind", + Source: "/map-source", + }, + "/map-tmpfs-target": DockerMount{ + Type: "tmpfs", + }, + } + cfg.Mounts = []DockerMount{ + { + Type: "bind", + Target: "/list-bind-target", + Source: "/list-source", + }, + { + Type: "tmpfs", + Target: "/list-tmpfs-target", + }, + } + + expectedSrcPrefix := "/" + if runtime.GOOS == "windows" { + expectedSrcPrefix = "redis-demo\\" + } + expected := []docker.HostMount{ + // from mount map + { + Type: "bind", + Target: "/map-bind-target", + Source: expectedSrcPrefix + "map-source", + BindOptions: &docker.BindOptions{}, + }, + { + Type: "tmpfs", + Target: "/map-tmpfs-target", + TempfsOptions: &docker.TempfsOptions{}, + }, + // from mount list + { + Type: "bind", + Target: "/list-bind-target", + Source: expectedSrcPrefix + "list-source", + BindOptions: &docker.BindOptions{}, + }, + { + Type: "tmpfs", + Target: "/list-tmpfs-target", + TempfsOptions: &docker.TempfsOptions{}, + }, + } + + require.NoError(t, task.EncodeConcreteDriverConfig(cfg)) + + dh := dockerDriverHarness(t, nil) + driver := dh.Impl().(*Driver) + driver.config.Volumes.Enabled = true + + cc, err := driver.createContainerConfig(task, cfg, "org/repo:0.1") + require.NoError(t, err) + + found := cc.HostConfig.Mounts + sort.Slice(found, func(i, j int) bool { return strings.Compare(found[i].Target, found[j].Target) < 0 }) + sort.Slice(expected, func(i, j int) bool { + return strings.Compare(expected[i].Target, expected[j].Target) < 0 + }) + + require.Equal(t, expected, found) +} + func TestDockerDriver_CreateContainerConfigWithRuntimes(t *testing.T) { if !tu.IsCI() { t.Parallel() diff --git a/helper/pluginutils/hclutils/util_test.go b/helper/pluginutils/hclutils/util_test.go index 9eb2d3f5eeab..bc897ff590ff 100644 --- a/helper/pluginutils/hclutils/util_test.go +++ b/helper/pluginutils/hclutils/util_test.go @@ -45,6 +45,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { Image: "redis:3.2", Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -63,6 +64,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { Image: "redis:3.2", Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -81,6 +83,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { PidsLimit: 2, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -101,6 +104,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { PidsLimit: 2, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -119,6 +123,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { PidsLimit: 4, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -139,6 +144,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { PidsLimit: 4, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -157,6 +163,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { Args: []string{"foo", "bar"}, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -177,6 +184,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { Args: []string{"foo", "bar"}, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -198,6 +206,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { PidsLimit: 4, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -218,6 +227,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { Args: []string{"foo", "bar"}, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -242,6 +252,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { }, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -268,6 +279,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { }, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -305,6 +317,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { }, }, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -337,6 +350,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { }, Devices: []docker.DockerDevice{}, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", }, @@ -376,6 +390,7 @@ func TestParseHclInterface_Hcl(t *testing.T) { }, }, Mounts: []docker.DockerMount{}, + MountMap: map[string]docker.DockerMount{}, CPUCFSPeriod: 100000, ImagePullTimeout: "5m", },