Merge pull request #3761 from ryane/f-provider-docker-improvements

provider/docker: support additional arguments for `docker_container` resource
This commit is contained in:
James Nugent 2015-12-02 11:46:29 -05:00
commit 50d7abcd8a
4 changed files with 264 additions and 10 deletions

View File

@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"regexp"
) )
func resourceDockerContainer() *schema.Resource { func resourceDockerContainer() *schema.Resource {
@ -71,6 +72,13 @@ func resourceDockerContainer() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
}, },
"entrypoint": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"dns": &schema.Schema{ "dns": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
@ -85,6 +93,27 @@ func resourceDockerContainer() *schema.Resource {
ForceNew: true, ForceNew: true,
}, },
"restart": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "no",
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(string)
if !regexp.MustCompile(`^(no|on-failure|always)$`).MatchString(value) {
es = append(es, fmt.Errorf(
"%q must be one of \"no\", \"on-failure\", or \"always\"", k))
}
return
},
},
"max_retry_count": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"volumes": &schema.Schema{ "volumes": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
@ -142,6 +171,72 @@ func resourceDockerContainer() *schema.Resource {
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
}, },
"labels": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
},
"memory": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(int)
if value < 0 {
es = append(es, fmt.Errorf("%q must be greater than or equal to 0", k))
}
return
},
},
"memory_swap": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(int)
if value < -1 {
es = append(es, fmt.Errorf("%q must be greater than or equal to -1", k))
}
return
},
},
"cpu_shares": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(int)
if value < 0 {
es = append(es, fmt.Errorf("%q must be greater than or equal to 0", k))
}
return
},
},
"log_driver": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "json-file",
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(string)
if !regexp.MustCompile(`^(json-file|syslog|journald|gelf|fluentd)$`).MatchString(value) {
es = append(es, fmt.Errorf(
"%q must be one of \"json-file\", \"syslog\", \"journald\", \"gelf\", or \"fluentd\"", k))
}
return
},
},
"log_opts": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
},
}, },
} }
} }

View File

@ -54,6 +54,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err
createOpts.Config.Cmd = stringListToStringSlice(v.([]interface{})) createOpts.Config.Cmd = stringListToStringSlice(v.([]interface{}))
} }
if v, ok := d.GetOk("entrypoint"); ok {
createOpts.Config.Entrypoint = stringListToStringSlice(v.([]interface{}))
}
exposedPorts := map[dc.Port]struct{}{} exposedPorts := map[dc.Port]struct{}{}
portBindings := map[dc.Port][]dc.PortBinding{} portBindings := map[dc.Port][]dc.PortBinding{}
@ -78,19 +82,20 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err
createOpts.Config.Volumes = volumes createOpts.Config.Volumes = volumes
} }
var retContainer *dc.Container if v, ok := d.GetOk("labels"); ok {
if retContainer, err = client.CreateContainer(createOpts); err != nil { createOpts.Config.Labels = mapTypeMapValsToString(v.(map[string]interface{}))
return fmt.Errorf("Unable to create container: %s", err)
} }
if retContainer == nil {
return fmt.Errorf("Returned container is nil")
}
d.SetId(retContainer.ID)
hostConfig := &dc.HostConfig{ hostConfig := &dc.HostConfig{
Privileged: d.Get("privileged").(bool), Privileged: d.Get("privileged").(bool),
PublishAllPorts: d.Get("publish_all_ports").(bool), PublishAllPorts: d.Get("publish_all_ports").(bool),
RestartPolicy: dc.RestartPolicy{
Name: d.Get("restart").(string),
MaximumRetryCount: d.Get("max_retry_count").(int),
},
LogConfig: dc.LogConfig{
Type: d.Get("log_driver").(string),
},
} }
if len(portBindings) != 0 { if len(portBindings) != 0 {
@ -112,6 +117,38 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err
hostConfig.Links = stringSetToStringSlice(v.(*schema.Set)) hostConfig.Links = stringSetToStringSlice(v.(*schema.Set))
} }
if v, ok := d.GetOk("memory"); ok {
hostConfig.Memory = int64(v.(int)) * 1024 * 1024
}
if v, ok := d.GetOk("memory_swap"); ok {
swap := int64(v.(int))
if swap > 0 {
swap = swap * 1024 * 1024
}
hostConfig.MemorySwap = swap
}
if v, ok := d.GetOk("cpu_shares"); ok {
hostConfig.CPUShares = int64(v.(int))
}
if v, ok := d.GetOk("log_opts"); ok {
hostConfig.LogConfig.Config = mapTypeMapValsToString(v.(map[string]interface{}))
}
createOpts.HostConfig = hostConfig
var retContainer *dc.Container
if retContainer, err = client.CreateContainer(createOpts); err != nil {
return fmt.Errorf("Unable to create container: %s", err)
}
if retContainer == nil {
return fmt.Errorf("Returned container is nil")
}
d.SetId(retContainer.ID)
creationTime = time.Now() creationTime = time.Now()
if err := client.StartContainer(retContainer.ID, hostConfig); err != nil { if err := client.StartContainer(retContainer.ID, hostConfig); err != nil {
return fmt.Errorf("Unable to start container: %s", err) return fmt.Errorf("Unable to start container: %s", err)
@ -223,6 +260,14 @@ func stringSetToStringSlice(stringSet *schema.Set) []string {
return ret return ret
} }
func mapTypeMapValsToString(typeMap map[string]interface{}) map[string]string {
mapped := make(map[string]string, len(typeMap))
for k, v := range typeMap {
mapped[k] = v.(string)
}
return mapped
}
func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, error) { func fetchDockerContainer(name string, client *dc.Client) (*dc.APIContainers, error) {
apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true}) apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true})

