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

azurerm_container_group: support exposed_port to configure ports at group level #10491

Merged
merged 7 commits into from
Apr 30, 2021
110 changes: 98 additions & 12 deletions azurerm/internal/services/containers/container_group_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,36 @@ func resourceContainerGroup() *schema.Resource {
ForceNew: true,
},

"exposed_port": {
Type: schema.TypeSet,
Optional: true, // change to 'Required' in 3.0 of the provider
ForceNew: true,
Computed: true, // remove in 3.0 of the provider
ConfigMode: schema.SchemaConfigModeAttr, // remove in 3.0 of the provider
manicminer marked this conversation as resolved.
Show resolved Hide resolved
Set: resourceContainerGroupPortsHash,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"port": {
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
ValidateFunc: validate.PortNumber,
},

"protocol": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: string(containerinstance.TCP),
ValidateFunc: validation.StringInSlice([]string{
string(containerinstance.TCP),
string(containerinstance.UDP),
}, false),
},
},
},
},

"container": {
Type: schema.TypeList,
Required: true,
Expand Down Expand Up @@ -671,6 +701,11 @@ func resourceContainerGroupRead(d *schema.ResourceData, meta interface{}) error
if address := props.IPAddress; address != nil {
d.Set("ip_address_type", address.Type)
d.Set("ip_address", address.IP)
exposedPorts := make([]interface{}, len(*resp.ContainerGroupProperties.IPAddress.Ports))
for i := range *resp.ContainerGroupProperties.IPAddress.Ports {
exposedPorts[i] = (*resp.ContainerGroupProperties.IPAddress.Ports)[i]
}
d.Set("exposed_port", flattenPorts(exposedPorts))
d.Set("dns_name_label", address.DNSNameLabel)
d.Set("fqdn", address.Fqdn)
}
Expand All @@ -687,6 +722,30 @@ func resourceContainerGroupRead(d *schema.ResourceData, meta interface{}) error
return tags.FlattenAndSet(d, resp.Tags)
}

func flattenPorts(ports []interface{}) *schema.Set {
if len(ports) > 0 {
flatPorts := make([]interface{}, 0)
for _, p := range ports {
port := make(map[string]interface{})
switch t := p.(type) {
case containerinstance.Port:
if v := t.Port; v != nil {
port["port"] = int(*v)
}
port["protocol"] = string(t.Protocol)
case containerinstance.ContainerPort:
if v := t.Port; v != nil {
port["port"] = int(*v)
}
port["protocol"] = string(t.Protocol)
}
flatPorts = append(flatPorts, port)
}
return schema.NewSet(resourceContainerGroupPortsHash, flatPorts)
}
return schema.NewSet(resourceContainerGroupPortsHash, make([]interface{}, 0))
}

func resourceContainerGroupDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).Containers.GroupsClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
Expand Down Expand Up @@ -806,6 +865,7 @@ func containerGroupEnsureDetachedFromNetworkProfileRefreshFunc(ctx context.Conte
func expandContainerGroupContainers(d *schema.ResourceData) (*[]containerinstance.Container, *[]containerinstance.Port, *[]containerinstance.Volume, error) {
containersConfig := d.Get("container").([]interface{})
containers := make([]containerinstance.Container, 0)
containerInstancePorts := make([]containerinstance.Port, 0)
containerGroupPorts := make([]containerinstance.Port, 0)
containerGroupVolumes := make([]containerinstance.Volume, 0)

Expand Down Expand Up @@ -857,7 +917,7 @@ func expandContainerGroupContainers(d *schema.ResourceData) (*[]containerinstanc
Port: &port,
Protocol: containerinstance.ContainerNetworkProtocol(proto),
})
containerGroupPorts = append(containerGroupPorts, containerinstance.Port{
containerInstancePorts = append(containerInstancePorts, containerinstance.Port{
Port: &port,
Protocol: containerinstance.ContainerGroupNetworkProtocol(proto),
})
Expand Down Expand Up @@ -917,6 +977,39 @@ func expandContainerGroupContainers(d *schema.ResourceData) (*[]containerinstanc
containers = append(containers, container)
}

// Determine ports to be exposed on the group level, based on exposed_ports
// and on what ports have been exposed on individual containers.
if v, ok := d.Get("exposed_port").(*schema.Set); ok && len(v.List()) > 0 {
cgpMap := make(map[int32]map[containerinstance.ContainerGroupNetworkProtocol]bool)
for _, p := range containerInstancePorts {
if val, ok := cgpMap[*p.Port]; ok {
val[p.Protocol] = true
cgpMap[*p.Port] = val
} else {
protoMap := map[containerinstance.ContainerGroupNetworkProtocol]bool{p.Protocol: true}
cgpMap[*p.Port] = protoMap
}
}

for _, p := range v.List() {
portConfig := p.(map[string]interface{})
port := int32(portConfig["port"].(int))
proto := portConfig["protocol"].(string)
if !cgpMap[port][containerinstance.ContainerGroupNetworkProtocol(proto)] {
return nil, nil, nil, fmt.Errorf("Port %d/%s is not exposed on any individual container in the container group.\n"+
"An exposed_ports block contains %d/%s, but no individual container has a ports block with the same port "+
"and protocol. Any ports exposed on the container group must also be exposed on an individual container.",
port, proto, port, proto)
}
containerGroupPorts = append(containerGroupPorts, containerinstance.Port{
Port: &port,
Protocol: containerinstance.ContainerGroupNetworkProtocol(proto),
})
}
} else {
containerGroupPorts = containerInstancePorts // remove in 3.0 of the provider
}

return &containers, &containerGroupPorts, &containerGroupVolumes, nil
}

Expand Down Expand Up @@ -1264,18 +1357,11 @@ func flattenContainerGroupContainers(d *schema.ResourceData, containers *[]conta
}
}

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(resourceContainerGroupPortsHash, ports)
containerPorts := make([]interface{}, len(*container.Ports))
for i := range *container.Ports {
containerPorts[i] = (*container.Ports)[i]
}
containerConfig["ports"] = flattenPorts(containerPorts)

if container.EnvironmentVariables != nil {
if len(*container.EnvironmentVariables) > 0 {
Expand Down
139 changes: 139 additions & 0 deletions azurerm/internal/services/containers/container_group_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,27 @@ func TestAccContainerGroup_linuxBasic(t *testing.T) {
})
}

func TestAccContainerGroup_exposedPort(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_container_group", "test")
r := ContainerGroupResource{}

data.ResourceTest(t, r, []resource.TestStep{
{
Config: r.exposedPort(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("container.#").HasValue("1"),
check.That(data.ResourceName).Key("os_type").HasValue("Linux"),
check.That(data.ResourceName).Key("container.0.ports.#").HasValue("2"),
),
},
data.ImportStep(
"image_registry_credential.0.password",
"image_registry_credential.1.password",
),
})
}

func TestAccContainerGroup_requiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_container_group", "test")
r := ContainerGroupResource{}
Expand Down Expand Up @@ -212,6 +233,29 @@ func TestAccContainerGroup_linuxBasicUpdate(t *testing.T) {
})
}

