diff --git a/internal/services/containerapps/container_app_resource_test.go b/internal/services/containerapps/container_app_resource_test.go index ade9bad6aac0..e4a1e48a5266 100644 --- a/internal/services/containerapps/container_app_resource_test.go +++ b/internal/services/containerapps/container_app_resource_test.go @@ -303,6 +303,57 @@ func TestAccContainerAppResource_secretRemoveWithAddShouldFail(t *testing.T) { }) } +func TestAccContainerAppResource_scaleRules(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_app", "test") + r := ContainerAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.scaleRules(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccContainerAppResource_scaleRulesUpdate(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.scaleRules(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.scaleRulesUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basicWithRetainedSecret(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func (r ContainerAppResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := containerapps.ParseContainerAppID(state.ID) if err != nil { @@ -342,6 +393,33 @@ resource "azurerm_container_app" "test" { `, r.template(data), data.RandomInteger) } +func (r ContainerAppResource) basicWithRetainedSecret(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" + + secret { + name = "queue-auth-secret" + value = "VGhpcyBJcyBOb3QgQSBHb29kIFBhc3N3b3JkCg==" + } + + template { + container { + name = "acctest-cont-%[2]d" + image = "jackofallops/azure-containerapps-python-acctest:v0.0.1" + cpu = 0.25 + memory = "0.5Gi" + } + } +} +`, r.template(data), data.RandomInteger) +} + func (r ContainerAppResource) withSystemIdentity(data acceptance.TestData) string { return fmt.Sprintf(` %s @@ -1325,6 +1403,145 @@ resource "azurerm_container_app" "test" { `, r.templatePlusExtras(data), data.RandomInteger, revisionSuffix) } +func (r ContainerAppResource) scaleRules(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" + + secret { + name = "queue-auth-secret" + value = "VGhpcyBJcyBOb3QgQSBHb29kIFBhc3N3b3JkCg==" + } + + template { + container { + name = "acctest-cont-%[2]d" + image = "jackofallops/azure-containerapps-python-acctest:v0.0.1" + cpu = 0.25 + memory = "0.5Gi" + } + + azure_queue_scale_rule { + name = "azq-1" + queue_name = "foo" + queue_length = 10 + + authentication { + secret_name = "queue-auth-secret" + trigger_parameter = "password" + } + } + + custom_scale_rule { + name = "csr-1" + custom_rule_type = "azure-monitor" + metadata = { + foo = "bar" + } + } + + http_scale_rule { + name = "http-1" + concurrent_requests = "100" + } + + tcp_scale_rule { + name = "tcp-1" + concurrent_requests = "1000" + } + } +} +`, r.template(data), data.RandomInteger) +} + +func (r ContainerAppResource) scaleRulesUpdate(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" + + secret { + name = "queue-auth-secret" + value = "VGhpcyBJcyBOb3QgQSBHb29kIFBhc3N3b3JkCg==" + } + + template { + container { + name = "acctest-cont-%[2]d" + image = "jackofallops/azure-containerapps-python-acctest:v0.0.1" + cpu = 0.25 + memory = "0.5Gi" + } + + azure_queue_scale_rule { + name = "azq-1" + queue_name = "foo" + queue_length = 10 + + authentication { + secret_name = "queue-auth-secret" + trigger_parameter = "password" + } + } + + azure_queue_scale_rule { + name = "azq-2" + queue_name = "bar" + queue_length = 20 + + authentication { + secret_name = "queue-auth-secret" + trigger_parameter = "another_password" + } + } + + custom_scale_rule { + name = "csr-1" + custom_rule_type = "rabbitmq" + + metadata = { + foo = "bar" + } + + authentication { + secret_name = "queue-auth-secret" + trigger_parameter = "password" + } + } + + http_scale_rule { + name = "http-1" + concurrent_requests = "200" + + authentication { + secret_name = "queue-auth-secret" + trigger_parameter = "password" + } + } + + tcp_scale_rule { + name = "tcp-1" + concurrent_requests = "1000" + + authentication { + secret_name = "queue-auth-secret" + trigger_parameter = "password" + } + } + } +} +`, r.template(data), data.RandomInteger) +} + func (ContainerAppResource) template(data acceptance.TestData) string { return ContainerAppEnvironmentResource{}.basic(data) } diff --git a/internal/services/containerapps/helpers/container_apps.go b/internal/services/containerapps/helpers/container_apps.go index f088b5508df8..8584503f08ff 100644 --- a/internal/services/containerapps/helpers/container_apps.go +++ b/internal/services/containerapps/helpers/container_apps.go @@ -669,11 +669,15 @@ func ContainerAppEnvironmentDaprMetadataSchema() *pluginsdk.Schema { } type ContainerTemplate struct { - Containers []Container `tfschema:"container"` - Suffix string `tfschema:"revision_suffix"` - MinReplicas int `tfschema:"min_replicas"` - MaxReplicas int `tfschema:"max_replicas"` - Volumes []ContainerVolume `tfschema:"volume"` + Containers []Container `tfschema:"container"` + Suffix string `tfschema:"revision_suffix"` + MinReplicas int `tfschema:"min_replicas"` + MaxReplicas int `tfschema:"max_replicas"` + AzureQueueScaleRules []AzureQueueScaleRule `tfschema:"azure_queue_scale_rule"` + CustomScaleRules []CustomScaleRule `tfschema:"custom_scale_rule"` + HTTPScaleRules []HTTPScaleRule `tfschema:"http_scale_rule"` + TCPScaleRules []TCPScaleRule `tfschema:"tcp_scale_rule"` + Volumes []ContainerVolume `tfschema:"volume"` } func ContainerTemplateSchema() *pluginsdk.Schema { @@ -701,6 +705,14 @@ func ContainerTemplateSchema() *pluginsdk.Schema { Description: "The maximum number of replicas for this container.", }, + "azure_queue_scale_rule": AzureQueueScaleRuleSchema(), + + "custom_scale_rule": CustomScaleRuleSchema(), + + "http_scale_rule": HTTPScaleRuleSchema(), + + "tcp_scale_rule": TCPScaleRuleSchema(), + "volume": ContainerVolumeSchema(), "revision_suffix": { @@ -734,12 +746,19 @@ func ContainerTemplateSchemaComputed() *pluginsdk.Schema { Description: "The maximum number of replicas for this container.", }, - "volume": ContainerVolumeSchema(), + "azure_queue_scale_rule": AzureQueueScaleRuleSchemaComputed(), + + "custom_scale_rule": CustomScaleRuleSchemaComputed(), + + "http_scale_rule": HTTPScaleRuleSchemaComputed(), + + "tcp_scale_rule": TCPScaleRuleSchemaComputed(), + + "volume": ContainerVolumeSchemaComputed(), "revision_suffix": { - Type: pluginsdk.TypeString, - Computed: true, - Description: "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.", + Type: pluginsdk.TypeString, + Computed: true, }, }, }, @@ -771,6 +790,14 @@ func ExpandContainerAppTemplate(input []ContainerTemplate, metadata sdk.Resource template.Scale.MinReplicas = pointer.To(int64(config.MinReplicas)) } + if rules := config.expandContainerAppScaleRules(); len(rules) != 0 { + if template.Scale == nil { + template.Scale = &containerapps.Scale{} + } + + template.Scale.Rules = pointer.To(rules) + } + if config.Suffix != "" { if metadata.ResourceData.HasChange("template.0.revision_suffix") { template.RevisionSuffix = pointer.To(config.Suffix) @@ -793,6 +820,7 @@ func FlattenContainerAppTemplate(input *containerapps.Template) []ContainerTempl if scale := input.Scale; scale != nil { result.MaxReplicas = int(pointer.From(scale.MaxReplicas)) result.MinReplicas = int(pointer.From(scale.MinReplicas)) + result.flattenContainerAppScaleRules(scale.Rules) } return []ContainerTemplate{result} @@ -1074,6 +1102,31 @@ func ContainerVolumeSchema() *pluginsdk.Schema { } } +func ContainerVolumeSchemaComputed() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "storage_type": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "storage_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + } +} + func expandContainerAppVolumes(input []ContainerVolume) *[]containerapps.Volume { if input == nil { return nil @@ -2347,3 +2400,617 @@ func ContainerAppProbesRemoved(metadata sdk.ResourceMetaData) bool { return !(hasLiveness || hasReadiness || hasStartup) } + +type AzureQueueScaleRule struct { + Name string `tfschema:"name"` + QueueLength int `tfschema:"queue_length"` + QueueName string `tfschema:"queue_name"` + Authentications []ScaleRuleAuthentication `tfschema:"authentication"` +} + +func AzureQueueScaleRuleSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "queue_length": { + Type: pluginsdk.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(1), + }, + + "queue_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Required: true, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.SecretName, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + }, + } +} +func AzureQueueScaleRuleSchemaComputed() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "queue_length": { + Type: pluginsdk.TypeInt, + Computed: true, + }, + + "queue_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + } +} + +type CustomScaleRule struct { + Name string `tfschema:"name"` + Metadata map[string]string `tfschema:"metadata"` + CustomRuleType string `tfschema:"custom_rule_type"` + Authentications []ScaleRuleAuthentication `tfschema:"authentication"` +} + +func CustomScaleRuleSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "metadata": { + Type: pluginsdk.TypeMap, + Required: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "custom_rule_type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "activemq", "artemis-queue", "kafka", "pulsar", "aws-cloudwatch", + "aws-dynamodb", "aws-dynamodb-streams", "aws-kinesis-stream", "aws-sqs-queue", + "azure-app-insights", "azure-blob", "azure-data-explorer", "azure-eventhub", + "azure-log-analytics", "azure-monitor", "azure-pipelines", "azure-servicebus", + "azure-queue", "cassandra", "cpu", "cron", "datadog", "elasticsearch", "external", + "external-push", "gcp-stackdriver", "gcp-storage", "gcp-pubsub", "graphite", "http", + "huawei-cloudeye", "ibmmq", "influxdb", "kubernetes-workload", "liiklus", "memory", + "metrics-api", "mongodb", "mssql", "mysql", "nats-jetstream", "stan", "tcp", "new-relic", + "openstack-metric", "openstack-swift", "postgresql", "predictkube", "prometheus", + "rabbitmq", "redis", "redis-cluster", "redis-sentinel", "redis-streams", + "redis-cluster-streams", "redis-sentinel-streams", "selenium-grid", + "solace-event-queue", "github-runner", + }, false), // Note - this can be any KEDA compatible source in a user's environment + }, + + "authentication": { + Type: pluginsdk.TypeList, + Optional: true, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.SecretName, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + }, + } +} + +func CustomScaleRuleSchemaComputed() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "metadata": { + Type: pluginsdk.TypeMap, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "custom_rule_type": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + } +} + +type HTTPScaleRule struct { + Name string `tfschema:"name"` + ConcurrentRequests string `tfschema:"concurrent_requests"` + Authentications []ScaleRuleAuthentication `tfschema:"authentication"` +} + +func HTTPScaleRuleSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "concurrent_requests": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.ContainerAppScaleRuleConcurrentRequests, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Optional: true, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.SecretName, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + }, + } +} + +func HTTPScaleRuleSchemaComputed() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "concurrent_requests": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + } +} + +type TCPScaleRule struct { + Name string `tfschema:"name"` + ConcurrentRequests string `tfschema:"concurrent_requests"` + Authentications []ScaleRuleAuthentication `tfschema:"authentication"` +} + +func TCPScaleRuleSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "concurrent_requests": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.ContainerAppScaleRuleConcurrentRequests, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Optional: true, + MinItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.SecretName, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + }, + } +} + +func TCPScaleRuleSchemaComputed() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "concurrent_requests": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "secret_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "trigger_parameter": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + } +} + +type ScaleRuleAuthentication struct { + SecretRef string `tfschema:"secret_name"` + TriggerParam string `tfschema:"trigger_parameter"` +} + +func (c *ContainerTemplate) expandContainerAppScaleRules() []containerapps.ScaleRule { + if len(c.AzureQueueScaleRules) == 0 && len(c.CustomScaleRules) == 0 && len(c.HTTPScaleRules) == 0 && len(c.TCPScaleRules) == 0 { + return nil + } + result := make([]containerapps.ScaleRule, 0) + for _, v := range c.AzureQueueScaleRules { + r := containerapps.ScaleRule{ + Name: pointer.To(v.Name), + AzureQueue: &containerapps.QueueScaleRule{ + QueueLength: pointer.To(int64(v.QueueLength)), + QueueName: pointer.To(v.QueueName), + }, + } + + auths := make([]containerapps.ScaleRuleAuth, 0) + for _, a := range v.Authentications { + auth := containerapps.ScaleRuleAuth{ + TriggerParameter: pointer.To(a.TriggerParam), + SecretRef: pointer.To(a.SecretRef), + } + auths = append(auths, auth) + } + + r.AzureQueue.Auth = pointer.To(auths) + + result = append(result, r) + } + + for _, v := range c.CustomScaleRules { + r := containerapps.ScaleRule{ + Name: pointer.To(v.Name), + Custom: &containerapps.CustomScaleRule{ + Metadata: &v.Metadata, + Type: pointer.To(v.CustomRuleType), + }, + } + + auths := make([]containerapps.ScaleRuleAuth, 0) + for _, a := range v.Authentications { + auth := containerapps.ScaleRuleAuth{ + TriggerParameter: pointer.To(a.TriggerParam), + SecretRef: pointer.To(a.SecretRef), + } + auths = append(auths, auth) + } + + r.Custom.Auth = pointer.To(auths) + + result = append(result, r) + } + + for _, v := range c.HTTPScaleRules { + metaData := make(map[string]string, 0) + metaData["concurrentRequests"] = v.ConcurrentRequests + r := containerapps.ScaleRule{ + Name: pointer.To(v.Name), + HTTP: &containerapps.HTTPScaleRule{ + Metadata: pointer.To(metaData), + }, + } + + auths := make([]containerapps.ScaleRuleAuth, 0) + for _, a := range v.Authentications { + auth := containerapps.ScaleRuleAuth{ + TriggerParameter: pointer.To(a.TriggerParam), + SecretRef: pointer.To(a.SecretRef), + } + auths = append(auths, auth) + } + + r.HTTP.Auth = pointer.To(auths) + + result = append(result, r) + } + + for _, v := range c.TCPScaleRules { + metaData := make(map[string]string, 0) + metaData["concurrentRequests"] = v.ConcurrentRequests + r := containerapps.ScaleRule{ + Name: pointer.To(v.Name), + Tcp: &containerapps.TcpScaleRule{ + Metadata: pointer.To(metaData), + }, + } + + auths := make([]containerapps.ScaleRuleAuth, 0) + for _, a := range v.Authentications { + auth := containerapps.ScaleRuleAuth{ + TriggerParameter: pointer.To(a.TriggerParam), + SecretRef: pointer.To(a.SecretRef), + } + auths = append(auths, auth) + } + + r.Tcp.Auth = pointer.To(auths) + + result = append(result, r) + } + + return result +} + +func (c *ContainerTemplate) flattenContainerAppScaleRules(input *[]containerapps.ScaleRule) { + if input != nil && len(*input) != 0 { + rules := *input + azureQueueScaleRules := make([]AzureQueueScaleRule, 0) + customScaleRules := make([]CustomScaleRule, 0) + httpScaleRules := make([]HTTPScaleRule, 0) + tcpScaleRules := make([]TCPScaleRule, 0) + for _, v := range rules { + if q := v.AzureQueue; q != nil { + rule := AzureQueueScaleRule{ + Name: pointer.From(v.Name), + QueueLength: int(pointer.From(q.QueueLength)), + QueueName: pointer.From(q.QueueName), + } + + authentications := make([]ScaleRuleAuthentication, 0) + if auths := q.Auth; auths != nil { + for _, a := range *auths { + authentications = append(authentications, ScaleRuleAuthentication{ + SecretRef: pointer.From(a.SecretRef), + TriggerParam: pointer.From(a.TriggerParameter), + }) + } + } + + rule.Authentications = authentications + + azureQueueScaleRules = append(azureQueueScaleRules, rule) + continue + } + + if r := v.Custom; r != nil { + rule := CustomScaleRule{ + Name: pointer.From(v.Name), + Metadata: pointer.From(r.Metadata), + CustomRuleType: pointer.From(r.Type), + } + + authentications := make([]ScaleRuleAuthentication, 0) + if auths := r.Auth; auths != nil { + for _, a := range *auths { + authentications = append(authentications, ScaleRuleAuthentication{ + SecretRef: pointer.From(a.SecretRef), + TriggerParam: pointer.From(a.TriggerParameter), + }) + } + } + rule.Authentications = authentications + + customScaleRules = append(customScaleRules, rule) + continue + } + + if r := v.HTTP; r != nil { + metaData := pointer.From(r.Metadata) + concurrentReqs := "" + + if m, ok := metaData["concurrentRequests"]; ok { + concurrentReqs = m + } + + rule := HTTPScaleRule{ + Name: pointer.From(v.Name), + ConcurrentRequests: concurrentReqs, + } + + authentications := make([]ScaleRuleAuthentication, 0) + if auths := r.Auth; auths != nil { + for _, a := range *auths { + authentications = append(authentications, ScaleRuleAuthentication{ + SecretRef: pointer.From(a.SecretRef), + TriggerParam: pointer.From(a.TriggerParameter), + }) + } + } + + rule.Authentications = authentications + + httpScaleRules = append(httpScaleRules, rule) + continue + } + + if r := v.Tcp; r != nil { + metaData := pointer.From(r.Metadata) + concurrentReqs := "" + + if m, ok := metaData["concurrentRequests"]; ok { + concurrentReqs = m + } + + rule := TCPScaleRule{ + Name: pointer.From(v.Name), + ConcurrentRequests: concurrentReqs, + } + + authentications := make([]ScaleRuleAuthentication, 0) + if auths := r.Auth; auths != nil { + for _, a := range *auths { + authentications = append(authentications, ScaleRuleAuthentication{ + SecretRef: pointer.From(a.SecretRef), + TriggerParam: pointer.From(a.TriggerParameter), + }) + } + } + rule.Authentications = authentications + + tcpScaleRules = append(tcpScaleRules, rule) + continue + } + } + + c.AzureQueueScaleRules = azureQueueScaleRules + c.CustomScaleRules = customScaleRules + c.HTTPScaleRules = httpScaleRules + c.TCPScaleRules = tcpScaleRules + } +} diff --git a/internal/services/containerapps/validate/validate.go b/internal/services/containerapps/validate/validate.go index b6147f522772..8c43ef0fac75 100644 --- a/internal/services/containerapps/validate/validate.go +++ b/internal/services/containerapps/validate/validate.go @@ -6,6 +6,7 @@ package validate import ( "fmt" "regexp" + "strconv" "strings" ) @@ -138,3 +139,23 @@ func ContainerAppContainerName(i interface{}, k string) (warnings []string, erro } return } + +func ContainerAppScaleRuleConcurrentRequests(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) + return + } + + c, err := strconv.Atoi(v) + if err != nil { + errors = append(errors, fmt.Errorf("expected %s to be a string representation of an integer, got %+v", k, v)) + return + } + + if c <= 0 { + errors = append(errors, fmt.Errorf("value for %s must be at least `1`, got %d", k, c)) + } + + return +} diff --git a/internal/services/containerapps/validate/validate_test.go b/internal/services/containerapps/validate/validate_test.go index 183e2c93f8c2..44203f9dc88f 100644 --- a/internal/services/containerapps/validate/validate_test.go +++ b/internal/services/containerapps/validate/validate_test.go @@ -274,3 +274,49 @@ func TestValidateInitTimeout(t *testing.T) { } } } + +func TestContainerAppScaleRuleConcurrentRequests(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + { + Input: "5", + Valid: true, + }, + { + Input: "m", + Valid: false, + }, + { + Input: "6d", + Valid: false, + }, + { + Input: "10s", + Valid: false, + }, + { + Input: "1h", + Valid: false, + }, + { + Input: "1200s", + Valid: false, + }, + { + Input: "-1", + Valid: false, + }, + } + + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := ContainerAppScaleRuleConcurrentRequests(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t for %s", tc.Valid, valid, tc.Input) + } + } +} diff --git a/website/docs/r/container_app.html.markdown b/website/docs/r/container_app.html.markdown index c5ab110a8469..5394f1f43521 100644 --- a/website/docs/r/container_app.html.markdown +++ b/website/docs/r/container_app.html.markdown @@ -97,12 +97,72 @@ A `template` block supports the following: * `min_replicas` - (Optional) The minimum number of replicas for this container. +* `azure_queue_scale_rule` - (Optional) One or more `azure_queue_scale_rule` blocks as defined below. + +* `custom_scale_rule` - (Optional) One or more `custom_scale_rule` blocks as defined below. + +* `http_scale_rule` - (Optional) One or more `http_scale_rule` blocks as defined below. + +* `tcp_scale_rule` - (Optional) One or more `tcp_scale_rule` blocks as defined 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. --- +An `azure_queue_scale_rule` block supports the following: + +* `name` - (Required) The name of the Scaling Rule + +* `queue_name` - (Required) The name of the Azure Queue + +* `queue_length` - (Required) The value of the length of the queue to trigger scaling actions. + +* `authentication` - (Required) One or more `authentication` blocks as defined below. + +--- + +A `custom_scale_rule` block supports the following: + +* `name` - (Required) The name of the Scaling Rule + +* `custom_rule_type` - (Required) The Custom rule type. Possible values include: `activemq`, `artemis-queue`, `kafka`, `pulsar`, `aws-cloudwatch`, `aws-dynamodb`, `aws-dynamodb-streams`, `aws-kinesis-stream`, `aws-sqs-queue`, `azure-app-insights`, `azure-blob`, `azure-data-explorer`, `azure-eventhub`, `azure-log-analytics`, `azure-monitor`, `azure-pipelines`, `azure-servicebus`, `azure-queue`, `cassandra`, `cpu`, `cron`, `datadog`, `elasticsearch`, `external`, `external-push`, `gcp-stackdriver`, `gcp-storage`, `gcp-pubsub`, `graphite`, `http`, `huawei-cloudeye`, `ibmmq`, `influxdb`, `kubernetes-workload`, `liiklus`, `memory`, `metrics-api`, `mongodb`, `mssql`, `mysql`, `nats-jetstream`, `stan`, `tcp`, `new-relic`, `openstack-metric`, `openstack-swift`, `postgresql`, `predictkube`, `prometheus`, `rabbitmq`, `redis`, `redis-cluster`, `redis-sentinel`, `redis-streams`, `redis-cluster-streams`, `redis-sentinel-streams`, `selenium-grid`,`solace-event-queue`, and `github-runner`. + +* `metadata`- (Required) - A map of string key-value pairs to configure the Custom Scale Rule. + +* `authentication` - (Optional) Zero or more `authentication` blocks as defined below. + +--- + +A `http_scale_rule` block supports the following: + +* `name` - (Required) The name of the Scaling Rule + +* `concurrent_requests`- (Required) - The number of concurrent requests to trigger scaling. + +* `authentication` - (Optional) Zero or more `authentication` blocks as defined below. + +--- + +A `tcp_scale_rule` block supports the following: + +* `name` - (Required) The name of the Scaling Rule + +* `concurrent_requests`- (Required) - The number of concurrent requests to trigger scaling. + +* `authentication` - (Optional) Zero or more `authentication` blocks as defined below. + +--- + +An `authentication` block supports the following: + +* `secret_name` - (Required) The name of the Container App Secret to use for this Scale Rule Authentication. + +* `trigger_parameter` - (Required) The Trigger Parameter name to use the supply the value retrieved from the `secret_name`. + +--- + A `volume` block supports the following: * `name` - (Required) The name of the volume.