diff --git a/azurerm/resource_arm_container_group.go b/azurerm/resource_arm_container_group.go index 84414d7d97fd..0bdf5bd2dfaf 100644 --- a/azurerm/resource_arm_container_group.go +++ b/azurerm/resource_arm_container_group.go @@ -1,13 +1,18 @@ package azurerm import ( + "bytes" "fmt" "log" "strings" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" @@ -24,9 +29,10 @@ func resourceArmContainerGroup() *schema.Resource { Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "location": locationSchema(), @@ -38,7 +44,7 @@ func resourceArmContainerGroup() *schema.Resource { Optional: true, Default: "Public", ForceNew: true, - DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + DiffSuppressFunc: suppress.CaseDifference, ValidateFunc: validation.StringInSlice([]string{ string(containerinstance.Public), }, true), @@ -48,7 +54,7 @@ func resourceArmContainerGroup() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + DiffSuppressFunc: suppress.CaseDifference, ValidateFunc: validation.StringInSlice([]string{ string(containerinstance.Windows), string(containerinstance.Linux), @@ -64,23 +70,23 @@ func resourceArmContainerGroup() *schema.Resource { "server": { Type: schema.TypeString, Required: true, - ValidateFunc: validate.NoEmptyStrings, ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "username": { Type: schema.TypeString, Required: true, - ValidateFunc: validate.NoEmptyStrings, ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "password": { Type: schema.TypeString, Required: true, Sensitive: true, - ValidateFunc: validate.NoEmptyStrings, ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, }, }, @@ -93,7 +99,7 @@ func resourceArmContainerGroup() *schema.Resource { Optional: true, ForceNew: true, Default: string(containerinstance.Always), - DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + DiffSuppressFunc: suppress.CaseDifference, ValidateFunc: validation.StringInSlice([]string{ string(containerinstance.Always), string(containerinstance.Never), @@ -101,16 +107,6 @@ func resourceArmContainerGroup() *schema.Resource { }, true), }, - "ip_address": { - Type: schema.TypeString, - Computed: true, - }, - - "fqdn": { - Type: schema.TypeString, - Computed: true, - }, - "dns_name_label": { Type: schema.TypeString, Optional: true, @@ -124,15 +120,17 @@ func resourceArmContainerGroup() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "image": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "cpu": { @@ -151,20 +149,55 @@ func resourceArmContainerGroup() *schema.Resource { Type: schema.TypeInt, Optional: true, ForceNew: true, - ValidateFunc: validation.IntBetween(1, 65535), + Computed: true, + Deprecated: "Deprecated in favor of `ports`", + ValidateFunc: validate.PortNumber, }, "protocol": { Type: schema.TypeString, Optional: true, ForceNew: true, - DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + Computed: true, + Deprecated: "Deprecated in favor of `ports`", + DiffSuppressFunc: suppress.CaseDifference, ValidateFunc: validation.StringInSlice([]string{ string(containerinstance.TCP), string(containerinstance.UDP), }, true), }, + "ports": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Computed: true, + Set: resourceArmContainerGroupPortsHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validate.PortNumber, + }, + + "protocol": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + //Default: string(containerinstance.TCP), restore in 2.0 + ValidateFunc: validation.StringInSlice([]string{ + string(containerinstance.TCP), + string(containerinstance.UDP), + }, false), + }, + }, + }, + }, + "environment_variables": { Type: schema.TypeMap, ForceNew: true, @@ -199,15 +232,17 @@ func resourceArmContainerGroup() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "mount_path": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "read_only": { @@ -218,21 +253,24 @@ func resourceArmContainerGroup() *schema.Resource { }, "share_name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "storage_account_name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, "storage_account_key": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, }, }, }, @@ -240,6 +278,16 @@ func resourceArmContainerGroup() *schema.Resource { }, }, }, + + "ip_address": { + Type: schema.TypeString, + Computed: true, + }, + + "fqdn": { + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -413,27 +461,44 @@ func expandContainerGroupContainers(d *schema.ResourceData) (*[]containerinstanc }, } - if v := data["port"]; v != 0 { - port := int32(v.(int)) + if v, ok := data["ports"].(*schema.Set); ok && len(v.List()) > 0 { + var ports []containerinstance.ContainerPort + for _, v := range v.List() { + portObj := v.(map[string]interface{}) - // container port (port number) - container.Ports = &[]containerinstance.ContainerPort{ - { - Port: &port, - }, - } + port := int32(portObj["port"].(int)) + proto := portObj["protocol"].(string) - // container group port (port number + protocol) - containerGroupPort := containerinstance.Port{ - Port: &port, + ports = append(ports, containerinstance.ContainerPort{ + Port: &port, + Protocol: containerinstance.ContainerNetworkProtocol(proto), + }) + containerGroupPorts = append(containerGroupPorts, containerinstance.Port{ + Port: &port, + Protocol: containerinstance.ContainerGroupNetworkProtocol(proto), + }) } + container.Ports = &ports + } else { + if v := int32(data["port"].(int)); v != 0 { + ports := []containerinstance.ContainerPort{ + { + Port: &v, + }, + } - if v, ok := data["protocol"]; ok { - protocol := v.(string) - containerGroupPort.Protocol = containerinstance.ContainerGroupNetworkProtocol(strings.ToUpper(protocol)) - } + port := containerinstance.Port{ + Port: &v, + } + + if v, ok := data["protocol"].(string); ok { + ports[0].Protocol = containerinstance.ContainerNetworkProtocol(v) + port.Protocol = containerinstance.ContainerGroupNetworkProtocol(v) + } - containerGroupPorts = append(containerGroupPorts, containerGroupPort) + container.Ports = &ports + containerGroupPorts = append(containerGroupPorts, port) + } } // Set both sensitive and non-secure environment variables @@ -620,7 +685,6 @@ func flattenContainerGroupContainers(d *schema.ResourceData, containers *[]conta for i, c := range d.Get("container").([]interface{}) { cfg := c.(map[string]interface{}) nameIndexMap[cfg["name"].(string)] = i - } containerCfg := make([]interface{}, 0, len(*containers)) @@ -650,8 +714,20 @@ func flattenContainerGroupContainers(d *schema.ResourceData, containers *[]conta } } - if len(*container.Ports) > 0 { - containerPort := *(*container.Ports)[0].Port + if cPorts := container.Ports; cPorts != nil && len(*cPorts) > 0 { + ports := make([]interface{}, 0) + for _, p := range *cPorts { + port := make(map[string]interface{}) + if v := p.Port; v != nil { + port["port"] = int(*v) + } + port["protocol"] = string(p.Protocol) + ports = append(ports, port) + } + containerConfig["ports"] = schema.NewSet(resourceArmContainerGroupPortsHash, ports) + + //old deprecated code + containerPort := *(*cPorts)[0].Port containerConfig["port"] = containerPort // protocol isn't returned in container config, have to search in container group ports protocol := "" @@ -799,3 +875,14 @@ func flattenContainerVolumes(volumeMounts *[]containerinstance.VolumeMount, cont return volumeConfigs } + +func resourceArmContainerGroupPortsHash(v interface{}) int { + var buf bytes.Buffer + + if m, ok := v.(map[string]interface{}); ok { + buf.WriteString(fmt.Sprintf("%d-", m["port"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) + } + + return hashcode.String(buf.String()) +} diff --git a/azurerm/resource_arm_container_group_test.go b/azurerm/resource_arm_container_group_test.go index 69a7eac9f379..6e3f3c34af8b 100644 --- a/azurerm/resource_arm_container_group_test.go +++ b/azurerm/resource_arm_container_group_test.go @@ -71,6 +71,8 @@ func TestAccAzureRMContainerGroup_imageRegistryCredentialsUpdate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "image_registry_credential.1.server", "mine.acr.io"), resource.TestCheckResourceAttr(resourceName, "image_registry_credential.1.username", "acrusername"), resource.TestCheckResourceAttr(resourceName, "image_registry_credential.1.password", "acrpassword"), + resource.TestCheckResourceAttr(resourceName, "container.0.port", "5443"), + resource.TestCheckResourceAttr(resourceName, "container.0.protocol", "UDP"), ), }, { @@ -81,6 +83,7 @@ func TestAccAzureRMContainerGroup_imageRegistryCredentialsUpdate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "image_registry_credential.0.server", "hub.docker.com"), resource.TestCheckResourceAttr(resourceName, "image_registry_credential.0.username", "updatedusername"), resource.TestCheckResourceAttr(resourceName, "image_registry_credential.0.password", "updatedpassword"), + resource.TestCheckResourceAttr(resourceName, "container.0.ports.#", "1"), ), }, }, @@ -104,6 +107,7 @@ func TestAccAzureRMContainerGroup_linuxBasic(t *testing.T) { testCheckAzureRMContainerGroupExists(resourceName), resource.TestCheckResourceAttr(resourceName, "container.#", "1"), resource.TestCheckResourceAttr(resourceName, "os_type", "Linux"), + resource.TestCheckResourceAttr(resourceName, "container.0.port", "80"), ), }, { @@ -172,6 +176,7 @@ func TestAccAzureRMContainerGroup_linuxBasicUpdate(t *testing.T) { Check: resource.ComposeTestCheckFunc( testCheckAzureRMContainerGroupExists(resourceName), resource.TestCheckResourceAttr(resourceName, "container.#", "2"), + resource.TestCheckResourceAttr(resourceName, "container.0.ports.#", "2"), ), }, }, @@ -194,6 +199,7 @@ func TestAccAzureRMContainerGroup_linuxComplete(t *testing.T) { Check: resource.ComposeTestCheckFunc( testCheckAzureRMContainerGroupExists(resourceName), resource.TestCheckResourceAttr(resourceName, "container.#", "1"), + resource.TestCheckResourceAttr(resourceName, "container.0.ports.#", "1"), resource.TestCheckResourceAttr(resourceName, "container.0.command", "/bin/bash -c ls"), resource.TestCheckResourceAttr(resourceName, "container.0.commands.#", "3"), resource.TestCheckResourceAttr(resourceName, "container.0.commands.0", "/bin/bash"), @@ -245,6 +251,7 @@ func TestAccAzureRMContainerGroup_windowsBasic(t *testing.T) { testCheckAzureRMContainerGroupExists(resourceName), resource.TestCheckResourceAttr(resourceName, "container.#", "1"), resource.TestCheckResourceAttr(resourceName, "os_type", "Windows"), + resource.TestCheckResourceAttr(resourceName, "container.0.ports.#", "2"), ), }, { @@ -272,6 +279,7 @@ func TestAccAzureRMContainerGroup_windowsComplete(t *testing.T) { Check: resource.ComposeTestCheckFunc( testCheckAzureRMContainerGroupExists(resourceName), resource.TestCheckResourceAttr(resourceName, "container.#", "1"), + resource.TestCheckResourceAttr(resourceName, "container.0.ports.#", "1"), resource.TestCheckResourceAttr(resourceName, "container.0.command", "cmd.exe echo hi"), resource.TestCheckResourceAttr(resourceName, "container.0.commands.#", "3"), resource.TestCheckResourceAttr(resourceName, "container.0.commands.0", "cmd.exe"), @@ -320,7 +328,7 @@ resource "azurerm_container_group" "test" { image = "microsoft/aci-helloworld:latest" cpu = "0.5" memory = "0.5" - port = "80" + port = 80 } tags { @@ -372,11 +380,12 @@ resource "azurerm_container_group" "test" { os_type = "Linux" container { - name = "hw" - image = "microsoft/aci-helloworld:latest" - cpu = "0.5" - memory = "0.5" - port = "80" + name = "hw" + image = "microsoft/aci-helloworld:latest" + cpu = "0.5" + memory = "0.5" + port = 5443 + protocol = "UDP" } image_registry_credential { @@ -424,7 +433,9 @@ resource "azurerm_container_group" "test" { image = "microsoft/aci-helloworld:latest" cpu = "0.5" memory = "0.5" - port = "80" + ports = { + port = 80 + } } image_registry_credential { @@ -466,7 +477,13 @@ resource "azurerm_container_group" "test" { image = "microsoft/aci-helloworld:latest" cpu = "0.5" memory = "0.5" - port = "80" + ports = { + port = 80 + } + ports = { + port = 5443 + protocol = "UDP" + } } container { @@ -502,7 +519,14 @@ resource "azurerm_container_group" "test" { image = "microsoft/windowsservercore:latest" cpu = "2.0" memory = "3.5" - port = "80" + ports = { + port = 80 + protocol = "TCP" + } + ports = { + port = 443 + protocol = "TCP" + } } tags { @@ -533,7 +557,10 @@ resource "azurerm_container_group" "test" { image = "microsoft/windowsservercore:latest" cpu = "2.0" memory = "3.5" - port = "80" + ports = { + port = 80 + protocol = "TCP" + } environment_variables { "foo" = "bar" @@ -594,8 +621,10 @@ resource "azurerm_container_group" "test" { cpu = "1" memory = "1.5" - port = "80" - protocol = "TCP" + ports = { + port = 80 + protocol = "TCP" + } volume { name = "logs" @@ -604,11 +633,11 @@ resource "azurerm_container_group" "test" { share_name = "${azurerm_storage_share.test.name}" storage_account_name = "${azurerm_storage_account.test.name}" - storage_account_key = "${azurerm_storage_account.test.primary_access_key}" + storage_account_key = "${azurerm_storage_account.test.primary_access_key}" } environment_variables { - "foo" = "bar" + "foo" = "bar" "foo1" = "bar1" } @@ -651,7 +680,6 @@ func testCheckAzureRMContainerGroupExists(resourceName string) resource.TestChec } return fmt.Errorf("Bad: Get on containerGroupsClient: %+v", err) } - return nil } } diff --git a/website/docs/r/container_group.html.markdown b/website/docs/r/container_group.html.markdown index 523950251b3b..6ab0bf58e4c8 100644 --- a/website/docs/r/container_group.html.markdown +++ b/website/docs/r/container_group.html.markdown @@ -49,7 +49,14 @@ resource "azurerm_container_group" "aci-helloworld" { image = "seanmckenna/aci-hellofiles" cpu = "0.5" memory = "1.5" - port = "80" + ports = { + port = 80 + protocol = "TCP" + } + ports = { + port = 443 + protocol = "TCP" + } environment_variables { "NODE_ENV" = "testing" @@ -121,9 +128,7 @@ The `container` block supports: * `memory` - (Required) The required memory of the containers in GB. Changing this forces a new resource to be created. -* `port` - (Optional) A public port for the container. Changing this forces a new resource to be created. - -* `protocol` - (Optional) The protocol associated with port for the container. Allowed values are `TCP` and `UDP`. +* `ports` - (Optional) A set of public ports for the container. Changing this forces a new resource to be created. Set as documented in the `ports` block below. * `environment_variables` - (Optional) A list of environment variables to be set on the container. Specified as a map of name/value pairs. Changing this forces a new resource to be created. @@ -159,6 +164,12 @@ The `image_registry_credential` block supports: * `server` - (Required) The address to use to connect to the registry without protocol ("https"/"http"). For example: "myacr.acr.io" +The `ports` block supports: + +* `port` - (Required) The port number the container will expose. + +* `protocol` - (Required) The network protocol associated with port. Possible values are `TCP` & `UDP`. + ## Attributes Reference The following attributes are exported: