From bc56f62190b1efe12ce0639054dc550f2b7032cd Mon Sep 17 00:00:00 2001 From: Manuel Vogel Date: Mon, 29 Oct 2018 06:36:21 +0100 Subject: [PATCH] Adds container static IPv4/IPv6 address. Marks network and network_alias as deprecated. Closes #105. --- docker/resource_docker_container.go | 49 ++++- docker/resource_docker_container_funcs.go | 32 +++- docker/resource_docker_container_test.go | 206 +++++++++++++++++++++- docker/resource_docker_network_test.go | 138 +++++++++++++-- website/docs/r/container.html.markdown | 17 +- 5 files changed, 417 insertions(+), 25 deletions(-) diff --git a/docker/resource_docker_container.go b/docker/resource_docker_container.go index f69ac100..6f683512 100644 --- a/docker/resource_docker_container.go +++ b/docker/resource_docker_container.go @@ -448,11 +448,13 @@ func resourceDockerContainer() *schema.Resource { }, "network_alias": &schema.Schema{ - Type: schema.TypeSet, - Optional: true, - ForceNew: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Description: "Set an alias for the container in all specified networks", + Deprecated: "Use networks_advanced instead. Will be removed in v2.0.0", }, "network_mode": &schema.Schema{ @@ -462,11 +464,44 @@ func resourceDockerContainer() *schema.Resource { }, "networks": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Deprecated: "Use networks_advanced instead. Will be removed in v2.0.0", + }, + + "networks_advanced": &schema.Schema{ Type: schema.TypeSet, Optional: true, ForceNew: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "aliases": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "ipv4_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "ipv6_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, }, "pid_mode": &schema.Schema{ diff --git a/docker/resource_docker_container_funcs.go b/docker/resource_docker_container_funcs.go index fbdb934f..da6719a4 100644 --- a/docker/resource_docker_container_funcs.go +++ b/docker/resource_docker_container_funcs.go @@ -238,12 +238,28 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err d.SetId(retContainer.ID) + // Still support the deprecated properties if v, ok := d.GetOk("networks"); ok { + if err := client.NetworkDisconnect(context.Background(), "bridge", retContainer.ID, false); err != nil { + if !strings.Contains(err.Error(), "is not connected to the network bridge") { + return fmt.Errorf("Unable to disconnect the default network: %s", err) + } + } endpointConfig := &network.EndpointSettings{} if v, ok := d.GetOk("network_alias"); ok { endpointConfig.Aliases = stringSetToStringSlice(v.(*schema.Set)) } + for _, rawNetwork := range v.(*schema.Set).List() { + networkID := rawNetwork.(string) + if err := client.NetworkConnect(context.Background(), networkID, retContainer.ID, endpointConfig); err != nil { + return fmt.Errorf("Unable to connect to network '%s': %s", networkID, err) + } + } + } + + // But overwrite them with the future ones, if set + if v, ok := d.GetOk("networks_advanced"); ok { if err := client.NetworkDisconnect(context.Background(), "bridge", retContainer.ID, false); err != nil { if !strings.Contains(err.Error(), "is not connected to the network bridge") { return fmt.Errorf("Unable to disconnect the default network: %s", err) @@ -251,7 +267,21 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err } for _, rawNetwork := range v.(*schema.Set).List() { - networkID := rawNetwork.(string) + networkID := rawNetwork.(map[string]interface{})["name"].(string) + + endpointConfig := &network.EndpointSettings{} + endpointIPAMConfig := &network.EndpointIPAMConfig{} + if v, ok := rawNetwork.(map[string]interface{})["aliases"]; ok { + endpointConfig.Aliases = stringSetToStringSlice(v.(*schema.Set)) + } + if v, ok := rawNetwork.(map[string]interface{})["ipv4_address"]; ok { + endpointIPAMConfig.IPv4Address = v.(string) + } + if v, ok := rawNetwork.(map[string]interface{})["ipv6_address"]; ok { + endpointIPAMConfig.IPv6Address = v.(string) + } + endpointConfig.IPAMConfig = endpointIPAMConfig + if err := client.NetworkConnect(context.Background(), networkID, retContainer.ID, endpointConfig); err != nil { return fmt.Errorf("Unable to connect to network '%s': %s", networkID, err) } diff --git a/docker/resource_docker_container_test.go b/docker/resource_docker_container_test.go index c6021f39..0d1b5e10 100644 --- a/docker/resource_docker_container_test.go +++ b/docker/resource_docker_container_test.go @@ -458,6 +458,7 @@ func TestAccDockerContainer_device(t *testing.T) { }, }) } + func TestAccDockerContainer_port_internal(t *testing.T) { var c types.ContainerJSON @@ -504,6 +505,7 @@ func TestAccDockerContainer_port_internal(t *testing.T) { }, }) } + func TestAccDockerContainer_port_multiple_internal(t *testing.T) { var c types.ContainerJSON @@ -619,6 +621,7 @@ func TestAccDockerContainer_port(t *testing.T) { }, }) } + func TestAccDockerContainer_multiple_ports(t *testing.T) { var c types.ContainerJSON @@ -821,6 +824,123 @@ func TestAccDockerContainer_nostart(t *testing.T) { }) } +func TestAccDockerContainer_ipv4address(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + networks := c.NetworkSettings.Networks + + if len(networks) != 1 { + return fmt.Errorf("Container doesn't have a correct network") + } + if _, ok := networks["tf-test"]; !ok { + return fmt.Errorf("Container doesn't have a correct network") + } + if c.NetworkSettings.Networks["tf-test"].IPAMConfig == nil { + return fmt.Errorf("Container doesn't have a correct IPAM config") + } + if c.NetworkSettings.Networks["tf-test"].IPAMConfig.IPv4Address != "10.0.1.123" { + return fmt.Errorf("Container doesn't have a correct IPv4 address") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerNetworksIPv4AddressConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + ), + }, + }, + }) +} + +func TestAccDockerContainer_ipv6address(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + networks := c.NetworkSettings.Networks + + if len(networks) != 1 { + return fmt.Errorf("Container doesn't have a correct network") + } + if _, ok := networks["tf-test"]; !ok { + return fmt.Errorf("Container doesn't have a correct network") + } + if c.NetworkSettings.Networks["tf-test"].IPAMConfig == nil { + return fmt.Errorf("Container doesn't have a correct IPAM config") + } + if c.NetworkSettings.Networks["tf-test"].IPAMConfig.IPv6Address != "fd00:0:0:0::123" { + return fmt.Errorf("Container doesn't have a correct IPv6 address") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerNetworksIPv6AddressConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + ), + }, + }, + }) +} + +func TestAccDockerContainer_dualstackaddress(t *testing.T) { + var c types.ContainerJSON + + testCheck := func(*terraform.State) error { + networks := c.NetworkSettings.Networks + + if len(networks) != 1 { + return fmt.Errorf("Container doesn't have a correct network") + } + if _, ok := networks["tf-test"]; !ok { + return fmt.Errorf("Container doesn't have a correct network") + } + if c.NetworkSettings.Networks["tf-test"].IPAMConfig == nil { + return fmt.Errorf("Container doesn't have a correct IPAM config") + } + if c.NetworkSettings.Networks["tf-test"].IPAMConfig.IPv4Address != "10.0.1.123" { + return fmt.Errorf("Container doesn't have a correct IPv4 address") + } + if c.NetworkSettings.Networks["tf-test"].IPAMConfig.IPv6Address != "fd00:0:0:0::123" { + return fmt.Errorf("Container doesn't have a correct IPv6 address") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerContainerNetworksDualStackAddressConfig, + Check: resource.ComposeTestCheckFunc( + testAccContainerRunning("docker_container.foo", &c), + testCheck, + resource.TestCheckResourceAttr("docker_container.foo", "name", "tf-test"), + ), + }, + }, + }) +} + func testAccContainerRunning(n string, container *types.ContainerJSON) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -1073,8 +1193,12 @@ resource "docker_container" "foo" { } network_mode = "bridge" - networks = ["${docker_network.test_network.name}"] - network_alias = ["tftest"] + networks_advanced = [ + { + name = "${docker_network.test_network.name}" + aliases = ["tftest"] + } + ] host { host = "testhost" @@ -1134,8 +1258,8 @@ resource "docker_container" "foo" { image = "${docker_image.foo.latest}" devices { - host_path = "/dev/zero" - container_path = "/dev/zero_test" + host_path = "/dev/zero" + container_path = "/dev/zero_test" } } ` @@ -1320,3 +1444,77 @@ resource "docker_container" "foo" { must_run = false } ` +const testAccDockerContainerNetworksIPv4AddressConfig = ` +resource "docker_network" "test" { + name = "tf-test" + ipam_config { + subnet = "10.0.1.0/24" + } +} +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + networks_advanced = [ + { + name = "${docker_network.test.name}", + ipv4_address = "10.0.1.123" + } + ] +} +` +const testAccDockerContainerNetworksIPv6AddressConfig = ` +resource "docker_network" "test" { + name = "tf-test" + ipv6 = true + ipam_config { + subnet = "fd00::1/64" + } +} +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + networks_advanced = [ + { + name = "${docker_network.test.name}", + ipv6_address = "fd00:0:0:0::123" + } + ] +} +` +const testAccDockerContainerNetworksDualStackAddressConfig = ` +resource "docker_network" "test" { + name = "tf-test" + ipv6 = true + ipam_config = [ + { + subnet = "10.0.1.0/24" + }, + { + subnet = "fd00::1/64" + } + ] +} +resource "docker_image" "foo" { + name = "nginx:latest" + keep_locally = true +} +resource "docker_container" "foo" { + name = "tf-test" + image = "${docker_image.foo.latest}" + networks_advanced = [ + { + name = "${docker_network.test.name}", + ipv4_address = "10.0.1.123" + ipv6_address = "fd00:0:0:0::123" + } + ] +} +` diff --git a/docker/resource_docker_network_test.go b/docker/resource_docker_network_test.go index 3534343e..d670bc9e 100644 --- a/docker/resource_docker_network_test.go +++ b/docker/resource_docker_network_test.go @@ -75,7 +75,7 @@ func TestAccDockerNetwork_internal(t *testing.T) { resource.TestStep{ Config: testAccDockerNetworkInternalConfig, Check: resource.ComposeTestCheckFunc( - testAccNetwork("docker_network.foobar", &n), + testAccNetwork("docker_network.foo", &n), testAccNetworkInternal(&n, true), ), }, @@ -93,8 +93,8 @@ func testAccNetworkInternal(network *types.NetworkResource, internal bool) resou } const testAccDockerNetworkInternalConfig = ` -resource "docker_network" "foobar" { - name = "foobar" +resource "docker_network" "foo" { + name = "bar" internal = true } ` @@ -109,7 +109,7 @@ func TestAccDockerNetwork_attachable(t *testing.T) { resource.TestStep{ Config: testAccDockerNetworkAttachableConfig, Check: resource.ComposeTestCheckFunc( - testAccNetwork("docker_network.foobar", &n), + testAccNetwork("docker_network.foo", &n), testAccNetworkAttachable(&n, true), ), }, @@ -127,12 +127,128 @@ func testAccNetworkAttachable(network *types.NetworkResource, attachable bool) r } const testAccDockerNetworkAttachableConfig = ` -resource "docker_network" "foobar" { - name = "foobar" +resource "docker_network" "foo" { + name = "bar" attachable = true } ` +//func TestAccDockerNetwork_ingress(t *testing.T) { +// var n types.NetworkResource +// +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { testAccPreCheck(t) }, +// Providers: testAccProviders, +// Steps: []resource.TestStep{ +// resource.TestStep{ +// Config: testAccDockerNetworkIngressConfig, +// Check: resource.ComposeTestCheckFunc( +// testAccNetwork("docker_network.foo", &n), +// testAccNetworkIngress(&n, true), +// ), +// }, +// }, +// }) +//} +// +//func testAccNetworkIngress(network *types.NetworkResource, internal bool) resource.TestCheckFunc { +// return func(s *terraform.State) error { +// if network.Internal != internal { +// return fmt.Errorf("Bad value for attribute 'ingress': %t", network.Ingress) +// } +// return nil +// } +//} +// +//const testAccDockerNetworkIngressConfig = ` +//resource "docker_network" "foo" { +// name = "bar" +// ingress = true +//} +//` + +func TestAccDockerNetwork_ipv4(t *testing.T) { + var n types.NetworkResource + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerNetworkIPv4Config, + Check: resource.ComposeTestCheckFunc( + testAccNetwork("docker_network.foo", &n), + testAccNetworkIPv4(&n, true), + ), + }, + }, + }) +} + +func testAccNetworkIPv4(network *types.NetworkResource, internal bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(network.IPAM.Config) != 1 { + return fmt.Errorf("Bad value for IPAM configuration count: %d", len(network.IPAM.Config)) + } + if network.IPAM.Config[0].Subnet != "10.0.1.0/24" { + return fmt.Errorf("Bad value for attribute 'subnet': %v", network.IPAM.Config[0].Subnet) + } + return nil + } +} + +const testAccDockerNetworkIPv4Config = ` +resource "docker_network" "foo" { + name = "bar" + ipam_config { + subnet = "10.0.1.0/24" + } +} +` + +func TestAccDockerNetwork_ipv6(t *testing.T) { + var n types.NetworkResource + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDockerNetworkIPv6Config, + Check: resource.ComposeTestCheckFunc( + testAccNetwork("docker_network.foo", &n), + testAccNetworkIPv6(&n, true), + ), + }, + }, + }) +} + +func testAccNetworkIPv6(network *types.NetworkResource, internal bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if !network.EnableIPv6 { + return fmt.Errorf("Bad value for attribute 'ipv6': %t", network.EnableIPv6) + } + if len(network.IPAM.Config) != 2 { + return fmt.Errorf("Bad value for IPAM configuration count: %d", len(network.IPAM.Config)) + } + if network.IPAM.Config[1].Subnet != "fd00::1/64" { + return fmt.Errorf("Bad value for attribute 'subnet': %v", network.IPAM.Config[0].Subnet) + } + return nil + } +} + +const testAccDockerNetworkIPv6Config = ` +resource "docker_network" "foo" { + name = "bar" + ipv6 = true + ipam_config { + subnet = "fd00::1/64" + } +} +` + func TestAccDockerNetwork_labels(t *testing.T) { var n types.NetworkResource @@ -143,8 +259,8 @@ func TestAccDockerNetwork_labels(t *testing.T) { resource.TestStep{ Config: testAccDockerNetworkLabelsConfig, Check: resource.ComposeTestCheckFunc( - testAccNetwork("docker_network.foobar", &n), - testAccNetworkLabel(&n, "com.docker.compose.network", "foobar"), + testAccNetwork("docker_network.foo", &n), + testAccNetworkLabel(&n, "com.docker.compose.network", "foo"), testAccNetworkLabel(&n, "com.docker.compose.project", "test"), ), }, @@ -162,10 +278,10 @@ func testAccNetworkLabel(network *types.NetworkResource, name string, value stri } const testAccDockerNetworkLabelsConfig = ` -resource "docker_network" "foobar" { - name = "test_foobar" +resource "docker_network" "foo" { + name = "test_foo" labels { - "com.docker.compose.network" = "foobar" + "com.docker.compose.network" = "foo" "com.docker.compose.project" = "test" } } diff --git a/website/docs/r/container.html.markdown b/website/docs/r/container.html.markdown index d5e8c730..314ac3a6 100644 --- a/website/docs/r/container.html.markdown +++ b/website/docs/r/container.html.markdown @@ -89,10 +89,11 @@ data is stored in them. See [the docker documentation][linkdoc] for more details Defaults to "json-file". * `log_opts` - (Optional, map of strings) Key/value pairs to use as options for the logging driver. -* `network_alias` - (Optional, set of strings) Network aliases of the container for user-defined networks only. +* `network_alias` - (Optional, set of strings) Network aliases of the container for user-defined networks only. *Deprecated:* use `networks_advanced` instead. * `network_mode` - (Optional, string) Network mode of the container. * `networks` - (Optional, set of strings) Id of the networks in which the - container is. + container is. *Deprecated:* use `networks_advanced` instead. +* `networks_advanced` - (Optional, block) See [Networks Advanced](#networks_advanced) below for details. * `destroy_grace_seconds` - (Optional, int) If defined will attempt to stop the container before destroying. Container will be destroyed after `n` seconds or on successful stop. * `upload` - (Optional, block) See [File Upload](#upload) below for details. * `ulimit` - (Optional, block) See [Ulimits](#ulimits) below for @@ -181,6 +182,18 @@ Each `upload` supports the following executable permission. Defaults to false. + +### Network advanced + +`networks_advanced` is a block within the configuration that can be repeated to specify +files to upload to the container before starting it. +Each `networks_advanced` supports the following: + +* `name` - (Required, string) A content of a file to upload. +* `aliases` - (Optional, set of strings) The network aliases of the container in the specific network. +* `ipv4_address` - (Optional, string) The IPV4 address of the container in the specific network. +* `ipv6_address` - (Optional, string) The IPV6 address of the container in the specific network. + ### Devices