diff --git a/internal/services/containerapps/container_app_resource.go b/internal/services/containerapps/container_app_resource.go index 3605a4aaa03a3..9f1c3967d6005 100644 --- a/internal/services/containerapps/container_app_resource.go +++ b/internal/services/containerapps/container_app_resource.go @@ -182,6 +182,11 @@ func (r ContainerAppResource) Create() sdk.ResourceFunc { return fmt.Errorf("invalid registry config for %s: %+v", id, err) } + template, err := helpers.ExpandContainerAppTemplate(app.Template, metadata) + if err != nil { + return fmt.Errorf("invalid template config for %s: %+v", id, err) + } + containerApp := containerapps.ContainerApp{ Location: location.Normalize(env.Model.Location), Properties: &containerapps.ContainerAppProperties{ @@ -192,7 +197,7 @@ func (r ContainerAppResource) Create() sdk.ResourceFunc { Registries: registries, }, ManagedEnvironmentId: pointer.To(app.ManagedEnvironmentId), - Template: helpers.ExpandContainerAppTemplate(app.Template, metadata), + Template: template, }, Tags: tags.Expand(app.Tags), } @@ -392,7 +397,10 @@ func (r ContainerAppResource) Update() sdk.ResourceFunc { model.Tags = tags.Expand(state.Tags) } - model.Properties.Template = helpers.ExpandContainerAppTemplate(state.Template, metadata) + model.Properties.Template, err = helpers.ExpandContainerAppTemplate(state.Template, metadata) + if err != nil { + return fmt.Errorf("invalid template config for %s: %+v", id, err) + } if err := client.CreateOrUpdateThenPoll(ctx, *id, *model); err != nil { return fmt.Errorf("updating %s: %+v", *id, err) diff --git a/internal/services/containerapps/container_app_resource_test.go b/internal/services/containerapps/container_app_resource_test.go index 9111530ae3cf5..d199447e15cbf 100644 --- a/internal/services/containerapps/container_app_resource_test.go +++ b/internal/services/containerapps/container_app_resource_test.go @@ -470,6 +470,27 @@ resource "azurerm_container_app" "test" { min_replicas = 2 max_replicas = 3 + scale { + rule { + name = "http-concurrency" + http { + metadata = { + concurrentRequests = "10" + } + } + } + rule { + name = "cpu-utilization" + custom { + type = "cpu" + metadata = { + type = "Utilization" + value = "90" + } + } + } + } + revision_suffix = "%[3]s" } diff --git a/internal/services/containerapps/helpers/container_apps.go b/internal/services/containerapps/helpers/container_apps.go index 8d27e7d7746fa..182d51bb11850 100644 --- a/internal/services/containerapps/helpers/container_apps.go +++ b/internal/services/containerapps/helpers/container_apps.go @@ -698,6 +698,7 @@ type ContainerTemplate struct { Suffix string `tfschema:"revision_suffix"` MinReplicas int `tfschema:"min_replicas"` MaxReplicas int `tfschema:"max_replicas"` + Scale []ContainerScale `tfschema:"scale"` Volumes []ContainerVolume `tfschema:"volume"` } @@ -726,6 +727,8 @@ func ContainerTemplateSchema() *pluginsdk.Schema { Description: "The maximum number of replicas for this container.", }, + "scale": ContainerScaleSchema(), + "volume": ContainerVolumeSchema(), "revision_suffix": { @@ -759,6 +762,8 @@ func ContainerTemplateSchemaComputed() *pluginsdk.Schema { Description: "The maximum number of replicas for this container.", }, + "scale": ContainerScaleSchema(), + "volume": ContainerVolumeSchema(), "revision_suffix": { @@ -771,38 +776,29 @@ func ContainerTemplateSchemaComputed() *pluginsdk.Schema { } } -func ExpandContainerAppTemplate(input []ContainerTemplate, metadata sdk.ResourceMetaData) *containerapps.Template { +func ExpandContainerAppTemplate(input []ContainerTemplate, metadata sdk.ResourceMetaData) (*containerapps.Template, error) { if len(input) != 1 { - return nil + return nil, nil } config := input[0] + scale, err := expandContainerScale(config.Scale, config.MaxReplicas, config.MinReplicas) + if err != nil { + return nil, err + } template := &containerapps.Template{ Containers: expandContainerAppContainers(config.Containers), + Scale: scale, Volumes: expandContainerAppVolumes(config.Volumes), } - if config.MaxReplicas != 0 { - if template.Scale == nil { - template.Scale = &containerapps.Scale{} - } - template.Scale.MaxReplicas = pointer.To(int64(config.MaxReplicas)) - } - - if config.MinReplicas != 0 { - if template.Scale == nil { - template.Scale = &containerapps.Scale{} - } - template.Scale.MinReplicas = pointer.To(int64(config.MinReplicas)) - } - if config.Suffix != "" { if metadata.ResourceData.HasChange("template.0.revision_suffix") { template.RevisionSuffix = pointer.To(config.Suffix) } } - return template + return template, nil } func FlattenContainerAppTemplate(input *containerapps.Template) []ContainerTemplate { @@ -811,6 +807,7 @@ func FlattenContainerAppTemplate(input *containerapps.Template) []ContainerTempl } result := ContainerTemplate{ Containers: flattenContainerAppContainers(input.Containers), + Scale: flattenContainerScale(input.Scale), Suffix: pointer.From(input.RevisionSuffix), Volumes: flattenContainerAppVolumes(input.Volumes), } @@ -1058,6 +1055,190 @@ func flattenContainerAppContainers(input *[]containerapps.Container) []Container return result } +type ContainerScale struct { + Rules []ContainerScaleRule `tfschema:"rule"` +} + +type ContainerScaleRule struct { + Name string `tfschema:"name"` + Custom []ContainerCustomScaleRule `tfschema:"custom"` + HTTP []ContainerHTTPScaleRule `tfschema:"http"` +} + +func ContainerScaleSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + MaxItems: 1, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "rule": { + Type: pluginsdk.TypeList, + Optional: true, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The name of the rule.", + }, + "custom": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The name of the volume.", + }, + "metadata": { + Type: pluginsdk.TypeMap, + Required: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + }, + }, + }, + "http": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "metadata": { + Type: pluginsdk.TypeMap, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func expandContainerScale(scale []ContainerScale, maxReplicas, minReplicas int) (*containerapps.Scale, error) { + result := &containerapps.Scale{} + + if maxReplicas != 0 { + result.MaxReplicas = pointer.To(int64(maxReplicas)) + } + if minReplicas != 0 { + result.MinReplicas = pointer.To(int64(minReplicas)) + } + + if scale == nil { + return result, nil + } + + rules := make([]containerapps.ScaleRule, 0) + for _, v := range scale { + for _, r := range v.Rules { + rule := containerapps.ScaleRule{} + rule.Name = pointer.To(r.Name) + rule.Custom = expandContainerCustomScaleRule(r.Custom) + rule.HTTP = expandContainerHTTPScaleRule(r.HTTP) + if rule.Custom != nil && rule.HTTP != nil { + return nil, fmt.Errorf("rule %q cannot specify multiple triggers", r.Name) + } + if rule.Custom == nil && rule.HTTP == nil { + return nil, fmt.Errorf("rule %q must specify at least one trigger: custom, http", r.Name) + } + rules = append(rules, rule) + } + } + + result.Rules = &rules + return result, nil +} + +func flattenContainerScale(input *containerapps.Scale) []ContainerScale { + if input == nil { + return nil + } + result := ContainerScale{} + rules := make([]ContainerScaleRule, 0) + for _, v := range *input.Rules { + rule := ContainerScaleRule{} + rule.Name = pointer.From(v.Name) + rule.Custom = flattenContainerCustomScaleRule(v.Custom) + rule.HTTP = flattenContainerHTTPScaleRule(v.HTTP) + rules = append(rules, rule) + } + result.Rules = rules + return []ContainerScale{result} +} + +type ContainerHTTPScaleRule struct { + Metadata map[string]string `tfschema:"metadata"` +} + +func expandContainerHTTPScaleRule(input []ContainerHTTPScaleRule) *containerapps.HTTPScaleRule { + if input == nil { + return nil + } + if len(input) != 1 { + return nil + } + rule := input[0] + return &containerapps.HTTPScaleRule{ + Metadata: pointer.To(rule.Metadata), + } +} + +func flattenContainerHTTPScaleRule(input *containerapps.HTTPScaleRule) []ContainerHTTPScaleRule { + if input == nil { + return nil + } + rule := ContainerHTTPScaleRule{ + Metadata: pointer.From(input.Metadata), + } + return []ContainerHTTPScaleRule{rule} +} + +type ContainerCustomScaleRule struct { + Type string `tfschema:"type"` + Metadata map[string]string `tfschema:"metadata"` +} + +func expandContainerCustomScaleRule(input []ContainerCustomScaleRule) *containerapps.CustomScaleRule { + if input == nil { + return nil + } + if len(input) != 1 { + return nil + } + rule := input[0] + return &containerapps.CustomScaleRule{ + Type: pointer.To(rule.Type), + Metadata: pointer.To(rule.Metadata), + } +} + +func flattenContainerCustomScaleRule(input *containerapps.CustomScaleRule) []ContainerCustomScaleRule { + if input == nil { + return nil + } + rule := ContainerCustomScaleRule{ + Metadata: pointer.From(input.Metadata), + Type: pointer.From(input.Type), + } + return []ContainerCustomScaleRule{rule} +} + type ContainerVolume struct { Name string `tfschema:"name"` StorageName string `tfschema:"storage_name"` diff --git a/website/docs/d/container_app.html.markdown b/website/docs/d/container_app.html.markdown index 71ff584051359..0a8561fe8dae5 100644 --- a/website/docs/d/container_app.html.markdown +++ b/website/docs/d/container_app.html.markdown @@ -69,6 +69,8 @@ A `template` block supports the following: * `min_replicas` - The minimum number of replicas for this container. +* `scale` - A `scale` block as detailed below. + * `revision_suffix` - The suffix for the revision. This value must be unique for the lifetime of the Resource. If omitted the service will use a hash function to create one. * `volume` - A `volume` block as detailed below. @@ -113,6 +115,36 @@ A `container` block supports the following: --- +A `scale` block supports the following: + +* `rule` - One or more `rule` blocks as outlined below. + +--- + +A `rule` block supports the following, `custom` and `http` are mutually exclusive: + +* `name` - The name of the rule. + +* `custom` - A `custom` block as detailed below. + +* `http` - A `http` block as detailed below. + +--- + +A `custom` block supports the following: + +* `type` - The type of custom rule. + +* `metadata` - Map of metadata values supplied to the custom scaler. + +--- + +A `http` block supports the following: + +* `metadata` - Map of metadata values supplied to the custom scaler. + +--- + A `liveness_probe` block supports the following: * `failure_count_threshold` - The number of consecutive failures required to consider this probe as failed. Possible values are between `1` and `10`. Defaults to `3`. diff --git a/website/docs/r/container_app.html.markdown b/website/docs/r/container_app.html.markdown index cf73d6f1deea9..2fa1d3a7a9ac4 100644 --- a/website/docs/r/container_app.html.markdown +++ b/website/docs/r/container_app.html.markdown @@ -32,6 +32,7 @@ resource "azurerm_container_app_environment" "example" { resource_group_name = azurerm_resource_group.example.name log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id } + resource "azurerm_container_app" "example" { name = "example-app" container_app_environment_id = azurerm_container_app_environment.example.id @@ -45,6 +46,27 @@ resource "azurerm_container_app" "example" { cpu = 0.25 memory = "0.5Gi" } + + scale { + rule { + name = "http-concurrency" + http { + metadata = { + concurrentRequests = "20" + } + } + } + rule { + name = "cpu-utilization" + custom { + type = "cpu" + metadata = { + type = "Utilization" + value = "90" + } + } + } + } } } ``` @@ -97,6 +119,8 @@ A `template` block supports the following: * `min_replicas` - (Optional) The minimum number of replicas for this container. +* `scale` - (Optional) A `scale` block as detailed below. + * `revision_suffix` - (Optional) The suffix for the revision. This value must be unique for the lifetime of the Resource. If omitted the service will use a hash function to create one. * `volume` - (Optional) A `volume` block as detailed below. @@ -147,6 +171,36 @@ A `container` block supports the following: --- +A `scale` block supports the following: + +* `rule` - (Optional) One or more `rule` blocks as outlined below. + +--- + +A `rule` block supports the following, `custom` and `http` are mutually exclusive, at least one is required to define a valid rule: + +* `name` - (Required) The name of the rule. + +* `custom` - (Optional) A `custom` block as detailed below. + +* `http` - (Optional) A `http` block as detailed below. + +--- + +A `custom` block supports the following: + +* `type` - (Required) The type of custom rule. + +* `metadata` - (Required) Map of metadata values to pass to the custom scaler. + +--- + +A `http` block supports the following: + +* `metadata` - (Required) Map of metadata values to pass to the custom scaler. + +--- + A `liveness_probe` block supports the following: * `failure_count_threshold` - (Optional) The number of consecutive failures required to consider this probe as failed. Possible values are between `1` and `10`. Defaults to `3`.