Skip to content

Commit

Permalink
apps: add support for termination configuration. (#1297)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewsomething authored Jan 6, 2025
1 parent 035e137 commit ce3608f
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 1 deletion.
112 changes: 112 additions & 0 deletions digitalocean/app/app_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const (
functionComponent appSpecComponentType = "function"
)

// AppSpecTermination is a type constraint for the termination attribute of an app component.
type AppSpecTermination interface {
godo.AppServiceSpecTermination | godo.AppWorkerSpecTermination | godo.AppJobSpecTermination
}

// appSpecSchema returns map[string]*schema.Schema for the App Specification.
// Set isResource to true in order to return a schema with additional attributes
// appropriate for a resource or false for one used with a data-source.
Expand Down Expand Up @@ -494,6 +499,28 @@ func appSpecAutoscalingSchema() map[string]*schema.Schema {
}
}

func appSpecTerminationSchema(component appSpecComponentType) map[string]*schema.Schema {
termination := map[string]*schema.Schema{
"grace_period_seconds": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntBetween(1, 600),
Description: "The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.",
},
}

if component == serviceComponent {
termination["drain_seconds"] = &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntBetween(1, 110),
Description: "The number of seconds to wait between selecting a container instance for termination and issuing the TERM signal. Selecting a container instance for termination begins an asynchronous drain of new requests on upstream load-balancers. Default: 15 seconds, Minimum 1, Maximum 110.",
}
}

return termination
}

func appSpecComponentBase(componentType appSpecComponentType) map[string]*schema.Schema {
baseSchema := map[string]*schema.Schema{
"name": {
Expand Down Expand Up @@ -648,6 +675,14 @@ func appSpecServicesSchema() *schema.Resource {
Schema: appSpecAutoscalingSchema(),
},
},
"termination": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: appSpecTerminationSchema(serviceComponent),
},
},
}

for k, v := range appSpecComponentBase(serviceComponent) {
Expand Down Expand Up @@ -743,6 +778,14 @@ func appSpecWorkerSchema() *schema.Resource {
Schema: appSpecAutoscalingSchema(),
},
},
"termination": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: appSpecTerminationSchema(workerComponent),
},
},
}

for k, v := range appSpecComponentBase(workerComponent) {
Expand Down Expand Up @@ -791,6 +834,14 @@ func appSpecJobSchema() *schema.Resource {
}, false),
Description: "The type of job and when it will be run during the deployment process.",
},
"termination": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: appSpecTerminationSchema(jobComponent),
},
},
}

for k, v := range appSpecComponentBase(jobComponent) {
Expand Down Expand Up @@ -1856,6 +1907,11 @@ func expandAppSpecServices(config []interface{}) []*godo.AppServiceSpec {
s.Autoscaling = expandAppAutoscaling(autoscaling)
}

termination := service["termination"].([]interface{})
if len(termination) > 0 {
s.Termination = expandAppTermination[godo.AppServiceSpecTermination](termination)
}

appServices = append(appServices, s)
}

Expand Down Expand Up @@ -1889,6 +1945,7 @@ func flattenAppSpecServices(services []*godo.AppServiceSpec) []map[string]interf
r["alert"] = flattenAppAlerts(s.Alerts)
r["log_destination"] = flattenAppLogDestinations(s.LogDestinations)
r["autoscaling"] = flattenAppAutoscaling(s.Autoscaling)
r["termination"] = flattenAppTermination(s.Termination)

result[i] = r
}
Expand Down Expand Up @@ -2039,6 +2096,11 @@ func expandAppSpecWorkers(config []interface{}) []*godo.AppWorkerSpec {
s.Autoscaling = expandAppAutoscaling(autoscaling)
}

termination := worker["termination"].([]interface{})
if len(termination) > 0 {
s.Termination = expandAppTermination[godo.AppWorkerSpecTermination](termination)
}

appWorkers = append(appWorkers, s)
}

