From 5d97eac31a455d068b203dbec20ce674bf9d49ff Mon Sep 17 00:00:00 2001 From: Moritz Bruder Date: Wed, 31 Jul 2024 23:26:48 +0100 Subject: [PATCH] digitalocean_app: Service Autoscaling (#1189) * add autoscaling config bump godo dependency to 1.119.0 * update basic test * undo dependency version bump * make fields required and remove instance_count default * Run make terrafmt * apps: move autoscaling to seperate acceptance test * apps: append the to the result in flattenAppAutoscaling * docs: add autoscaling to apps resource docs --------- Co-authored-by: Moritz Bruder Co-authored-by: Andrew Starr-Bochicchio Co-authored-by: Andrew Starr-Bochicchio --- digitalocean/app/app_spec.go | 114 +++++++++++++++++++++++++- digitalocean/app/resource_app_test.go | 77 +++++++++++++++++ docs/data-sources/app.md | 6 ++ docs/resources/app.md | 8 +- 4 files changed, 201 insertions(+), 4 deletions(-) diff --git a/digitalocean/app/app_spec.go b/digitalocean/app/app_spec.go index f6facaab4..f85f2987c 100644 --- a/digitalocean/app/app_spec.go +++ b/digitalocean/app/app_spec.go @@ -445,6 +445,50 @@ func appSpecCORSSchema() map[string]*schema.Schema { } } +func appSpecAutoscalingSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "min_instance_count": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(1), + Description: "The minimum amount of instances for this component. Must be less than max_instance_count.", + }, + "max_instance_count": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(1), + Description: "The maximum amount of instances for this component. Must be more than min_instance_count.", + }, + "metrics": { + Type: schema.TypeList, + MaxItems: 1, + MinItems: 1, + Required: true, + Description: "The metrics that the component is scaled on.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cpu": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Settings for scaling the component based on CPU utilization.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "percent": { + Type: schema.TypeInt, + ValidateFunc: validation.IntBetween(1, 100), + Required: true, + Description: "The average target CPU utilization for the component.", + }, + }, + }, + }, + }, + }, + }, + } +} + func appSpecComponentBase(componentType appSpecComponentType) map[string]*schema.Schema { baseSchema := map[string]*schema.Schema{ "name": { @@ -549,7 +593,6 @@ func appSpecServicesSchema() *schema.Resource { "instance_count": { Type: schema.TypeInt, Optional: true, - Default: 1, Description: "The amount of instances that this component should be scaled to.", }, "health_check": { @@ -592,6 +635,14 @@ func appSpecServicesSchema() *schema.Resource { Schema: appSpecCORSSchema(), }, }, + "autoscaling": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: appSpecAutoscalingSchema(), + }, + }, } for k, v := range appSpecComponentBase(serviceComponent) { @@ -677,7 +728,6 @@ func appSpecWorkerSchema() *schema.Resource { "instance_count": { Type: schema.TypeInt, Optional: true, - Default: 1, Description: "The amount of instances that this component should be scaled to.", }, } @@ -714,7 +764,6 @@ func appSpecJobSchema() *schema.Resource { "instance_count": { Type: schema.TypeInt, Optional: true, - Default: 1, Description: "The amount of instances that this component should be scaled to.", }, "kind": { @@ -1264,6 +1313,59 @@ func flattenAppLogDestinations(destinations []*godo.AppLogDestinationSpec) []map return result } +func expandAppAutoscaling(config []interface{}) *godo.AppAutoscalingSpec { + autoscalingConfig := config[0].(map[string]interface{}) + + autoscalingSpec := &godo.AppAutoscalingSpec{ + MinInstanceCount: int64(autoscalingConfig["min_instance_count"].(int)), + MaxInstanceCount: int64(autoscalingConfig["max_instance_count"].(int)), + Metrics: expandAppAutoscalingMetrics(autoscalingConfig["metrics"].([]interface{})), + } + + return autoscalingSpec +} + +func expandAppAutoscalingMetrics(config []interface{}) *godo.AppAutoscalingSpecMetrics { + metrics := &godo.AppAutoscalingSpecMetrics{} + + for _, rawMetric := range config { + metric := rawMetric.(map[string]interface{}) + cpu := metric["cpu"].([]interface{}) + if len(cpu) > 0 { + cpuMetric := cpu[0].(map[string]interface{}) + metrics.CPU = &godo.AppAutoscalingSpecMetricCPU{ + Percent: int64(cpuMetric["percent"].(int)), + } + } + } + + return metrics +} + +func flattenAppAutoscaling(autoscaling *godo.AppAutoscalingSpec) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + + if autoscaling != nil { + r := make(map[string]interface{}) + r["min_instance_count"] = autoscaling.MinInstanceCount + r["max_instance_count"] = autoscaling.MaxInstanceCount + metrics := make(map[string]interface{}) + if autoscaling.Metrics.CPU != nil { + cpuMetric := make([]map[string]interface{}, 1) + cpuMetric[0] = make(map[string]interface{}) + cpuMetric[0]["percent"] = autoscaling.Metrics.CPU.Percent + metrics["cpu"] = cpuMetric + } + metricsList := make([]map[string]interface{}, 1) + metricsList[0] = metrics + r["metrics"] = metricsList + + result = append(result, r) + } + + return result +} + // expandAppDomainSpec has been deprecated in favor of expandAppSpecDomains. func expandAppDomainSpec(config []interface{}) []*godo.AppDomainSpec { appDomains := make([]*godo.AppDomainSpec, 0, len(config)) @@ -1649,6 +1751,11 @@ func expandAppSpecServices(config []interface{}) []*godo.AppServiceSpec { s.LogDestinations = expandAppLogDestinations(logDestinations) } + autoscaling := service["autoscaling"].([]interface{}) + if len(autoscaling) > 0 { + s.Autoscaling = expandAppAutoscaling(autoscaling) + } + appServices = append(appServices, s) } @@ -1681,6 +1788,7 @@ func flattenAppSpecServices(services []*godo.AppServiceSpec) []map[string]interf r["cors"] = flattenAppCORSPolicy(s.CORS) r["alert"] = flattenAppAlerts(s.Alerts) r["log_destination"] = flattenAppLogDestinations(s.LogDestinations) + r["autoscaling"] = flattenAppAutoscaling(s.Autoscaling) result[i] = r } diff --git a/digitalocean/app/resource_app_test.go b/digitalocean/app/resource_app_test.go index 5855408a6..ff551ec1a 100644 --- a/digitalocean/app/resource_app_test.go +++ b/digitalocean/app/resource_app_test.go @@ -950,6 +950,54 @@ func TestAccDigitalOceanApp_nonDefaultProject(t *testing.T) { }) } +func TestAccDigitalOceanApp_autoScale(t *testing.T) { + var app godo.App + appName := acceptance.RandomTestName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: testAccCheckDigitalOceanAppDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCheckDigitalOceanAppConfig_autoScale, appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.name", appName), + resource.TestCheckResourceAttrSet( + "digitalocean_app.foobar", "project_id"), + resource.TestCheckResourceAttrSet("digitalocean_app.foobar", "default_ingress"), + resource.TestCheckResourceAttrSet("digitalocean_app.foobar", "live_url"), + resource.TestCheckResourceAttrSet("digitalocean_app.foobar", "active_deployment_id"), + resource.TestCheckResourceAttrSet("digitalocean_app.foobar", "urn"), + resource.TestCheckResourceAttrSet("digitalocean_app.foobar", "updated_at"), + resource.TestCheckResourceAttrSet("digitalocean_app.foobar", "created_at"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.instance_size_slug", "apps-d-1vcpu-0.5gb"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.match.0.path.0.prefix", "/"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.preserve_path_prefix", "false"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.name", "go-service"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.git.0.repo_clone_url", + "https://github.com/digitalocean/sample-golang.git"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.git.0.branch", "main"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.autoscaling.0.min_instance_count", "2"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.autoscaling.0.max_instance_count", "4"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.service.0.autoscaling.0.metrics.0.cpu.0.percent", "60"), + ), + }, + }, + }) +} + var testAccCheckDigitalOceanAppConfig_basic = ` resource "digitalocean_app" "foobar" { spec { @@ -1497,3 +1545,32 @@ resource "digitalocean_app" "foobar" { } } }` + +var testAccCheckDigitalOceanAppConfig_autoScale = ` +resource "digitalocean_app" "foobar" { + spec { + name = "%s" + region = "nyc" + + service { + name = "go-service" + environment_slug = "go" + instance_size_slug = "apps-d-1vcpu-0.5gb" + + git { + repo_clone_url = "https://github.com/digitalocean/sample-golang.git" + branch = "main" + } + + autoscaling { + min_instance_count = 2 + max_instance_count = 4 + metrics { + cpu { + percent = 60 + } + } + } + } + } +}` diff --git a/docs/data-sources/app.md b/docs/data-sources/app.md index fbc063832..db0d5ec20 100644 --- a/docs/data-sources/app.md +++ b/docs/data-sources/app.md @@ -88,6 +88,12 @@ A `service` can contain: - `timeout_seconds` - The number of seconds after which the check times out. - `success_threshold` - The number of successful health checks before considered healthy. - `failure_threshold` - The number of failed health checks before considered unhealthy. +* `autoscaling` - Configuration for automatically scaling this component based on metrics. + - `min_instance_count` - The minimum amount of instances for this component. Must be less than max_instance_count. + - `max_instance_count` - The maximum amount of instances for this component. Must be more than min_instance_count. + - `metrics` - The metrics that the component is scaled on. + - `cpu` - Settings for scaling the component based on CPU utilization. + - `percent` - The average target CPU utilization for the component. A `static_site` can contain: diff --git a/docs/resources/app.md b/docs/resources/app.md index 542360c2f..2dca9f2d1 100644 --- a/docs/resources/app.md +++ b/docs/resources/app.md @@ -269,7 +269,13 @@ A `service` can contain: - `api_key` - Datadog API key. - `logtail` - Logtail configuration. - `token` - Logtail token. - +* `autoscaling` - Configuration for automatically scaling this component based on metrics. + - `min_instance_count` - The minimum amount of instances for this component. Must be less than max_instance_count. + - `max_instance_count` - The maximum amount of instances for this component. Must be more than min_instance_count. + - `metrics` - The metrics that the component is scaled on. + - `cpu` - Settings for scaling the component based on CPU utilization. + - `percent` - The average target CPU utilization for the component. + A `static_site` can contain: * `name` - The name of the component.