diff --git a/.changelog/1794.txt b/.changelog/1794.txt new file mode 100644 index 0000000000..13fb7aba8c --- /dev/null +++ b/.changelog/1794.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/cloudflare_ruleset: add support and configuration for `serve_errors` action +``` diff --git a/docs/resources/ruleset.md b/docs/resources/ruleset.md index 06fe8950dc..2402052ee7 100644 --- a/docs/resources/ruleset.md +++ b/docs/resources/ruleset.md @@ -378,6 +378,26 @@ resource "cloudflare_ruleset" "redirect_from_value_example" { enabled = true } } + +# Serve some custom error response +resource "cloudflare_ruleset" "http_custom_error_example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + name = "Serve some error response" + description = "Serve some error response" + kind = "zone" + phase = "http_custom_errors" + rules { + action = "serve_error" + action_parameters { + content = "some error html" + content_type = "text/html" + status_code = "530" + } + expression = "(http.request.uri.path matches \"^/api/\")" + description = "serve some error response" + enabled = true + } +} ``` @@ -432,6 +452,8 @@ Optional: - `browser_ttl` (Block List, Max: 1) List of browser TTL parameters to apply to the request. (see [below for nested schema](#nestedblock--rules--action_parameters--browser_ttl)) - `cache` (Boolean) Whether to cache if expression matches. - `cache_key` (Block List, Max: 1) List of cache key parameters to apply to the request. (see [below for nested schema](#nestedblock--rules--action_parameters--cache_key)) +- `content` (String) Content of the custom error response. +- `content_type` (String) Content-Type of the custom error response. - `cookie_fields` (Set of String) List of cookie values to include as part of custom fields logging. - `edge_ttl` (Block List, Max: 1) List of edge TTL parameters to apply to the request. (see [below for nested schema](#nestedblock--rules--action_parameters--edge_ttl)) - `from_list` (Block List, Max: 1) Use a list to lookup information for the action. (see [below for nested schema](#nestedblock--rules--action_parameters--from_list)) @@ -455,6 +477,7 @@ Optional: - `rulesets` (Set of String) List of managed WAF rule IDs to target. Only valid when the `"action"` is set to skip. - `serve_stale` (Block List, Max: 1) List of serve stale parameters to apply to the request. (see [below for nested schema](#nestedblock--rules--action_parameters--serve_stale)) - `sni` (Block List, Max: 1) List of properties to manange Server Name Indication. (see [below for nested schema](#nestedblock--rules--action_parameters--sni)) +- `status_code` (Number) HTTP status code of the custom error response. - `uri` (Block List, Max: 1) List of URI properties to configure for the ruleset rule when performing URL rewrite transformations. (see [below for nested schema](#nestedblock--rules--action_parameters--uri)) - `version` (String) Version of the ruleset to deploy. diff --git a/examples/resources/cloudflare_ruleset/resource.tf b/examples/resources/cloudflare_ruleset/resource.tf index 3de1d4c92a..f3da842b17 100644 --- a/examples/resources/cloudflare_ruleset/resource.tf +++ b/examples/resources/cloudflare_ruleset/resource.tf @@ -343,3 +343,23 @@ resource "cloudflare_ruleset" "redirect_from_value_example" { enabled = true } } + +# Serve some custom error response +resource "cloudflare_ruleset" "http_custom_error_example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + name = "Serve some error response" + description = "Serve some error response" + kind = "zone" + phase = "http_custom_errors" + rules { + action = "serve_error" + action_parameters { + content = "some error html" + content_type = "text/html" + status_code = "530" + } + expression = "(http.request.uri.path matches \"^/api/\")" + description = "serve some error response" + enabled = true + } +} diff --git a/internal/provider/resource_cloudflare_ruleset.go b/internal/provider/resource_cloudflare_ruleset.go index 759a69147b..dc36b6214d 100644 --- a/internal/provider/resource_cloudflare_ruleset.go +++ b/internal/provider/resource_cloudflare_ruleset.go @@ -545,6 +545,9 @@ func buildStateFromRulesetRules(rules []cloudflare.RulesetRule) interface{} { "origin_error_page_passthru": r.ActionParameters.OriginErrorPagePassthru, "from_list": fromListFields, "from_value": fromValueFields, + "content": r.ActionParameters.Content, + "content_type": r.ActionParameters.ContentType, + "status_code": r.ActionParameters.StatusCode, }) rule["action_parameters"] = actionParameters @@ -782,6 +785,15 @@ func buildRulesetRulesFromResource(d *schema.ResourceData) ([]cloudflare.Ruleset rule.ActionParameters.Headers = headers + case "content": + rule.ActionParameters.Content = pValue.(string) + + case "content_type": + rule.ActionParameters.ContentType = pValue.(string) + + case "status_code": + rule.ActionParameters.StatusCode = uint16(pValue.(int)) + case "host_header": rule.ActionParameters.HostHeader = pValue.(string) diff --git a/internal/provider/resource_cloudflare_ruleset_test.go b/internal/provider/resource_cloudflare_ruleset_test.go index 4a609f3cf0..4130821d64 100644 --- a/internal/provider/resource_cloudflare_ruleset_test.go +++ b/internal/provider/resource_cloudflare_ruleset_test.go @@ -776,6 +776,48 @@ func TestAccCloudflareRuleset_RateLimit(t *testing.T) { }) } +func TestAccCloudflareRuleset_CustomErrors(t *testing.T) { + // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF + // service does not yet support the API tokens and it results in + // misleading state error messages. + if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { + defer func(apiToken string) { + os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) + }(os.Getenv("CLOUDFLARE_API_TOKEN")) + os.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + t.Parallel() + rnd := generateRandomResourceName() + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + zoneName := os.Getenv("CLOUDFLARE_DOMAIN") + resourceName := "cloudflare_ruleset." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareRulesetCustomErrors(rnd, "example HTTP custom error response", zoneID, zoneName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", "example HTTP custom error response"), + resource.TestCheckResourceAttr(resourceName, "description", rnd+" ruleset description"), + resource.TestCheckResourceAttr(resourceName, "kind", "zone"), + resource.TestCheckResourceAttr(resourceName, "phase", "http_custom_errors"), + + resource.TestCheckResourceAttr(resourceName, "rules.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action", "serve_error"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.content", "my example error page"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.content_type", "text/plain"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.status_code", "530"), + resource.TestCheckResourceAttr(resourceName, "rules.0.expression", "(http.request.uri.path matches \"^/api/\")"), + resource.TestCheckResourceAttr(resourceName, "rules.0.description", "example http custom error response"), + ), + }, + }, + }) +} + func TestAccCloudflareRuleset_RequestOrigin(t *testing.T) { // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the WAF // service does not yet support the API tokens and it results in @@ -2372,6 +2414,29 @@ func testAccCheckCloudflareRulesetManagedWAFPayloadLogging(rnd, name, zoneID, zo }`, rnd, name, zoneID, zoneName) } +func testAccCheckCloudflareRulesetCustomErrors(rnd, name, zoneID, zoneName string) string { + return fmt.Sprintf(` + resource "cloudflare_ruleset" "%[1]s" { + zone_id = "%[3]s" + name = "%[2]s" + description = "%[1]s ruleset description" + kind = "zone" + phase = "http_custom_errors" + + rules { + action = "serve_error" + action_parameters { + content = "my example error page" + content_type = "text/plain" + status_code = "530" + } + expression = "(http.request.uri.path matches \"^/api/\")" + description = "example http custom error response" + enabled = true + } + }`, rnd, name, zoneID, zoneName) +} + func testAccCheckCloudflareRulesetOrigin(rnd, name, zoneID, zoneName string) string { return fmt.Sprintf(` resource "cloudflare_ruleset" "%[1]s" { diff --git a/internal/provider/schema_cloudflare_ruleset.go b/internal/provider/schema_cloudflare_ruleset.go index c50f9415d9..1ba952e608 100644 --- a/internal/provider/schema_cloudflare_ruleset.go +++ b/internal/provider/schema_cloudflare_ruleset.go @@ -378,6 +378,21 @@ func resourceCloudflareRulesetSchema() map[string]*schema.Schema { }, }, }, + "content": { + Type: schema.TypeString, + Optional: true, + Description: "Content of the custom error response", + }, + "content_type": { + Type: schema.TypeString, + Optional: true, + Description: "Content-Type of the custom error response", + }, + "status_code": { + Type: schema.TypeInt, + Optional: true, + Description: "HTTP status code of the custom error response", + }, "host_header": { Type: schema.TypeString, Optional: true,