Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docker provider additions #3761

Merged
merged 8 commits into from
Dec 2, 2015
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions builtin/providers/docker/resource_docker_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"regexp"
)

func resourceDockerContainer() *schema.Resource {
Expand Down Expand Up @@ -71,6 +72,13 @@ func resourceDockerContainer() *schema.Resource {
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{
Type: schema.TypeSet,
Optional: true,
Expand All @@ -85,6 +93,27 @@ func resourceDockerContainer() *schema.Resource {
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{
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -142,6 +171,51 @@ func resourceDockerContainer() *schema.Resource {
Optional: true,
ForceNew: true,
},

"labels": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
},

"memory": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},

"memory_swap": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},

"cpu_shares": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},

"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,
},
},
}
}
Expand Down
69 changes: 61 additions & 8 deletions builtin/providers/docker/resource_docker_container_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err
createOpts.Config.Cmd = stringListToStringSlice(v.([]interface{}))
}

if v, ok := d.GetOk("entrypoint"); ok {
createOpts.Config.Entrypoint = stringListToStringSlice(v.([]interface{}))
}

exposedPorts := map[dc.Port]struct{}{}
portBindings := map[dc.Port][]dc.PortBinding{}

Expand All @@ -78,19 +82,20 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err
createOpts.Config.Volumes = volumes
}

var retContainer *dc.Container
if retContainer, err = client.CreateContainer(createOpts); err != nil {
return fmt.Errorf("Unable to create container: %s", err)
if v, ok := d.GetOk("labels"); ok {
createOpts.Config.Labels = mapTypeMapValsToString(v.(map[string]interface{}))
}
if retContainer == nil {
return fmt.Errorf("Returned container is nil")
}

d.SetId(retContainer.ID)

hostConfig := &dc.HostConfig{
Privileged: d.Get("privileged").(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 {
Expand All @@ -112,6 +117,46 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err
hostConfig.Links = stringSetToStringSlice(v.(*schema.Set))
}

if v, ok := d.GetOk("memory"); ok {
memory := int64(v.(int))
if memory > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this check, we should be able to trust that d.GetOk("memory") will return a non-zero value, and instead put in a check for negative values into the ValidateFunc for this schema such that obviously bad values are raised to the user.

hostConfig.Memory = memory * 1024 * 1024
}
}

if v, ok := d.GetOk("memory_swap"); ok {
swap := int64(v.(int))
if swap != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same applies to this check as to the one for memory. In this case the comparison with 0 is unnecessary since GetOk will not return ok == true.

if swap > 0 { // only convert positive #s to bytes
swap = swap * 1024 * 1024
}
hostConfig.MemorySwap = swap
}
}

if v, ok := d.GetOk("cpu_shares"); ok {
shares := int64(v.(int))
if shares > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same applies as for memory.

hostConfig.CPUShares = shares
}
}

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()
if err := client.StartContainer(retContainer.ID, hostConfig); err != nil {
return fmt.Errorf("Unable to start container: %s", err)
Expand Down Expand Up @@ -223,6 +268,14 @@ func stringSetToStringSlice(stringSet *schema.Set) []string {
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) {
apiContainers, err := client.ListContainers(dc.ListContainersOptions{All: true})

Expand Down
100 changes: 98 additions & 2 deletions builtin/providers/docker/resource_docker_container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,87 @@ import (
)

func TestAccDockerContainer_basic(t *testing.T) {
var c dc.Container
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDockerContainerConfig,
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 {
rs, ok := s.RootModule().Resources[n]
if !ok {
Expand All @@ -43,6 +109,11 @@ func testAccContainerRunning(n string) resource.TestCheckFunc {

for _, c := range containers {
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
}
}
Expand All @@ -61,3 +132,28 @@ resource "docker_container" "foo" {
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
}
}
`
18 changes: 18 additions & 0 deletions website/source/docs/providers/docker/r/container.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,37 @@ The following arguments are supported:
* `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
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.
* `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
connectivity between containers that are running on the same host.
* `hostname` - (Optional, string) Hostname 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
kept running. If false, then as long as the container exists, Terraform
assumes it is successful.
* `ports` - (Optional) See [Ports](#ports) below for details.
* `privileged` - (Optional, bool) Run container in privileged mode.
* `publish_all_ports` - (Optional, bool) Publish all ports of the container.
* `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>
## Ports
Expand Down