Expand Down Expand Up @@ -2067,6 +2129,7 @@ func flattenAppSpecWorkers(workers []*godo.AppWorkerSpec) []map[string]interface
r["alert"] = flattenAppAlerts(w.Alerts)
r["log_destination"] = flattenAppLogDestinations(w.LogDestinations)
r["autoscaling"] = flattenAppAutoscaling(w.Autoscaling)
r["termination"] = flattenAppTermination(w.Termination)

result[i] = r
}
Expand Down Expand Up @@ -2123,6 +2186,11 @@ func expandAppSpecJobs(config []interface{}) []*godo.AppJobSpec {
s.LogDestinations = expandAppLogDestinations(logDestinations)
}

termination := job["termination"].([]interface{})
if len(termination) > 0 {
s.Termination = expandAppTermination[godo.AppJobSpecTermination](termination)
}

appJobs = append(appJobs, s)
}

Expand Down Expand Up @@ -2151,6 +2219,7 @@ func flattenAppSpecJobs(jobs []*godo.AppJobSpec) []map[string]interface{} {
r["kind"] = string(j.Kind)
r["alert"] = flattenAppAlerts(j.Alerts)
r["log_destination"] = flattenAppLogDestinations(j.LogDestinations)
r["termination"] = flattenAppTermination(j.Termination)

result[i] = r
}
Expand Down Expand Up @@ -2445,6 +2514,49 @@ func expandAppIngressMatch(config []interface{}) *godo.AppIngressSpecRuleMatch {
}
}

func expandAppTermination[T AppSpecTermination](config []interface{}) *T {
if len(config) == 0 || config[0] == nil {
return nil
}

terminationConfig := config[0].(map[string]interface{})

termination := new(T)
switch t := any(termination).(type) {
case *godo.AppServiceSpecTermination:
t.GracePeriodSeconds = int32(terminationConfig["grace_period_seconds"].(int))
t.DrainSeconds = int32(terminationConfig["drain_seconds"].(int))
case *godo.AppWorkerSpecTermination:
t.GracePeriodSeconds = int32(terminationConfig["grace_period_seconds"].(int))
case *godo.AppJobSpecTermination:
t.GracePeriodSeconds = int32(terminationConfig["grace_period_seconds"].(int))
}

return termination
}

func flattenAppTermination[T AppSpecTermination](termination *T) []interface{} {
result := make([]interface{}, 0)

if termination != nil {
r := make(map[string]interface{})

switch t := any(termination).(type) {
case *godo.AppServiceSpecTermination:
r["grace_period_seconds"] = t.GracePeriodSeconds
r["drain_seconds"] = t.DrainSeconds
case *godo.AppWorkerSpecTermination:
r["grace_period_seconds"] = t.GracePeriodSeconds
case *godo.AppJobSpecTermination:
r["grace_period_seconds"] = t.GracePeriodSeconds
}

result = append(result, r)
}

return result
}

func flattenAppEgress(egress *godo.AppEgressSpec) []map[string]interface{} {
if egress != nil {
result := make([]map[string]interface{}, 0)
Expand Down
60 changes: 60 additions & 0 deletions digitalocean/app/resource_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,33 @@ func TestAccDigitalOceanApp_ImageDigest(t *testing.T) {
})
}

func TestAccDigitalOceanApp_termination(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_withTermination, appName),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app),
resource.TestCheckResourceAttr(
"digitalocean_app.foobar", "spec.0.name", appName),
resource.TestCheckResourceAttr(
"digitalocean_app.foobar", "spec.0.service.0.termination.0.grace_period_seconds", "60"),
resource.TestCheckResourceAttr(
"digitalocean_app.foobar", "spec.0.service.0.termination.0.drain_seconds", "30"),
resource.TestCheckResourceAttr(
"digitalocean_app.foobar", "spec.0.worker.0.termination.0.grace_period_seconds", "30"),
),
},
},
})
}

var testAccCheckDigitalOceanAppConfig_basic = `
resource "digitalocean_app" "foobar" {
spec {
Expand Down Expand Up @@ -1749,3 +1776,36 @@ resource "digitalocean_app" "foobar" {
ingress {}
}
}`

