diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8c7017..94c2d9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ IMPROVEMENTS * Add support for running tests on Windows [GH-54] and ([#90](https://github.com/terraform-providers/terraform-provider-docker/pull/90)) +BUG FIXES +* Fixes issue with internal and external ports on containers [GH-8] and ([#89](https://github.com/terraform-providers/terraform-provider-docker/pull/90)) +* Fixes `tfstate` having correct external port for containers [GH-73] + ## 1.0.2 (September 27, 2018) BUG FIXES diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index a3c114b0..cab013ab 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -205,12 +205,14 @@ func resourceDockerContainer() *schema.Resource { "external": &schema.Schema{ Type: schema.TypeInt, + Default: "32678", Optional: true, ForceNew: true, }, "ip": &schema.Schema{ Type: schema.TypeString, + Default: "0.0.0.0", Optional: true, ForceNew: true, }, diff --git a/docker/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go index 8be51f4d..250325d8 100644 --- a/docker/resource_docker_container_funcs.go +++ b/docker/resource_docker_container_funcs.go @@ -3,12 +3,17 @@ package docker import ( "archive/tar" "bytes" + "encoding/json" "errors" "fmt" + "log" "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" @@ -16,7 +21,6 @@ import ( "github.com/docker/go-connections/nat" "github.com/docker/go-units" "github.com/hashicorp/terraform/helper/schema" - "math/rand" ) var ( @@ -284,6 +288,9 @@ func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error inspecting container %s: %s", apiContainer.ID, err) } + jsonObj, _ := json.MarshalIndent(container, "", "\t") + log.Printf("[DEBUG] Docker container inspect: %s", jsonObj) + if container.State.Running || !container.State.Running && !d.Get("must_run").(bool) { break @@ -319,12 +326,16 @@ func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error d.Set("ip_prefix_length", container.NetworkSettings.IPPrefixLen) d.Set("gateway", container.NetworkSettings.Gateway) d.Set("bridge", container.NetworkSettings.Bridge) + if err := d.Set("ports", flattenContainerPorts(container.NetworkSettings.Ports)); err != nil { + log.Printf("[WARN] failed to set ports from API: %s", err) + } } return nil } func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) error { + // TODO call resourceDockerContainerRead here return nil } @@ -354,6 +365,28 @@ func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) err return nil } +// TODO extract to structures_container.go +func flattenContainerPorts(in nat.PortMap) *schema.Set { + var out = make([]interface{}, 0) + for port, portBindings := range in { + m := make(map[string]interface{}) + for _, portBinding := range portBindings { + portProtocolSplit := strings.Split(string(port), "/") + convertedInternal, _ := strconv.Atoi(portProtocolSplit[0]) + convertedExternal, _ := strconv.Atoi(portBinding.HostPort) + m["internal"] = convertedInternal + m["external"] = convertedExternal + m["ip"] = portBinding.HostIP + m["protocol"] = portProtocolSplit[1] + out = append(out, m) + } + } + portsSpecResource := resourceDockerContainer().Schema["ports"].Elem.(*schema.Resource) + f := schema.HashResource(portsSpecResource) + return schema.NewSet(f, out) +} + +// TODO move to separate flattener file func stringListToStringSlice(stringList []interface{}) []string { ret := []string{} for _, v := range stringList { @@ -424,18 +457,19 @@ func portSetToDockerPorts(ports *schema.Set) (map[nat.Port]struct{}, map[nat.Por exposedPort := nat.Port(strconv.Itoa(internal) + "/" + protocol) retExposedPorts[exposedPort] = struct{}{} - external, extOk := port["external"].(int) - ip, ipOk := port["ip"].(string) + portBinding := nat.PortBinding{} + external, extOk := port["external"].(int) if extOk { - portBinding := nat.PortBinding{ - HostPort: strconv.Itoa(external), - } - if ipOk { - portBinding.HostIP = ip - } - retPortBindings[exposedPort] = append(retPortBindings[exposedPort], portBinding) + portBinding.HostPort = strconv.Itoa(external) } + + ip, ipOk := port["ip"].(string) + if ipOk { + portBinding.HostIP = ip + } + + retPortBindings[exposedPort] = append(retPortBindings[exposedPort], portBinding) } return retExposedPorts, retPortBindings diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index ca23e8f3..5099429b 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -9,6 +9,7 @@ import ( "testing" "context" + "github.com/docker/docker/api/types" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -375,6 +376,98 @@ func TestAccDockerContainer_device(t *testing.T) { }, }) } +func TestAccDockerContainer_port_internal(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + portMap := c.NetworkSettings.NetworkSettingsBase.Ports + portBindings, ok := portMap["80/tcp"] + if !ok || len(portMap["80/tcp"]) == 0 { + return fmt.Errorf("Port 80 on tcp is not set") + } + + portBindingsLength := len(portBindings) + if portBindingsLength != 1 { + return fmt.Errorf("Expected 1 binding on port 80, but was %d", portBindingsLength) + } + + if len(portBindings[0].HostIP) == 0 { + return fmt.Errorf("Expected host IP to be set, but was empty") + } + + if len(portBindings[0].HostPort) == 0 { + return fmt.Errorf("Expected host port to be set, but was empty") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerInternalPortConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.#", "1"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2978131916.internal", "80"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2978131916.ip", "0.0.0.0"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2978131916.protocol", "tcp"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2978131916.external", "32678"), + ), + }, + }, + }) +} +func TestAccDockerContainer_port(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + portMap := c.NetworkSettings.NetworkSettingsBase.Ports + portBindings, ok := portMap["80/tcp"] + if !ok || len(portMap["80/tcp"]) == 0 { + return fmt.Errorf("Port 80 on tcp is not set") + } + + portBindingsLength := len(portBindings) + if portBindingsLength != 1 { + return fmt.Errorf("Expected 1 binding on port 80, but was %d", portBindingsLength) + } + + if len(portBindings[0].HostIP) == 0 { + return fmt.Errorf("Expected host IP to be set, but was empty") + } + + if len(portBindings[0].HostPort) == 0 { + return fmt.Errorf("Expected host port to be set, but was empty") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerPortConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.#", "1"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2498386340.internal", "80"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2498386340.ip", "0.0.0.0"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2498386340.protocol", "tcp"), + resource.TestCheckResourceAttr("docker_container.foo", "ports.2498386340.external", "32787"), + ), + }, + }, + }) +} func testAccContainerRunning(n string, container *types.ContainerJSON) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -539,3 +632,35 @@ resource "docker_container" "foo" { } } ` + +const testAccDockerContainerInternalPortConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + ports { + internal = "80" + } +} +` +const testAccDockerContainerPortConfig = ` +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} + +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + + ports { + internal = "80" + external = "32787" + } +} +` diff --git a/website/docs/r/container.html.markdown b/website/docs/r/container.html.markdown index c000a195..ccd3abcc 100644 --- a/website/docs/r/container.html.markdown +++ b/website/docs/r/container.html.markdown @@ -122,8 +122,8 @@ the port mappings of the container. Each `ports` block supports the following: * `internal` - (Required, int) Port within the container. -* `external` - (Required, int) Port exposed out of the container. -* `ip` - (Optional, string) IP address/mask that can access this port. +* `external` - (Optional, int) Port exposed out of the container, defaults to `32768`. +* `ip` - (Optional, string) IP address/mask that can access this port, default to `0.0.0.0` * `protocol` - (Optional, string) Protocol that can be used over this port, defaults to TCP. @@ -213,4 +213,4 @@ The following attributes are exported: NetworkSettings. -[linkdoc] https://docs.docker.com/network/links/ \ No newline at end of file +[linkdoc] https://docs.docker.com/network/links/