diff --git a/.changelog/2220.txt b/.changelog/2220.txt new file mode 100644 index 0000000000..50a4594bdd --- /dev/null +++ b/.changelog/2220.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +cloudflare_rulesets +``` diff --git a/docs/data-sources/record.md b/docs/data-sources/record.md index 0ab72eaf3c..140719b49e 100644 --- a/docs/data-sources/record.md +++ b/docs/data-sources/record.md @@ -13,7 +13,7 @@ Use this data source to lookup a single [DNS Record](https://api.cloudflare.com/ ```terraform data "cloudflare_record" "example" { - zone_id = var.zone_id + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" hostname = "example.com" } ``` diff --git a/docs/data-sources/rulesets.md b/docs/data-sources/rulesets.md new file mode 100644 index 0000000000..f4fa19ec32 --- /dev/null +++ b/docs/data-sources/rulesets.md @@ -0,0 +1,432 @@ +--- +page_title: "cloudflare_rulesets Data Source - Cloudflare" +subcategory: "" +description: |- + Use this datasource to lookup Rulesets in an account or zone. +--- + +# cloudflare_rulesets (Data Source) + +Use this datasource to lookup Rulesets in an account or zone. + +## Example Usage + +```terraform +data "cloudflare_rulesets" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + + filter { + name = ".*OWASP.*" + } +} +``` + + +## Schema + +### Optional + +- `account_id` (String) The account identifier to target for the resource. Must provide only one of `zone_id`, `account_id`. +- `filter` (Block List, Max: 1) (see [below for nested schema](#nestedblock--filter)) +- `include_rules` (Boolean) Include rule data in response. +- `zone_id` (String) The zone identifier to target for the resource. Must provide only one of `zone_id`, `account_id`. + +### Read-Only + +- `id` (String) The ID of this resource. +- `rulesets` (List of Object) (see [below for nested schema](#nestedatt--rulesets)) + + +### Nested Schema for `filter` + +Optional: + +- `id` (String) The ID of the Ruleset to target. +- `kind` (String) Type of Ruleset to create. Available values: `custom`, `managed`, `root`, `schema`, `zone`. +- `name` (String) Name of the ruleset. +- `phase` (String) Point in the request/response lifecycle where the ruleset will be created. Available values: `ddos_l4`, `ddos_l7`, `http_custom_errors`, `http_log_custom_fields`, `http_request_cache_settings`, `http_request_firewall_custom`, `http_request_firewall_managed`, `http_request_late_transform`, `http_request_late_transform_managed`, `http_request_main`, `http_request_origin`, `http_request_dynamic_redirect`, `http_request_redirect`, `http_request_sanitize`, `http_request_transform`, `http_response_firewall_managed`, `http_response_headers_transform`, `http_response_headers_transform_managed`, `magic_transit`, `http_ratelimit`, `http_request_sbfm`, `http_config_settings`. +- `version` (String) Version of the ruleset to filter on. + + + +### Nested Schema for `rulesets` + +Read-Only: + +- `description` (String) +- `id` (String) +- `kind` (String) +- `name` (String) +- `phase` (String) +- `rules` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules)) +- `version` (String) + + +### Nested Schema for `rulesets.rules` + +Read-Only: + +- `action` (String) +- `action_parameters` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters)) +- `description` (String) +- `enabled` (Boolean) +- `exposed_credential_check` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--exposed_credential_check)) +- `expression` (String) +- `id` (String) +- `last_updated` (String) +- `logging` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--logging)) +- `ratelimit` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--ratelimit)) +- `ref` (String) +- `version` (String) + + +### Nested Schema for `rulesets.rules.action_parameters` + +Read-Only: + +- `automatic_https_rewrites` (Boolean) +- `autominify` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--autominify)) +- `bic` (Boolean) +- `browser_ttl` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--browser_ttl)) +- `cache` (Boolean) +- `cache_key` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--cache_key)) +- `content` (String) +- `content_type` (String) +- `cookie_fields` (Set of String) +- `disable_apps` (Boolean) +- `disable_railgun` (Boolean) +- `disable_zaraz` (Boolean) +- `edge_ttl` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--edge_ttl)) +- `email_obfuscation` (Boolean) +- `from_list` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--from_list)) +- `from_value` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--from_value)) +- `headers` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--headers)) +- `host_header` (String) +- `hotlink_protection` (Boolean) +- `id` (String) +- `increment` (Number) +- `matched_data` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--matched_data)) +- `mirage` (Boolean) +- `opportunistic_encryption` (Boolean) +- `origin` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--origin)) +- `origin_error_page_passthru` (Boolean) +- `overrides` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--overrides)) +- `phases` (Set of String) +- `polish` (String) +- `products` (Set of String) +- `request_fields` (Set of String) +- `respect_strong_etags` (Boolean) +- `response` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--response)) +- `response_fields` (Set of String) +- `rocket_loader` (Boolean) +- `rules` (Map of String) +- `ruleset` (String) +- `rulesets` (Set of String) +- `security_level` (String) +- `serve_stale` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--serve_stale)) +- `server_side_excludes` (Boolean) +- `sni` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--sni)) +- `ssl` (String) +- `status_code` (Number) +- `sxg` (Boolean) +- `uri` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--uri)) +- `version` (String) + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `css` (Boolean) +- `html` (Boolean) +- `js` (Boolean) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `default` (Number) +- `mode` (String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `cache_by_device_type` (Boolean) +- `cache_deception_armor` (Boolean) +- `custom_key` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--custom_key)) +- `ignore_query_strings_order` (Boolean) + + +### Nested Schema for `rulesets.rules.action_parameters.version.custom_key` + +Read-Only: + +- `cookie` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--custom_key--cookie)) +- `header` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--custom_key--header)) +- `host` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--custom_key--host)) +- `query_string` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--custom_key--query_string)) +- `user` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--custom_key--user)) + + +### Nested Schema for `rulesets.rules.action_parameters.version.custom_key.user` + +Read-Only: + +- `check_presence` (List of String) +- `include` (List of String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version.custom_key.user` + +Read-Only: + +- `check_presence` (List of String) +- `exclude_origin` (Boolean) +- `include` (List of String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version.custom_key.user` + +Read-Only: + +- `resolved` (Boolean) + + + +### Nested Schema for `rulesets.rules.action_parameters.version.custom_key.user` + +Read-Only: + +- `exclude` (List of String) +- `include` (List of String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version.custom_key.user` + +Read-Only: + +- `device_type` (Boolean) +- `geo` (Boolean) +- `lang` (Boolean) + + + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `default` (Number) +- `mode` (String) +- `status_code_ttl` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--status_code_ttl)) + + +### Nested Schema for `rulesets.rules.action_parameters.version.status_code_ttl` + +Read-Only: + +- `status_code` (Number) +- `status_code_range` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--status_code_ttl--status_code_range)) +- `value` (Number) + + +### Nested Schema for `rulesets.rules.action_parameters.version.status_code_ttl.value` + +Read-Only: + +- `from` (Number) +- `to` (Number) + + + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `key` (String) +- `name` (String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `preserve_query_string` (Boolean) +- `status_code` (Number) +- `target_url` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--target_url)) + + +### Nested Schema for `rulesets.rules.action_parameters.version.target_url` + +Read-Only: + +- `expression` (String) +- `value` (String) + + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `expression` (String) +- `name` (String) +- `operation` (String) +- `value` (String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `public_key` (String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `host` (String) +- `port` (Number) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `action` (String) +- `categories` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--categories)) +- `enabled` (Boolean) +- `rules` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--rules)) +- `sensitivity_level` (String) +- `status` (String) + + +### Nested Schema for `rulesets.rules.action_parameters.version.categories` + +Read-Only: + +- `action` (String) +- `category` (String) +- `enabled` (Boolean) +- `status` (String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version.rules` + +Read-Only: + +- `action` (String) +- `enabled` (Boolean) +- `id` (String) +- `score_threshold` (Number) +- `sensitivity_level` (String) +- `status` (String) + + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `content` (String) +- `content_type` (String) +- `status_code` (Number) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `disable_stale_while_updating` (Boolean) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `value` (String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version` + +Read-Only: + +- `origin` (Boolean) +- `path` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--path)) +- `query` (List of Object) (see [below for nested schema](#nestedobjatt--rulesets--rules--action_parameters--version--query)) + + +### Nested Schema for `rulesets.rules.action_parameters.version.path` + +Read-Only: + +- `expression` (String) +- `value` (String) + + + +### Nested Schema for `rulesets.rules.action_parameters.version.query` + +Read-Only: + +- `expression` (String) +- `value` (String) + + + + + +### Nested Schema for `rulesets.rules.exposed_credential_check` + +Read-Only: + +- `password_expression` (String) +- `username_expression` (String) + + + +### Nested Schema for `rulesets.rules.logging` + +Read-Only: + +- `enabled` (Boolean) +- `status` (String) + + + +### Nested Schema for `rulesets.rules.ratelimit` + +Read-Only: + +- `characteristics` (Set of String) +- `counting_expression` (String) +- `mitigation_timeout` (Number) +- `period` (Number) +- `requests_per_period` (Number) +- `requests_to_origin` (Boolean) +- `score_per_period` (Number) +- `score_response_header_name` (String) + + diff --git a/docs/resources/pages_project.md b/docs/resources/pages_project.md index d5ec46a7ad..9c91862daa 100644 --- a/docs/resources/pages_project.md +++ b/docs/resources/pages_project.md @@ -117,7 +117,7 @@ resource "cloudflare_pages_project" "deployment_configs" { ### Required - `account_id` (String) The account identifier to target for the resource. -- `name` (String) Name of the project. +- `name` (String) Name of the project. **Modifying this attribute will force creation of a new resource.** - `production_branch` (String) The name of the branch that is used for the production environment. ### Optional diff --git a/docs/resources/ruleset.md b/docs/resources/ruleset.md index c98ca08405..0401ea2a6c 100644 --- a/docs/resources/ruleset.md +++ b/docs/resources/ruleset.md @@ -456,6 +456,7 @@ Optional: - `description` (String) Brief summary of the ruleset rule and its intended use. - `enabled` (Boolean) Whether the rule is active. - `exposed_credential_check` (Block List, Max: 1) List of parameters that configure exposed credential checks. (see [below for nested schema](#nestedblock--rules--exposed_credential_check)) +- `last_updated` (String) The most recent update to this rule. - `logging` (Block List, Max: 1) List parameters to configure how the rule generates logs. (see [below for nested schema](#nestedblock--rules--logging)) - `ratelimit` (Block List, Max: 1) List of parameters that configure HTTP rate limiting behaviour. (see [below for nested schema](#nestedblock--rules--ratelimit)) diff --git a/examples/data-sources/cloudflare_record/data-source.tf b/examples/data-sources/cloudflare_record/data-source.tf index b45b6a4436..ab01614588 100644 --- a/examples/data-sources/cloudflare_record/data-source.tf +++ b/examples/data-sources/cloudflare_record/data-source.tf @@ -1,4 +1,4 @@ data "cloudflare_record" "example" { - zone_id = var.zone_id + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" hostname = "example.com" } diff --git a/examples/data-sources/cloudflare_rulesets/data-source.tf b/examples/data-sources/cloudflare_rulesets/data-source.tf new file mode 100644 index 0000000000..1185577fcc --- /dev/null +++ b/examples/data-sources/cloudflare_rulesets/data-source.tf @@ -0,0 +1,7 @@ +data "cloudflare_rulesets" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + + filter { + name = ".*OWASP.*" + } +} diff --git a/internal/sdkv2provider/data_source_rulesets.go b/internal/sdkv2provider/data_source_rulesets.go new file mode 100644 index 0000000000..bfcbbcb786 --- /dev/null +++ b/internal/sdkv2provider/data_source_rulesets.go @@ -0,0 +1,285 @@ +package sdkv2provider + +import ( + "context" + "fmt" + "regexp" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudflareRulesets() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceCloudflareRulesetsRead, + + Description: heredoc.Doc(` + Use this datasource to lookup Rulesets in an account or zone. + `), + + Schema: map[string]*schema.Schema{ + "account_id": { + Description: "The account identifier to target for the resource.", + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"zone_id", "account_id"}, + }, + "zone_id": { + Description: "The zone identifier to target for the resource.", + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"zone_id", "account_id"}, + }, + + "include_rules": { + Description: "Include rule data in response", + Type: schema.TypeBool, + Optional: true, + }, + + "filter": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + Description: "The ID of the Ruleset to target.", + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the ruleset.", + }, + "phase": { + Type: schema.TypeString, + Optional: true, + Description: fmt.Sprintf("Point in the request/response lifecycle where the ruleset will be created. %s", renderAvailableDocumentationValuesStringSlice(cloudflare.RulesetPhaseValues())), + }, + "kind": { + Type: schema.TypeString, + Optional: true, + Description: fmt.Sprintf("Type of Ruleset to create. %s", renderAvailableDocumentationValuesStringSlice(cloudflare.RulesetKindValues())), + }, + "version": { + Type: schema.TypeString, + Optional: true, + Description: "Version of the ruleset to filter on.", + }, + }, + }, + }, + + "rulesets": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: "ID of the ruleset.", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the ruleset.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Brief summary of the ruleset and its intended use.", + }, + "kind": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Type of Ruleset. %s", renderAvailableDocumentationValuesStringSlice(cloudflare.RulesetKindValues())), + }, + "version": { + Type: schema.TypeString, + Required: true, + Description: "Version of the ruleset.", + }, + "phase": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Point in the request/response lifecycle where the ruleset executes. %s", renderAvailableDocumentationValuesStringSlice(cloudflare.RulesetPhaseValues())), + }, + "rules": resourceCloudflareRulesetSchema()["rules"], + }, + }, + }, + }, + } +} + +func dataSourceCloudflareRulesetsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + accountID := d.Get("account_id").(string) + includeRules := d.Get("include_rules").(bool) + filter, err := expandFilterRulesets(d.Get("filter")) + if err != nil { + return diag.FromErr(err) + } + + var rulesetsList []cloudflare.Ruleset + if accountID != "" { + rulesetsList, err = client.ListAccountRulesets(ctx, accountID) + } else { + rulesetsList, err = client.ListZoneRulesets(ctx, zoneID) + } + if err != nil { + return diag.FromErr(err) + } + + rulesets := make([]interface{}, 0) + rulesetIds := make([]string, 0) + for _, ruleset := range rulesetsList { + if filter.ID != "" && filter.ID != ruleset.ID { + continue + } + if filter.Phase != "" && filter.Phase != ruleset.Phase { + continue + } + if filter.Kind != "" && filter.Kind != ruleset.Kind { + continue + } + if filter.Version != "" && filter.Version != ruleset.Version { + continue + } + if filter.Name != nil && !filter.Name.Match([]byte(ruleset.Name)) { + continue + } + + rulesetIds = append(rulesetIds, ruleset.ID) + resultRuleset := map[string]interface{}{ + "id": ruleset.ID, + "name": ruleset.Name, + "description": ruleset.Description, + "kind": ruleset.Kind, + "version": ruleset.Version, + "phase": ruleset.Phase, + } + + if includeRules { + var fullRuleset cloudflare.Ruleset + if accountID != "" { + fullRuleset, err = client.GetAccountRuleset(ctx, accountID, ruleset.ID) + } else { + fullRuleset, err = client.GetZoneRuleset(ctx, zoneID, ruleset.ID) + } + if err != nil { + return diag.FromErr(err) + } + + rules := make([]interface{}, 0) + for _, rule := range fullRuleset.Rules { + fullRulesetRule := map[string]interface{}{ + "id": rule.ID, + "version": rule.Version, + "action": rule.Action, + "expression": rule.Expression, + "description": rule.Description, + "last_updated": rule.LastUpdated.String(), + "ref": rule.Ref, + "enabled": rule.Enabled, + } + + if rule.RateLimit != nil { + rl := make([]interface{}, 0) + fullRulesetRule["ratelimit"] = append(rl, map[string]interface{}{ + "characteristics": rule.RateLimit.Characteristics, + "requests_per_period": rule.RateLimit.RequestsPerPeriod, + "period": rule.RateLimit.Period, + "mitigation_timeout": rule.RateLimit.MitigationTimeout, + "counting_expression": rule.RateLimit.CountingExpression, + "requests_to_origin": rule.RateLimit.RequestsToOrigin, + }) + } + + if rule.ExposedCredentialCheck != nil { + ecc := make([]interface{}, 0) + fullRulesetRule["exposed_credential_check"] = append(ecc, map[string]interface{}{ + "username_expression": rule.ExposedCredentialCheck.UsernameExpression, + "password_expression": rule.ExposedCredentialCheck.PasswordExpression, + }) + } + if rule.Logging != nil { + lg := make([]interface{}, 0) + fullRulesetRule["logging"] = append(lg, map[string]interface{}{ + "enabled": rule.Logging.Enabled, + "status": loggingStatus(rule.Logging), + }) + } + rules = append(rules, fullRulesetRule) + } + resultRuleset["rules"] = rules + } + + rulesets = append(rulesets, resultRuleset) + } + + err = d.Set("rulesets", rulesets) + if err != nil { + return diag.FromErr(fmt.Errorf("error setting Ruleset Ids: %w", err)) + } + + d.SetId(stringListChecksum(rulesetIds)) + return nil +} + +func loggingStatus(logging *cloudflare.RulesetRuleLogging) string { + if logging == nil || logging.Enabled == nil { + return "default" + } + if *logging.Enabled { + return "enabled" + } + return "disabled" +} + +func expandFilterRulesets(d interface{}) (*searchFilterRulesets, error) { + cfg := d.([]interface{}) + filter := &searchFilterRulesets{} + if len(cfg) == 0 || cfg[0] == nil { + return filter, nil + } + + m := cfg[0].(map[string]interface{}) + if name, ok := m["name"]; ok { + match, err := regexp.Compile(name.(string)) + if err != nil { + return nil, err + } + + filter.Name = match + } + + if kind, ok := m["kind"]; ok { + filter.Kind = kind.(string) + } + if phase, ok := m["phase"]; ok { + filter.Phase = phase.(string) + } + if id, ok := m["id"]; ok { + filter.ID = id.(string) + } + if version, ok := m["version"]; ok { + filter.Version = version.(string) + } + + return filter, nil +} + +type searchFilterRulesets struct { + Name *regexp.Regexp + Kind string + Phase string + ID string + Version string +} diff --git a/internal/sdkv2provider/data_source_rulesets_test.go b/internal/sdkv2provider/data_source_rulesets_test.go new file mode 100644 index 0000000000..b5a0afce22 --- /dev/null +++ b/internal/sdkv2provider/data_source_rulesets_test.go @@ -0,0 +1,134 @@ +package sdkv2provider + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccCloudflareRulesetsProviderDataSource_PreventZoneIdAndAccountIdConflicts(t *testing.T) { + rnd := generateRandomResourceName() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testCloudflareRulesetsProviderDataSourceConfigConflictingFields(rnd), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("only one of `account_id,zone_id` can be specified")), + }, + }, + }) +} + +func testCloudflareRulesetsProviderDataSourceConfigConflictingFields(rnd string) string { + return fmt.Sprintf(` +data "cloudflare_rulesets" "%[1]s" { + account_id = "123abc" + zone_id = "456def" +} +`, rnd) +} + +func TestAccCloudflareRulesetsProviderDataSource_RequireOneOfZoneAccountID(t *testing.T) { + rnd := generateRandomResourceName() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testCloudflareRulesetsProviderDataSourceRequireOneOfZoneAccountID(rnd), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("one of `account_id,zone_id` must be specified")), + }, + }, + }) +} + +func testCloudflareRulesetsProviderDataSourceRequireOneOfZoneAccountID(rnd string) string { + return fmt.Sprintf(` +data "cloudflare_rulesets" "%[1]s" { +} +`, rnd) +} + +func TestAccCloudflareRulesetsProviderDataSource_FetchOWASPRulesetByName(t *testing.T) { + rnd := generateRandomResourceName() + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + name := fmt.Sprintf("data.cloudflare_rulesets.%s", rnd) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testCloudflareRulesetsProviderDataSourceFetchOWASPRulesetByName(rnd, zoneID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareRulesetsDataSourceID(name), + resource.TestCheckResourceAttr(name, "rulesets.0.name", "Cloudflare OWASP Core Ruleset"), + ), + }, + }, + }) +} + +func testCloudflareRulesetsProviderDataSourceFetchOWASPRulesetByName(rnd string, zoneID string) string { + return fmt.Sprintf(` +data "cloudflare_rulesets" "%[1]s" { + zone_id = "%[2]s" + + filter { + name = ".*OWASP.*" + } +} +`, rnd, zoneID) +} + +func TestAccCloudflareRulesetsProviderDataSource_FetchOWASPRulesetByID(t *testing.T) { + rnd := generateRandomResourceName() + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + name := fmt.Sprintf("data.cloudflare_rulesets.%s", rnd) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testCloudflareRulesetsProviderDataSourceFetchOWASPRulesetByID(rnd, zoneID), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareRulesetsDataSourceID(name), + resource.TestCheckResourceAttr(name, "rulesets.0.name", "Cloudflare OWASP Core Ruleset"), + ), + }, + }, + }) +} + +func testCloudflareRulesetsProviderDataSourceFetchOWASPRulesetByID(rnd string, zoneID string) string { + return fmt.Sprintf(` +data "cloudflare_rulesets" "%[1]s" { + zone_id = "%[2]s" + + filter { + id = "4814384a9e5d4991b9815dcfc25d2f1f" + } +} +`, rnd, zoneID) +} + +func testAccCheckCloudflareRulesetsDataSourceID(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + all := s.RootModule().Resources + rs, ok := all[n] + if !ok { + return fmt.Errorf("can't find Rulesets data source: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Snapshot Rulesets source ID not set") + } + return nil + } +} diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index f027477c68..a6b970c973 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -171,6 +171,7 @@ func New(version string) func() *schema.Provider { "cloudflare_load_balancer_pools": dataSourceCloudflareLoadBalancerPools(), "cloudflare_origin_ca_root_certificate": dataSourceCloudflareOriginCARootCertificate(), "cloudflare_record": dataSourceCloudflareRecord(), + "cloudflare_rulesets": dataSourceCloudflareRulesets(), "cloudflare_waf_groups": dataSourceCloudflareWAFGroups(), "cloudflare_waf_packages": dataSourceCloudflareWAFPackages(), "cloudflare_waf_rules": dataSourceCloudflareWAFRules(), diff --git a/internal/sdkv2provider/schema_cloudflare_ruleset.go b/internal/sdkv2provider/schema_cloudflare_ruleset.go index ff26cc8ad1..339f33150b 100644 --- a/internal/sdkv2provider/schema_cloudflare_ruleset.go +++ b/internal/sdkv2provider/schema_cloudflare_ruleset.go @@ -93,6 +93,11 @@ func resourceCloudflareRulesetSchema() map[string]*schema.Schema { Optional: true, Description: "Brief summary of the ruleset rule and its intended use.", }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Description: "The most recent update to this rule.", + }, "action_parameters": { Type: schema.TypeList, MaxItems: 1,