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_app - Add scale rules #21274

Closed
wants to merge 1 commit into from
Closed
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
azurerm_container_app - Add scale rules
  • Loading branch information
jsok committed Jun 12, 2023
commit a0d8dd3346cb0373a18d3df54339fed4db87ca97
12 changes: 10 additions & 2 deletions internal/services/containerapps/container_app_resource.go
Original file line number Diff line number Diff line change
@@ -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)
21 changes: 21 additions & 0 deletions internal/services/containerapps/container_app_resource_test.go
Original file line number Diff line number Diff line change
@@ -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"
}

215 changes: 198 additions & 17 deletions internal/services/containerapps/helpers/container_apps.go
Original file line number Diff line number Diff line change
@@ -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.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy paste error here?

Suggested change
Description: "The name of the volume.",
Description: "The custom rule type.",

},
"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,
},
},
},
},
},
},
},
},
},
},
}
}
Comment on lines +1068 to +1131
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we flatten this schema to containing rule types, we can provide better schema validation here? (Which will allow us to remove the apply-time error messaging in the expand functions later?
e.g.

Suggested change
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 ContainerScaleSchema() *pluginsdk.Schema {
return &pluginsdk.Schema{
Type: pluginsdk.TypeList,
MaxItems: 1,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"http_rule": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
Description: "The name of the rule.",
},
"metadata": {
Type: pluginsdk.TypeMap,
Required: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
},
},
},
},
},
"custom_rule": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
Description: "The name of the rule.",
},
"type": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"metadata": {
Type: pluginsdk.TypeMap,
Required: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
},
},
},
},
},
"azure_queue_rule": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
Description: "The name of the rule.",
},
"queue_name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"queue_length": {
Type: pluginsdk.TypeInt,
Required: true,
},
},
},
},
},
},
}
}


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 || input.Rules == nil {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return an empty list

Suggested change
return nil
return []ContainerScale{}

}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return an empty list

Suggested change
return nil
return []ContainerHTTPScaleRule{}

}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return an empty list

Suggested change
return nil
return []ContainerCustomScaleRule{}

}
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"`
32 changes: 32 additions & 0 deletions website/docs/d/container_app.html.markdown
Original file line number Diff line number Diff line change
@@ -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`.
Loading