diff --git a/internal/services/containerapps/container_app_resource_test.go b/internal/services/containerapps/container_app_resource_test.go index 0ac984f6b61e..f1d8a5868261 100644 --- a/internal/services/containerapps/container_app_resource_test.go +++ b/internal/services/containerapps/container_app_resource_test.go @@ -418,6 +418,42 @@ func TestAccContainerAppResource_scaleRulesUpdate(t *testing.T) { }) } +func TestAccContainerAppResource_ipSecurityRulesUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_app", "test") + r := ContainerAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.ingressSecurityRestriction(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.ingressSecurityRestrictionUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccContainerAppResource_ingressTrafficValidation(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_container_app", "test") r := ContainerAppResource{} @@ -1656,6 +1692,87 @@ resource "azurerm_container_app" "test" { `, r.template(data), data.RandomInteger) } +func (r ContainerAppResource) ingressSecurityRestriction(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_container_app" "test" { + name = "acctest-capp-%[2]d" + resource_group_name = azurerm_resource_group.test.name + container_app_environment_id = azurerm_container_app_environment.test.id + revision_mode = "Single" + + template { + container { + name = "acctest-cont-%[2]d" + image = "jackofallops/azure-containerapps-python-acctest:v0.0.1" + cpu = 0.25 + memory = "0.5Gi" + } + } + + ingress { + target_port = 5000 + ip_security_restriction { + name = "test" + description = "test" + action = "Allow" + ip_address_range = "0.0.0.0/0" + } + + traffic_weight { + latest_revision = true + percentage = 100 + } + } +} +`, r.template(data), data.RandomInteger) +} + +func (r ContainerAppResource) ingressSecurityRestrictionUpdate(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_container_app" "test" { + name = "acctest-capp-%[2]d" + resource_group_name = azurerm_resource_group.test.name + container_app_environment_id = azurerm_container_app_environment.test.id + revision_mode = "Single" + + template { + container { + name = "acctest-cont-%[2]d" + image = "jackofallops/azure-containerapps-python-acctest:v0.0.1" + cpu = 0.25 + memory = "0.5Gi" + } + } + + ingress { + target_port = 5000 + ip_security_restriction { + name = "test" + description = "test" + action = "Allow" + ip_address_range = "10.1.0.0/16" + } + + ip_security_restriction { + name = "test2" + description = "test2" + action = "Allow" + ip_address_range = "10.2.0.0/16" + } + + traffic_weight { + latest_revision = true + percentage = 100 + } + } +} +`, r.template(data), data.RandomInteger) +} + func (r ContainerAppResource) scaleRulesUpdate(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/internal/services/containerapps/helpers/container_apps.go b/internal/services/containerapps/helpers/container_apps.go index b53b89177e7e..ec04e4def85f 100644 --- a/internal/services/containerapps/helpers/container_apps.go +++ b/internal/services/containerapps/helpers/container_apps.go @@ -147,14 +147,15 @@ func FlattenContainerAppRegistries(input *[]containerapps.RegistryCredentials) [ } type Ingress struct { - AllowInsecure bool `tfschema:"allow_insecure_connections"` - CustomDomains []CustomDomain `tfschema:"custom_domain"` - IsExternal bool `tfschema:"external_enabled"` - FQDN string `tfschema:"fqdn"` - TargetPort int `tfschema:"target_port"` - ExposedPort int `tfschema:"exposed_port"` - TrafficWeights []TrafficWeight `tfschema:"traffic_weight"` - Transport string `tfschema:"transport"` + AllowInsecure bool `tfschema:"allow_insecure_connections"` + CustomDomains []CustomDomain `tfschema:"custom_domain"` + IsExternal bool `tfschema:"external_enabled"` + FQDN string `tfschema:"fqdn"` + TargetPort int `tfschema:"target_port"` + ExposedPort int `tfschema:"exposed_port"` + TrafficWeights []TrafficWeight `tfschema:"traffic_weight"` + Transport string `tfschema:"transport"` + IpSecurityRestrictions []IpSecurityRestriction `tfschema:"ip_security_restriction"` } func ContainerAppIngressSchema() *pluginsdk.Schema { @@ -186,6 +187,8 @@ func ContainerAppIngressSchema() *pluginsdk.Schema { Description: "The FQDN of the ingress.", }, + "ip_security_restriction": ContainerAppIngressIpSecurityRestriction(), + "target_port": { Type: pluginsdk.TypeInt, Required: true, @@ -271,13 +274,14 @@ func ExpandContainerAppIngress(input []Ingress, appName string) *containerapps.I ingress := input[0] result := &containerapps.Ingress{ - AllowInsecure: pointer.To(ingress.AllowInsecure), - CustomDomains: expandContainerAppIngressCustomDomain(ingress.CustomDomains), - External: pointer.To(ingress.IsExternal), - Fqdn: pointer.To(ingress.FQDN), - TargetPort: pointer.To(int64(ingress.TargetPort)), - ExposedPort: pointer.To(int64(ingress.ExposedPort)), - Traffic: expandContainerAppIngressTraffic(ingress.TrafficWeights, appName), + AllowInsecure: pointer.To(ingress.AllowInsecure), + CustomDomains: expandContainerAppIngressCustomDomain(ingress.CustomDomains), + External: pointer.To(ingress.IsExternal), + Fqdn: pointer.To(ingress.FQDN), + TargetPort: pointer.To(int64(ingress.TargetPort)), + ExposedPort: pointer.To(int64(ingress.ExposedPort)), + Traffic: expandContainerAppIngressTraffic(ingress.TrafficWeights, appName), + IPSecurityRestrictions: expandIpSecurityRestrictions(ingress.IpSecurityRestrictions), } transport := containerapps.IngressTransportMethod(ingress.Transport) result.Transport = &transport @@ -292,13 +296,14 @@ func FlattenContainerAppIngress(input *containerapps.Ingress, appName string) [] ingress := *input result := Ingress{ - AllowInsecure: pointer.From(ingress.AllowInsecure), - CustomDomains: flattenContainerAppIngressCustomDomain(ingress.CustomDomains), - IsExternal: pointer.From(ingress.External), - FQDN: pointer.From(ingress.Fqdn), - TargetPort: int(pointer.From(ingress.TargetPort)), - ExposedPort: int(pointer.From(ingress.ExposedPort)), - TrafficWeights: flattenContainerAppIngressTraffic(ingress.Traffic, appName), + AllowInsecure: pointer.From(ingress.AllowInsecure), + CustomDomains: flattenContainerAppIngressCustomDomain(ingress.CustomDomains), + IsExternal: pointer.From(ingress.External), + FQDN: pointer.From(ingress.Fqdn), + TargetPort: int(pointer.From(ingress.TargetPort)), + ExposedPort: int(pointer.From(ingress.ExposedPort)), + TrafficWeights: flattenContainerAppIngressTraffic(ingress.Traffic, appName), + IpSecurityRestrictions: flattenContainerAppIngressIpSecurityRestrictions(ingress.IPSecurityRestrictions), } if ingress.Transport != nil { @@ -417,6 +422,26 @@ func flattenContainerAppIngressCustomDomain(input *[]containerapps.CustomDomain) return result } +func flattenContainerAppIngressIpSecurityRestrictions(input *[]containerapps.IPSecurityRestrictionRule) []IpSecurityRestriction { + if input == nil { + return []IpSecurityRestriction{} + } + + result := make([]IpSecurityRestriction, 0) + for _, v := range *input { + ipSecurityRestriction := IpSecurityRestriction{ + Description: pointer.From(v.Description), + IpAddressRange: v.IPAddressRange, + Action: string(v.Action), + Name: v.Name, + } + + result = append(result, ipSecurityRestriction) + } + + return result +} + type TrafficWeight struct { Label string `tfschema:"label"` LatestRevision bool `tfschema:"latest_revision"` @@ -424,6 +449,50 @@ type TrafficWeight struct { Weight int `tfschema:"percentage"` } +type IpSecurityRestriction struct { + Action string `tfschema:"action"` + Description string `tfschema:"description"` + IpAddressRange string `tfschema:"ip_address_range"` + Name string `tfschema:"name"` +} + +func ContainerAppIngressIpSecurityRestriction() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "action": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(containerapps.PossibleValuesForAction(), false), + Description: "The action. Allow or Deny.", + }, + + "description": { + Type: pluginsdk.TypeString, + Optional: true, + Description: "Describe the IP restriction rule that is being sent to the container-app.", + }, + + "ip_address_range": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.IsCIDR, + Description: "CIDR notation to match incoming IP address.", + }, + + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "Name for the IP restriction rule.", + }, + }, + }, + } +} + func ContainerAppIngressTrafficWeight() *pluginsdk.Schema { return &pluginsdk.Schema{ Type: pluginsdk.TypeList, @@ -542,6 +611,25 @@ func flattenContainerAppIngressTraffic(input *[]containerapps.TrafficWeight, app return result } +func expandIpSecurityRestrictions(input []IpSecurityRestriction) *[]containerapps.IPSecurityRestrictionRule { + if input == nil { + return &[]containerapps.IPSecurityRestrictionRule{} + } + + result := make([]containerapps.IPSecurityRestrictionRule, 0) + for _, v := range input { + ipSecurityRestrictionRule := containerapps.IPSecurityRestrictionRule{ + Action: containerapps.Action(v.Action), + Name: v.Name, + IPAddressRange: v.IpAddressRange, + Description: pointer.To(v.Description), + } + result = append(result, ipSecurityRestrictionRule) + } + + return &result +} + type Dapr struct { AppId string `tfschema:"app_id"` AppPort int `tfschema:"app_port"` diff --git a/website/docs/r/container_app.html.markdown b/website/docs/r/container_app.html.markdown index 891f7c8782b5..872ed1f0fb75 100644 --- a/website/docs/r/container_app.html.markdown +++ b/website/docs/r/container_app.html.markdown @@ -371,6 +371,8 @@ An `ingress` block supports the following: * `external_enabled` - (Optional) Are connections to this Ingress from outside the Container App Environment enabled? Defaults to `false`. +* `ip_security_restriction` - (Optional) IP-filtering rules. + * `target_port` - (Required) The target port on the container for the Ingress traffic. * `exposed_port` - (Optional) The exposed port on the container for the Ingress traffic. @@ -393,6 +395,20 @@ A `custom_domain` block supports the following: --- +A `ip_security_restriction` block supports the following: + +* `action` - (Required) The IP-filter action. `Allow` or `Deny`. + +~> **NOTE:** The `action` types in an all `ip_security_restriction` blocks must be the same for the `ingress`, mixing `Allow` and `Deny` rules is not currently supported by the service. + +* `description` - (Optional) Describe the IP restriction rule that is being sent to the container-app. + +* `ip_address_range` - (Required) CIDR notation to match incoming IP address. + +* `name` - (Required) Name for the IP restriction rule. + +--- + A `traffic_weight` block supports the following: ~> **Note:** This block only applies when `revision_mode` is set to `Multiple`.