Skip to content

Commit

Permalink
digitalocean_app: Service Autoscaling (#1189)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
  • Loading branch information
4 people authored Jul 31, 2024
1 parent cffa11a commit 5d97eac
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 4 deletions.
114 changes: 111 additions & 3 deletions digitalocean/app/app_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.",
},
}
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
Expand Down
77 changes: 77 additions & 0 deletions digitalocean/app/resource_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
}
}
}
}`
6 changes: 6 additions & 0 deletions docs/data-sources/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
8 changes: 7 additions & 1 deletion docs/resources/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 5d97eac

Please sign in to comment.