View File

@ -10,6 +10,7 @@ import (
) )
func TestAccDockerContainer_basic(t *testing.T) { func TestAccDockerContainer_basic(t *testing.T) {
var c dc.Container
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
@ -17,14 +18,79 @@ func TestAccDockerContainer_basic(t *testing.T) {
resource.TestStep{ resource.TestStep{
Config: testAccDockerContainerConfig, Config: testAccDockerContainerConfig,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccContainerRunning("docker_container.foo"), testAccContainerRunning("docker_container.foo", &c),
), ),
}, },
}, },
}) })
} }
func testAccContainerRunning(n string) resource.TestCheckFunc { func TestAccDockerContainer_customized(t *testing.T) {
var c dc.Container
testCheck := func(*terraform.State) error {
if len(c.Config.Entrypoint) < 3 ||
(c.Config.Entrypoint[0] != "/bin/bash" &&
c.Config.Entrypoint[1] != "-c" &&
c.Config.Entrypoint[2] != "ping localhost") {
return fmt.Errorf("Container wrong entrypoint: %s", c.Config.Entrypoint)
}
if c.HostConfig.RestartPolicy.Name == "on-failure" {
if c.HostConfig.RestartPolicy.MaximumRetryCount != 5 {
return fmt.Errorf("Container has wrong restart policy max retry count: %d", c.HostConfig.RestartPolicy.MaximumRetryCount)
}
} else {
return fmt.Errorf("Container has wrong restart policy: %s", c.HostConfig.RestartPolicy.Name)
}
if c.HostConfig.Memory != (512 * 1024 * 1024) {
return fmt.Errorf("Container has wrong memory setting: %d", c.HostConfig.Memory)
}
if c.HostConfig.MemorySwap != (2048 * 1024 * 1024) {
return fmt.Errorf("Container has wrong memory swap setting: %d", c.HostConfig.MemorySwap)
}
if c.HostConfig.CPUShares != 32 {
return fmt.Errorf("Container has wrong cpu shares setting: %d", c.HostConfig.CPUShares)
}
if c.Config.Labels["env"] != "prod" || c.Config.Labels["role"] != "test" {
return fmt.Errorf("Container does not have the correct labels")
}
if c.HostConfig.LogConfig.Type != "json-file" {
return fmt.Errorf("Container does not have the correct log config: %s", c.HostConfig.LogConfig.Type)
}
if c.HostConfig.LogConfig.Config["max-size"] != "10m" {
return fmt.Errorf("Container does not have the correct max-size log option: %v", c.HostConfig.LogConfig.Config["max-size"])
}
if c.HostConfig.LogConfig.Config["max-file"] != "20" {
return fmt.Errorf("Container does not have the correct max-file log option: %v", c.HostConfig.LogConfig.Config["max-file"])
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDockerContainerCustomizedConfig,
Check: resource.ComposeTestCheckFunc(
testAccContainerRunning("docker_container.foo", &c),
testCheck,
),
},
},
})
}
func testAccContainerRunning(n string, container *dc.Container) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
if !ok { if !ok {
@ -43,6 +109,11 @@ func testAccContainerRunning(n string) resource.TestCheckFunc {
for _, c := range containers { for _, c := range containers {
if c.ID == rs.Primary.ID { if c.ID == rs.Primary.ID {
inspected, err := client.InspectContainer(c.ID)
if err != nil {
return fmt.Errorf("Container could not be inspected: %s", err)
}
*container = *inspected
return nil return nil
} }
} }
@ -61,3 +132,28 @@ resource "docker_container" "foo" {
image = "${docker_image.foo.latest}" image = "${docker_image.foo.latest}"
} }
` `
const testAccDockerContainerCustomizedConfig = `
resource "docker_image" "foo" {
name = "nginx:latest"
}
resource "docker_container" "foo" {
name = "tf-test"
image = "${docker_image.foo.latest}"
entrypoint = ["/bin/bash", "-c", "ping localhost"]
restart = "on-failure"
max_retry_count = 5
memory = 512
memory_swap = 2048
cpu_shares = 32
labels {
env = "prod"
role = "test"
}
log_driver = "json-file"
log_opts = {
max-size = "10m"
max-file = 20
}
}
`

View File

@ -37,12 +37,22 @@ The following arguments are supported:
* `command` - (Optional, list of strings) The command to use to start the * `command` - (Optional, list of strings) The command to use to start the
container. For example, to run `/usr/bin/myprogram -f baz.conf` set the container. For example, to run `/usr/bin/myprogram -f baz.conf` set the
command to be `["/usr/bin/myprogram", "-f", "baz.conf"]`. command to be `["/usr/bin/myprogram", "-f", "baz.conf"]`.
* `entrypoint` - (Optional, list of strings) The command to use as the
Entrypoint for the container. The Entrypoint allows you to configure a
container to run as an executable. For example, to run `/usr/bin/myprogram`
when starting a container, set the entrypoint to be
`["/usr/bin/myprogram"]`.
* `dns` - (Optional, set of strings) Set of DNS servers. * `dns` - (Optional, set of strings) Set of DNS servers.
* `env` - (Optional, set of strings) Environmental variables to set. * `env` - (Optional, set of strings) Environmental variables to set.
* `labels` - (Optional) Key/value pairs to set as labels on the container.
* `links` - (Optional, set of strings) Set of links for link based * `links` - (Optional, set of strings) Set of links for link based
connectivity between containers that are running on the same host. connectivity between containers that are running on the same host.
* `hostname` - (Optional, string) Hostname of the container. * `hostname` - (Optional, string) Hostname of the container.
* `domainname` - (Optional, string) Domain name of the container. * `domainname` - (Optional, string) Domain name of the container.
* `restart` - (Optional, string) The restart policy for the container. Must be
one of "no", "on-failure", "always".
* `max_retry_count` - (Optional, int) The maximum amount of times to an attempt
a restart when `restart` is set to "on-failure"
* `must_run` - (Optional, bool) If true, then the Docker container will be * `must_run` - (Optional, bool) If true, then the Docker container will be
kept running. If false, then as long as the container exists, Terraform kept running. If false, then as long as the container exists, Terraform
assumes it is successful. assumes it is successful.
@ -50,6 +60,14 @@ The following arguments are supported:
* `privileged` - (Optional, bool) Run container in privileged mode. * `privileged` - (Optional, bool) Run container in privileged mode.
* `publish_all_ports` - (Optional, bool) Publish all ports of the container. * `publish_all_ports` - (Optional, bool) Publish all ports of the container.
* `volumes` - (Optional) See [Volumes](#volumes) below for details. * `volumes` - (Optional) See [Volumes](#volumes) below for details.
* `memory` - (Optional, int) The memory limit for the container in MBs.
* `memory_swap` - (Optional, int) The total memory limit (memory + swap) for the
container in MBs.
* `cpu_shares` - (Optional, int) CPU shares (relative weight) for the container.
* `log_driver` - (Optional, string) The logging driver to use for the container.
Defaults to "json-file".
* `log_opts` - (Optional) Key/value pairs to use as options for the logging
driver.
<a id="ports"></a> <a id="ports"></a>
## Ports ## Ports