diff --git a/digitalocean/app/app_spec.go b/digitalocean/app/app_spec.go index f98a56f54..68ccb1ea7 100644 --- a/digitalocean/app/app_spec.go +++ b/digitalocean/app/app_spec.go @@ -2,6 +2,7 @@ package app import ( "log" + "net/http" "github.com/digitalocean/godo" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -90,6 +91,13 @@ func appSpecSchema(isResource bool) map[string]*schema.Schema { Elem: appSpecAppLevelAlerts(), Set: schema.HashResource(appSpecAppLevelAlerts()), }, + "ingress": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: appSpecIngressSchema(), + }, } if isResource { @@ -521,9 +529,10 @@ func appSpecServicesSchema() *schema.Resource { }, }, "routes": { - Type: schema.TypeList, - Optional: true, - Computed: true, + Type: schema.TypeList, + Optional: true, + Computed: true, + Deprecated: "Service level routes are deprecated in favor of ingresses", Elem: &schema.Resource{ Schema: appSpecRouteSchema(), }, @@ -534,9 +543,10 @@ func appSpecServicesSchema() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeInt}, }, "cors": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Deprecated: "Service level CORS rules are deprecated in favor of ingresses", Elem: &schema.Resource{ Schema: appSpecCORSSchema(), }, @@ -575,17 +585,19 @@ func appSpecStaticSiteSchema() *schema.Resource { Description: "The name of the document to use as the fallback for any requests to documents that are not found when serving this static site.", }, "routes": { - Type: schema.TypeList, - Optional: true, - Computed: true, + Type: schema.TypeList, + Optional: true, + Computed: true, + Deprecated: "Service level routes are deprecated in favor of ingresses", Elem: &schema.Resource{ Schema: appSpecRouteSchema(), }, }, "cors": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Deprecated: "Service level CORS rules are deprecated in favor of ingresses", Elem: &schema.Resource{ Schema: appSpecCORSSchema(), }, @@ -690,17 +702,19 @@ func appSpecJobSchema() *schema.Resource { func appSpecFunctionsSchema() *schema.Resource { functionsSchema := map[string]*schema.Schema{ "routes": { - Type: schema.TypeList, - Optional: true, - Computed: true, + Type: schema.TypeList, + Optional: true, + Computed: true, + Deprecated: "Service level routes are deprecated in favor of ingresses", Elem: &schema.Resource{ Schema: appSpecRouteSchema(), }, }, "cors": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Deprecated: "Service level CORS rules are deprecated in favor of ingresses", Elem: &schema.Resource{ Schema: appSpecCORSSchema(), }, @@ -875,6 +889,111 @@ func appSpecDatabaseSchema() *schema.Resource { } } +func appSpecIngressSchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "rule": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "match": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "path": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "cors": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: appSpecCORSSchema(), + }, + }, + "component": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "preserve_path_prefix": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "rewrite": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "redirect": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "uri": { + Type: schema.TypeString, + Optional: true, + }, + "authority": { + Type: schema.TypeString, + Optional: true, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + }, + "scheme": { + Type: schema.TypeString, + Optional: true, + }, + "redirect_code": { + Type: schema.TypeInt, + Optional: true, + Default: http.StatusFound, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func expandAppSpec(config []interface{}) *godo.AppSpec { if len(config) == 0 || config[0] == nil { return &godo.AppSpec{} @@ -892,6 +1011,7 @@ func expandAppSpec(config []interface{}) *godo.AppSpec { Databases: expandAppSpecDatabases(appSpecConfig["database"].([]interface{})), Envs: expandAppEnvs(appSpecConfig["env"].(*schema.Set).List()), Alerts: expandAppAlerts(appSpecConfig["alert"].(*schema.Set).List()), + Ingress: expandAppIngress(appSpecConfig["ingress"].([]interface{})), } // Prefer the `domain` block over `domains` if it is set. @@ -953,6 +1073,10 @@ func flattenAppSpec(d *schema.ResourceData, spec *godo.AppSpec) []map[string]int r["alert"] = flattenAppAlerts((*spec).Alerts) } + if (*spec).Ingress != nil { + r["ingress"] = flattenAppIngress((*spec).Ingress) + } + result = append(result, r) } @@ -1874,7 +1998,7 @@ func flattenAppSpecDatabases(databases []*godo.AppDatabaseSpec) []map[string]int func expandAppCORSPolicy(config []interface{}) *godo.AppCORSPolicy { if len(config) == 0 || config[0] == nil { - return &godo.AppCORSPolicy{} + return nil } appCORSConfig := config[0].(map[string]interface{}) @@ -1917,8 +2041,8 @@ func expandAppCORSPolicy(config []interface{}) *godo.AppCORSPolicy { } } -func flattenAppCORSPolicy(policy *godo.AppCORSPolicy) []interface{} { - result := make([]interface{}, 0) +func flattenAppCORSPolicy(policy *godo.AppCORSPolicy) []map[string]interface{} { + result := make([]map[string]interface{}, 0) if policy != nil { r := make(map[string]interface{}) @@ -1951,3 +2075,161 @@ func flattenAppCORSPolicy(policy *godo.AppCORSPolicy) []interface{} { return result } + +func expandAppIngress(config []interface{}) *godo.AppIngressSpec { + if len(config) == 0 || config[0] == nil { + return nil + } + + ingress := &godo.AppIngressSpec{} + ingressConfig := config[0].(map[string]interface{}) + rules := ingressConfig["rule"].([]interface{}) + + for _, r := range rules { + rule := r.(map[string]interface{}) + result := &godo.AppIngressSpecRule{ + Match: expandAppIngressMatch(rule["match"].([]interface{})), + Component: expandAppIngressComponent(rule["component"].([]interface{})), + Redirect: expandAppIngressRedirect(rule["redirect"].([]interface{})), + CORS: expandAppCORSPolicy(rule["cors"].([]interface{})), + } + + ingress.Rules = append(ingress.Rules, result) + } + + return ingress +} + +func expandAppIngressComponent(config []interface{}) *godo.AppIngressSpecRuleRoutingComponent { + if len(config) == 0 || config[0] == nil { + return nil + } + + component := config[0].(map[string]interface{}) + + return &godo.AppIngressSpecRuleRoutingComponent{ + Name: component["name"].(string), + PreservePathPrefix: component["preserve_path_prefix"].(bool), + Rewrite: component["rewrite"].(string), + } +} + +func expandAppIngressRedirect(config []interface{}) *godo.AppIngressSpecRuleRoutingRedirect { + if len(config) == 0 || config[0] == nil { + return nil + } + + redirect := config[0].(map[string]interface{}) + + return &godo.AppIngressSpecRuleRoutingRedirect{ + Uri: redirect["uri"].(string), + Authority: redirect["authority"].(string), + Port: int64(redirect["port"].(int)), + Scheme: redirect["scheme"].(string), + RedirectCode: int64(redirect["redirect_code"].(int)), + } +} + +func expandAppIngressMatch(config []interface{}) *godo.AppIngressSpecRuleMatch { + if len(config) == 0 || config[0] == nil { + return nil + } + + match := config[0].(map[string]interface{}) + path := match["path"].([]interface{})[0].(map[string]interface{}) + + return &godo.AppIngressSpecRuleMatch{ + Path: &godo.AppIngressSpecRuleStringMatch{ + Prefix: path["prefix"].(string), + }, + } +} + +func flattenAppIngress(ingress *godo.AppIngressSpec) []map[string]interface{} { + if ingress != nil { + rules := make([]map[string]interface{}, 0) + + for _, r := range ingress.Rules { + rules = append(rules, flattenAppIngressRule(r)) + } + + return []map[string]interface{}{ + { + "rule": rules, + }, + } + } + + return nil +} + +func flattenAppIngressRule(rule *godo.AppIngressSpecRule) map[string]interface{} { + result := make(map[string]interface{}, 0) + + if rule != nil { + r := make(map[string]interface{}) + + r["component"] = flattenAppIngressRuleComponent(rule.Component) + r["match"] = flattenAppIngressRuleMatch(rule.Match) + r["cors"] = flattenAppCORSPolicy(rule.CORS) + r["redirect"] = flattenAppIngressRuleRedirect(rule.Redirect) + + result = r + } + + return result +} + +func flattenAppIngressRuleComponent(component *godo.AppIngressSpecRuleRoutingComponent) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + + if component != nil { + r := make(map[string]interface{}) + + r["name"] = component.Name + r["preserve_path_prefix"] = component.PreservePathPrefix + r["rewrite"] = component.Rewrite + + result = append(result, r) + } + + return result +} + +func flattenAppIngressRuleMatch(match *godo.AppIngressSpecRuleMatch) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + + if match != nil { + r := make(map[string]interface{}) + + pathResult := make([]map[string]interface{}, 0) + path := make(map[string]interface{}) + if match.Path != nil { + path["prefix"] = match.Path.Prefix + } + pathResult = append(pathResult, path) + r["path"] = pathResult + + result = append(result, r) + } + + return result +} + +func flattenAppIngressRuleRedirect(redirect *godo.AppIngressSpecRuleRoutingRedirect) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + + if redirect != nil { + r := make(map[string]interface{}) + + r["uri"] = redirect.Uri + r["authority"] = redirect.Authority + r["port"] = redirect.Port + r["scheme"] = redirect.Scheme + r["redirect_code"] = redirect.RedirectCode + + result = append(result, r) + } + + return result +} diff --git a/digitalocean/app/datasource_app_test.go b/digitalocean/app/datasource_app_test.go index 297ac0013..27e71e014 100644 --- a/digitalocean/app/datasource_app_test.go +++ b/digitalocean/app/datasource_app_test.go @@ -52,7 +52,9 @@ func TestAccDataSourceDigitalOceanApp_Basic(t *testing.T) { resource.TestCheckResourceAttr( "data.digitalocean_app.foobar", "spec.0.service.0.instance_size_slug", "basic-xxs"), resource.TestCheckResourceAttr( - "data.digitalocean_app.foobar", "spec.0.service.0.routes.0.path", "/"), + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.0.match.0.path.0.prefix", "/"), + resource.TestCheckResourceAttr( + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.name", "go-service"), resource.TestCheckResourceAttr( "data.digitalocean_app.foobar", "spec.0.service.0.git.0.repo_clone_url", "https://github.com/digitalocean/sample-golang.git"), @@ -81,11 +83,19 @@ func TestAccDataSourceDigitalOceanApp_Basic(t *testing.T) { resource.TestCheckResourceAttr( "data.digitalocean_app.foobar", "spec.0.service.0.name", "go-service"), resource.TestCheckResourceAttr( - "data.digitalocean_app.foobar", "spec.0.service.0.routes.0.path", "/go"), + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.0.match.0.path.0.prefix", "/go"), + resource.TestCheckResourceAttr( + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.preserve_path_prefix", "false"), + resource.TestCheckResourceAttr( + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.name", "go-service"), resource.TestCheckResourceAttr( "data.digitalocean_app.foobar", "spec.0.service.1.name", "python-service"), resource.TestCheckResourceAttr( - "data.digitalocean_app.foobar", "spec.0.service.1.routes.0.path", "/python"), + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.1.match.0.path.0.prefix", "/python"), + resource.TestCheckResourceAttr( + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.1.component.0.preserve_path_prefix", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_app.foobar", "spec.0.ingress.0.rule.1.component.0.name", "python-service"), ), }, }, diff --git a/digitalocean/app/resource_app_test.go b/digitalocean/app/resource_app_test.go index c448cc50f..a901782e2 100644 --- a/digitalocean/app/resource_app_test.go +++ b/digitalocean/app/resource_app_test.go @@ -26,10 +26,6 @@ func TestAccDigitalOceanApp_Image(t *testing.T) { Config: fmt.Sprintf(testAccCheckDigitalOceanAppConfig_addImage, appName), Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), - resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.routes.0.path", "/"), - resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.routes.0.preserve_path_prefix", "false"), resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.service.0.image.0.registry_type", "DOCKER_HUB"), resource.TestCheckResourceAttr( @@ -38,6 +34,12 @@ func TestAccDigitalOceanApp_Image(t *testing.T) { "digitalocean_app.foobar", "spec.0.service.0.image.0.repository", "caddy"), resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.service.0.image.0.tag", "2.2.1-alpine"), + 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.name", "image-service"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.preserve_path_prefix", "false"), ), }, }, @@ -72,9 +74,11 @@ func TestAccDigitalOceanApp_Basic(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.service.0.instance_size_slug", "basic-xxs"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.routes.0.path", "/"), + "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.service.0.routes.0.preserve_path_prefix", "false"), + "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"), @@ -105,15 +109,19 @@ func TestAccDigitalOceanApp_Basic(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.service.0.name", "go-service"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.routes.0.path", "/go"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.match.0.path.0.prefix", "/go"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.routes.0.preserve_path_prefix", "false"), + "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.1.name", "python-service"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.1.routes.0.path", "/python"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.1.match.0.path.0.prefix", "/python"), + resource.TestCheckResourceAttr( + "digitalocean_app.foobar", "spec.0.ingress.0.rule.1.component.0.preserve_path_prefix", "true"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.1.routes.0.preserve_path_prefix", "true"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.1.component.0.name", "python-service"), resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.service.0.alert.0.value", "85"), resource.TestCheckResourceAttr( @@ -133,9 +141,11 @@ func TestAccDigitalOceanApp_Basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.routes.0.path", "/"), + "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.service.0.routes.0.preserve_path_prefix", "false"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.name", "go-service"), resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.database.0.name", "test-db"), resource.TestCheckResourceAttr( @@ -214,9 +224,9 @@ func TestAccDigitalOceanApp_StaticSite(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.static_site.0.catchall_document", "404.html"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.static_site.0.routes.0.path", "/"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.match.0.path.0.prefix", "/"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.static_site.0.routes.0.preserve_path_prefix", "false"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.component.0.preserve_path_prefix", "false"), resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.static_site.0.build_command", "bundle exec jekyll build -d ./public"), resource.TestCheckResourceAttr( @@ -529,7 +539,7 @@ func TestAccDigitalOceanApp_Function(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.function.0.source_dir", "/"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.function.0.routes.0.path", "/api"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.match.0.path.0.prefix", "/api"), resource.TestCheckResourceAttr( "digitalocean_app.foobar", "spec.0.function.0.git.0.repo_clone_url", "https://github.com/digitalocean/sample-functions-nodejs-helloworld.git"), @@ -541,17 +551,17 @@ func TestAccDigitalOceanApp_Function(t *testing.T) { Config: updatedFnConfig, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.function.0.cors.0.allow_origins.0.prefix", "https://example.com"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_origins.0.prefix", "https://example.com"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.function.0.cors.0.allow_methods.*", "GET"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_methods.*", "GET"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.function.0.cors.0.allow_headers.*", "X-Custom-Header"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_headers.*", "X-Custom-Header"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.function.0.cors.0.expose_headers.*", "Content-Encoding"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.expose_headers.*", "Content-Encoding"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.function.0.cors.0.expose_headers.*", "ETag"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.expose_headers.*", "ETag"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.function.0.cors.0.max_age", "1h"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.max_age", "1h"), ), }, }, @@ -737,7 +747,7 @@ func TestAccDigitalOceanApp_CORS(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_origins.0.exact", "https://example.com"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_origins.0.exact", "https://example.com"), ), }, { @@ -745,7 +755,7 @@ func TestAccDigitalOceanApp_CORS(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_origins.0.prefix", "https://example.com"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_origins.0.prefix", "https://example.com"), ), }, { @@ -753,7 +763,7 @@ func TestAccDigitalOceanApp_CORS(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_origins.0.regex", "https://[0-9a-z]*.digitalocean.com"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_origins.0.regex", "https://[0-9a-z]*.digitalocean.com"), ), }, { @@ -761,23 +771,23 @@ func TestAccDigitalOceanApp_CORS(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanAppExists("digitalocean_app.foobar", &app), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_origins.0.prefix", "https://example.com"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_origins.0.prefix", "https://example.com"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_methods.*", "GET"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_methods.*", "GET"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_methods.*", "PUT"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_methods.*", "PUT"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_headers.*", "X-Custom-Header"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_headers.*", "X-Custom-Header"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_headers.*", "Upgrade-Insecure-Requests"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_headers.*", "Upgrade-Insecure-Requests"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.expose_headers.*", "Content-Encoding"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.expose_headers.*", "Content-Encoding"), resource.TestCheckTypeSetElemAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.expose_headers.*", "ETag"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.expose_headers.*", "ETag"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.max_age", "1h"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.max_age", "1h"), resource.TestCheckResourceAttr( - "digitalocean_app.foobar", "spec.0.service.0.cors.0.allow_credentials", "true"), + "digitalocean_app.foobar", "spec.0.ingress.0.rule.0.cors.0.allow_credentials", "true"), ), }, }, @@ -929,10 +939,6 @@ resource "digitalocean_app" "foobar" { branch = "main" } - routes { - path = "/go" - } - alert { value = 85 operator = "GREATER_THAN" @@ -958,10 +964,30 @@ resource "digitalocean_app" "foobar" { repo_clone_url = "https://github.com/digitalocean/sample-python.git" branch = "main" } + } - routes { - path = "/python" - preserve_path_prefix = true + ingress { + rule { + component { + name = "go-service" + } + match { + path { + prefix = "/go" + } + } + } + + rule { + component { + name = "python-service" + preserve_path_prefix = true + } + match { + path { + prefix = "/python" + } + } } } } @@ -1033,10 +1059,6 @@ resource "digitalocean_app" "foobar" { branch = "main" } - routes { - path = "/" - } - alert { value = 85 operator = "GREATER_THAN" @@ -1052,6 +1074,19 @@ resource "digitalocean_app" "foobar" { } } + ingress { + rule { + component { + name = "go-service" + } + match { + path { + prefix = "/" + } + } + } + } + database { name = "test-db" engine = "PG" @@ -1077,14 +1112,6 @@ resource "digitalocean_app" "foobar" { repo_clone_url = "https://github.com/digitalocean/sample-jekyll.git" branch = "main" } - - routes { - path = "/" - } - - routes { - path = "/foo" - } } } }` @@ -1102,12 +1129,22 @@ resource "digitalocean_app" "foobar" { repo_clone_url = "https://github.com/digitalocean/sample-functions-nodejs-helloworld.git" branch = "master" } - routes { - path = "/api" - } + } -%s + ingress { + rule { + component { + name = "example" + } + match { + path { + prefix = "/api" + } + } + + %s + } } } }` @@ -1269,12 +1306,26 @@ resource "digitalocean_app" "foobar" { instance_count = 1 instance_size_slug = "basic-xxs" - %s - git { repo_clone_url = "https://github.com/digitalocean/sample-golang.git" branch = "main" } } + + ingress { + rule { + component { + name = "go-service" + } + + match { + path { + prefix = "/" + } + } + + %s + } + } } }` diff --git a/docs/resources/app.md b/docs/resources/app.md index e72bb305e..8a2a56b70 100644 --- a/docs/resources/app.md +++ b/docs/resources/app.md @@ -87,10 +87,6 @@ resource "digitalocean_app" "mono-repo-example" { source_dir = "api/" http_port = 3000 - routes { - path = "/api" - } - alert { value = 75 operator = "GREATER_THAN" @@ -119,10 +115,6 @@ resource "digitalocean_app" "mono-repo-example" { deploy_on_push = true repo = "username/repo" } - - routes { - path = "/" - } } database { @@ -130,6 +122,31 @@ resource "digitalocean_app" "mono-repo-example" { engine = "PG" production = false } + + ingress { + rule { + component { + name = "api" + } + match { + path { + prefix = "/api" + } + } + } + + rule { + component { + name = "web" + } + + match { + path { + prefix = "/" + } + } + } + } } } ``` @@ -157,6 +174,31 @@ The following arguments are supported: - `alert` - Describes an alert policy for the app. * `rule` - The type of the alert to configure. Top-level app alert policies can be: `DEPLOYMENT_FAILED`, `DEPLOYMENT_LIVE`, `DOMAIN_FAILED`, or `DOMAIN_LIVE`. * `disabled` - Determines whether or not the alert is disabled (default: `false`). + - `ingress` - Specification for component routing, rewrites, and redirects. + * `rule` - Rules for configuring HTTP ingress for component routes, CORS, rewrites, and redirects. + - `component` - The component to route to. Only one of `component` or `redirect` may be set. + * `name` - The name of the component to route to. + * `preserve_path_prefix` - An optional boolean flag to preserve the path that is forwarded to the backend service. By default, the HTTP request path will be trimmed from the left when forwarded to the component. + * `rewrite` - An optional field that will rewrite the path of the component to be what is specified here. This is mutually exclusive with `preserve_path_prefix`. + - `match` - The match configuration for the rule + * `path` - The path to match on. + - `prefix` - Prefix-based match. + - `redirect` - The redirect configuration for the rule. Only one of `component` or `redirect` may be set. + * `uri` - An optional URI path to redirect to. + * `authority` - The authority/host to redirect to. This can be a hostname or IP address. + * `port` - The port to redirect to. + * `scheme` - The scheme to redirect to. Supported values are `http` or `https` + * `redirect_code` - The redirect code to use. Supported values are `300`, `301`, `302`, `303`, `304`, `307`, `308`. + - `cors` - The [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policies of the app. + * `allow_origins` - The `Access-Control-Allow-Origin` can be + - `exact` - The `Access-Control-Allow-Origin` header will be set to the client's origin only if the client's origin exactly matches the value you provide. + - `prefix` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the beginning of the client's origin matches the value you provide. + - `regex` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the client’s origin matches the regex you provide, in [RE2 style syntax](https://github.com/google/re2/wiki/Syntax). + * `allow_headers` - The set of allowed HTTP request headers. This configures the `Access-Control-Allow-Headers` header. + * `max_age` - An optional duration specifying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`. + * `expose_headers` - The set of HTTP response headers that browsers are allowed to access. This configures the `Access-Control-Expose-Headers` header. + * `allow_methods` - The set of allowed HTTP methods. This configures the `Access-Control-Allow-Methods` header. + * `allow_credentials` - Whether browsers should expose the response to the client-side JavaScript code when the request's credentials mode is `include`. This configures the `Access-Control-Allow-Credentials` header. A spec can contain multiple components. @@ -195,7 +237,7 @@ A `service` can contain: - `value` - The value of the environment variable. - `scope` - The visibility scope of the environment variable. One of `RUN_TIME`, `BUILD_TIME`, or `RUN_AND_BUILD_TIME` (default). - `type` - The type of the environment variable, `GENERAL` or `SECRET`. -* `routes` - An HTTP paths that should be routed to this component. +* `routes` - (Deprecated - use `ingress`) An HTTP paths that should be routed to this component. - `path` - Paths must start with `/` and must be unique within the app. - `preserve_path_prefix` - An optional flag to preserve the path that is forwarded to the backend service. * `health_check` - A health check to determine the availability of this component. @@ -205,16 +247,7 @@ 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. -* `cors` - The [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policies of the app. - - `allow_origins` - The `Access-Control-Allow-Origin` can be - - `exact` - The `Access-Control-Allow-Origin` header will be set to the client's origin only if the client's origin exactly matches the value you provide. - - `prefix` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the beginning of the client's origin matches the value you provide. - - `regex` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the client’s origin matches the regex you provide, in [RE2 style syntax](https://github.com/google/re2/wiki/Syntax). - - `allow_headers` - The set of allowed HTTP request headers. This configures the `Access-Control-Allow-Headers` header. - - `max_age` - An optional duration specifying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`. - - `expose_headers` - The set of HTTP response headers that browsers are allowed to access. This configures the `Access-Control-Expose-Headers` header. - - `allow_methods` - The set of allowed HTTP methods. This configures the `Access-Control-Allow-Methods` header. - - `allow_credentials` - Whether browsers should expose the response to the client-side JavaScript code when the request's credentials mode is `include`. This configures the `Access-Control-Allow-Credentials` header. +* `cors` - (Deprecated - use `ingress`) The [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policies of the app. * `alert` - Describes an alert policy for the component. - `rule` - The type of the alert to configure. Component app alert policies can be: `CPU_UTILIZATION`, `MEM_UTILIZATION`, or `RESTART_COUNT`. - `value` - The threshold for the type of the warning. @@ -258,19 +291,10 @@ A `static_site` can contain: - `value` - The value of the environment variable. - `scope` - The visibility scope of the environment variable. One of `RUN_TIME`, `BUILD_TIME`, or `RUN_AND_BUILD_TIME` (default). - `type` - The type of the environment variable, `GENERAL` or `SECRET`. -* `routes` - An HTTP paths that should be routed to this component. +* `routes` - (Deprecated - use `ingress`) An HTTP paths that should be routed to this component. - `path` - Paths must start with `/` and must be unique within the app. - `preserve_path_prefix` - An optional flag to preserve the path that is forwarded to the backend service. -* `cors` - The [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policies of the app. - - `allow_origins` - The `Access-Control-Allow-Origin` can be - - `exact` - The `Access-Control-Allow-Origin` header will be set to the client's origin only if the client's origin exactly matches the value you provide. - - `prefix` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the beginning of the client's origin matches the value you provide. - - `regex` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the client’s origin matches the regex you provide, in [RE2 style syntax](https://github.com/google/re2/wiki/Syntax). - - `allow_headers` - The set of allowed HTTP request headers. This configures the `Access-Control-Allow-Headers` header. - - `max_age` - An optional duration specifying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`. - - `expose_headers` - The set of HTTP response headers that browsers are allowed to access. This configures the `Access-Control-Expose-Headers` header. - - `allow_methods` - The set of allowed HTTP methods. This configures the `Access-Control-Allow-Methods` header. - - `allow_credentials` - Whether browsers should expose the response to the client-side JavaScript code when the request's credentials mode is `include`. This configures the `Access-Control-Allow-Credentials` header. +* `cors` - (Deprecated - use `ingress`) The [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policies of the app. A `worker` can contain: @@ -396,19 +420,10 @@ A `function` component can contain: - `value` - The value of the environment variable. - `scope` - The visibility scope of the environment variable. One of `RUN_TIME`, `BUILD_TIME`, or `RUN_AND_BUILD_TIME` (default). - `type` - The type of the environment variable, `GENERAL` or `SECRET`. -* `routes` - An HTTP paths that should be routed to this component. +* `routes` - (Deprecated - use `ingress`) An HTTP paths that should be routed to this component. - `path` - Paths must start with `/` and must be unique within the app. - `preserve_path_prefix` - An optional flag to preserve the path that is forwarded to the backend service. -* `cors` - The [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policies of the app. - - `allow_origins` - The `Access-Control-Allow-Origin` can be - - `exact` - The `Access-Control-Allow-Origin` header will be set to the client's origin only if the client's origin exactly matches the value you provide. - - `prefix` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the beginning of the client's origin matches the value you provide. - - `regex` - The `Access-Control-Allow-Origin` header will be set to the client's origin if the client’s origin matches the regex you provide, in [RE2 style syntax](https://github.com/google/re2/wiki/Syntax). - - `allow_headers` - The set of allowed HTTP request headers. This configures the `Access-Control-Allow-Headers` header. - - `max_age` - An optional duration specifying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`. - - `expose_headers` - The set of HTTP response headers that browsers are allowed to access. This configures the `Access-Control-Expose-Headers` header. - - `allow_methods` - The set of allowed HTTP methods. This configures the `Access-Control-Allow-Methods` header. - - `allow_credentials` - Whether browsers should expose the response to the client-side JavaScript code when the request's credentials mode is `include`. This configures the `Access-Control-Allow-Credentials` header. +* `cors` - (Deprecated - use `ingress`) The [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policies of the app. * `alert` - Describes an alert policy for the component. - `rule` - The type of the alert to configure. Component app alert policies can be: `CPU_UTILIZATION`, `MEM_UTILIZATION`, or `RESTART_COUNT`. - `value` - The threshold for the type of the warning.