diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index 68e86960..323725f2 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -189,7 +189,104 @@ func resourceDockerContainer() *schema.Resource { }, }, }, - + "mounts": { + Type: schema.TypeSet, + Description: "Specification for mounts to be added to containers created as part of the service", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target": { + Type: schema.TypeString, + Description: "Container path", + Required: true, + }, + "source": { + Type: schema.TypeString, + Description: "Mount source (e.g. a volume name, a host path)", + Optional: true, + }, + "type": { + Type: schema.TypeString, + Description: "The mount type", + Required: true, + ValidateFunc: validateStringMatchesPattern(`^(bind|volume|tmpfs)$`), + }, + "read_only": { + Type: schema.TypeBool, + Description: "Whether the mount should be read-only", + Optional: true, + }, + "bind_options": { + Type: schema.TypeList, + Description: "Optional configuration for the bind type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "propagation": { + Type: schema.TypeString, + Description: "A propagation mode with the value", + Optional: true, + ValidateFunc: validateStringMatchesPattern(`^(private|rprivate|shared|rshared|slave|rslave)$`), + }, + }, + }, + }, + "volume_options": { + Type: schema.TypeList, + Description: "Optional configuration for the volume type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "no_copy": { + Type: schema.TypeBool, + Description: "Populate volume with data from the target", + Optional: true, + }, + "labels": { + Type: schema.TypeMap, + Description: "User-defined key/value metadata", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "driver_name": { + Type: schema.TypeString, + Description: "Name of the driver to use to create the volume.", + Optional: true, + }, + "driver_options": { + Type: schema.TypeMap, + Description: "key/value map of driver specific options", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "tmpfs_options": { + Type: schema.TypeList, + Description: "Optional configuration for the tmpfs type", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "size_bytes": { + Type: schema.TypeInt, + Description: "The size for the tmpfs mount in bytes", + Optional: true, + }, + "mode": { + Type: schema.TypeInt, + Description: "The permission mode for the tmpfs mount in an integer", + Optional: true, + }, + }, + }, + }, + }, + }, + }, "volumes": { Type: schema.TypeSet, Optional: true, @@ -230,6 +327,11 @@ func resourceDockerContainer() *schema.Resource { }, }, + "tmpfs": { + Type: schema.TypeMap, + Optional: true, + }, + "ports": { Type: schema.TypeList, Optional: true, diff --git a/docker/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go index 1971a8f9..b742f09d 100644 --- a/docker/resource_docker_container_funcs.go +++ b/docker/resource_docker_container_funcs.go @@ -9,6 +9,7 @@ import ( "fmt" "log" "math/rand" + "os" "sort" "strconv" "strings" @@ -18,6 +19,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" @@ -127,6 +129,82 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } } + mounts := []mount.Mount{} + + if value, ok := d.GetOk("mounts"); ok { + for _, rawMount := range value.(*schema.Set).List() { + rawMount := rawMount.(map[string]interface{}) + mountType := mount.Type(rawMount["type"].(string)) + mountInstance := mount.Mount{ + Type: mountType, + Target: rawMount["target"].(string), + Source: rawMount["source"].(string), + } + if value, ok := rawMount["read_only"]; ok { + mountInstance.ReadOnly = value.(bool) + } + + if mountType == mount.TypeBind { + if value, ok := rawMount["bind_options"]; ok { + if len(value.([]interface{})) > 0 { + mountInstance.BindOptions = &mount.BindOptions{} + for _, rawBindOptions := range value.([]interface{}) { + rawBindOptions := rawBindOptions.(map[string]interface{}) + if value, ok := rawBindOptions["propagation"]; ok { + mountInstance.BindOptions.Propagation = mount.Propagation(value.(string)) + } + } + } + } + } else if mountType == mount.TypeVolume { + if value, ok := rawMount["volume_options"]; ok { + if len(value.([]interface{})) > 0 { + mountInstance.VolumeOptions = &mount.VolumeOptions{} + for _, rawVolumeOptions := range value.([]interface{}) { + rawVolumeOptions := rawVolumeOptions.(map[string]interface{}) + if value, ok := rawVolumeOptions["no_copy"]; ok { + mountInstance.VolumeOptions.NoCopy = value.(bool) + } + if value, ok := rawVolumeOptions["labels"]; ok { + mountInstance.VolumeOptions.Labels = mapTypeMapValsToString(value.(map[string]interface{})) + } + // because it is not possible to nest maps + if value, ok := rawVolumeOptions["driver_name"]; ok { + if mountInstance.VolumeOptions.DriverConfig == nil { + mountInstance.VolumeOptions.DriverConfig = &mount.Driver{} + } + mountInstance.VolumeOptions.DriverConfig.Name = value.(string) + } + if value, ok := rawVolumeOptions["driver_options"]; ok { + if mountInstance.VolumeOptions.DriverConfig == nil { + mountInstance.VolumeOptions.DriverConfig = &mount.Driver{} + } + mountInstance.VolumeOptions.DriverConfig.Options = mapTypeMapValsToString(value.(map[string]interface{})) + } + } + } + } + } else if mountType == mount.TypeTmpfs { + if value, ok := rawMount["tmpfs_options"]; ok { + if len(value.([]interface{})) > 0 { + mountInstance.TmpfsOptions = &mount.TmpfsOptions{} + for _, rawTmpfsOptions := range value.([]interface{}) { + rawTmpfsOptions := rawTmpfsOptions.(map[string]interface{}) + if value, ok := rawTmpfsOptions["size_bytes"]; ok { + mountInstance.TmpfsOptions.SizeBytes = value.(int64) + } + if value, ok := rawTmpfsOptions["mode"]; ok { + mountInstance.TmpfsOptions.Mode = os.FileMode(value.(int)) + } + } + } + } + } + + mounts = append(mounts, mountInstance) + } + } + hostConfig := &container.HostConfig{ Privileged: d.Get("privileged").(bool), PublishAllPorts: d.Get("publish_all_ports").(bool), @@ -134,12 +212,17 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err Name: d.Get("restart").(string), MaximumRetryCount: d.Get("max_retry_count").(int), }, + Mounts: mounts, AutoRemove: d.Get("rm").(bool), LogConfig: container.LogConfig{ Type: d.Get("log_driver").(string), }, } + if v, ok := d.GetOk("tmpfs"); ok { + hostConfig.Tmpfs = mapTypeMapValsToString(v.(map[string]interface{})) + } + if len(portBindings) != 0 { hostConfig.PortBindings = portBindings } diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index e46b6c1b..9f5fe764 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -200,6 +200,70 @@ func TestAccDockerContainer_volume(t *testing.T) { }) } +func TestAccDockerContainer_mounts(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + if len(c.Mounts) != 2 { + return fmt.Errorf("Incorrect number of mounts: expected 2, got %d", len(c.Mounts)) + } + + for _, v := range c.Mounts { + if v.Destination != "/mount/test" && v.Destination != "/mount/tmpfs" { + return fmt.Errorf("Bad destination on mount: expected /mount/test or /mount/tmpfs, got %q", v.Destination) + } + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDockerContainerMountsConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo_mounts", &c), + testCheck, + ), + }, + }, + }) +} + +func TestAccDockerContainer_tmpfs(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + if len(c.HostConfig.Tmpfs) != 1 { + return fmt.Errorf("Incorrect number of tmpfs: expected 1, got %d", len(c.HostConfig.Tmpfs)) + } + + for mountPath, _ := range c.HostConfig.Tmpfs { + if mountPath != "/mount/tmpfs" { + return fmt.Errorf("Bad destination on tmpfs: expected /mount/tmpfs, got %q", mountPath) + } + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDockerContainerTmpfsConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + ), + }, + }, + }) +} + func TestAccDockerContainer_customized(t *testing.T) { var c types.ContainerJSON @@ -1219,6 +1283,49 @@ resource "docker_container" "foo" { } ` +const testAccDockerContainerMountsConfig = ` +resource "docker_image" "foo_mounts" { + name = "nginx:latest" +} + +resource "docker_volume" "foo_mounts" { + name = "testAccDockerContainerMounts_volume" +} + +resource "docker_container" "foo_mounts" { + name = "tf-test" + image = "${docker_image.foo_mounts.latest}" + + mounts = [ + { + target = "/mount/test" + source = "${docker_volume.foo_mounts.name}" + type = "volume" + read_only = true + }, + { + target = "/mount/tmpfs" + type = "tmpfs" + } + ] +} +` + +const testAccDockerContainerTmpfsConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + tmpfs = { + "/mount/tmpfs" = "rw,noexec,nosuid" + } +} +` + const testAccDockerContainerCustomizedConfig = ` resource "docker_image" "foo" { name = "nginx:latest" diff --git a/docker/resource_docker_service.go b/docker/resource_docker_service.go index 916d1c6b..e96b1f3d 100644 --- a/docker/resource_docker_service.go +++ b/docker/resource_docker_service.go @@ -208,7 +208,7 @@ func resourceDockerService() *schema.Resource { "source": { Type: schema.TypeString, Description: "Mount source (e.g. a volume name, a host path)", - Required: true, + Optional: true, }, "type": { Type: schema.TypeString,