diff --git a/digitalocean/app_spec.go b/digitalocean/app_spec.go index 420830a5d..dfafb2f5a 100644 --- a/digitalocean/app_spec.go +++ b/digitalocean/app_spec.go @@ -8,8 +8,11 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -func appSpecSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ +// 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. +func appSpecSchema(isResource bool) map[string]*schema.Schema { + spec := map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, @@ -21,10 +24,18 @@ func appSpecSchema() map[string]*schema.Schema { Optional: true, Description: "The slug for the DigitalOcean data center region hosting the app", }, - "domains": { - Type: schema.TypeSet, + "domain": { + Type: schema.TypeList, Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Elem: appSpecDomainSchema(), + }, + "domains": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Deprecated: "This attribute has been replaced by `domain` which supports additional functionality.", }, "service": { Type: schema.TypeList, @@ -59,6 +70,46 @@ func appSpecSchema() map[string]*schema.Schema { Set: schema.HashResource(appSpecEnvSchema()), }, } + + if isResource { + spec["domain"].ConflictsWith = []string{"spec.0.domains"} + } + + return spec +} + +func appSpecDomainSchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The hostname for the domain.", + }, + "type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + "DEFAULT", + "PRIMARY", + "ALIAS", + }, false), + Description: "The type of the domain.", + }, + "wildcard": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Indicates whether the domain includes all sub-domains, in addition to the given domain.", + }, + "zone": { + Type: schema.TypeString, + Optional: true, + Description: "If the domain uses DigitalOcean DNS and you would like App Platform to automatically manage it for you, set this to the name of the domain on your account.", + }, + }, + } } func appSpecGitSourceSchema() map[string]*schema.Schema { @@ -529,7 +580,6 @@ func expandAppSpec(config []interface{}) *godo.AppSpec { appSpec := &godo.AppSpec{ Name: appSpecConfig["name"].(string), Region: appSpecConfig["region"].(string), - Domains: expandAppDomainSpec(appSpecConfig["domains"].(*schema.Set).List()), Services: expandAppSpecServices(appSpecConfig["service"].([]interface{})), StaticSites: expandAppSpecStaticSites(appSpecConfig["static_site"].([]interface{})), Workers: expandAppSpecWorkers(appSpecConfig["worker"].([]interface{})), @@ -538,10 +588,18 @@ func expandAppSpec(config []interface{}) *godo.AppSpec { Envs: expandAppEnvs(appSpecConfig["env"].(*schema.Set).List()), } + // Prefer the `domain` block over `domains` if it is set. + domainConfig := appSpecConfig["domain"].([]interface{}) + if len(domainConfig) > 0 { + appSpec.Domains = expandAppSpecDomains(domainConfig) + } else { + appSpec.Domains = expandAppDomainSpec(appSpecConfig["domains"].(*schema.Set).List()) + } + return appSpec } -func flattenAppSpec(spec *godo.AppSpec) []map[string]interface{} { +func flattenAppSpec(d *schema.ResourceData, spec *godo.AppSpec) []map[string]interface{} { result := make([]map[string]interface{}, 0, 1) if spec != nil { @@ -549,7 +607,13 @@ func flattenAppSpec(spec *godo.AppSpec) []map[string]interface{} { r := make(map[string]interface{}) r["name"] = (*spec).Name r["region"] = (*spec).Region - r["domains"] = flattenAppDomainSpec((*spec).Domains) + + if len((*spec).Domains) > 0 { + r["domains"] = flattenAppDomainSpec((*spec).Domains) + if _, ok := d.GetOk("spec.0.domain"); ok { + r["domain"] = flattenAppSpecDomains((*spec).Domains) + } + } if len((*spec).Services) > 0 { r["service"] = flattenAppSpecServices((*spec).Services) @@ -581,6 +645,7 @@ func flattenAppSpec(spec *godo.AppSpec) []map[string]interface{} { return result } +// expandAppDomainSpec has been deprecated in favor of expandAppSpecDomains. func expandAppDomainSpec(config []interface{}) []*godo.AppDomainSpec { appDomains := make([]*godo.AppDomainSpec, 0, len(config)) @@ -595,6 +660,26 @@ func expandAppDomainSpec(config []interface{}) []*godo.AppDomainSpec { return appDomains } +func expandAppSpecDomains(config []interface{}) []*godo.AppDomainSpec { + appDomains := make([]*godo.AppDomainSpec, 0, len(config)) + + for _, rawDomain := range config { + domain := rawDomain.(map[string]interface{}) + + d := &godo.AppDomainSpec{ + Domain: domain["name"].(string), + Type: godo.AppDomainSpecType(domain["type"].(string)), + Wildcard: domain["wildcard"].(bool), + Zone: domain["zone"].(string), + } + + appDomains = append(appDomains, d) + } + + return appDomains +} + +// flattenAppDomainSpec has been deprecated in favor of flattenAppSpecDomains func flattenAppDomainSpec(spec []*godo.AppDomainSpec) *schema.Set { result := schema.NewSet(schema.HashString, []interface{}{}) @@ -605,6 +690,23 @@ func flattenAppDomainSpec(spec []*godo.AppDomainSpec) *schema.Set { return result } +func flattenAppSpecDomains(domains []*godo.AppDomainSpec) []map[string]interface{} { + result := make([]map[string]interface{}, len(domains)) + + for i, d := range domains { + r := make(map[string]interface{}) + + r["name"] = d.Domain + r["type"] = string(d.Type) + r["wildcard"] = d.Wildcard + r["zone"] = d.Zone + + result[i] = r + } + + return result +} + func expandAppGitHubSourceSpec(config []interface{}) *godo.GitHubSourceSpec { gitHubSourceConfig := config[0].(map[string]interface{}) diff --git a/digitalocean/datasource_digitalocean_app.go b/digitalocean/datasource_digitalocean_app.go index 428317e72..92e730822 100644 --- a/digitalocean/datasource_digitalocean_app.go +++ b/digitalocean/datasource_digitalocean_app.go @@ -20,7 +20,7 @@ func dataSourceDigitalOceanApp() *schema.Resource { Computed: true, Description: "A DigitalOcean App Platform Spec", Elem: &schema.Resource{ - Schema: appSpecSchema(), + Schema: appSpecSchema(false), }, }, "default_ingress": { diff --git a/digitalocean/resource_digitalocean_app.go b/digitalocean/resource_digitalocean_app.go index f7b512a55..c623f241c 100644 --- a/digitalocean/resource_digitalocean_app.go +++ b/digitalocean/resource_digitalocean_app.go @@ -29,7 +29,7 @@ func resourceDigitalOceanApp() *schema.Resource { MaxItems: 1, Description: "A DigitalOcean App Platform Spec", Elem: &schema.Resource{ - Schema: appSpecSchema(), + Schema: appSpecSchema(true), }, }, @@ -112,7 +112,7 @@ func resourceDigitalOceanAppRead(ctx context.Context, d *schema.ResourceData, me d.Set("updated_at", app.UpdatedAt.UTC().String()) d.Set("created_at", app.CreatedAt.UTC().String()) - if err := d.Set("spec", flattenAppSpec(app.Spec)); err != nil { + if err := d.Set("spec", flattenAppSpec(d, app.Spec)); err != nil { return diag.Errorf("[DEBUG] Error setting app spec: %#v", err) } diff --git a/digitalocean/resource_digitalocean_app_test.go b/digitalocean/resource_digitalocean_app_test.go index 87d87ca5e..264e367bd 100644 --- a/digitalocean/resource_digitalocean_app_test.go +++ b/digitalocean/resource_digitalocean_app_test.go @@ -436,6 +436,121 @@ func TestAccDigitalOceanApp_Worker(t *testing.T) { }) } +func TestAccDigitalOceanApp_Domain(t *testing.T) { + var app godo.App + appName := randomTestName() + + domain := fmt.Sprintf(` + domain { + name = "%s.com" + wildcard = true + } +`, appName) + + updatedDomain := fmt.Sprintf(` + domain { + name = "%s.net" + wildcard = true + } +`, appName) + + domainsConfig := fmt.Sprintf(testAccCheckDigitalOceanAppConfig_Domains, appName, domain) + updatedDomainConfig := fmt.Sprintf(testAccCheckDigitalOceanAppConfig_Domains, appName, updatedDomain) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanAppDestroy, + Steps: []resource.TestStep{ + { + Config: domainsConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.name", appName), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domain.0.name", appName+".com"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domain.0.wildcard", "true"), + ), + }, + { + Config: updatedDomainConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.name", appName), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domain.0.name", appName+".net"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domain.0.wildcard", "true"), + ), + }, + }, + }) +} + +func TestAccDigitalOceanApp_DomainsDeprecation(t *testing.T) { + var app godo.App + appName := randomTestName() + + deprecatedStyleDomain := fmt.Sprintf(` + domains = ["%s.com"] +`, appName) + + updatedDeprecatedStyleDomain := fmt.Sprintf(` + domains = ["%s.net"] +`, appName) + + newStyleDomain := fmt.Sprintf(` + domain { + name = "%s.com" + wildcard = true + } +`, appName) + + domainsConfig := fmt.Sprintf(testAccCheckDigitalOceanAppConfig_Domains, appName, deprecatedStyleDomain) + updateDomainsConfig := fmt.Sprintf(testAccCheckDigitalOceanAppConfig_Domains, appName, updatedDeprecatedStyleDomain) + replaceDomainsConfig := fmt.Sprintf(testAccCheckDigitalOceanAppConfig_Domains, appName, newStyleDomain) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanAppDestroy, + Steps: []resource.TestStep{ + { + Config: domainsConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.name", appName), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domains.0", appName+".com"), + ), + }, + { + Config: updateDomainsConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.name", appName), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domains.0", appName+".net"), + ), + }, + { + Config: replaceDomainsConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domain.0.name", appName+".com"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.domain.0.wildcard", "true"), + ), + }, + }, + }) +} + func testAccCheckDigitalOceanAppDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*CombinedConfig).godoClient() @@ -722,3 +837,25 @@ resource "digitalocean_app" "foobar" { } } }` + +var testAccCheckDigitalOceanAppConfig_Domains = ` +resource "digitalocean_app" "foobar" { + spec { + name = "%s" + region = "ams" + + %s + + service { + name = "go-service" + environment_slug = "go" + instance_count = 1 + instance_size_slug = "basic-xxs" + + git { + repo_clone_url = "https://github.com/digitalocean/sample-golang.git" + branch = "main" + } + } + } +}`