Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3,015 changes: 1,512 additions & 1,503 deletions internal/services/ruleset/schema.go

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions internal/services/ruleset_rule/README.md
Original file line number Diff line number Diff line change
@@ -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/...
```
130 changes: 130 additions & 0 deletions internal/services/ruleset_rule/data_source.go
Original file line number Diff line number Diff line change
@@ -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)...)
}
93 changes: 93 additions & 0 deletions internal/services/ruleset_rule/data_source_model.go
Original file line number Diff line number Diff line change
@@ -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
Loading