func TestAccContainerGroup_exposedPortUpdate(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_container_group", "test")
r := ContainerGroupResource{}

data.ResourceTest(t, r, []resource.TestStep{
{
Config: r.exposedPort(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("exposed_port.#").HasValue("1"),
),
},
{
Config: r.exposedPortUpdated(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("container.0.ports.#").HasValue("2"),
check.That(data.ResourceName).Key("exposed_port.#").HasValue("2"),
),
},
})
}

func TestAccContainerGroup_linuxBasicTagsUpdate(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_container_group", "test")
r := ContainerGroupResource{}
Expand Down Expand Up @@ -635,6 +679,51 @@ resource "azurerm_container_group" "test" {
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}

func (ContainerGroupResource) exposedPort(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "test" {
name = "acctestRG-%d"
location = "%s"
}

resource "azurerm_container_group" "test" {
name = "acctestcontainergroup-%d"
location = "${azurerm_resource_group.test.location}"
resource_group_name = "${azurerm_resource_group.test.name}"
ip_address_type = "public"
os_type = "Linux"

exposed_port {
port = 80
protocol = "TCP"
}

amasover marked this conversation as resolved.
Show resolved Hide resolved
container {
name = "hw"
image = "microsoft/aci-helloworld:latest"
cpu = "0.5"
memory = "0.5"
ports {
port = 80
protocol = "TCP"
}
ports {
port = 5443
protocol = "UDP"
}
}

tags = {
environment = "Testing"
}
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}

func (ContainerGroupResource) linuxBasicTagsUpdated(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
Expand Down Expand Up @@ -902,6 +991,56 @@ resource "azurerm_container_group" "test" {
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}

func (ContainerGroupResource) exposedPortUpdated(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "test" {
name = "acctestRG-%d"
location = "%s"
}

resource "azurerm_container_group" "test" {
name = "acctestcontainergroup-%d"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
ip_address_type = "public"
os_type = "Linux"

exposed_port {
port = 80
}

exposed_port {
port = 5443
protocol = "UDP"
}

amasover marked this conversation as resolved.
Show resolved Hide resolved
container {
name = "hw"
image = "microsoft/aci-helloworld:latest"
cpu = "0.5"
memory = "0.5"

ports {
port = 80
}

ports {
port = 5443
protocol = "UDP"
}
}

tags = {
environment = "Testing"
}
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}

func (ContainerGroupResource) virtualNetwork(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
Expand Down
16 changes: 16 additions & 0 deletions website/docs/r/container_group.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ The following arguments are supported:

~> **Note:** DNS label/name is not supported when deploying to virtual networks.

* `exposed_port` - (Optional) Zero or more `exposed_port` blocks as defined below. Changing this forces a new resource to be created.

~> **Note:** The `exposed_port` can only contain ports that are also exposed on one or more containers in the group.

* `ip_address_type` - (Optional) Specifies the ip address type of the container. `Public` or `Private`. Changing this forces a new resource to be created. If set to `Private`, `network_profile_id` also needs to be set.

~> **Note:** `dns_name_label`, `identity` and `os_type` set to `windows` are not compatible with `Private` `ip_address_type`
Expand Down Expand Up @@ -136,6 +140,16 @@ A `container` block supports:

---

A `exposed_port` block supports:

* `port` - (Required) The port number the container will expose. Changing this forces a new resource to be created.

* `protocol` - (Required) The network protocol associated with port. Possible values are `TCP` & `UDP`. Changing this forces a new resource to be created.

~> **Note:** Removing all `exposed_port` blocks requires setting `exposed_port = []`.

---

A `diagnostics` block supports:

* `log_analytics` - (Required) A `log_analytics` block as defined below. Changing this forces a new resource to be created.
Expand Down Expand Up @@ -170,6 +184,8 @@ A `ports` block supports:

* `protocol` - (Required) The network protocol associated with port. Possible values are `TCP` & `UDP`. Changing this forces a new resource to be created.

~> **Note:** Omitting these blocks will default the exposed ports on the group to all ports on all containers defined in the `container` blocks of this group.

--

A `gpu` block supports:
Expand Down