diff --git a/internal/services/containerapps/container_app_resource.go b/internal/services/containerapps/container_app_resource.go index 1cb40fc7ed81..833f38e5a312 100644 --- a/internal/services/containerapps/container_app_resource.go +++ b/internal/services/containerapps/container_app_resource.go @@ -421,7 +421,51 @@ func (r ContainerAppResource) Update() sdk.ResourceFunc { func (r ContainerAppResource) CustomizeDiff() sdk.ResourceFunc { return sdk.ResourceFunc{ Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { - if metadata.ResourceDiff != nil && metadata.ResourceDiff.HasChange("secret") { + if metadata.ResourceDiff == nil { + return nil + } + var app ContainerAppModel + if err := metadata.DecodeDiff(&app); err != nil { + return err + } + // Ingress traffic weight validations + if len(app.Ingress) != 0 { + ingress := app.Ingress[0] + if metadata.ResourceDiff.HasChange("name") { + // Validation for create time + // (Above is a trick to tell whether this is for a new create apply, as the "name" is a force new property) + if len(ingress.TrafficWeights) != 0 { + if len(ingress.TrafficWeights) > 1 { + return fmt.Errorf("at most one `ingress.0.traffic_weight` can be specified during creation") + } + tw := ingress.TrafficWeights[0] + if !tw.LatestRevision { + return fmt.Errorf("`ingress.0.traffic_weight.0.latest_revision` must be set to true during creation") + } + if tw.RevisionSuffix != "" { + return fmt.Errorf("`ingress.0.traffic_weight.0.revision_suffix` must not be set during creation") + } + } + } else { + // Validation for update time + var latestRevCount int + for i, tw := range ingress.TrafficWeights { + if tw.LatestRevision { + latestRevCount++ + if tw.RevisionSuffix != "" { + return fmt.Errorf("`ingress.0.traffic_weight.%[1]d.revision_suffix` conflicts with `ingress.0.traffic_weight.%[1]d.latest_revision`", i) + } + } else if tw.RevisionSuffix == "" { + return fmt.Errorf("`ingress.0.traffic_weight.%[1]d.revision_suffix` is not specified", i) + } + } + if latestRevCount > 1 { + return fmt.Errorf("more than one `ingress.0.traffic_weight` has `latest_revision` set to `true`") + } + } + } + + if metadata.ResourceDiff.HasChange("secret") { stateSecretsRaw, configSecretsRaw := metadata.ResourceDiff.GetChange("secret") stateSecrets := stateSecretsRaw.(*schema.Set).List() configSecrets := configSecretsRaw.(*schema.Set).List() diff --git a/internal/services/containerapps/container_app_resource_test.go b/internal/services/containerapps/container_app_resource_test.go index 848fa6361807..0ac984f6b61e 100644 --- a/internal/services/containerapps/container_app_resource_test.go +++ b/internal/services/containerapps/container_app_resource_test.go @@ -344,39 +344,24 @@ func TestAccContainerAppResource_removeDaprAppPort(t *testing.T) { }) } -func TestAccContainerAppResource_secretRemoveShouldFail(t *testing.T) { +func TestAccContainerAppResource_secretFail(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_container_app", "test") r := ContainerAppResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.completeUpdate(data, "rev1"), + Config: r.secretBasic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), { - Config: r.complete(data, "rev2"), + Config: r.secretRemove(data), ExpectError: regexp.MustCompile("cannot remove secrets from Container Apps at this time"), }, - }) -} - -func TestAccContainerAppResource_secretRemoveWithAddShouldFail(t *testing.T) { - data := acceptance.BuildTestData(t, "azurerm_container_app", "test") - r := ContainerAppResource{} - - data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.completeUpdate(data, "rev1"), - Check: acceptance.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - { - Config: r.completeChangedSecret(data, "rev2"), + Config: r.secretChangeName(data), ExpectError: regexp.MustCompile("previously configured secret"), }, }) @@ -433,6 +418,26 @@ func TestAccContainerAppResource_scaleRulesUpdate(t *testing.T) { }) } +func TestAccContainerAppResource_ingressTrafficValidation(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_app", "test") + r := ContainerAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.ingressTrafficValidation(data, r.trafficBlockMoreThanOne()), + ExpectError: regexp.MustCompile(fmt.Sprintf(`at most one %s can be specified during creation`, "`ingress.0.traffic_weight`")), + }, + { + Config: r.ingressTrafficValidation(data, r.trafficBlockLatestRevisionNotSet()), + ExpectError: regexp.MustCompile(fmt.Sprintf(`%s must be set to true during creation`, "`ingress.0.traffic_weight.0.latest_revision`")), + }, + { + Config: r.ingressTrafficValidation(data, r.trafficBlockRevisionSuffixSet()), + ExpectError: regexp.MustCompile(fmt.Sprintf(`%s must not be set during creation`, "`ingress.0.traffic_weight.0.revision_suffix`")), + }, + }) +} + func (r ContainerAppResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := containerapps.ParseContainerAppID(state.ID) if err != nil { @@ -1330,114 +1335,6 @@ resource "azurerm_container_app" "test" { `, r.templatePlusExtras(data), data.RandomInteger, revisionSuffix) } -func (r ContainerAppResource) completeChangedSecret(data acceptance.TestData, revisionSuffix string) 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 = "Multiple" - - template { - container { - name = "acctest-cont-%[2]d" - image = "jackofallops/azure-containerapps-python-acctest:v0.0.1" - - cpu = 0.5 - memory = "1Gi" - - readiness_probe { - transport = "HTTP" - port = 5000 - path = "/uptime" - timeout = 2 - failure_count_threshold = 1 - success_count_threshold = 1 - - header { - name = "Cache-Control" - value = "no-cache" - } - } - - liveness_probe { - transport = "HTTP" - port = 5000 - path = "/health" - - header { - name = "Cache-Control" - value = "no-cache" - } - - initial_delay = 5 - timeout = 2 - failure_count_threshold = 3 - } - - startup_probe { - transport = "TCP" - port = 5000 - timeout = 5 - failure_count_threshold = 1 - } - } - - min_replicas = 1 - max_replicas = 4 - - revision_suffix = "%[3]s" - } - - ingress { - allow_insecure_connections = true - external_enabled = true - target_port = 5000 - transport = "auto" - - traffic_weight { - latest_revision = true - percentage = 20 - } - - traffic_weight { - revision_suffix = "rev1" - percentage = 80 - } - } - - registry { - server = azurerm_container_registry.test.login_server - username = azurerm_container_registry.test.admin_username - password_secret_name = "registry-password" - } - - secret { - name = "registry-password" - value = azurerm_container_registry.test.admin_password - } - - secret { - name = "pickle" - value = "morty" - } - - dapr { - app_id = "acctest-cont-%[2]d" - app_port = 5000 - app_protocol = "http" - } - - tags = { - foo = "Bar" - accTest = "1" - } -} -`, r.templatePlusExtras(data), data.RandomInteger, revisionSuffix) -} - func (r ContainerAppResource) completeUpdate(data acceptance.TestData, revisionSuffix string) string { return fmt.Sprintf(` %s @@ -1637,12 +1534,7 @@ resource "azurerm_container_app" "test" { traffic_weight { latest_revision = true - percentage = 20 - } - - traffic_weight { - revision_suffix = "rev1" - percentage = 80 + percentage = 100 } } @@ -1928,3 +1820,153 @@ resource "azurerm_container_app_environment_storage" "test" { } `, ContainerAppEnvironmentDaprComponentResource{}.complete(data), data.RandomInteger, data.RandomString) } + +func (r ContainerAppResource) ingressTrafficValidation(data acceptance.TestData, trafficBlock string) 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 { + allow_insecure_connections = true + external_enabled = true + target_port = 5000 + transport = "http" + %s + } +} +`, r.template(data), data.RandomInteger, trafficBlock) +} + +func (r ContainerAppResource) secretBasic(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" + } + } + + secret { + name = "foo" + value = "bar" + } + + secret { + name = "rick" + value = "morty" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r ContainerAppResource) secretRemove(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" + } + } + + secret { + name = "foo" + value = "bar" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r ContainerAppResource) secretChangeName(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" + } + } + + secret { + name = "foo" + value = "bar" + } + + secret { + name = "pickle" + value = "morty" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r ContainerAppResource) trafficBlockMoreThanOne() string { + return ` + traffic_weight { + percentage = 50 + } + traffic_weight { + percentage = 50 + } +` +} + +func (r ContainerAppResource) trafficBlockLatestRevisionNotSet() string { + return ` + traffic_weight { + percentage = 100 + } +` +} + +func (r ContainerAppResource) trafficBlockRevisionSuffixSet() string { + return ` + traffic_weight { + percentage = 100 + latest_revision = true + revision_suffix = "foo" + } +` +} diff --git a/website/docs/r/container_app.html.markdown b/website/docs/r/container_app.html.markdown index 6c1bd135e61f..891f7c8782b5 100644 --- a/website/docs/r/container_app.html.markdown +++ b/website/docs/r/container_app.html.markdown @@ -377,9 +377,7 @@ An `ingress` block supports the following: ~> **Note:** `exposed_port` can only be specified when `transport` is set to `tcp`. -* `traffic_weight` - (Required) A `traffic_weight` block as detailed below. - -~> **Note:** `traffic_weight` can only be specified when `revision_mode` is set to `Multiple`. +* `traffic_weight` - (Required) One or more `traffic_weight` blocks as detailed below. * `transport` - (Optional) The transport method for the Ingress. Possible values are `auto`, `http`, `http2` and `tcp`. Defaults to `auto`. @@ -401,10 +399,12 @@ A `traffic_weight` block supports the following: * `label` - (Optional) The label to apply to the revision as a name prefix for routing traffic. -* `latest_revision` - (Optional) This traffic Weight relates to the latest stable Container Revision. +* `latest_revision` - (Optional) This traffic Weight applies to the latest stable Container Revision. At most only one `traffic_weight` block can have the `latest_revision` set to `true`. * `revision_suffix` - (Optional) The suffix string to which this `traffic_weight` applies. +~> **Note:** `latest_revision` conflicts with `revision_suffix`, which means you shall either set `latest_revision` to `true` or specify `revision_suffix`. Especially for creation, there shall only be one `traffic_weight`, with the `latest_revision` set to `true`, and leave the `revision_suffix` empty. + * `percentage` - (Required) The percentage of traffic which should be sent this revision. ~> **Note:** The cumulative values for `weight` must equal 100 exactly and explicitly, no default weights are assumed.