diff --git a/.changelog/1477.txt b/.changelog/1477.txt new file mode 100644 index 00000000000..0e9eb6fe5b8 --- /dev/null +++ b/.changelog/1477.txt @@ -0,0 +1,3 @@ +```release-note:breaking-change +resource/cloudflare_ruleset: rename `mitigation_expression` to `counting_expression` +``` diff --git a/cloudflare/resource_cloudflare_ruleset.go b/cloudflare/resource_cloudflare_ruleset.go index df84f29d8a1..b06368204a8 100644 --- a/cloudflare/resource_cloudflare_ruleset.go +++ b/cloudflare/resource_cloudflare_ruleset.go @@ -30,6 +30,14 @@ func resourceCloudflareRuleset() *schema.Resource { Importer: &schema.ResourceImporter{ State: resourceCloudflareRulesetImport, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceCloudflareRulesetSchemaV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceCloudflareRulesetStateUpgradeV0ToV1, + Version: 0, + }, + }, } } @@ -340,6 +348,7 @@ func buildStateFromRulesetRules(rules []cloudflare.RulesetRule) interface{} { "period": r.RateLimit.Period, "requests_per_period": r.RateLimit.RequestsPerPeriod, "mitigation_timeout": r.RateLimit.MitigationTimeout, + "counting_expression": r.RateLimit.CountingExpression, }) rule["ratelimit"] = rateLimit @@ -559,8 +568,8 @@ func buildRulesetRulesFromResource(d *schema.ResourceData) ([]cloudflare.Ruleset rule.RateLimit.RequestsPerPeriod = pValue.(int) case "mitigation_timeout": rule.RateLimit.MitigationTimeout = pValue.(int) - case "mitigation_expression": - rule.RateLimit.MitigationExpression = pValue.(string) + case "counting_expression": + rule.RateLimit.CountingExpression = pValue.(string) default: log.Printf("[DEBUG] unknown key encountered in buildRulesetRulesFromResource for ratelimit: %s", pKey) diff --git a/cloudflare/resource_cloudflare_ruleset_migrate.go b/cloudflare/resource_cloudflare_ruleset_migrate.go new file mode 100644 index 00000000000..832a7ebbb2c --- /dev/null +++ b/cloudflare/resource_cloudflare_ruleset_migrate.go @@ -0,0 +1,348 @@ +package cloudflare + +import ( + "context" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceCloudflareRulesetSchemaV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"zone_id"}, + }, + "zone_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"account_id"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "kind": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(cloudflare.RulesetKindValues(), false), + }, + "phase": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(cloudflare.RulesetPhaseValues(), false), + }, + "shareable_entitlement_name": { + Type: schema.TypeString, + Optional: true, + }, + "rules": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeString, + Computed: true, + }, + "ref": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(cloudflare.RulesetRuleActionValues(), false), + }, + "expression": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Required: true, + }, + "action_parameters": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "products": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "phases": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "uri": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "path": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": { + Type: schema.TypeString, + Optional: true, + }, + "expression": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "query": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": { + Type: schema.TypeString, + Optional: true, + }, + "expression": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "origin": { + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + "headers": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + }, + "expression": { + Type: schema.TypeString, + Optional: true, + }, + "operation": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "increment": { + Type: schema.TypeInt, + Optional: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "ruleset": { + Type: schema.TypeString, + Optional: true, + }, + "rulesets": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "rules": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "overrides": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(cloudflare.RulesetRuleActionValues(), false), + }, + "categories": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "category": { + Type: schema.TypeString, + Optional: true, + }, + "action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(cloudflare.RulesetRuleActionValues(), false), + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + "rules": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(cloudflare.RulesetRuleActionValues(), false), + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "score_threshold": { + Type: schema.TypeInt, + Optional: true, + }, + "sensitivity_level": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "matched_data": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "public_key": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "ratelimit": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "characteristics": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "period": { + Type: schema.TypeInt, + Optional: true, + }, + "requests_per_period": { + Type: schema.TypeInt, + Optional: true, + }, + "mitigation_timeout": { + Type: schema.TypeInt, + Optional: true, + }, + "mitigation_expression": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "exposed_credential_check": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "username_expression": { + Type: schema.TypeString, + Optional: true, + }, + "password_expression": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func resourceCloudflareRulesetStateUpgradeV0ToV1(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + rawState["ratelimit"].([]map[string]interface{})[0]["counting_expression"] = rawState["ratelimit"].([]map[string]interface{})[0]["mitigation_expression"] + delete(rawState["ratelimit"].([]map[string]interface{})[0], "mitigation_expression") + + return rawState, nil +} diff --git a/cloudflare/resource_cloudflare_ruleset_migrate_test.go b/cloudflare/resource_cloudflare_ruleset_migrate_test.go new file mode 100644 index 00000000000..8719510a359 --- /dev/null +++ b/cloudflare/resource_cloudflare_ruleset_migrate_test.go @@ -0,0 +1,51 @@ +package cloudflare + +import ( + "context" + "reflect" + "testing" +) + +func testCloudflareRulesetStateDataV0() map[string]interface{} { + return map[string]interface{}{ + "action": "block", + "description": "example http rate limit", + "enabled": true, + "expression": "(http.request.uri.path matches \"^/api/\")", "id": "5a297db75a614c318edb406fe5eef502", + "ratelimit": []map[string]interface{}{{ + "characteristics": []string{"cf.colo.id", "ip.src"}, + "mitigation_timeout": 600, + "period": 60, + "requests_per_period": 100, + "mitigation_expression": "", + }}, + } +} + +func testCloudflareRulesetStateDataV1() map[string]interface{} { + return map[string]interface{}{ + "action": "block", + "description": "example http rate limit", + "enabled": true, + "expression": "(http.request.uri.path matches \"^/api/\")", "id": "5a297db75a614c318edb406fe5eef502", + "ratelimit": []map[string]interface{}{{ + "characteristics": []string{"cf.colo.id", "ip.src"}, + "mitigation_timeout": 600, + "period": 60, + "requests_per_period": 100, + "counting_expression": "", + }}, + } +} + +func TestCloudflareRulesetStateUpgradeV0(t *testing.T) { + expected := testCloudflareRulesetStateDataV1() + actual, err := resourceCloudflareRulesetStateUpgradeV0ToV1(context.Background(), testCloudflareRulesetStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating ruleset state V0 to V1: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } +} diff --git a/cloudflare/schema_cloudflare_ruleset.go b/cloudflare/schema_cloudflare_ruleset.go index 4133fe05734..07cc7678ac1 100644 --- a/cloudflare/schema_cloudflare_ruleset.go +++ b/cloudflare/schema_cloudflare_ruleset.go @@ -306,7 +306,7 @@ func resourceCloudflareRulesetSchema() map[string]*schema.Schema { Type: schema.TypeInt, Optional: true, }, - "mitigation_expression": { + "counting_expression": { Type: schema.TypeString, Optional: true, },