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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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"
}

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"
}

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