var testAccCheckDigitalOceanAppConfig_withTermination = `
resource "digitalocean_app" "foobar" {
spec {
name = "%s"
region = "nyc"
service {
name = "go-service"
instance_size_slug = "apps-d-1vcpu-0.5gb"
instance_count = 1
termination {
drain_seconds = 30
grace_period_seconds = 60
}
git {
repo_clone_url = "https://github.com/digitalocean/sample-golang.git"
branch = "main"
}
}
worker {
name = "go-worker"
instance_size_slug = "apps-d-1vcpu-0.5gb"
instance_count = 1
termination {
grace_period_seconds = 30
}
git {
repo_clone_url = "https://github.com/digitalocean/sample-sleeper.git"
branch = "main"
}
}
}
}`
8 changes: 7 additions & 1 deletion docs/data-sources/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ A `service` can contain:
- `password` - Password for user defined in User. Is required when endpoint is set. Cannot be set if using a DigitalOcean DBaaS OpenSearch cluster.
- `index_name` - The index name to use for the logs. If not set, the default index name is `logs`.
- `cluster_name` - The name of a DigitalOcean DBaaS OpenSearch cluster to use as a log forwarding destination. Cannot be specified if endpoint is also specified.
* `termination` - Contains a component's termination parameters.
- `grace_period_seconds` - The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.
- `drain_seconds` - The number of seconds to wait between selecting a container instance for termination and issuing the TERM signal. Selecting a container instance for termination begins an asynchronous drain of new requests on upstream load-balancers. Default: 15 seconds, Minimum 1, Maximum 110.

A `static_site` can contain:

Expand Down Expand Up @@ -201,6 +204,8 @@ A `worker` can contain:
- `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.
* `termination` - Contains a component's termination parameters.
- `grace_period_seconds` - The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.

A `job` can contain:

Expand Down Expand Up @@ -257,6 +262,8 @@ A `job` can contain:
- `password` - Password for user defined in User. Is required when endpoint is set. Cannot be set if using a DigitalOcean DBaaS OpenSearch cluster.
- `index_name` - The index name to use for the logs. If not set, the default index name is `logs`.
- `cluster_name` - The name of a DigitalOcean DBaaS OpenSearch cluster to use as a log forwarding destination. Cannot be specified if endpoint is also specified.
* `termination` - Contains a component's termination parameters.
- `grace_period_seconds` - The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.

A `function` component can contain:

Expand Down Expand Up @@ -314,7 +321,6 @@ A `function` component can contain:
- `index_name` - The index name to use for the logs. If not set, the default index name is `logs`.
- `cluster_name` - The name of a DigitalOcean DBaaS OpenSearch cluster to use as a log forwarding destination. Cannot be specified if endpoint is also specified.


A `database` can contain:

* `name` - The name of the component.
Expand Down
7 changes: 7 additions & 0 deletions docs/resources/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ A `service` can contain:
- `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.
- `termination` - Contains a component's termination parameters.
- `grace_period_seconds` - The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.
- `drain_seconds` - The number of seconds to wait between selecting a container instance for termination and issuing the TERM signal. Selecting a container instance for termination begins an asynchronous drain of new requests on upstream load-balancers. Default: 15 seconds, Minimum 1, Maximum 110.

A `static_site` can contain:

Expand Down Expand Up @@ -413,6 +416,8 @@ A `worker` can contain:
- `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.
- `termination` - Contains a component's termination parameters.
- `grace_period_seconds` - The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.

A `job` can contain:

Expand Down Expand Up @@ -476,6 +481,8 @@ A `job` can contain:
- `password` - Password for user defined in User. Is required when endpoint is set. Cannot be set if using a DigitalOcean DBaaS OpenSearch cluster.
- `index_name` - The index name to use for the logs. If not set, the default index name is `logs`.
- `cluster_name` - The name of a DigitalOcean DBaaS OpenSearch cluster to use as a log forwarding destination. Cannot be specified if endpoint is also specified.
- `termination` - Contains a component's termination parameters.
- `grace_period_seconds` - The number of seconds to wait between sending a TERM signal to a container and issuing a KILL which causes immediate shutdown. Default: 120, Minimum 1, Maximum 600.

A `function` component can contain:

Expand Down

0 comments on commit ce3608f

Please sign in to comment.