docker_container resource mounts support (#147)

* Add mounts support to docker container resource
* Source is not a required mount attribute. 
* Add tmpfs map support to docker container resource
* Add tests around tmpfs map
This commit is contained in:
Sean Bryant 2019-05-26 02:59:29 -07:00 committed by Manuel Vogel
parent a6fdf4c2a6
commit a7b3f314d6
4 changed files with 294 additions and 2 deletions

View file

@ -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,

View file

@ -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
}

View file

@ -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"

View file

@ -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,