diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index f0b997df..f0950d74 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -18,6 +18,12 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, }, + "rm": &schema.Schema{ + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + // Indicates whether the container must be running. // // An assumption is made that configured containers @@ -39,6 +45,11 @@ func resourceDockerContainer() *schema.Resource { Optional: true, }, + "exit_code": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + // ForceNew is not true for image because we need to // sane this against Docker image IDs, as each image // can have multiple names/tags attached do it. @@ -60,6 +71,12 @@ func resourceDockerContainer() *schema.Resource { ForceNew: true, }, + "attach": &schema.Schema{ + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + "command": &schema.Schema{ Type: schema.TypeList, Optional: true, diff --git a/docker/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go index 1d28c010..65a7b371 100644 --- a/docker/resource_docker_container_funcs.go +++ b/docker/resource_docker_container_funcs.go @@ -7,13 +7,12 @@ import ( "errors" "fmt" "log" + "math/rand" "strconv" "strings" "time" "context" - "math/rand" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" @@ -45,9 +44,12 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } config := &container.Config{ - Image: image, - Hostname: d.Get("hostname").(string), - Domainname: d.Get("domainname").(string), + Image: image, + Hostname: d.Get("hostname").(string), + Domainname: d.Get("domainname").(string), + AttachStdin: d.Get("attach").(bool), + AttachStdout: d.Get("attach").(bool), + AttachStderr: d.Get("attach").(bool), } if v, ok := d.GetOk("env"); ok { @@ -115,6 +117,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err Name: d.Get("restart").(string), MaximumRetryCount: d.Get("max_retry_count").(int), }, + AutoRemove: d.Get("rm").(bool), LogConfig: container.LogConfig{ Type: d.Get("log_driver").(string), }, @@ -264,12 +267,23 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } } + ctx := context.Background() creationTime = time.Now() - options := types.ContainerStartOptions{} - if err := client.ContainerStart(context.Background(), retContainer.ID, options); err != nil { + if err := client.ContainerStart(ctx, retContainer.ID, types.ContainerStartOptions{}); err != nil { return fmt.Errorf("Unable to start container: %s", err) } + if d.Get("attach").(bool) { + statusCh, errCh := client.ContainerWait(ctx, retContainer.ID, container.WaitConditionNotRunning) + select { + case err := <-errCh: + if err != nil { + return fmt.Errorf("Unable to wait container end of execution: %s", err) + } + case <-statusCh: + } + } + return resourceDockerContainerRead(d, meta) } @@ -333,6 +347,10 @@ func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Container %s failed to be in running state", apiContainer.ID) } + if !container.State.Running { + d.Set("exit_code", container.State.ExitCode) + } + // Read Network Settings if container.NetworkSettings != nil { // TODO remove deprecated attributes in next major @@ -369,13 +387,20 @@ func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) err func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*ProviderConfig).DockerClient - // Stop the container before removing if destroy_grace_seconds is defined - if d.Get("destroy_grace_seconds").(int) > 0 { - mapped := int32(d.Get("destroy_grace_seconds").(int)) - timeoutInSeconds := rand.Int31n(mapped) - timeout := time.Duration(time.Duration(timeoutInSeconds) * time.Second) - if err := client.ContainerStop(context.Background(), d.Id(), &timeout); err != nil { - return fmt.Errorf("Error stopping container %s: %s", d.Id(), err) + if d.Get("rm").(bool) { + d.SetId("") + return nil + } + + if !d.Get("attach").(bool) { + // Stop the container before removing if destroy_grace_seconds is defined + if d.Get("destroy_grace_seconds").(int) > 0 { + mapped := int32(d.Get("destroy_grace_seconds").(int)) + timeoutInSeconds := rand.Int31n(mapped) + timeout := time.Duration(time.Duration(timeoutInSeconds) * time.Second) + if err := client.ContainerStop(context.Background(), d.Id(), &timeout); err != nil { + return fmt.Errorf("Error stopping container %s: %s", d.Id(), err) + } } } diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index 43e33482..fc513569 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -4,9 +4,11 @@ import ( "archive/tar" "bytes" "fmt" + "github.com/docker/docker/api/types/container" "strconv" "strings" "testing" + "time" "context" @@ -680,6 +682,88 @@ func TestAccDockerContainer_multiple_ports(t *testing.T) { }) } +func TestAccDockerContainer_rm(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + if !c.HostConfig.AutoRemove { + return fmt.Errorf("Container doesn't have a correct autoremove flag") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccContainerWaitConditionRemoved("docker_container.foo", &c), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerRmConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "rm", "true"), + ), + }, + }, + }) +} + +func TestAccDockerContainer_attach(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + if !c.Config.AttachStdin { + return fmt.Errorf("Container doesn't have the correct value to stderr attach flag") + } + if !c.Config.AttachStdout { + return fmt.Errorf("Container doesn't have the correct value to stdout flag") + } + if !c.Config.AttachStderr { + return fmt.Errorf("Container doesn't have the correct value to stderr attach flag") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerAttachConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerNotRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "attach", "true"), + ), + }, + }, + }) +} + +func TestAccDockerContainer_exitcode(t *testing.T) { + var c types.ContainerJSON + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerExitCodeConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerWaitConditionNotRunning("docker_container.foo", &c), + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "exit_code", "123"), + ), + }, + }, + }) +} + func testAccContainerRunning(n string, container *types.ContainerJSON) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -712,6 +796,107 @@ func testAccContainerRunning(n string, container *types.ContainerJSON) resource. } } +func testAccContainerNotRunning(n string, container *types.ContainerJSON) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*ProviderConfig).DockerClient + containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{ + All: true, + }) + if err != nil { + return err + } + + for _, c := range containers { + if c.ID == rs.Primary.ID { + inspected, err := client.ContainerInspect(context.Background(), c.ID) + if err != nil { + return fmt.Errorf("Container could not be inspected: %s", err) + } + *container = inspected + + if container.State.Running { + return fmt.Errorf("Container is running: %s", rs.Primary.ID) + } + } + } + + return nil + } +} + +func testAccContainerWaitConditionNotRunning(n string, ct *types.ContainerJSON) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*ProviderConfig).DockerClient + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + statusC, errC := client.ContainerWait(ctx, rs.Primary.ID, container.WaitConditionNotRunning) + + select { + case err := <-errC: + { + if err != nil { + return fmt.Errorf("Container is still running") + } + } + + case <-statusC: + } + + return nil + } +} + +func testAccContainerWaitConditionRemoved(n string, ct *types.ContainerJSON) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*ProviderConfig).DockerClient + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + statusC, errC := client.ContainerWait(ctx, rs.Primary.ID, container.WaitConditionRemoved) + + select { + case err := <-errC: + { + if err != nil { + return fmt.Errorf("Container has not been removed") + } + } + + case <-statusC: + } + + return nil + } +} + func testValueHigherEqualThan(name, key string, value int) resource.TestCheckFunc { return func(s *terraform.State) error { ms := s.RootModule() @@ -912,7 +1097,7 @@ resource "docker_container" "foo" { image = "${docker_image.foo.latest}" ports { - internal = "80" + internal = 80 } } ` @@ -929,10 +1114,10 @@ resource "docker_container" "foo" { ports = [ { - internal = "80" + internal = 80 }, { - internal = "81" + internal = 81 } ] } @@ -948,8 +1133,8 @@ resource "docker_container" "foo" { image = "${docker_image.foo.latest}" ports { - internal = "80" - external = "32787" + internal = 80 + external = 32787 } } ` @@ -965,16 +1150,60 @@ resource "docker_container" "foo" { ports = [ { - internal = "80" - external = "32787" + internal = 80 + external = 32787 }, { - internal = "81" - external = "32788" + internal = 81 + external = 32788 } ] } ` +const testAccDockerContainerRmConfig = ` +resource "docker_image" "foo" { + name = "busybox:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + command = ["/bin/sleep", "15"] + rm = true +} +` + +const testAccDockerContainerAttachConfig = ` +resource "docker_image" "foo" { + name = "busybox:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + command = ["/bin/sh", "-c", "for i in $(seq 1 15); do sleep 1 && echo \"test $i\"; done"] + attach = true + must_run = false +} +` + +const testAccDockerContainerExitCodeConfig = ` +resource "docker_image" "foo" { + name = "busybox:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + command = ["/bin/sh", "-c", "exit 123"] + attach = true + must_run = false +} +` + const testAccDockerContainer2NetworksConfig = ` resource "docker_image" "foo" { name = "nginx:latest" diff --git a/website/docs/r/container.html.markdown b/website/docs/r/container.html.markdown index 6c1822c8..10b3ecd3 100644 --- a/website/docs/r/container.html.markdown +++ b/website/docs/r/container.html.markdown @@ -61,10 +61,13 @@ data is stored in them. See [the docker documentation][linkdoc] for more details * `hostname` - (Optional, string) Hostname of the container. * `domainname` - (Optional, string) Domain name of the container. +* `attach` - (Optional, bool) Attach to container. * `restart` - (Optional, string) The restart policy for the container. Must be one of "no", "on-failure", "always", "unless-stopped". * `max_retry_count` - (Optional, int) The maximum amount of times to an attempt a restart when `restart` is set to "on-failure" +* `rm` - (Optional, bool) If true, then the container will be automatically removed after his execution. Terraform + won't check this container after creation. * `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. @@ -205,6 +208,7 @@ the following: The following attributes are exported: + * `exit_code` - The exit code of the container if it is not running (and should not i.e. `must_run` is disabled). * `network_data` - (Map of a block) The IP addresses of the container on each network. Key are the network names, values are the IP addresses. * `ip_address` - The IP address of the container.