From d33a506485055a050fd551591d5402ab90081be0 Mon Sep 17 00:00:00 2001 From: Aleksandar Kinanov Date: Mon, 6 Oct 2025 16:06:06 +0300 Subject: [PATCH] Adds ruleset rule as standalone object instead of element in list under ruleset --- internal/provider.go | 3 + internal/services/ruleset/schema.go | 3015 +++++++++-------- internal/services/ruleset_rule/README.md | 118 + internal/services/ruleset_rule/data_source.go | 130 + .../ruleset_rule/data_source_model.go | 93 + .../ruleset_rule/data_source_schema.go | 100 + .../ruleset_rule/data_source_schema_test.go | 21 + internal/services/ruleset_rule/model.go | 33 + internal/services/ruleset_rule/resource.go | 428 +++ .../ruleset_rule/ruleset_rule_test.go | 160 + internal/services/ruleset_rule/schema.go | 87 + .../1.tf | 26 + .../2.tf | 27 + .../3.tf | 27 + 14 files changed, 2765 insertions(+), 1503 deletions(-) create mode 100644 internal/services/ruleset_rule/README.md create mode 100644 internal/services/ruleset_rule/data_source.go create mode 100644 internal/services/ruleset_rule/data_source_model.go create mode 100644 internal/services/ruleset_rule/data_source_schema.go create mode 100644 internal/services/ruleset_rule/data_source_schema_test.go create mode 100644 internal/services/ruleset_rule/model.go create mode 100644 internal/services/ruleset_rule/resource.go create mode 100644 internal/services/ruleset_rule/ruleset_rule_test.go create mode 100644 internal/services/ruleset_rule/schema.go create mode 100644 internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/1.tf create mode 100644 internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/2.tf create mode 100644 internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/3.tf diff --git a/internal/provider.go b/internal/provider.go index cdf8235b78..a067fbb223 100644 --- a/internal/provider.go +++ b/internal/provider.go @@ -125,6 +125,7 @@ import ( "github.com/cloudflare/terraform-provider-cloudflare/internal/services/registrar_domain" "github.com/cloudflare/terraform-provider-cloudflare/internal/services/resource_group" "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset" + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset_rule" "github.com/cloudflare/terraform-provider-cloudflare/internal/services/schema_validation_operation_settings" "github.com/cloudflare/terraform-provider-cloudflare/internal/services/schema_validation_schemas" "github.com/cloudflare/terraform-provider-cloudflare/internal/services/schema_validation_settings" @@ -477,6 +478,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re managed_transforms.NewResource, page_shield_policy.NewResource, ruleset.NewResource, + ruleset_rule.NewResource, url_normalization_settings.NewResource, spectrum_application.NewResource, regional_hostname.NewResource, @@ -741,6 +743,7 @@ func (p *CloudflareProvider) DataSources(ctx context.Context) []func() datasourc page_shield_cookies.NewPageShieldCookiesListDataSource, ruleset.NewRulesetDataSource, ruleset.NewRulesetsDataSource, + ruleset_rule.NewRulesetRuleDataSource, url_normalization_settings.NewURLNormalizationSettingsDataSource, spectrum_application.NewSpectrumApplicationDataSource, spectrum_application.NewSpectrumApplicationsDataSource, diff --git a/internal/services/ruleset/schema.go b/internal/services/ruleset/schema.go index ce9e7ef50b..1216be04fc 100644 --- a/internal/services/ruleset/schema.go +++ b/internal/services/ruleset/schema.go @@ -6,26 +6,27 @@ import ( "context" "regexp" - "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield" "github.com/cloudflare/terraform-provider-cloudflare/internal/customvalidator" - "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" ) var _ resource.ResourceWithConfigValidators = (*RulesetResource)(nil) @@ -127,46 +128,199 @@ func ResourceSchema(ctx context.Context) schema.Schema { Default: stringdefault.StaticString(""), }, "rules": schema.ListNestedAttribute{ - Description: "The list of rules in the ruleset.", + Description: "The list of rules in the ruleset.", + Computed: true, + Optional: true, + Default: listdefault.StaticValue(customfield.NewObjectListMust(ctx, []RulesetRulesModel{}).ListValue), + CustomType: customfield.NewNestedObjectListType[RulesetRulesModel](ctx), + NestedObject: RuleNestedSchema(ctx), + }, + "last_updated": schema.StringAttribute{ + Description: "The timestamp of when the ruleset was last modified.", Computed: true, - Optional: true, - Default: listdefault.StaticValue(customfield.NewObjectListMust(ctx, []RulesetRulesModel{}).ListValue), - CustomType: customfield.NewNestedObjectListType[RulesetRulesModel](ctx), - NestedObject: schema.NestedAttributeObject{ + CustomType: timetypes.RFC3339Type{}, + }, + "version": schema.StringAttribute{ + Description: "The version of the ruleset.", + Computed: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9]+$"), + "value must be a non-empty string containing only numbers", + ), + }, + }, + }, + } +} + +func RuleAttributes(ctx context.Context) map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique ID of the rule.", + Computed: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + }, + "action": schema.StringAttribute{ + Description: "The action to perform when the rule matches.\nAvailable values: \"block\", \"challenge\", \"compress_response\", \"ddos_dynamic\", \"execute\", \"force_connection_close\", \"js_challenge\", \"log\", \"log_custom_field\", \"managed_challenge\", \"redirect\", \"rewrite\", \"route\", \"score\", \"serve_error\", \"set_cache_settings\", \"set_config\", \"skip\".", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "block", + "challenge", + "compress_response", + "ddos_dynamic", + "execute", + "force_connection_close", + "js_challenge", + "log", + "log_custom_field", + "managed_challenge", + "redirect", + "rewrite", + "route", + "score", + "serve_error", + "set_cache_settings", + "set_config", + "skip", + ), + stringvalidator.RegexMatches( + regexp.MustCompile("^[a-z_]+$"), + "value must be a non-empty string containing only lowercase characters and underscores", + ), + }, + }, + "action_parameters": schema.SingleNestedAttribute{ + Description: "The parameters configuring the rule's action.", + Computed: true, + Optional: true, + Default: objectdefault.StaticValue(customfield.NewObjectMust(ctx, &RulesetRulesActionParametersModel{}).ObjectValue), + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersModel](ctx), + Attributes: map[string]schema.Attribute{ + "response": schema.SingleNestedAttribute{ + Description: "The response to show when the block is applied.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "block", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersResponseModel](ctx), Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "The unique ID of the rule.", - Computed: true, + "content": schema.StringAttribute{ + Description: "The content to return.", + Required: true, Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile("^[0-9a-f]{32}$"), - "value must be a 32-character hexadecimal string", - ), + stringvalidator.LengthAtLeast(1), }, }, - "action": schema.StringAttribute{ - Description: "The action to perform when the rule matches.\nAvailable values: \"block\", \"challenge\", \"compress_response\", \"ddos_dynamic\", \"execute\", \"force_connection_close\", \"js_challenge\", \"log\", \"log_custom_field\", \"managed_challenge\", \"redirect\", \"rewrite\", \"route\", \"score\", \"serve_error\", \"set_cache_settings\", \"set_config\", \"skip\".", + "content_type": schema.StringAttribute{ + Description: "The type of the content to return.", Required: true, Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "block", - "challenge", - "compress_response", - "ddos_dynamic", - "execute", - "force_connection_close", - "js_challenge", - "log", - "log_custom_field", - "managed_challenge", - "redirect", - "rewrite", - "route", - "score", - "serve_error", - "set_cache_settings", - "set_config", - "skip", + stringvalidator.LengthAtLeast(1), + }, + }, + "status_code": schema.Int64Attribute{ + Description: "The status code to return.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(400, 499), + }, + }, + }, + }, + "algorithms": schema.ListNestedAttribute{ + Description: "Custom order for compression algorithms.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "compress_response", + ), + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersAlgorithmsModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Name of the compression algorithm to enable.\nAvailable values: \"none\", \"auto\", \"default\", \"gzip\", \"brotli\", \"zstd\".", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "none", + "auto", + "default", + "gzip", + "brotli", + "zstd", + ), + }, + }, + }, + }, + }, + "id": schema.StringAttribute{ + Description: "The ID of the ruleset to execute.", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "execute", + ), + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + }, + "matched_data": schema.SingleNestedAttribute{ + Description: "The configuration to use for matched data logging.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "execute", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersMatchedDataModel](ctx), + Attributes: map[string]schema.Attribute{ + "public_key": schema.StringAttribute{ + Description: "The public key to encrypt matched data logs with.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + }, + "overrides": schema.SingleNestedAttribute{ + Description: "A set of overrides to apply to the target ruleset.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "execute", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersOverridesModel](ctx), + Attributes: map[string]schema.Attribute{ + "action": schema.StringAttribute{ + Description: "An action to override all rules with. This option has lower precedence than rule and category overrides.", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("categories"), + path.MatchRelative().AtParent().AtName("enabled"), + path.MatchRelative().AtParent().AtName("rules"), + path.MatchRelative().AtParent().AtName("sensitivity_level"), ), stringvalidator.RegexMatches( regexp.MustCompile("^[a-z_]+$"), @@ -174,1594 +328,1449 @@ func ResourceSchema(ctx context.Context) schema.Schema { ), }, }, - "action_parameters": schema.SingleNestedAttribute{ - Description: "The parameters configuring the rule's action.", - Computed: true, + "categories": schema.ListNestedAttribute{ + Description: "A list of category-level overrides. This option has the second-highest precedence after rule-level overrides.", Optional: true, - Default: objectdefault.StaticValue(customfield.NewObjectMust(ctx, &RulesetRulesActionParametersModel{}).ObjectValue), - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersModel](ctx), - Attributes: map[string]schema.Attribute{ - "response": schema.SingleNestedAttribute{ - Description: "The response to show when the block is applied.", - Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "block", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersResponseModel](ctx), - Attributes: map[string]schema.Attribute{ - "content": schema.StringAttribute{ - Description: "The content to return.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersOverridesCategoriesModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "category": schema.StringAttribute{ + Description: "The name of the category to override.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, - "content_type": schema.StringAttribute{ - Description: "The type of the content to return.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + }, + "action": schema.StringAttribute{ + Description: "The action to override rules in the category with.", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("enabled"), + path.MatchRelative().AtParent().AtName("sensitivity_level"), + ), + stringvalidator.RegexMatches( + regexp.MustCompile("^[a-z_]+$"), + "value must be a non-empty string containing only lowercase characters and underscores", + ), }, - "status_code": schema.Int64Attribute{ - Description: "The status code to return.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(400, 499), - }, + }, + "enabled": schema.BoolAttribute{ + Description: "Whether to enable execution of rules in the category.", + Optional: true, + }, + "sensitivity_level": schema.StringAttribute{ + Description: "The sensitivity level to use for rules in the category. This option is only applicable for DDoS phases.\nAvailable values: \"default\", \"medium\", \"low\", \"eoff\".", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "default", + "medium", + "low", + "eoff", + ), }, }, }, - "algorithms": schema.ListNestedAttribute{ - Description: "Custom order for compression algorithms.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "compress_response", - ), - listvalidator.SizeAtLeast(1), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Whether to enable execution of all rules. This option has lower precedence than rule and category overrides.", + Optional: true, + }, + "rules": schema.ListNestedAttribute{ + Description: "A list of rule-level overrides. This option has the highest precedence.", + Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersOverridesRulesModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the rule to override.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersAlgorithmsModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "Name of the compression algorithm to enable.\nAvailable values: \"none\", \"auto\", \"default\", \"gzip\", \"brotli\", \"zstd\".", - Optional: true, - Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "none", - "auto", - "default", - "gzip", - "brotli", - "zstd", - ), - }, - }, + "action": schema.StringAttribute{ + Description: "The action to override the rule with.", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("enabled"), + path.MatchRelative().AtParent().AtName("score_threshold"), + path.MatchRelative().AtParent().AtName("sensitivity_level"), + ), + stringvalidator.RegexMatches( + regexp.MustCompile("^[a-z_]+$"), + "value must be a non-empty string containing only lowercase characters and underscores", + ), }, }, - }, - "id": schema.StringAttribute{ - Description: "The ID of the ruleset to execute.", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "execute", - ), - stringvalidator.RegexMatches( - regexp.MustCompile("^[0-9a-f]{32}$"), - "value must be a 32-character hexadecimal string", - ), + "enabled": schema.BoolAttribute{ + Description: "Whether to enable execution of the rule.", + Optional: true, }, - }, - "matched_data": schema.SingleNestedAttribute{ - Description: "The configuration to use for matched data logging.", - Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "execute", - ), + "score_threshold": schema.Int64Attribute{ + Description: "The score threshold to use for the rule.", + Optional: true, }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersMatchedDataModel](ctx), - Attributes: map[string]schema.Attribute{ - "public_key": schema.StringAttribute{ - Description: "The public key to encrypt matched data logs with.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + "sensitivity_level": schema.StringAttribute{ + Description: "The sensitivity level to use for the rule. This option is only applicable for DDoS phases.\nAvailable values: \"default\", \"medium\", \"low\", \"eoff\".", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "default", + "medium", + "low", + "eoff", + ), }, }, }, - "overrides": schema.SingleNestedAttribute{ - Description: "A set of overrides to apply to the target ruleset.", + }, + }, + "sensitivity_level": schema.StringAttribute{ + Description: "A sensitivity level to set for all rules. This option has lower precedence than rule and category overrides and is only applicable for DDoS phases.\nAvailable values: \"default\", \"medium\", \"low\", \"eoff\".", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "default", + "medium", + "low", + "eoff", + ), + }, + }, + }, + }, + "from_list": schema.SingleNestedAttribute{ + Description: "A redirect based on a bulk list lookup.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "redirect", + ), + objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("from_value")), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersFromListModel](ctx), + Attributes: map[string]schema.Attribute{ + "key": schema.StringAttribute{ + Description: "An expression that evaluates to the list lookup key.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the list to match against.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[a-zA-Z0-9_]+$"), + "value must be a non-empty string containing only alphanumeric characters and underscores", + ), + }, + }, + }, + }, + "from_value": schema.SingleNestedAttribute{ + Description: "A redirect based on the request properties.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "redirect", + ), + objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("from_list")), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersFromValueModel](ctx), + Attributes: map[string]schema.Attribute{ + "preserve_query_string": schema.BoolAttribute{ + Description: "Whether to keep the query string of the original request.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, + "status_code": schema.Int64Attribute{ + Description: "The status code to use for the redirect.", + Optional: true, + Validators: []validator.Int64{ + int64validator.OneOf( + 301, + 302, + 303, + 307, + 308, + ), + }, + }, + "target_url": schema.SingleNestedAttribute{ + Description: "A URL to redirect the request to.", + Required: true, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersFromValueTargetURLModel](ctx), + Attributes: map[string]schema.Attribute{ + "value": schema.StringAttribute{ + Description: "A URL to redirect the request to.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "execute", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersOverridesModel](ctx), - Attributes: map[string]schema.Attribute{ - "action": schema.StringAttribute{ - Description: "An action to override all rules with. This option has lower precedence than rule and category overrides.", - Optional: true, - Validators: []validator.String{ - stringvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName("categories"), - path.MatchRelative().AtParent().AtName("enabled"), - path.MatchRelative().AtParent().AtName("rules"), - path.MatchRelative().AtParent().AtName("sensitivity_level"), - ), - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-z_]+$"), - "value must be a non-empty string containing only lowercase characters and underscores", - ), - }, - }, - "categories": schema.ListNestedAttribute{ - Description: "A list of category-level overrides. This option has the second-highest precedence after rule-level overrides.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersOverridesCategoriesModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "category": schema.StringAttribute{ - Description: "The name of the category to override.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "action": schema.StringAttribute{ - Description: "The action to override rules in the category with.", - Optional: true, - Validators: []validator.String{ - stringvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName("enabled"), - path.MatchRelative().AtParent().AtName("sensitivity_level"), - ), - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-z_]+$"), - "value must be a non-empty string containing only lowercase characters and underscores", - ), - }, - }, - "enabled": schema.BoolAttribute{ - Description: "Whether to enable execution of rules in the category.", - Optional: true, - }, - "sensitivity_level": schema.StringAttribute{ - Description: "The sensitivity level to use for rules in the category. This option is only applicable for DDoS phases.\nAvailable values: \"default\", \"medium\", \"low\", \"eoff\".", - Optional: true, - Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "default", - "medium", - "low", - "eoff", - ), - }, - }, - }, - }, - }, - "enabled": schema.BoolAttribute{ - Description: "Whether to enable execution of all rules. This option has lower precedence than rule and category overrides.", - Optional: true, - }, - "rules": schema.ListNestedAttribute{ - Description: "A list of rule-level overrides. This option has the highest precedence.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersOverridesRulesModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "The ID of the rule to override.", - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile("^[0-9a-f]{32}$"), - "value must be a 32-character hexadecimal string", - ), - }, - }, - "action": schema.StringAttribute{ - Description: "The action to override the rule with.", - Optional: true, - Validators: []validator.String{ - stringvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName("enabled"), - path.MatchRelative().AtParent().AtName("score_threshold"), - path.MatchRelative().AtParent().AtName("sensitivity_level"), - ), - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-z_]+$"), - "value must be a non-empty string containing only lowercase characters and underscores", - ), - }, - }, - "enabled": schema.BoolAttribute{ - Description: "Whether to enable execution of the rule.", - Optional: true, - }, - "score_threshold": schema.Int64Attribute{ - Description: "The score threshold to use for the rule.", - Optional: true, - }, - "sensitivity_level": schema.StringAttribute{ - Description: "The sensitivity level to use for the rule. This option is only applicable for DDoS phases.\nAvailable values: \"default\", \"medium\", \"low\", \"eoff\".", - Optional: true, - Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "default", - "medium", - "low", - "eoff", - ), - }, - }, - }, - }, - }, - "sensitivity_level": schema.StringAttribute{ - Description: "A sensitivity level to set for all rules. This option has lower precedence than rule and category overrides and is only applicable for DDoS phases.\nAvailable values: \"default\", \"medium\", \"low\", \"eoff\".", - Optional: true, - Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "default", - "medium", - "low", - "eoff", - ), - }, - }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("expression")), + stringvalidator.LengthAtLeast(1), }, }, - "from_list": schema.SingleNestedAttribute{ - Description: "A redirect based on a bulk list lookup.", + "expression": schema.StringAttribute{ + Description: "An expression that evaluates to a URL to redirect the request to.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "redirect", - ), - objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("from_value")), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersFromListModel](ctx), - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "An expression that evaluates to the list lookup key.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the list to match against.", - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-zA-Z0-9_]+$"), - "value must be a non-empty string containing only alphanumeric characters and underscores", - ), - }, - }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, }, - "from_value": schema.SingleNestedAttribute{ - Description: "A redirect based on the request properties.", + }, + }, + }, + }, + "headers": schema.MapNestedAttribute{ + Description: "A map of headers to rewrite.", + Optional: true, + Validators: []validator.Map{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "rewrite", + ), + mapvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectMapType[RulesetRulesActionParametersHeadersModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "operation": schema.StringAttribute{ + Description: "The operation to perform on the header.\nAvailable values: \"add\", \"set\", \"remove\".", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "add", + "set", + "remove", + ), + }, + }, + "value": schema.StringAttribute{ + Description: "A static value for the header.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("expression")), + stringvalidator.LengthAtLeast(1), + }, + }, + "expression": schema.StringAttribute{ + Description: "An expression that evaluates to a value for the header.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("value")), + stringvalidator.LengthAtLeast(1), + }, + }, + }, + }, + }, + "uri": schema.SingleNestedAttribute{ + Description: "A URI rewrite.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "rewrite", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersURIModel](ctx), + Attributes: map[string]schema.Attribute{ + "path": schema.SingleNestedAttribute{ + Description: "A URI path rewrite.", + Optional: true, + Validators: []validator.Object{ + objectvalidator.AtLeastOneOf(path.MatchRelative().AtParent().AtName("query")), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersURIPathModel](ctx), + Attributes: map[string]schema.Attribute{ + "value": schema.StringAttribute{ + Description: "A value to rewrite the URI path to.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "redirect", - ), - objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("from_list")), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersFromValueModel](ctx), - Attributes: map[string]schema.Attribute{ - "preserve_query_string": schema.BoolAttribute{ - Description: "Whether to keep the query string of the original request.", - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), - }, - "status_code": schema.Int64Attribute{ - Description: "The status code to use for the redirect.", - Optional: true, - Validators: []validator.Int64{ - int64validator.OneOf( - 301, - 302, - 303, - 307, - 308, - ), - }, - }, - "target_url": schema.SingleNestedAttribute{ - Description: "A URL to redirect the request to.", - Required: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersFromValueTargetURLModel](ctx), - Attributes: map[string]schema.Attribute{ - "value": schema.StringAttribute{ - Description: "A URL to redirect the request to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("expression")), - stringvalidator.LengthAtLeast(1), - }, - }, - "expression": schema.StringAttribute{ - Description: "An expression that evaluates to a URL to redirect the request to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - }, - }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("expression")), + stringvalidator.LengthAtLeast(1), }, }, - "headers": schema.MapNestedAttribute{ - Description: "A map of headers to rewrite.", + "expression": schema.StringAttribute{ + Description: "An expression that evaluates to a value to rewrite the URI path to.", Optional: true, - Validators: []validator.Map{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "rewrite", - ), - mapvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectMapType[RulesetRulesActionParametersHeadersModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "operation": schema.StringAttribute{ - Description: "The operation to perform on the header.\nAvailable values: \"add\", \"set\", \"remove\".", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "add", - "set", - "remove", - ), - }, - }, - "value": schema.StringAttribute{ - Description: "A static value for the header.", - Optional: true, - Validators: []validator.String{ - stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("expression")), - stringvalidator.LengthAtLeast(1), - }, - }, - "expression": schema.StringAttribute{ - Description: "An expression that evaluates to a value for the header.", - Optional: true, - Validators: []validator.String{ - stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("value")), - stringvalidator.LengthAtLeast(1), - }, - }, - }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, }, - "uri": schema.SingleNestedAttribute{ - Description: "A URI rewrite.", + }, + }, + "query": schema.SingleNestedAttribute{ + Description: "A URI query rewrite.", + Optional: true, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersURIQueryModel](ctx), + Attributes: map[string]schema.Attribute{ + "value": schema.StringAttribute{ + Description: "A value to rewrite the URI query to.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "rewrite", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersURIModel](ctx), - Attributes: map[string]schema.Attribute{ - "path": schema.SingleNestedAttribute{ - Description: "A URI path rewrite.", - Optional: true, - Validators: []validator.Object{ - objectvalidator.AtLeastOneOf(path.MatchRelative().AtParent().AtName("query")), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersURIPathModel](ctx), - Attributes: map[string]schema.Attribute{ - "value": schema.StringAttribute{ - Description: "A value to rewrite the URI path to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("expression")), - stringvalidator.LengthAtLeast(1), - }, - }, - "expression": schema.StringAttribute{ - Description: "An expression that evaluates to a value to rewrite the URI path to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - }, - }, - "query": schema.SingleNestedAttribute{ - Description: "A URI query rewrite.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersURIQueryModel](ctx), - Attributes: map[string]schema.Attribute{ - "value": schema.StringAttribute{ - Description: "A value to rewrite the URI query to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("expression")), - }, - }, - "expression": schema.StringAttribute{ - Description: "An expression that evaluates to a value to rewrite the URI query to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - }, - }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("expression")), }, }, - "host_header": schema.StringAttribute{ - Description: "A value to rewrite the HTTP host header to.", + "expression": schema.StringAttribute{ + Description: "An expression that evaluates to a value to rewrite the URI query to.", Optional: true, Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "route", - ), stringvalidator.LengthAtLeast(1), }, }, - "origin": schema.SingleNestedAttribute{ - Description: "An origin to route to.", - Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "route", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersOriginModel](ctx), - Attributes: map[string]schema.Attribute{ - "host": schema.StringAttribute{ - Description: "A resolved host to route to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.AtLeastOneOf(path.MatchRelative().AtParent().AtName("port")), - stringvalidator.LengthAtLeast(1), - }, - }, - "port": schema.Int64Attribute{ - Description: "A destination port to route to.", - Optional: true, - Validators: []validator.Int64{ - int64validator.Between(1, 65535), - }, - }, - }, - }, - "sni": schema.SingleNestedAttribute{ - Description: "A Server Name Indication (SNI) override.", - Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "route", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersSNIModel](ctx), - Attributes: map[string]schema.Attribute{ - "value": schema.StringAttribute{ - Description: "A value to override the SNI to.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - }, - }, - "increment": schema.Int64Attribute{ - Description: "A delta to change the score by, which can be either positive or negative.", - Optional: true, - Validators: []validator.Int64{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "score", - ), - }, - }, - "asset_name": schema.StringAttribute{ - Description: "The name of a custom asset to serve as the response.", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "serve_error", - ), - stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("content")), - stringvalidator.LengthAtLeast(1), - }, - }, - "content": schema.StringAttribute{ - Description: "The response content.", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "serve_error", - ), - stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("asset_name")), - stringvalidator.LengthAtLeast(1), - }, - }, - "content_type": schema.StringAttribute{ - Description: "The content type header to set with the error response.\nAvailable values: \"application/json\", \"text/html\", \"text/plain\", \"text/xml\".", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "serve_error", - ), - stringvalidator.OneOfCaseInsensitive( - "application/json", - "text/html", - "text/plain", - "text/xml", - ), - }, - }, - "status_code": schema.Int64Attribute{ - Description: "The status code to use for the error.", - Optional: true, - Validators: []validator.Int64{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "serve_error", - ), - int64validator.Between(400, 999), - }, - }, - "automatic_https_rewrites": schema.BoolAttribute{ - Description: "Whether to enable Automatic HTTPS Rewrites.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "autominify": schema.SingleNestedAttribute{ - Description: "Which file extensions to minify automatically.", + }, + }, + }, + }, + "host_header": schema.StringAttribute{ + Description: "A value to rewrite the HTTP host header to.", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "route", + ), + stringvalidator.LengthAtLeast(1), + }, + }, + "origin": schema.SingleNestedAttribute{ + Description: "An origin to route to.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "route", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersOriginModel](ctx), + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + Description: "A resolved host to route to.", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.MatchRelative().AtParent().AtName("port")), + stringvalidator.LengthAtLeast(1), + }, + }, + "port": schema.Int64Attribute{ + Description: "A destination port to route to.", + Optional: true, + Validators: []validator.Int64{ + int64validator.Between(1, 65535), + }, + }, + }, + }, + "sni": schema.SingleNestedAttribute{ + Description: "A Server Name Indication (SNI) override.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "route", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersSNIModel](ctx), + Attributes: map[string]schema.Attribute{ + "value": schema.StringAttribute{ + Description: "A value to override the SNI to.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + }, + "increment": schema.Int64Attribute{ + Description: "A delta to change the score by, which can be either positive or negative.", + Optional: true, + Validators: []validator.Int64{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "score", + ), + }, + }, + "asset_name": schema.StringAttribute{ + Description: "The name of a custom asset to serve as the response.", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "serve_error", + ), + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("content")), + stringvalidator.LengthAtLeast(1), + }, + }, + "content": schema.StringAttribute{ + Description: "The response content.", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "serve_error", + ), + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("asset_name")), + stringvalidator.LengthAtLeast(1), + }, + }, + "content_type": schema.StringAttribute{ + Description: "The content type header to set with the error response.\nAvailable values: \"application/json\", \"text/html\", \"text/plain\", \"text/xml\".", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "serve_error", + ), + stringvalidator.OneOfCaseInsensitive( + "application/json", + "text/html", + "text/plain", + "text/xml", + ), + }, + }, + "status_code": schema.Int64Attribute{ + Description: "The status code to use for the error.", + Optional: true, + Validators: []validator.Int64{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "serve_error", + ), + int64validator.Between(400, 999), + }, + }, + "automatic_https_rewrites": schema.BoolAttribute{ + Description: "Whether to enable Automatic HTTPS Rewrites.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "autominify": schema.SingleNestedAttribute{ + Description: "Which file extensions to minify automatically.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersAutominifyModel](ctx), + Attributes: map[string]schema.Attribute{ + "css": schema.BoolAttribute{ + Description: "Whether to minify CSS files.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, + "html": schema.BoolAttribute{ + Description: "Whether to minify HTML files.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, + "js": schema.BoolAttribute{ + Description: "Whether to minify JavaScript files.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + "bic": schema.BoolAttribute{ + Description: "Whether to enable Browser Integrity Check (BIC).", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "disable_apps": schema.BoolAttribute{ + Description: "Whether to disable Cloudflare Apps.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + boolvalidator.Equals(true), + }, + }, + "disable_rum": schema.BoolAttribute{ + Description: "Whether to disable Real User Monitoring (RUM).", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + boolvalidator.Equals(true), + }, + }, + "disable_zaraz": schema.BoolAttribute{ + Description: "Whether to disable Zaraz.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + boolvalidator.Equals(true), + }, + }, + "email_obfuscation": schema.BoolAttribute{ + Description: "Whether to enable Email Obfuscation.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "fonts": schema.BoolAttribute{ + Description: "Whether to enable Cloudflare Fonts.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "hotlink_protection": schema.BoolAttribute{ + Description: "Whether to enable Hotlink Protection.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "mirage": schema.BoolAttribute{ + Description: "Whether to enable Mirage.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "opportunistic_encryption": schema.BoolAttribute{ + Description: "Whether to enable Opportunistic Encryption.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "polish": schema.StringAttribute{ + Description: "The Polish level to configure.\nAvailable values: \"off\", \"lossless\", \"lossy\", \"webp\".", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + stringvalidator.OneOfCaseInsensitive( + "off", + "lossless", + "lossy", + "webp", + ), + }, + }, + "rocket_loader": schema.BoolAttribute{ + Description: "Whether to enable Rocket Loader.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "security_level": schema.StringAttribute{ + Description: "The Security Level to configure.\nAvailable values: \"off\", \"essentially_off\", \"low\", \"medium\", \"high\", \"under_attack\".", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + stringvalidator.OneOfCaseInsensitive( + "off", + "essentially_off", + "low", + "medium", + "high", + "under_attack", + ), + }, + }, + "server_side_excludes": schema.BoolAttribute{ + Description: "Whether to enable Server-Side Excludes.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + "ssl": schema.StringAttribute{ + Description: "The SSL level to configure.\nAvailable values: \"off\", \"flexible\", \"full\", \"strict\", \"origin_pull\".", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + stringvalidator.OneOfCaseInsensitive( + "off", + "flexible", + "full", + "strict", + "origin_pull", + ), + }, + }, + "sxg": schema.BoolAttribute{ + Description: "Whether to enable Signed Exchanges (SXG).", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_config", + ), + }, + }, + // "phase": schema.StringAttribute{ + // Description: "A phase to skip the execution of. This option is only compatible with the products option.\nAvailable values: \"current\".", + // Optional: true, + // Validators: []validator.String{ + // customvalidator.RequiresOtherStringAttributeToBe( + // path.MatchRelative().AtParent().AtParent().AtName("action"), + // "skip", + // ), + // stringvalidator.OneOfCaseInsensitive("current"), + // }, + // }, + "phases": schema.ListAttribute{ + Description: "A list of phases to skip the execution of. This option is incompatible with the rulesets option.\nAvailable values: \"ddos_l4\", \"ddos_l7\", \"http_config_settings\", \"http_custom_errors\", \"http_log_custom_fields\", \"http_ratelimit\", \"http_request_cache_settings\", \"http_request_dynamic_redirect\", \"http_request_firewall_custom\", \"http_request_firewall_managed\", \"http_request_late_transform\", \"http_request_origin\", \"http_request_redirect\", \"http_request_sanitize\", \"http_request_sbfm\", \"http_request_transform\", \"http_response_compression\", \"http_response_firewall_managed\", \"http_response_headers_transform\", \"magic_transit\", \"magic_transit_ids_managed\", \"magic_transit_managed\", \"magic_transit_ratelimit\".", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "skip", + ), + listvalidator.SizeAtLeast(1), + listvalidator.ValueStringsAre( + stringvalidator.OneOfCaseInsensitive( + "ddos_l4", + "ddos_l7", + "http_config_settings", + "http_custom_errors", + "http_log_custom_fields", + "http_ratelimit", + "http_request_cache_settings", + "http_request_dynamic_redirect", + "http_request_firewall_custom", + "http_request_firewall_managed", + "http_request_late_transform", + "http_request_origin", + "http_request_redirect", + "http_request_sanitize", + "http_request_sbfm", + "http_request_transform", + "http_response_compression", + "http_response_firewall_managed", + "http_response_headers_transform", + "magic_transit", + "magic_transit_ids_managed", + "magic_transit_managed", + "magic_transit_ratelimit", + ), + ), + }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, + }, + "products": schema.ListAttribute{ + Description: "A list of legacy security products to skip the execution of.\nAvailable values: \"bic\", \"hot\", \"rateLimit\", \"securityLevel\", \"uaBlock\", \"waf\", \"zoneLockdown\".", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "skip", + ), + listvalidator.SizeAtLeast(1), + listvalidator.ValueStringsAre( + stringvalidator.OneOfCaseInsensitive( + "bic", + "hot", + "rateLimit", + "securityLevel", + "uaBlock", + "waf", + "zoneLockdown", + ), + ), + }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, + }, + "rules": schema.MapAttribute{ + Description: "A mapping of ruleset IDs to a list of rule IDs in that ruleset to skip the execution of. This option is incompatible with the ruleset option.", + Optional: true, + Validators: []validator.Map{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "skip", + ), + mapvalidator.SizeAtLeast(1), + mapvalidator.ValueListsAre( + listvalidator.SizeAtLeast(1), + listvalidator.ValueStringsAre( + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + ), + ), + }, + CustomType: customfield.NewMapType[customfield.List[types.String]](ctx), + ElementType: types.ListType{ + ElemType: types.StringType, + }, + }, + "ruleset": schema.StringAttribute{ + Description: "A ruleset to skip the execution of. This option is incompatible with the rulesets option.\nAvailable values: \"current\".", + Optional: true, + Validators: []validator.String{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "skip", + ), + stringvalidator.OneOfCaseInsensitive("current"), + }, + }, + "rulesets": schema.ListAttribute{ + Description: "A list of ruleset IDs to skip the execution of. This option is incompatible with the ruleset and phases options.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "skip", + ), + listvalidator.SizeAtLeast(1), + listvalidator.ValueStringsAre( + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + ), + }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, + }, + "additional_cacheable_ports": schema.ListAttribute{ + Description: "A list of additional ports that caching should be enabled on.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + listvalidator.SizeAtLeast(1), + listvalidator.ValueInt64sAre(int64validator.Between(1, 65535)), + }, + CustomType: customfield.NewListType[types.Int64](ctx), + ElementType: types.Int64Type, + }, + "browser_ttl": schema.SingleNestedAttribute{ + Description: "How long client browsers should cache the response. Cloudflare cache purge will not purge content cached on client browsers, so high browser TTLs may lead to stale content.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersBrowserTTLModel](ctx), + Attributes: map[string]schema.Attribute{ + "mode": schema.StringAttribute{ + Description: "The browser TTL mode.\nAvailable values: \"respect_origin\", \"bypass_by_default\", \"override_origin\", \"bypass\".", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "respect_origin", + "bypass_by_default", + "override_origin", + "bypass", + ), + }, + }, + "default": schema.Int64Attribute{ + Description: "The browser TTL (in seconds) if you choose the \"override_origin\" mode.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + }, + }, + "cache": schema.BoolAttribute{ + Description: "Whether the request's response from the origin is eligible for caching. Caching itself will still depend on the cache control header and your other caching configurations.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + }, + "cache_key": schema.SingleNestedAttribute{ + Description: "Which components of the request are included in or excluded from the cache key Cloudflare uses to store the response in cache.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "cache_by_device_type": schema.BoolAttribute{ + Description: "Whether to separate cached content based on the visitor's device type.", + Optional: true, + }, + "cache_deception_armor": schema.BoolAttribute{ + Description: "Whether to protect from web cache deception attacks, while allowing static assets to be cached.", + Optional: true, + }, + "custom_key": schema.SingleNestedAttribute{ + Description: "Which components of the request are included or excluded from the cache key.", + Optional: true, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "cookie": schema.SingleNestedAttribute{ + Description: "Which cookies to include in the cache key.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersAutominifyModel](ctx), + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyCookieModel](ctx), Attributes: map[string]schema.Attribute{ - "css": schema.BoolAttribute{ - Description: "Whether to minify CSS files.", - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), - }, - "html": schema.BoolAttribute{ - Description: "Whether to minify HTML files.", - Computed: true, + "check_presence": schema.ListAttribute{ + Description: "A list of cookies to check for the presence of. The presence of these cookies is included in the cache key.", Optional: true, - Default: booldefault.StaticBool(false), - }, - "js": schema.BoolAttribute{ - Description: "Whether to minify JavaScript files.", - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), - }, - }, - }, - "bic": schema.BoolAttribute{ - Description: "Whether to enable Browser Integrity Check (BIC).", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "disable_apps": schema.BoolAttribute{ - Description: "Whether to disable Cloudflare Apps.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - boolvalidator.Equals(true), - }, - }, - "disable_rum": schema.BoolAttribute{ - Description: "Whether to disable Real User Monitoring (RUM).", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - boolvalidator.Equals(true), - }, - }, - "disable_zaraz": schema.BoolAttribute{ - Description: "Whether to disable Zaraz.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - boolvalidator.Equals(true), - }, - }, - "email_obfuscation": schema.BoolAttribute{ - Description: "Whether to enable Email Obfuscation.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "fonts": schema.BoolAttribute{ - Description: "Whether to enable Cloudflare Fonts.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "hotlink_protection": schema.BoolAttribute{ - Description: "Whether to enable Hotlink Protection.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "mirage": schema.BoolAttribute{ - Description: "Whether to enable Mirage.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "opportunistic_encryption": schema.BoolAttribute{ - Description: "Whether to enable Opportunistic Encryption.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "polish": schema.StringAttribute{ - Description: "The Polish level to configure.\nAvailable values: \"off\", \"lossless\", \"lossy\", \"webp\".", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - stringvalidator.OneOfCaseInsensitive( - "off", - "lossless", - "lossy", - "webp", - ), - }, - }, - "rocket_loader": schema.BoolAttribute{ - Description: "Whether to enable Rocket Loader.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "security_level": schema.StringAttribute{ - Description: "The Security Level to configure.\nAvailable values: \"off\", \"essentially_off\", \"low\", \"medium\", \"high\", \"under_attack\".", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - stringvalidator.OneOfCaseInsensitive( - "off", - "essentially_off", - "low", - "medium", - "high", - "under_attack", - ), - }, - }, - "server_side_excludes": schema.BoolAttribute{ - Description: "Whether to enable Server-Side Excludes.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - "ssl": schema.StringAttribute{ - Description: "The SSL level to configure.\nAvailable values: \"off\", \"flexible\", \"full\", \"strict\", \"origin_pull\".", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - stringvalidator.OneOfCaseInsensitive( - "off", - "flexible", - "full", - "strict", - "origin_pull", - ), - }, - }, - "sxg": schema.BoolAttribute{ - Description: "Whether to enable Signed Exchanges (SXG).", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_config", - ), - }, - }, - // "phase": schema.StringAttribute{ - // Description: "A phase to skip the execution of. This option is only compatible with the products option.\nAvailable values: \"current\".", - // Optional: true, - // Validators: []validator.String{ - // customvalidator.RequiresOtherStringAttributeToBe( - // path.MatchRelative().AtParent().AtParent().AtName("action"), - // "skip", - // ), - // stringvalidator.OneOfCaseInsensitive("current"), - // }, - // }, - "phases": schema.ListAttribute{ - Description: "A list of phases to skip the execution of. This option is incompatible with the rulesets option.\nAvailable values: \"ddos_l4\", \"ddos_l7\", \"http_config_settings\", \"http_custom_errors\", \"http_log_custom_fields\", \"http_ratelimit\", \"http_request_cache_settings\", \"http_request_dynamic_redirect\", \"http_request_firewall_custom\", \"http_request_firewall_managed\", \"http_request_late_transform\", \"http_request_origin\", \"http_request_redirect\", \"http_request_sanitize\", \"http_request_sbfm\", \"http_request_transform\", \"http_response_compression\", \"http_response_firewall_managed\", \"http_response_headers_transform\", \"magic_transit\", \"magic_transit_ids_managed\", \"magic_transit_managed\", \"magic_transit_ratelimit\".", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "skip", - ), - listvalidator.SizeAtLeast(1), - listvalidator.ValueStringsAre( - stringvalidator.OneOfCaseInsensitive( - "ddos_l4", - "ddos_l7", - "http_config_settings", - "http_custom_errors", - "http_log_custom_fields", - "http_ratelimit", - "http_request_cache_settings", - "http_request_dynamic_redirect", - "http_request_firewall_custom", - "http_request_firewall_managed", - "http_request_late_transform", - "http_request_origin", - "http_request_redirect", - "http_request_sanitize", - "http_request_sbfm", - "http_request_transform", - "http_response_compression", - "http_response_firewall_managed", - "http_response_headers_transform", - "magic_transit", - "magic_transit_ids_managed", - "magic_transit_managed", - "magic_transit_ratelimit", - ), - ), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "products": schema.ListAttribute{ - Description: "A list of legacy security products to skip the execution of.\nAvailable values: \"bic\", \"hot\", \"rateLimit\", \"securityLevel\", \"uaBlock\", \"waf\", \"zoneLockdown\".", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "skip", - ), - listvalidator.SizeAtLeast(1), - listvalidator.ValueStringsAre( - stringvalidator.OneOfCaseInsensitive( - "bic", - "hot", - "rateLimit", - "securityLevel", - "uaBlock", - "waf", - "zoneLockdown", - ), - ), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "rules": schema.MapAttribute{ - Description: "A mapping of ruleset IDs to a list of rule IDs in that ruleset to skip the execution of. This option is incompatible with the ruleset option.", - Optional: true, - Validators: []validator.Map{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "skip", - ), - mapvalidator.SizeAtLeast(1), - mapvalidator.ValueListsAre( - listvalidator.SizeAtLeast(1), - listvalidator.ValueStringsAre( - stringvalidator.RegexMatches( - regexp.MustCompile("^[0-9a-f]{32}$"), - "value must be a 32-character hexadecimal string", - ), - ), - ), - }, - CustomType: customfield.NewMapType[customfield.List[types.String]](ctx), - ElementType: types.ListType{ - ElemType: types.StringType, - }, - }, - "ruleset": schema.StringAttribute{ - Description: "A ruleset to skip the execution of. This option is incompatible with the rulesets option.\nAvailable values: \"current\".", - Optional: true, - Validators: []validator.String{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "skip", - ), - stringvalidator.OneOfCaseInsensitive("current"), - }, - }, - "rulesets": schema.ListAttribute{ - Description: "A list of ruleset IDs to skip the execution of. This option is incompatible with the ruleset and phases options.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "skip", - ), - listvalidator.SizeAtLeast(1), - listvalidator.ValueStringsAre( - stringvalidator.RegexMatches( - regexp.MustCompile("^[0-9a-f]{32}$"), - "value must be a 32-character hexadecimal string", - ), - ), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "additional_cacheable_ports": schema.ListAttribute{ - Description: "A list of additional ports that caching should be enabled on.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - listvalidator.SizeAtLeast(1), - listvalidator.ValueInt64sAre(int64validator.Between(1, 65535)), - }, - CustomType: customfield.NewListType[types.Int64](ctx), - ElementType: types.Int64Type, - }, - "browser_ttl": schema.SingleNestedAttribute{ - Description: "How long client browsers should cache the response. Cloudflare cache purge will not purge content cached on client browsers, so high browser TTLs may lead to stale content.", - Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersBrowserTTLModel](ctx), - Attributes: map[string]schema.Attribute{ - "mode": schema.StringAttribute{ - Description: "The browser TTL mode.\nAvailable values: \"respect_origin\", \"bypass_by_default\", \"override_origin\", \"bypass\".", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "respect_origin", - "bypass_by_default", - "override_origin", - "bypass", - ), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, }, - "default": schema.Int64Attribute{ - Description: "The browser TTL (in seconds) if you choose the \"override_origin\" mode.", + "include": schema.ListAttribute{ + Description: "A list of cookies to include in the cache key.", Optional: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, }, }, }, - "cache": schema.BoolAttribute{ - Description: "Whether the request's response from the origin is eligible for caching. Caching itself will still depend on the cache control header and your other caching configurations.", + "header": schema.SingleNestedAttribute{ + Description: "Which headers to include in the cache key.", Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - }, - "cache_key": schema.SingleNestedAttribute{ - Description: "Which components of the request are included in or excluded from the cache key Cloudflare uses to store the response in cache.", - Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyModel](ctx), + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyHeaderModel](ctx), Attributes: map[string]schema.Attribute{ - "cache_by_device_type": schema.BoolAttribute{ - Description: "Whether to separate cached content based on the visitor's device type.", + "check_presence": schema.ListAttribute{ + Description: "A list of headers to check for the presence of. The presence of these headers is included in the cache key.", Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, }, - "cache_deception_armor": schema.BoolAttribute{ - Description: "Whether to protect from web cache deception attacks, while allowing static assets to be cached.", + "contains": schema.MapAttribute{ + Description: "A mapping of header names to a list of values. If a header is present in the request and contains any of the values provided, its value is included in the cache key.", Optional: true, + Validators: []validator.Map{ + mapvalidator.SizeAtLeast(1), + mapvalidator.ValueListsAre(listvalidator.SizeAtLeast(1)), + }, + CustomType: customfield.NewMapType[customfield.List[types.String]](ctx), + ElementType: types.ListType{ + ElemType: types.StringType, + }, }, - "custom_key": schema.SingleNestedAttribute{ - Description: "Which components of the request are included or excluded from the cache key.", + "exclude_origin": schema.BoolAttribute{ + Description: "Whether to exclude the origin header in the cache key.", Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyModel](ctx), - Attributes: map[string]schema.Attribute{ - "cookie": schema.SingleNestedAttribute{ - Description: "Which cookies to include in the cache key.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyCookieModel](ctx), - Attributes: map[string]schema.Attribute{ - "check_presence": schema.ListAttribute{ - Description: "A list of cookies to check for the presence of. The presence of these cookies is included in the cache key.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "include": schema.ListAttribute{ - Description: "A list of cookies to include in the cache key.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - }, - }, - "header": schema.SingleNestedAttribute{ - Description: "Which headers to include in the cache key.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyHeaderModel](ctx), - Attributes: map[string]schema.Attribute{ - "check_presence": schema.ListAttribute{ - Description: "A list of headers to check for the presence of. The presence of these headers is included in the cache key.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "contains": schema.MapAttribute{ - Description: "A mapping of header names to a list of values. If a header is present in the request and contains any of the values provided, its value is included in the cache key.", - Optional: true, - Validators: []validator.Map{ - mapvalidator.SizeAtLeast(1), - mapvalidator.ValueListsAre(listvalidator.SizeAtLeast(1)), - }, - CustomType: customfield.NewMapType[customfield.List[types.String]](ctx), - ElementType: types.ListType{ - ElemType: types.StringType, - }, - }, - "exclude_origin": schema.BoolAttribute{ - Description: "Whether to exclude the origin header in the cache key.", - Optional: true, - }, - "include": schema.ListAttribute{ - Description: "A list of headers to include in the cache key.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - }, - }, - "host": schema.SingleNestedAttribute{ - Description: "How to use the host in the cache key.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyHostModel](ctx), - Attributes: map[string]schema.Attribute{ - "resolved": schema.BoolAttribute{ - Description: "Whether to use the resolved host in the cache key.", - Optional: true, - }, - }, - }, - "query_string": schema.SingleNestedAttribute{ - Description: "Which query string parameters to include in or exclude from the cache key.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyQueryStringModel](ctx), - Attributes: map[string]schema.Attribute{ - "include": schema.SingleNestedAttribute{ - Description: "Which query string parameters to include in the cache key.", - Optional: true, - Validators: []validator.Object{ - objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("exclude")), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyQueryStringIncludeModel](ctx), - Attributes: map[string]schema.Attribute{ - "list": schema.ListAttribute{ - Description: "A list of query string parameters to include in the cache key.", - Optional: true, - Validators: []validator.List{ - listvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("all")), - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "all": schema.BoolAttribute{ - Description: "Whether to include all query string parameters in the cache key.", - Optional: true, - Validators: []validator.Bool{ - boolvalidator.Equals(true), - }, - }, - }, - }, - "exclude": schema.SingleNestedAttribute{ - Description: "Which query string parameters to exclude from the cache key.", - Optional: true, - Validators: []validator.Object{ - objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("include")), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyQueryStringExcludeModel](ctx), - Attributes: map[string]schema.Attribute{ - "list": schema.ListAttribute{ - Description: "A list of query string parameters to exclude from the cache key.", - Optional: true, - Validators: []validator.List{ - listvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("all")), - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "all": schema.BoolAttribute{ - Description: "Whether to exclude all query string parameters from the cache key.", - Optional: true, - Validators: []validator.Bool{ - boolvalidator.Equals(true), - }, - }, - }, - }, - }, - }, - "user": schema.SingleNestedAttribute{ - Description: "How to use characteristics of the request user agent in the cache key.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyUserModel](ctx), - Attributes: map[string]schema.Attribute{ - "device_type": schema.BoolAttribute{ - Description: "Whether to use the user agent's device type in the cache key.", - Optional: true, - }, - "geo": schema.BoolAttribute{ - Description: "Whether to use the user agents's country in the cache key.", - Optional: true, - }, - "lang": schema.BoolAttribute{ - Description: "Whether to use the user agent's language in the cache key.", - Optional: true, - }, - }, - }, - }, }, - "ignore_query_strings_order": schema.BoolAttribute{ - Description: "Whether to treat requests with the same query parameters the same, regardless of the order those query parameters are in.", + "include": schema.ListAttribute{ + Description: "A list of headers to include in the cache key.", Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, }, }, }, - "cache_reserve": schema.SingleNestedAttribute{ - Description: "Settings to determine whether the request's response from origin is eligible for Cache Reserve (requires a Cache Reserve add-on plan).", + "host": schema.SingleNestedAttribute{ + Description: "How to use the host in the cache key.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheReserveModel](ctx), + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyHostModel](ctx), Attributes: map[string]schema.Attribute{ - "eligible": schema.BoolAttribute{ - Description: "Whether Cache Reserve is enabled. If this is true and a request meets eligibility criteria, Cloudflare will write the resource to Cache Reserve.", - Required: true, - }, - "minimum_file_size": schema.Int64Attribute{ - Description: "The minimum file size eligible for storage in Cache Reserve.", + "resolved": schema.BoolAttribute{ + Description: "Whether to use the resolved host in the cache key.", Optional: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - }, }, }, }, - "edge_ttl": schema.SingleNestedAttribute{ - Description: "How long the Cloudflare edge network should cache the response.", + "query_string": schema.SingleNestedAttribute{ + Description: "Which query string parameters to include in or exclude from the cache key.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersEdgeTTLModel](ctx), + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyQueryStringModel](ctx), Attributes: map[string]schema.Attribute{ - "default": schema.Int64Attribute{ - Description: "The edge TTL (in seconds) if you choose the \"override_origin\" mode.", + "include": schema.SingleNestedAttribute{ + Description: "Which query string parameters to include in the cache key.", Optional: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), + Validators: []validator.Object{ + objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("exclude")), }, - }, - "mode": schema.StringAttribute{ - Description: "The edge TTL mode.\nAvailable values: \"respect_origin\", \"bypass_by_default\", \"override_origin\".", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOfCaseInsensitive( - "respect_origin", - "bypass_by_default", - "override_origin", - ), + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyQueryStringIncludeModel](ctx), + Attributes: map[string]schema.Attribute{ + "list": schema.ListAttribute{ + Description: "A list of query string parameters to include in the cache key.", + Optional: true, + Validators: []validator.List{ + listvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("all")), + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, + }, + "all": schema.BoolAttribute{ + Description: "Whether to include all query string parameters in the cache key.", + Optional: true, + Validators: []validator.Bool{ + boolvalidator.Equals(true), + }, + }, }, }, - "status_code_ttl": schema.ListNestedAttribute{ - Description: "A list of TTLs to apply to specific status codes or status code ranges.", + "exclude": schema.SingleNestedAttribute{ + Description: "Which query string parameters to exclude from the cache key.", Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), + Validators: []validator.Object{ + objectvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("include")), }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersEdgeTTLStatusCodeTTLModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "status_code_range": schema.SingleNestedAttribute{ - Description: "A range of status codes to apply the TTL to.", - Optional: true, - Validators: []validator.Object{ - objectvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("status_code")), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersEdgeTTLStatusCodeTTLStatusCodeRangeModel](ctx), - Attributes: map[string]schema.Attribute{ - "from": schema.Int64Attribute{ - Description: "The lower bound of the range.", - Optional: true, - Validators: []validator.Int64{ - int64validator.AtLeastOneOf(path.MatchRelative().AtParent().AtName("to")), - int64validator.Between(100, 999), - }, - }, - "to": schema.Int64Attribute{ - Description: "The upper bound of the range.", - Optional: true, - Validators: []validator.Int64{ - int64validator.Between(100, 999), - }, - }, - }, - }, - "status_code": schema.Int64Attribute{ - Description: "A single status code to apply the TTL to.", - Optional: true, - Validators: []validator.Int64{ - int64validator.Between(100, 999), - }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyQueryStringExcludeModel](ctx), + Attributes: map[string]schema.Attribute{ + "list": schema.ListAttribute{ + Description: "A list of query string parameters to exclude from the cache key.", + Optional: true, + Validators: []validator.List{ + listvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("all")), + listvalidator.SizeAtLeast(1), }, - "value": schema.Int64Attribute{ - Description: "The time to cache the response for (in seconds). A value of 0 is equivalent to setting the cache control header with the value \"no-cache\". A value of -1 is equivalent to setting the cache control header with the value of \"no-store\".", - Required: true, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, + }, + "all": schema.BoolAttribute{ + Description: "Whether to exclude all query string parameters from the cache key.", + Optional: true, + Validators: []validator.Bool{ + boolvalidator.Equals(true), }, }, }, }, }, }, - "origin_cache_control": schema.BoolAttribute{ - Description: "Whether Cloudflare will aim to strictly adhere to RFC 7234.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - }, - "origin_error_page_passthru": schema.BoolAttribute{ - Description: "Whether to generate Cloudflare error pages for issues from the origin server.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - }, - "read_timeout": schema.Int64Attribute{ - Description: "A timeout value between two successive read operations to use for your origin server. Historically, the timeout value between two read options from Cloudflare to an origin server is 100 seconds. If you are attempting to reduce HTTP 524 errors because of timeouts from an origin server, try increasing this timeout value.", - Optional: true, - Validators: []validator.Int64{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - int64validator.Between(100, 6000), - }, - }, - "respect_strong_etags": schema.BoolAttribute{ - Description: "Whether Cloudflare should respect strong ETag (entity tag) headers. If false, Cloudflare converts strong ETag headers to weak ETag headers.", - Optional: true, - Validators: []validator.Bool{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - }, - "serve_stale": schema.SingleNestedAttribute{ - Description: "When to serve stale content from cache.", + "user": schema.SingleNestedAttribute{ + Description: "How to use characteristics of the request user agent in the cache key.", Optional: true, - Validators: []validator.Object{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "set_cache_settings", - ), - }, - CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersServeStaleModel](ctx), + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheKeyCustomKeyUserModel](ctx), Attributes: map[string]schema.Attribute{ - "disable_stale_while_updating": schema.BoolAttribute{ - Description: "Whether Cloudflare should disable serving stale content while getting the latest content from the origin.", + "device_type": schema.BoolAttribute{ + Description: "Whether to use the user agent's device type in the cache key.", Optional: true, }, - }, - }, - "cookie_fields": schema.ListNestedAttribute{ - Description: "The cookie fields to log.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "log_custom_field", - ), - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersCookieFieldsModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "The name of the cookie.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, + "geo": schema.BoolAttribute{ + Description: "Whether to use the user agents's country in the cache key.", + Optional: true, }, - }, - }, - "raw_response_fields": schema.ListNestedAttribute{ - Description: "The raw response fields to log.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "log_custom_field", - ), - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersRawResponseFieldsModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "The name of the response header.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "preserve_duplicates": schema.BoolAttribute{ - Description: "Whether to log duplicate values of the same header.", - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), - }, + "lang": schema.BoolAttribute{ + Description: "Whether to use the user agent's language in the cache key.", + Optional: true, }, }, }, - "request_fields": schema.ListNestedAttribute{ - Description: "The raw request fields to log.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "log_custom_field", - ), - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersRequestFieldsModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "The name of the header.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, + }, + }, + "ignore_query_strings_order": schema.BoolAttribute{ + Description: "Whether to treat requests with the same query parameters the same, regardless of the order those query parameters are in.", + Optional: true, + }, + }, + }, + "cache_reserve": schema.SingleNestedAttribute{ + Description: "Settings to determine whether the request's response from origin is eligible for Cache Reserve (requires a Cache Reserve add-on plan).", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersCacheReserveModel](ctx), + Attributes: map[string]schema.Attribute{ + "eligible": schema.BoolAttribute{ + Description: "Whether Cache Reserve is enabled. If this is true and a request meets eligibility criteria, Cloudflare will write the resource to Cache Reserve.", + Required: true, + }, + "minimum_file_size": schema.Int64Attribute{ + Description: "The minimum file size eligible for storage in Cache Reserve.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + }, + }, + "edge_ttl": schema.SingleNestedAttribute{ + Description: "How long the Cloudflare edge network should cache the response.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersEdgeTTLModel](ctx), + Attributes: map[string]schema.Attribute{ + "default": schema.Int64Attribute{ + Description: "The edge TTL (in seconds) if you choose the \"override_origin\" mode.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "mode": schema.StringAttribute{ + Description: "The edge TTL mode.\nAvailable values: \"respect_origin\", \"bypass_by_default\", \"override_origin\".", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive( + "respect_origin", + "bypass_by_default", + "override_origin", + ), + }, + }, + "status_code_ttl": schema.ListNestedAttribute{ + Description: "A list of TTLs to apply to specific status codes or status code ranges.", + Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersEdgeTTLStatusCodeTTLModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "status_code_range": schema.SingleNestedAttribute{ + Description: "A range of status codes to apply the TTL to.", + Optional: true, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("status_code")), }, - }, - }, - "response_fields": schema.ListNestedAttribute{ - Description: "The transformed response fields to log.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "log_custom_field", - ), - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersResponseFieldsModel](ctx), - NestedObject: schema.NestedAttributeObject{ + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersEdgeTTLStatusCodeTTLStatusCodeRangeModel](ctx), Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "The name of the response header.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), + "from": schema.Int64Attribute{ + Description: "The lower bound of the range.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeastOneOf(path.MatchRelative().AtParent().AtName("to")), + int64validator.Between(100, 999), }, }, - "preserve_duplicates": schema.BoolAttribute{ - Description: "Whether to log duplicate values of the same header.", - Computed: true, + "to": schema.Int64Attribute{ + Description: "The upper bound of the range.", Optional: true, - Default: booldefault.StaticBool(false), + Validators: []validator.Int64{ + int64validator.Between(100, 999), + }, }, }, }, - }, - "transformed_request_fields": schema.ListNestedAttribute{ - Description: "The transformed request fields to log.", - Optional: true, - Validators: []validator.List{ - customvalidator.RequiresOtherStringAttributeToBe( - path.MatchRelative().AtParent().AtParent().AtName("action"), - "log_custom_field", - ), - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersTransformedRequestFieldsModel](ctx), - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "The name of the header.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, + "status_code": schema.Int64Attribute{ + Description: "A single status code to apply the TTL to.", + Optional: true, + Validators: []validator.Int64{ + int64validator.Between(100, 999), }, }, + "value": schema.Int64Attribute{ + Description: "The time to cache the response for (in seconds). A value of 0 is equivalent to setting the cache control header with the value \"no-cache\". A value of -1 is equivalent to setting the cache control header with the value of \"no-store\".", + Required: true, + }, }, }, }, - "description": schema.StringAttribute{ - Description: "An informative description of the rule.", - Computed: true, - Optional: true, - Default: stringdefault.StaticString(""), - }, - "enabled": schema.BoolAttribute{ - Description: "Whether the rule should be executed.", - Computed: true, + }, + }, + "origin_cache_control": schema.BoolAttribute{ + Description: "Whether Cloudflare will aim to strictly adhere to RFC 7234.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + }, + "origin_error_page_passthru": schema.BoolAttribute{ + Description: "Whether to generate Cloudflare error pages for issues from the origin server.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + }, + "read_timeout": schema.Int64Attribute{ + Description: "A timeout value between two successive read operations to use for your origin server. Historically, the timeout value between two read options from Cloudflare to an origin server is 100 seconds. If you are attempting to reduce HTTP 524 errors because of timeouts from an origin server, try increasing this timeout value.", + Optional: true, + Validators: []validator.Int64{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + int64validator.Between(100, 6000), + }, + }, + "respect_strong_etags": schema.BoolAttribute{ + Description: "Whether Cloudflare should respect strong ETag (entity tag) headers. If false, Cloudflare converts strong ETag headers to weak ETag headers.", + Optional: true, + Validators: []validator.Bool{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + }, + "serve_stale": schema.SingleNestedAttribute{ + Description: "When to serve stale content from cache.", + Optional: true, + Validators: []validator.Object{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "set_cache_settings", + ), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesActionParametersServeStaleModel](ctx), + Attributes: map[string]schema.Attribute{ + "disable_stale_while_updating": schema.BoolAttribute{ + Description: "Whether Cloudflare should disable serving stale content while getting the latest content from the origin.", Optional: true, - Default: booldefault.StaticBool(true), }, - "exposed_credential_check": schema.SingleNestedAttribute{ - Description: "Configuration for exposed credential checking.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesExposedCredentialCheckModel](ctx), - Attributes: map[string]schema.Attribute{ - "password_expression": schema.StringAttribute{ - Description: "An expression that selects the password used in the credentials check.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "username_expression": schema.StringAttribute{ - Description: "An expression that selects the user ID used in the credentials check.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + }, + }, + "cookie_fields": schema.ListNestedAttribute{ + Description: "The cookie fields to log.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "log_custom_field", + ), + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersCookieFieldsModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the cookie.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, }, }, - "expression": schema.StringAttribute{ - Description: "The expression defining which traffic will match the rule.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), + }, + }, + "raw_response_fields": schema.ListNestedAttribute{ + Description: "The raw response fields to log.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "log_custom_field", + ), + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersRawResponseFieldsModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the response header.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, }, - }, - "logging": schema.SingleNestedAttribute{ - Description: "An object configuring the rule's logging behavior.", - Computed: true, - Optional: true, - Validators: []validator.Object{ - objectvalidator.AlsoRequires(path.MatchRelative().AtName("enabled")), + "preserve_duplicates": schema.BoolAttribute{ + Description: "Whether to log duplicate values of the same header.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), }, - CustomType: customfield.NewNestedObjectType[RulesetRulesLoggingModel](ctx), - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Whether to generate a log when the rule matches.", - Computed: true, - Optional: true, + }, + }, + }, + "request_fields": schema.ListNestedAttribute{ + Description: "The raw request fields to log.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "log_custom_field", + ), + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersRequestFieldsModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the header.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, }, }, - "ratelimit": schema.SingleNestedAttribute{ - Description: "An object configuring the rule's rate limit behavior.", - Optional: true, - CustomType: customfield.NewNestedObjectType[RulesetRulesRatelimitModel](ctx), - Attributes: map[string]schema.Attribute{ - "characteristics": schema.ListAttribute{ - Description: "Characteristics of the request on which the rate limit counter will be incremented.", - Required: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - CustomType: customfield.NewListType[types.String](ctx), - ElementType: types.StringType, - }, - "period": schema.Int64Attribute{ - Description: "Period in seconds over which the counter is being incremented.", - Required: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - }, - }, - "counting_expression": schema.StringAttribute{ - Description: "An expression that defines when the rate limit counter should be incremented. It defaults to the same as the rule's expression.", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "mitigation_timeout": schema.Int64Attribute{ - Description: "Period of time in seconds after which the action will be disabled following its first execution.", - Computed: true, - Optional: true, - }, - "requests_per_period": schema.Int64Attribute{ - Description: "The threshold of requests per period after which the action will be executed for the first time.", - Optional: true, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - }, - }, - "requests_to_origin": schema.BoolAttribute{ - Description: "Whether counting is only performed when an origin is reached.", - Computed: true, - Optional: true, - Default: booldefault.StaticBool(false), - }, - "score_per_period": schema.Int64Attribute{ - Description: "The score threshold per period for which the action will be executed the first time.", - Optional: true, - }, - "score_response_header_name": schema.StringAttribute{ - Description: "A response header name provided by the origin, which contains the score to increment rate limit counter with.", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, + }, + }, + "response_fields": schema.ListNestedAttribute{ + Description: "The transformed response fields to log.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "log_custom_field", + ), + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersResponseFieldsModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the response header.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, }, + "preserve_duplicates": schema.BoolAttribute{ + Description: "Whether to log duplicate values of the same header.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, }, - "ref": schema.StringAttribute{ - Description: "The reference of the rule (the rule's ID by default).", - Computed: true, - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), + }, + }, + "transformed_request_fields": schema.ListNestedAttribute{ + Description: "The transformed request fields to log.", + Optional: true, + Validators: []validator.List{ + customvalidator.RequiresOtherStringAttributeToBe( + path.MatchRelative().AtParent().AtParent().AtName("action"), + "log_custom_field", + ), + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewNestedObjectListType[RulesetRulesActionParametersTransformedRequestFieldsModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the header.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, }, }, }, }, }, - "last_updated": schema.StringAttribute{ - Description: "The timestamp of when the ruleset was last modified.", - Computed: true, - CustomType: timetypes.RFC3339Type{}, + }, + "description": schema.StringAttribute{ + Description: "An informative description of the rule.", + Computed: true, + Optional: true, + Default: stringdefault.StaticString(""), + }, + "enabled": schema.BoolAttribute{ + Description: "Whether the rule should be executed.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(true), + }, + "exposed_credential_check": schema.SingleNestedAttribute{ + Description: "Configuration for exposed credential checking.", + Optional: true, + CustomType: customfield.NewNestedObjectType[RulesetRulesExposedCredentialCheckModel](ctx), + Attributes: map[string]schema.Attribute{ + "password_expression": schema.StringAttribute{ + Description: "An expression that selects the password used in the credentials check.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "username_expression": schema.StringAttribute{ + Description: "An expression that selects the user ID used in the credentials check.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, }, - "version": schema.StringAttribute{ - Description: "The version of the ruleset.", - Computed: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile("^[0-9]+$"), - "value must be a non-empty string containing only numbers", - ), + }, + "expression": schema.StringAttribute{ + Description: "The expression defining which traffic will match the rule.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "logging": schema.SingleNestedAttribute{ + Description: "An object configuring the rule's logging behavior.", + Computed: true, + Optional: true, + Validators: []validator.Object{ + objectvalidator.AlsoRequires(path.MatchRelative().AtName("enabled")), + }, + CustomType: customfield.NewNestedObjectType[RulesetRulesLoggingModel](ctx), + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Description: "Whether to generate a log when the rule matches.", + Computed: true, + Optional: true, + }, + }, + }, + "ratelimit": schema.SingleNestedAttribute{ + Description: "An object configuring the rule's rate limit behavior.", + Optional: true, + CustomType: customfield.NewNestedObjectType[RulesetRulesRatelimitModel](ctx), + Attributes: map[string]schema.Attribute{ + "characteristics": schema.ListAttribute{ + Description: "Characteristics of the request on which the rate limit counter will be incremented.", + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + CustomType: customfield.NewListType[types.String](ctx), + ElementType: types.StringType, }, + "period": schema.Int64Attribute{ + Description: "Period in seconds over which the counter is being incremented.", + Required: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "counting_expression": schema.StringAttribute{ + Description: "An expression that defines when the rate limit counter should be incremented. It defaults to the same as the rule's expression.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "mitigation_timeout": schema.Int64Attribute{ + Description: "Period of time in seconds after which the action will be disabled following its first execution.", + Computed: true, + Optional: true, + }, + "requests_per_period": schema.Int64Attribute{ + Description: "The threshold of requests per period after which the action will be executed for the first time.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + }, + "requests_to_origin": schema.BoolAttribute{ + Description: "Whether counting is only performed when an origin is reached.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, + "score_per_period": schema.Int64Attribute{ + Description: "The score threshold per period for which the action will be executed the first time.", + Optional: true, + }, + "score_response_header_name": schema.StringAttribute{ + Description: "A response header name provided by the origin, which contains the score to increment rate limit counter with.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + }, + "ref": schema.StringAttribute{ + Description: "The reference of the rule (the rule's ID by default).", + Computed: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, }, } } +func RuleNestedSchema(ctx context.Context) schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: RuleAttributes(ctx), + } +} + func (r *RulesetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = ResourceSchema(ctx) } diff --git a/internal/services/ruleset_rule/README.md b/internal/services/ruleset_rule/README.md new file mode 100644 index 0000000000..29b6f23a67 --- /dev/null +++ b/internal/services/ruleset_rule/README.md @@ -0,0 +1,118 @@ +# Cloudflare Ruleset Rule Resource + +This resource manages individual rules within existing Cloudflare Rulesets. It provides a way to manage rules separately from the parent ruleset, similar to how AWS manages security group rules. + +## Key Features + +- **Individual Rule Management**: Manage single rules within existing rulesets without affecting other rules +- **Account and Zone Support**: Works with both account-level and zone-level rulesets +- **Full CRUD Operations**: Create, read, update, and delete individual rules +- **Import Support**: Import existing rules using various ID formats +- **No GET Endpoint Handling**: Gracefully handles the lack of individual rule GET endpoint by reading the entire ruleset + +## API Endpoints Used + +- `POST /{accounts_or_zones}/{account_or_zone_id}/rulesets/{ruleset_id}/rules` - Create rule +- `PATCH /{accounts_or_zones}/{account_or_zone_id}/rulesets/{ruleset_id}/rules/{rule_id}` - Update rule +- `DELETE /{accounts_or_zones}/{account_or_zone_id}/rulesets/{ruleset_id}/rules/{rule_id}` - Delete rule +- `GET /{accounts_or_zones}/{account_or_zone_id}/rulesets/{ruleset_id}` - Read ruleset (to find individual rule) + +## Import Formats + +The resource supports multiple import formats: + +1. `{account_or_zone_id}/{ruleset_id}/{rule_id}` - Auto-detects account vs zone +2. `account/{account_id}/{ruleset_id}/{rule_id}` - Explicit account-level +3. `zone/{zone_id}/{ruleset_id}/{rule_id}` - Explicit zone-level + +## Example Usage + +```hcl +# Create a parent ruleset first +resource "cloudflare_ruleset" "example" { + zone_id = "your_zone_id" + name = "Example Ruleset" + description = "Example ruleset for testing" + kind = "zone" + phase = "http_request_firewall_custom" +} + +# Create individual rules within the ruleset +resource "cloudflare_ruleset_rule" "block_bad_ips" { + zone_id = "your_zone_id" + ruleset_id = cloudflare_ruleset.example.id + action = "block" + expression = "ip.src in {192.0.2.0/24}" + description = "Block traffic from bad IP range" + enabled = true +} + +resource "cloudflare_ruleset_rule" "redirect_example" { + zone_id = "your_zone_id" + ruleset_id = cloudflare_ruleset.example.id + action = "redirect" + expression = "http.request.uri.path eq \"/old-path\"" + description = "Redirect old path to new location" + enabled = true + + action_parameters { + from_value { + status_code = 301 + target_url { + value = "https://example.com/new-path" + } + preserve_query_string = true + } + } +} +``` + +## Implementation Notes + +### Handling Missing GET Endpoint + +Since Cloudflare doesn't provide a GET endpoint for individual rules, this resource: + +1. **On Read**: Fetches the entire parent ruleset and searches for the rule by ID +2. **On Create**: Creates the rule and extracts the ID from the returned ruleset +3. **On Update/Delete**: Uses the rule ID directly with the appropriate endpoints + +### State Management + +- The resource tracks the parent ruleset's `last_updated` and `version` fields +- If a rule is not found during read, it's considered deleted and removed from state +- The resource requires either `account_id` or `zone_id` to be specified + +### Action Parameters + +The resource supports comprehensive action parameters for different rule types: + +- **Managed Ruleset Execution**: `id`, `ruleset`, `rulesets`, `overrides` +- **URI Rewriting**: `uri.path`, `uri.query` +- **Header Modifications**: `headers` array with operations +- **Custom Responses**: `response` with status code and content +- **Redirects**: `from_value` with target URL and options +- **Rate Limiting**: `ratelimit` configuration +- **Skip Actions**: `increment` parameter + +### Validation + +- Validates account/zone ID format (32-character hex) +- Validates ruleset ID format +- Ensures exactly one of `account_id` or `zone_id` is specified +- Validates action parameter combinations based on action type + +## Testing + +The resource includes comprehensive tests: + +- Basic CRUD operations +- Update scenarios +- Import functionality +- Error handling +- Account vs zone level operations + +Run tests with: +```bash +go test ./internal/services/ruleset_rule/... +``` diff --git a/internal/services/ruleset_rule/data_source.go b/internal/services/ruleset_rule/data_source.go new file mode 100644 index 0000000000..8ccbfe6b7c --- /dev/null +++ b/internal/services/ruleset_rule/data_source.go @@ -0,0 +1,130 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package ruleset_rule + +import ( + "context" + "fmt" + "io" + "net/http" + + "github.com/cloudflare/cloudflare-go/v6" + "github.com/cloudflare/cloudflare-go/v6/option" + "github.com/cloudflare/terraform-provider-cloudflare/internal/apijsoncustom" + "github.com/cloudflare/terraform-provider-cloudflare/internal/logging" + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset" + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +type RulesetRuleDataSource struct { + client *cloudflare.Client +} + +var _ datasource.DataSourceWithConfigure = (*RulesetRuleDataSource)(nil) + +func NewRulesetRuleDataSource() datasource.DataSource { + return &RulesetRuleDataSource{} +} + +func (d *RulesetRuleDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ruleset_rule" +} + +func (d *RulesetRuleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*cloudflare.Client) + + if !ok { + resp.Diagnostics.AddError( + "unexpected resource configure type", + fmt.Sprintf("Expected *cloudflare.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *RulesetRuleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data *RulesetRuleDataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Prepare parameters for reading the parent ruleset + params, diags := data.toReadParams(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Read the entire ruleset to find the specific rule + res := new(http.Response) + rulesetEnv := ruleset.RulesetResultDataSourceEnvelope{} + + _, err := d.client.Rulesets.Get( + ctx, + data.RulesetID.ValueString(), + params, + option.WithResponseBodyInto(&res), + option.WithMiddleware(logging.Middleware(ctx)), + ) + if err != nil { + resp.Diagnostics.AddError("failed to make http request", err.Error()) + return + } + + bytes, _ := io.ReadAll(res.Body) + err = apijsoncustom.UnmarshalComputed(bytes, &rulesetEnv) + if err != nil { + resp.Diagnostics.AddError("failed to deserialize http request", err.Error()) + return + } + + // Find the specific rule within the ruleset + ruleID := data.RuleID.ValueString() + found := false + + // Convert the custom field list to a slice + rules, diags := rulesetEnv.Result.Rules.AsStructSliceT(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + for _, rule := range rules { + if rule.ID.ValueString() == ruleID { + // Copy rule data to our model (preserve lookup fields) + data.ID = rule.ID + data.Action = rule.Action + data.ActionParameters = rule.ActionParameters + data.Description = rule.Description + data.Enabled = rule.Enabled + data.ExposedCredentialCheck = rule.ExposedCredentialCheck + data.Expression = rule.Expression + data.Logging = rule.Logging + data.Ratelimit = rule.Ratelimit + data.Ref = rule.Ref + data.Categories = rule.Categories + found = true + break + } + } + + if !found { + resp.Diagnostics.AddError( + "rule not found", + fmt.Sprintf("Rule with ID %s not found in ruleset %s", ruleID, data.RulesetID.ValueString()), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/services/ruleset_rule/data_source_model.go b/internal/services/ruleset_rule/data_source_model.go new file mode 100644 index 0000000000..a18658fd75 --- /dev/null +++ b/internal/services/ruleset_rule/data_source_model.go @@ -0,0 +1,93 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package ruleset_rule + +import ( + "context" + + "github.com/cloudflare/cloudflare-go/v6" + "github.com/cloudflare/cloudflare-go/v6/rulesets" + "github.com/cloudflare/terraform-provider-cloudflare/internal/customfield" + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type RulesetRuleResultDataSourceEnvelope struct { +} + +// RulesetRuleDataSourceModel represents the data source model for a single ruleset rule. +type RulesetRuleDataSourceModel struct { + // Rule fields from ruleset (includes ID) + ID types.String `tfsdk:"id" json:"id,computed"` + Action types.String `tfsdk:"action" json:"action,computed"` + ActionParameters customfield.NestedObject[RulesetRuleActionParametersDataSourceModel] `tfsdk:"action_parameters" json:"action_parameters,computed,decode_null_to_zero"` + Description types.String `tfsdk:"description" json:"description,computed,decode_null_to_zero"` + Enabled types.Bool `tfsdk:"enabled" json:"enabled,computed"` + ExposedCredentialCheck customfield.NestedObject[RulesetRuleExposedCredentialCheckDataSourceModel] `tfsdk:"exposed_credential_check" json:"exposed_credential_check,computed"` + Expression types.String `tfsdk:"expression" json:"expression,computed"` + Logging customfield.NestedObject[RulesetRuleLoggingDataSourceModel] `tfsdk:"logging" json:"logging,computed"` + Ratelimit customfield.NestedObject[RulesetRuleRatelimitDataSourceModel] `tfsdk:"ratelimit" json:"ratelimit,computed"` + Ref types.String `tfsdk:"ref" json:"ref,computed"` + Categories customfield.List[types.String] `tfsdk:"categories" json:"categories,computed"` + + // Additional fields for lookup + RuleID types.String `tfsdk:"rule_id"` + RulesetID types.String `tfsdk:"ruleset_id"` + AccountID types.String `tfsdk:"account_id"` + ZoneID types.String `tfsdk:"zone_id"` +} + +func (m *RulesetRuleDataSourceModel) toReadParams(_ context.Context) (params rulesets.RulesetGetParams, diags diag.Diagnostics) { + params = rulesets.RulesetGetParams{} + + if !m.AccountID.IsNull() { + params.AccountID = cloudflare.F(m.AccountID.ValueString()) + } else { + params.ZoneID = cloudflare.F(m.ZoneID.ValueString()) + } + + return +} + +// Type aliases for action parameters and nested models from ruleset package +type RulesetRuleActionParametersDataSourceModel = ruleset.RulesetRulesActionParametersDataSourceModel +type RulesetRuleActionParametersResponseDataSourceModel = ruleset.RulesetRulesActionParametersResponseDataSourceModel +type RulesetRuleActionParametersAlgorithmsDataSourceModel = ruleset.RulesetRulesActionParametersAlgorithmsDataSourceModel +type RulesetRuleActionParametersMatchedDataDataSourceModel = ruleset.RulesetRulesActionParametersMatchedDataDataSourceModel +type RulesetRuleActionParametersOverridesDataSourceModel = ruleset.RulesetRulesActionParametersOverridesDataSourceModel +type RulesetRuleActionParametersOverridesCategoriesDataSourceModel = ruleset.RulesetRulesActionParametersOverridesCategoriesDataSourceModel +type RulesetRuleActionParametersOverridesRulesDataSourceModel = ruleset.RulesetRulesActionParametersOverridesRulesDataSourceModel +type RulesetRuleActionParametersFromListDataSourceModel = ruleset.RulesetRulesActionParametersFromListDataSourceModel +type RulesetRuleActionParametersFromValueDataSourceModel = ruleset.RulesetRulesActionParametersFromValueDataSourceModel +type RulesetRuleActionParametersFromValueTargetURLDataSourceModel = ruleset.RulesetRulesActionParametersFromValueTargetURLDataSourceModel +type RulesetRuleActionParametersHeadersDataSourceModel = ruleset.RulesetRulesActionParametersHeadersDataSourceModel +type RulesetRuleActionParametersURIDataSourceModel = ruleset.RulesetRulesActionParametersURIDataSourceModel +type RulesetRuleActionParametersURIPathDataSourceModel = ruleset.RulesetRulesActionParametersURIPathDataSourceModel +type RulesetRuleActionParametersURIQueryDataSourceModel = ruleset.RulesetRulesActionParametersURIQueryDataSourceModel +type RulesetRuleActionParametersOriginDataSourceModel = ruleset.RulesetRulesActionParametersOriginDataSourceModel +type RulesetRuleActionParametersSNIDataSourceModel = ruleset.RulesetRulesActionParametersSNIDataSourceModel +type RulesetRuleActionParametersAutominifyDataSourceModel = ruleset.RulesetRulesActionParametersAutominifyDataSourceModel +type RulesetRuleActionParametersBrowserTTLDataSourceModel = ruleset.RulesetRulesActionParametersBrowserTTLDataSourceModel +type RulesetRuleActionParametersCacheKeyDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyCookieDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyCookieDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyHeaderDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyHeaderDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyHostDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyHostDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyQueryStringDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyQueryStringDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyQueryStringIncludeDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyQueryStringIncludeDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyQueryStringExcludeDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyQueryStringExcludeDataSourceModel +type RulesetRuleActionParametersCacheKeyCustomKeyUserDataSourceModel = ruleset.RulesetRulesActionParametersCacheKeyCustomKeyUserDataSourceModel +type RulesetRuleActionParametersCacheReserveDataSourceModel = ruleset.RulesetRulesActionParametersCacheReserveDataSourceModel +type RulesetRuleActionParametersEdgeTTLDataSourceModel = ruleset.RulesetRulesActionParametersEdgeTTLDataSourceModel +type RulesetRuleActionParametersEdgeTTLStatusCodeTTLDataSourceModel = ruleset.RulesetRulesActionParametersEdgeTTLStatusCodeTTLDataSourceModel +type RulesetRuleActionParametersEdgeTTLStatusCodeTTLStatusCodeRangeDataSourceModel = ruleset.RulesetRulesActionParametersEdgeTTLStatusCodeTTLStatusCodeRangeDataSourceModel +type RulesetRuleActionParametersServeStaleDataSourceModel = ruleset.RulesetRulesActionParametersServeStaleDataSourceModel +type RulesetRuleActionParametersCookieFieldsDataSourceModel = ruleset.RulesetRulesActionParametersCookieFieldsDataSourceModel +type RulesetRuleActionParametersRawResponseFieldsDataSourceModel = ruleset.RulesetRulesActionParametersRawResponseFieldsDataSourceModel +type RulesetRuleActionParametersRequestFieldsDataSourceModel = ruleset.RulesetRulesActionParametersRequestFieldsDataSourceModel +type RulesetRuleActionParametersResponseFieldsDataSourceModel = ruleset.RulesetRulesActionParametersResponseFieldsDataSourceModel +type RulesetRuleActionParametersTransformedRequestFieldsDataSourceModel = ruleset.RulesetRulesActionParametersTransformedRequestFieldsDataSourceModel +type RulesetRuleExposedCredentialCheckDataSourceModel = ruleset.RulesetRulesExposedCredentialCheckDataSourceModel +type RulesetRuleLoggingDataSourceModel = ruleset.RulesetRulesLoggingDataSourceModel +type RulesetRuleRatelimitDataSourceModel = ruleset.RulesetRulesRatelimitDataSourceModel diff --git a/internal/services/ruleset_rule/data_source_schema.go b/internal/services/ruleset_rule/data_source_schema.go new file mode 100644 index 0000000000..42eab9d3d9 --- /dev/null +++ b/internal/services/ruleset_rule/data_source_schema.go @@ -0,0 +1,100 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package ruleset_rule + +import ( + "context" + "regexp" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func DataSourceSchema(ctx context.Context) schema.Schema { + // Get the parent ruleset schema to extract the rule schema + rulesetSchema := ruleset.DataSourceSchema(ctx) + rulesListAttr := rulesetSchema.Attributes["rules"].(schema.ListNestedAttribute) + ruleAttributes := rulesListAttr.NestedObject.Attributes + + // Create schema for individual rule datasource + return schema.Schema{ + Description: "Use this data source to lookup a single rule within a Cloudflare Ruleset.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique ID of the rule.", + Computed: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + }, + "rule_id": schema.StringAttribute{ + Description: "The unique ID of the rule to lookup.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + }, + "ruleset_id": schema.StringAttribute{ + Description: "The unique ID of the ruleset containing the rule.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + }, + "account_id": schema.StringAttribute{ + Description: "The unique ID of the account.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("zone_id")), + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + }, + "zone_id": schema.StringAttribute{ + Description: "The unique ID of the zone.", + Optional: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + }, + // Include all rule attributes from the parent ruleset schema except "id" + // which we've already defined above + "action": ruleAttributes["action"], + "action_parameters": ruleAttributes["action_parameters"], + "description": ruleAttributes["description"], + "enabled": ruleAttributes["enabled"], + "exposed_credential_check": ruleAttributes["exposed_credential_check"], + "expression": ruleAttributes["expression"], + "logging": ruleAttributes["logging"], + "ratelimit": ruleAttributes["ratelimit"], + "ref": ruleAttributes["ref"], + "categories": ruleAttributes["categories"], + }, + } +} + +func (d *RulesetRuleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = DataSourceSchema(ctx) +} + +func (d *RulesetRuleDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{} +} diff --git a/internal/services/ruleset_rule/data_source_schema_test.go b/internal/services/ruleset_rule/data_source_schema_test.go new file mode 100644 index 0000000000..dadb8ef2fe --- /dev/null +++ b/internal/services/ruleset_rule/data_source_schema_test.go @@ -0,0 +1,21 @@ +package ruleset_rule_test + +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +// TODO Figreout what is Stainless + +import ( + "context" + "testing" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset_rule" + "github.com/cloudflare/terraform-provider-cloudflare/internal/test_helpers" +) + +func TestRulesetRuleDataSourceModelSchemaParity(t *testing.T) { + t.Parallel() + model := (*ruleset_rule.RulesetRuleDataSourceModel)(nil) + schema := ruleset_rule.DataSourceSchema(context.TODO()) + errs := test_helpers.ValidateDataSourceModelSchemaIntegrity(model, schema) + errs.Report(t) +} diff --git a/internal/services/ruleset_rule/model.go b/internal/services/ruleset_rule/model.go new file mode 100644 index 0000000000..8158c90743 --- /dev/null +++ b/internal/services/ruleset_rule/model.go @@ -0,0 +1,33 @@ +package ruleset_rule + +import ( + "github.com/cloudflare/terraform-provider-cloudflare/internal/apijsoncustom" + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// RulesetRuleModel represents a single rule within a ruleset with resource-specific fields +type RulesetRuleModel struct { + // Embed the existing rule model to reuse all rule fields + ruleset.RulesetRulesModel + + RulesetID types.String `tfsdk:"ruleset_id"` + AccountID types.String `tfsdk:"account_id"` + ZoneID types.String `tfsdk:"zone_id"` + + Position *PositionModel `tfsdk:"position" json:"position,omitempty"` +} + +func (m RulesetRuleModel) MarshalJSON() (data []byte, err error) { + return apijsoncustom.MarshalRoot(m) +} + +func (m RulesetRuleModel) MarshalJSONForUpdate(state RulesetRuleModel) (data []byte, err error) { + return apijsoncustom.MarshalForUpdate(m, state) +} + +type PositionModel struct { + Index *int `tfsdk:"index" json:"index,omitempty"` + Before *string `tfsdk:"before" json:"before,omitempty"` + After *string `tfsdk:"after" json:"after,omitempty"` +} diff --git a/internal/services/ruleset_rule/resource.go b/internal/services/ruleset_rule/resource.go new file mode 100644 index 0000000000..bdc2deaf7b --- /dev/null +++ b/internal/services/ruleset_rule/resource.go @@ -0,0 +1,428 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package ruleset_rule + +import ( + "context" + "fmt" + "github.com/cloudflare/cloudflare-go/v6" + "github.com/cloudflare/cloudflare-go/v6/option" + "github.com/cloudflare/cloudflare-go/v6/rulesets" + "github.com/cloudflare/terraform-provider-cloudflare/internal/apijsoncustom" + "github.com/cloudflare/terraform-provider-cloudflare/internal/importpath" + "github.com/cloudflare/terraform-provider-cloudflare/internal/logging" + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "io" + "net/http" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.ResourceWithConfigure = (*RulesetRuleResource)(nil) +var _ resource.ResourceWithImportState = (*RulesetRuleResource)(nil) + +func NewResource() resource.Resource { + return &RulesetRuleResource{} +} + +// RulesetRuleResource defines the resource implementation. +type RulesetRuleResource struct { + client *cloudflare.Client +} + +func (r *RulesetRuleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ruleset_rule" +} + +func (r *RulesetRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*cloudflare.Client) + + if !ok { + resp.Diagnostics.AddError( + "unexpected resource configure type", + fmt.Sprintf("Expected *cloudflare.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *RulesetRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *RulesetRuleModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + postRuleRes := new(http.Response) + postRuleEnv := ruleset.RulesetResultEnvelope{} + postRuleParams := rulesets.RuleNewParams{} + + if !data.AccountID.IsNull() { + postRuleParams.AccountID = cloudflare.F(data.AccountID.ValueString()) + } else { + postRuleParams.ZoneID = cloudflare.F(data.ZoneID.ValueString()) + } + + dataBytes, err := data.MarshalJSON() + if err != nil { + resp.Diagnostics.AddError("failed to serialize http request", err.Error()) + return + } + + resp.Diagnostics.AddWarning("kur4ence", fmt.Sprintf("sending body %#v", string(dataBytes))) + + _, err = r.client.Rulesets.Rules.New( + ctx, + data.RulesetID.ValueString(), + postRuleParams, + option.WithRequestBody("application/json", dataBytes), + option.WithResponseBodyInto(&postRuleRes), + option.WithMiddleware(logging.Middleware(ctx)), + ) + if err != nil { + resp.Diagnostics.AddError("failed to update ruleset", err.Error()) + return + } + + updateResultBytes, _ := io.ReadAll(postRuleRes.Body) + err = apijsoncustom.Unmarshal(updateResultBytes, &postRuleEnv) + if err != nil { + resp.Diagnostics.AddError("failed to deserialize updated ruleset", err.Error()) + return + } + + updatedRulesSlice, diags := postRuleEnv.Result.Rules.AsStructSliceT(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + if len(updatedRulesSlice) > 0 { + for _, rule := range updatedRulesSlice { + if rule.Expression.ValueString() == data.Expression.ValueString() && !rule.ID.IsNull() { + data.ID = rule.ID + data.Ref = rule.Ref + data.Logging = rule.Logging + break + } + } + } + + if data.ID.IsNull() { + resp.Diagnostics.AddError("API Error", "Unable to determine the ID of the created rule") + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *RulesetRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *RulesetRuleModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Since there's no GET endpoint for individual rules, we need to read the entire ruleset + res := new(http.Response) + env := ruleset.RulesetResultEnvelope{} + params := rulesets.RulesetGetParams{} + + if !data.AccountID.IsNull() { + params.AccountID = cloudflare.F(data.AccountID.ValueString()) + } else { + params.ZoneID = cloudflare.F(data.ZoneID.ValueString()) + } + _, err := r.client.Rulesets.Get( + ctx, + data.RulesetID.ValueString(), + params, + option.WithResponseBodyInto(&res), + option.WithMiddleware(logging.Middleware(ctx)), + ) + if res != nil && res.StatusCode == 404 { + resp.Diagnostics.AddWarning("Resource not found", "The ruleset was not found on the server and will be removed from state.") + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("failed to make http request", err.Error()) + return + } + + bytes, _ := io.ReadAll(res.Body) + err = apijsoncustom.Unmarshal(bytes, &env) + if err != nil { + resp.Diagnostics.AddError("failed to deserialize http request", err.Error()) + return + } + + // Find our rule in the ruleset + var foundRule *ruleset.RulesetRulesModel + ruleID := data.ID.ValueString() + + rules, diags := env.Result.Rules.AsStructSliceT(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + for _, rule := range rules { + if rule.ID.ValueString() == ruleID { + foundRule = &rule + break + } + } + + if foundRule == nil { + // Rule not found, it may have been deleted outside of Terraform + resp.State.RemoveResource(ctx) + return + } + + // Update the model with the current rule data + data.Action = foundRule.Action + data.Expression = foundRule.Expression + data.Description = foundRule.Description + data.Enabled = foundRule.Enabled + data.ActionParameters = foundRule.ActionParameters + data.ExposedCredentialCheck = foundRule.ExposedCredentialCheck + data.Logging = foundRule.Logging + data.Ratelimit = foundRule.Ratelimit + data.Ref = foundRule.Ref + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *RulesetRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *RulesetRuleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + var state *RulesetRuleModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + dataBytes, err := data.MarshalJSONForUpdate(*state) + if err != nil { + resp.Diagnostics.AddError("failed to serialize http request", err.Error()) + return + } + res := new(http.Response) + env := ruleset.RulesetResultEnvelope{} + params := rulesets.RuleEditParams{} + + if !data.AccountID.IsNull() { + params.AccountID = cloudflare.F(data.AccountID.ValueString()) + } else { + params.ZoneID = cloudflare.F(data.ZoneID.ValueString()) + } + + _, err = r.client.Rulesets.Rules.Edit( + ctx, + data.RulesetID.ValueString(), + state.ID.ValueString(), + params, + option.WithRequestBody("application/json", dataBytes), + option.WithResponseBodyInto(&res), + option.WithMiddleware(logging.Middleware(ctx)), + ) + if err != nil { + resp.Diagnostics.AddError("failed to make http request", err.Error()) + return + } + bytes, _ := io.ReadAll(res.Body) + err = apijsoncustom.Unmarshal(bytes, &env) + if err != nil { + resp.Diagnostics.AddError("failed to deserialize http request", err.Error()) + return + } + + var foundRule *ruleset.RulesetRulesModel + + rules, diags := env.Result.Rules.AsStructSliceT(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + for _, rule := range rules { + if rule.Expression.ValueString() == data.Expression.ValueString() && !rule.ID.IsNull() { + foundRule = &rule + break + } + } + + if foundRule == nil { + resp.Diagnostics.AddError("Rule not found", "The rule was not found in the ruleset after update") + return + } + + data.ID = foundRule.ID + data.Ref = foundRule.Ref + data.Action = foundRule.Action + data.ActionParameters = foundRule.ActionParameters + data.Description = foundRule.Description + data.Enabled = foundRule.Enabled + data.ExposedCredentialCheck = foundRule.ExposedCredentialCheck + data.Expression = foundRule.Expression + data.Logging = foundRule.Logging + data.Ratelimit = foundRule.Ratelimit + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *RulesetRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *RulesetRuleModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + res := new(http.Response) + //env := ruleset.RulesetResultEnvelope{} + params := rulesets.RuleDeleteParams{} + + if !data.AccountID.IsNull() { + params.AccountID = cloudflare.F(data.AccountID.ValueString()) + } else { + params.ZoneID = cloudflare.F(data.ZoneID.ValueString()) + } + + _, err := r.client.Rulesets.Rules.Delete( + ctx, + data.RulesetID.ValueString(), + data.ID.ValueString(), + params, + option.WithResponseBodyInto(&res), + option.WithMiddleware(logging.Middleware(ctx)), + ) + if res != nil && res.StatusCode == 404 { + resp.Diagnostics.AddWarning("Resource not found", "The resource was not found on the server and will be removed from state.") + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("failed to make http request", err.Error()) + return + } + + resp.State.RemoveResource(ctx) +} + +func (r *RulesetRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + var data *RulesetRuleModel = new(RulesetRuleModel) + params := rulesets.RulesetGetParams{} + + path_accounts_or_zones, path_account_id_or_zone_id := "", "" + path_ruleset_id, path_rule_id := "", "" + + diags := importpath.ParseImportID( + req.ID, + "<{accounts|zones}/{account_id|zone_id}>//", + &path_accounts_or_zones, + &path_account_id_or_zone_id, + &path_ruleset_id, + &path_rule_id, + ) + resp.Diagnostics.Append(diags...) + switch path_accounts_or_zones { + case "accounts": + params.AccountID = cloudflare.F(path_account_id_or_zone_id) + data.AccountID = types.StringValue(path_account_id_or_zone_id) + case "zones": + params.ZoneID = cloudflare.F(path_account_id_or_zone_id) + data.ZoneID = types.StringValue(path_account_id_or_zone_id) + default: + resp.Diagnostics.AddError("invalid discriminator segment - <{accounts|zones}/{account_id|zone_id}>", "expected discriminator to be one of {accounts|zones}") + } + if resp.Diagnostics.HasError() { + return + } + + data.ID = types.StringValue(path_rule_id) + data.RulesetID = types.StringValue(path_ruleset_id) + + res := new(http.Response) + env := ruleset.RulesetResultEnvelope{} + + if !data.AccountID.IsNull() { + params.AccountID = cloudflare.F(data.AccountID.ValueString()) + } else { + params.ZoneID = cloudflare.F(data.ZoneID.ValueString()) + } + _, err := r.client.Rulesets.Get( + ctx, + data.RulesetID.ValueString(), + params, + option.WithResponseBodyInto(&res), + option.WithMiddleware(logging.Middleware(ctx)), + ) + if res != nil && res.StatusCode == 404 { + resp.Diagnostics.AddWarning("Resource not found", "The ruleset was not found on the server and will be removed from state.") + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("failed to make http request", err.Error()) + return + } + + bytes, _ := io.ReadAll(res.Body) + err = apijsoncustom.Unmarshal(bytes, &env) + if err != nil { + resp.Diagnostics.AddError("failed to deserialize http request", err.Error()) + return + } + + // Find our rule in the ruleset + var foundRule *ruleset.RulesetRulesModel + ruleID := data.ID.ValueString() + + rules, diags := env.Result.Rules.AsStructSliceT(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + for _, rule := range rules { + if rule.ID.ValueString() == ruleID { + foundRule = &rule + break + } + } + + if foundRule == nil { + // Rule not found + return + } + + // Update the model with the current rule data + data.Action = foundRule.Action + data.Expression = foundRule.Expression + data.Description = foundRule.Description + data.Enabled = foundRule.Enabled + data.ActionParameters = foundRule.ActionParameters + data.ExposedCredentialCheck = foundRule.ExposedCredentialCheck + data.Logging = foundRule.Logging + data.Ratelimit = foundRule.Ratelimit + data.Ref = foundRule.Ref + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/services/ruleset_rule/ruleset_rule_test.go b/internal/services/ruleset_rule/ruleset_rule_test.go new file mode 100644 index 0000000000..2d1cfc9f79 --- /dev/null +++ b/internal/services/ruleset_rule/ruleset_rule_test.go @@ -0,0 +1,160 @@ +package ruleset_rule_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/cloudflare/cloudflare-go/v6" + "github.com/cloudflare/cloudflare-go/v6/rulesets" + "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var ( + accountID = os.Getenv("CLOUDFLARE_ACCOUNT_ID") + zoneID = os.Getenv("CLOUDFLARE_ZONE_ID") + domain = os.Getenv("CLOUDFLARE_DOMAIN") + + configVariables = config.Variables{ + "account_id": config.StringVariable(accountID), + "zone_id": config.StringVariable(zoneID), + "domain": config.StringVariable(domain), + } +) + +func TestMain(m *testing.M) { + resource.TestMain(m) +} + +func init() { + resource.AddTestSweepers("cloudflare_ruleset_rule", &resource.Sweeper{ + Name: "cloudflare_ruleset_rule", + F: func(region string) error { + ctx := context.Background() + + client := acctest.SharedClient() + + type ruleset struct { + rulesets.RulesetListParams + rulesets.RulesetListResponse + } + + var entrypointRulesets, customRulesets []ruleset + + for _, params := range []rulesets.RulesetListParams{ + {AccountID: cloudflare.F(accountID)}, + {ZoneID: cloudflare.F(zoneID)}, + } { + iter := client.Rulesets.ListAutoPaging(ctx, params) + + for iter.Next() { + switch iter.Current().Kind { + case rulesets.KindManaged: + case rulesets.KindCustom: + customRulesets = append(customRulesets, ruleset{params, iter.Current()}) + case rulesets.KindRoot: + entrypointRulesets = append(entrypointRulesets, ruleset{params, iter.Current()}) + case rulesets.KindZone: + entrypointRulesets = append(entrypointRulesets, ruleset{params, iter.Current()}) + default: + return fmt.Errorf("unknown ruleset kind %q", iter.Current().Kind) + } + } + + if err := iter.Err(); err != nil { + return fmt.Errorf("failed to list rulesets: %w", err) + } + } + + for _, ruleset := range append(entrypointRulesets, customRulesets...) { + if err := client.Rulesets.Delete(ctx, ruleset.ID, rulesets.RulesetDeleteParams{ + AccountID: ruleset.AccountID, + ZoneID: ruleset.ZoneID, + }); err != nil { + return fmt.Errorf("failed to delete ruleset %q: %w", ruleset.ID, err) + } + } + + return nil + }, + }) +} + +func TestAccCloudflareRulesetRule_Description(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + ConfigFile: config.TestNameFile("1.tf"), + ConfigVariables: configVariables, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction( + "cloudflare_ruleset.block_external_traffic", + plancheck.ResourceActionCreate, + ), + plancheck.ExpectResourceAction( + "cloudflare_ruleset_rule.allow_rancher", + plancheck.ResourceActionCreate, + ), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "cloudflare_ruleset_rule.allow_rancher", + tfjsonpath.New("description"), + knownvalue.StringExact(""), + ), + }, + }, + { + ConfigFile: config.TestNameFile("2.tf"), + ConfigVariables: configVariables, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "cloudflare_ruleset_rule.allow_rancher", + tfjsonpath.New("description"), + knownvalue.StringExact(""), + ), + }, + }, + { + ConfigFile: config.TestNameFile("3.tf"), + ConfigVariables: configVariables, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction( + "cloudflare_ruleset_rule.allow_rancher", + plancheck.ResourceActionUpdate, + ), + plancheck.ExpectKnownValue( + "cloudflare_ruleset_rule.allow_rancher", + tfjsonpath.New("description"), + knownvalue.StringExact("My rule description"), + ), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "cloudflare_ruleset_rule.allow_rancher", + tfjsonpath.New("description"), + knownvalue.StringExact("My rule description"), + ), + }, + }, + }, + }) +} diff --git a/internal/services/ruleset_rule/schema.go b/internal/services/ruleset_rule/schema.go new file mode 100644 index 0000000000..734e291d77 --- /dev/null +++ b/internal/services/ruleset_rule/schema.go @@ -0,0 +1,87 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package ruleset_rule + +import ( + "context" + "github.com/cloudflare/terraform-provider-cloudflare/internal/services/ruleset" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "regexp" +) + +var _ resource.ResourceWithConfigValidators = (*RulesetRuleResource)(nil) + +func ResourceSchema(ctx context.Context) schema.Schema { + attributes := ruleset.RuleAttributes(ctx) + attributes["ruleset_id"] = schema.StringAttribute{ + Description: "The unique ID of the ruleset.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + } + attributes["account_id"] = schema.StringAttribute{ + Description: "The unique ID of the account.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("zone_id")), + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + } + attributes["zone_id"] = schema.StringAttribute{ + Description: "The unique ID of the zone.", + Optional: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile("^[0-9a-f]{32}$"), + "value must be a 32-character hexadecimal string", + ), + }, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + } + attributes["position"] = schema.SingleNestedAttribute{ + Description: "Specifies where to place the rule. Only one of `index`, `before`, or `after` can be set.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "index": schema.Int64Attribute{ + Description: "The absolute index position for the rule (starting at 1).", + Optional: true, + }, + "before": schema.StringAttribute{ + Description: "Place this rule immediately before the rule with the given ID.", + Optional: true, + }, + "after": schema.StringAttribute{ + Description: "Place this rule immediately after the rule with the given ID.", + Optional: true, + }, + }, + } + + return schema.Schema{ + Attributes: attributes, + } +} + +func (r *RulesetRuleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = ResourceSchema(ctx) +} + +func (r *RulesetRuleResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + // Ensure exactly one of account_id or zone_id is specified + } +} diff --git a/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/1.tf b/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/1.tf new file mode 100644 index 0000000000..4ee891a450 --- /dev/null +++ b/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/1.tf @@ -0,0 +1,26 @@ +variable "zone_id" {} + +resource "cloudflare_ruleset" "block_external_traffic" { + kind = "zone" + name = "Block external traffic" + phase = "http_request_firewall_custom" + zone_id = var.zone_id + lifecycle { + ignore_changes = [rules] + } +} + +resource "cloudflare_ruleset_rule" "allow_rancher" { + ruleset_id = cloudflare_ruleset.block_external_traffic.id + action = "skip" + action_parameters = { + ruleset = "current" + } + enabled = true + expression = "(starts_with(http.host, \"provisioning\") and ip.src eq 151.251.76.61)" + + zone_id = var.zone_id + position = { + index = 1 + } +} diff --git a/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/2.tf b/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/2.tf new file mode 100644 index 0000000000..eab7eda0e0 --- /dev/null +++ b/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/2.tf @@ -0,0 +1,27 @@ +variable "zone_id" {} + +resource "cloudflare_ruleset" "block_external_traffic" { + kind = "zone" + name = "Block external traffic" + phase = "http_request_firewall_custom" + zone_id = var.zone_id + lifecycle { + ignore_changes = [rules] + } +} + +resource "cloudflare_ruleset_rule" "allow_rancher" { + ruleset_id = cloudflare_ruleset.block_external_traffic.id + description = "" + action = "skip" + action_parameters = { + ruleset = "current" + } + enabled = true + expression = "(starts_with(http.host, \"provisioning\") and ip.src eq 151.251.76.61)" + + zone_id = var.zone_id + position = { + index = 1 + } +} diff --git a/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/3.tf b/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/3.tf new file mode 100644 index 0000000000..5fdd02c9b7 --- /dev/null +++ b/internal/services/ruleset_rule/testdata/TestAccCloudflareRulesetRule_Description/3.tf @@ -0,0 +1,27 @@ +variable "zone_id" {} + +resource "cloudflare_ruleset" "block_external_traffic" { + kind = "zone" + name = "Block external traffic" + phase = "http_request_firewall_custom" + zone_id = var.zone_id + lifecycle { + ignore_changes = [rules] + } +} + +resource "cloudflare_ruleset_rule" "allow_rancher" { + ruleset_id = cloudflare_ruleset.block_external_traffic.id + description = "My rule description" + action = "skip" + action_parameters = { + ruleset = "current" + } + enabled = true + expression = "(starts_with(http.host, \"provisioning\") and ip.src eq 151.251.76.61)" + + zone_id = var.zone_id + position = { + index = 1 + } +}