diff --git a/.changelog/3820.txt b/.changelog/3820.txt new file mode 100644 index 0000000000..d409a6437d --- /dev/null +++ b/.changelog/3820.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/rulesets: add "contains" support to custom cache key headers +``` diff --git a/docs/data-sources/rulesets.md b/docs/data-sources/rulesets.md index 951ea7acbb..dc0a8e2276 100644 --- a/docs/data-sources/rulesets.md +++ b/docs/data-sources/rulesets.md @@ -189,6 +189,7 @@ Read-Only: Read-Only: - `check_presence` (List of String) +- `contains` (Map of Set of String) - `exclude_origin` (Boolean) - `include` (List of String) diff --git a/docs/resources/ruleset.md b/docs/resources/ruleset.md index ba416709e9..8ff160c4d2 100644 --- a/docs/resources/ruleset.md +++ b/docs/resources/ruleset.md @@ -300,6 +300,11 @@ resource "cloudflare_ruleset" "cache_settings_example" { include = ["habc", "hdef"] check_presence = ["habc_t", "hdef_t"] exclude_origin = true + contains = { + "accept" = ["image/web", "image/png"] + "accept-encoding" = ["br", "zstd"] + "some-header" = ["some-value", "some-other-value"] + } } cookie { include = ["cabc", "cdef"] @@ -602,6 +607,7 @@ Optional: Optional: - `check_presence` (Set of String) List of headers to check for presence in the custom key. +- `contains` (Map of Set of String) Dictionary of headers mapping to lists of values to check for presence in the custom key. - `exclude_origin` (Boolean) Exclude the origin header from the custom key. - `include` (Set of String) List of headers to include in the custom key. diff --git a/examples/resources/cloudflare_ruleset/resource.tf b/examples/resources/cloudflare_ruleset/resource.tf index f713cc09f7..e94b3b5bf1 100644 --- a/examples/resources/cloudflare_ruleset/resource.tf +++ b/examples/resources/cloudflare_ruleset/resource.tf @@ -275,6 +275,11 @@ resource "cloudflare_ruleset" "cache_settings_example" { include = ["habc", "hdef"] check_presence = ["habc_t", "hdef_t"] exclude_origin = true + contains = { + "accept" = ["image/web", "image/png"] + "accept-encoding" = ["br", "zstd"] + "some-header" = ["some-value", "some-other-value"] + } } cookie { include = ["cabc", "cdef"] diff --git a/internal/framework/service/rulesets/model.go b/internal/framework/service/rulesets/model.go index abacc3a645..8347104f71 100644 --- a/internal/framework/service/rulesets/model.go +++ b/internal/framework/service/rulesets/model.go @@ -196,9 +196,10 @@ type ActionParameterCacheKeyCustomKeyQueryStringModel struct { } type ActionParameterCacheKeyCustomKeyHeaderModel struct { - Include types.Set `tfsdk:"include"` - CheckPresence types.Set `tfsdk:"check_presence"` - ExcludeOrigin types.Bool `tfsdk:"exclude_origin"` + Include types.Set `tfsdk:"include"` + CheckPresence types.Set `tfsdk:"check_presence"` + ExcludeOrigin types.Bool `tfsdk:"exclude_origin"` + Contains map[string]types.Set `tfsdk:"contains"` } type ActionParameterCacheKeyCustomKeyCookieModel struct { diff --git a/internal/framework/service/rulesets/resource.go b/internal/framework/service/rulesets/resource.go index 7d55df5075..a5edb82f74 100644 --- a/internal/framework/service/rulesets/resource.go +++ b/internal/framework/service/rulesets/resource.go @@ -542,11 +542,20 @@ func toRulesetResourceModel(ctx context.Context, zoneID, accountID basetypes.Str } else { excludeOrigin = types.BoolNull() } - if len(include.Elements()) > 0 || len(checkPresence.Elements()) > 0 || excludeOrigin.ValueBool() { + contains := map[string]types.Set{} + for k, v := range ruleResponse.ActionParameters.CacheKey.CustomKey.Header.Contains { + set, _ := basetypes.NewSetValueFrom(ctx, types.StringType, v) + contains[k] = set + } + if len(include.Elements()) > 0 || len(checkPresence.Elements()) > 0 || excludeOrigin.ValueBool() || len(contains) > 0 { + if len(contains) == 0 { + contains = nil + } key.Header = []*ActionParameterCacheKeyCustomKeyHeaderModel{{ Include: include, CheckPresence: checkPresence, ExcludeOrigin: excludeOrigin, + Contains: contains, }} } } @@ -1174,6 +1183,10 @@ func (r *RulesModel) toRulesetRule(ctx context.Context) cfv1.RulesetRule { if len(ap.CacheKey[0].CustomKey[0].Header) > 0 { includeQueryList := expanders.StringSet(ctx, ap.CacheKey[0].CustomKey[0].Header[0].Include) checkPresenceList := expanders.StringSet(ctx, basetypes.SetValue(ap.CacheKey[0].CustomKey[0].Header[0].CheckPresence)) + containsMap := map[string][]string{} + for k, v := range ap.CacheKey[0].CustomKey[0].Header[0].Contains { + containsMap[k] = expanders.StringSet(ctx, v) + } customKey.Header = &cfv1.RulesetRuleActionParametersCustomKeyHeader{ RulesetRuleActionParametersCustomKeyFields: cfv1.RulesetRuleActionParametersCustomKeyFields{ @@ -1181,6 +1194,7 @@ func (r *RulesModel) toRulesetRule(ctx context.Context) cfv1.RulesetRule { CheckPresence: checkPresenceList, }, ExcludeOrigin: cfv1.BoolPtr(ap.CacheKey[0].CustomKey[0].Header[0].ExcludeOrigin.ValueBool()), + Contains: containsMap, } } diff --git a/internal/framework/service/rulesets/resource_test.go b/internal/framework/service/rulesets/resource_test.go index 786596f0a5..676ddfef6d 100644 --- a/internal/framework/service/rulesets/resource_test.go +++ b/internal/framework/service/rulesets/resource_test.go @@ -1875,6 +1875,9 @@ func TestAccCloudflareRuleset_CacheSettingsAllEnabled(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.check_presence.0", "habc_t"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.check_presence.1", "hdef_t"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.exclude_origin", "true"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.%", "3"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.*", "image/web"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.cookie.0.include.#", "2"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.cookie.0.include.0", "cabc"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.cookie.0.include.1", "cdef"), @@ -1950,6 +1953,7 @@ func TestAccCloudflareRuleset_CacheSettingsOnlyExludeOrigin(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.include.#", "0"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.check_presence.#", "0"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.%", "0"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.exclude_origin", "true"), ), }, @@ -2580,6 +2584,9 @@ func TestAccCloudflareRuleset_CacheSettingsHandleDefaultHeaderExcludeOrigin(t *t resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.include.0", "x-test"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.include.1", "x-test2"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.exclude_origin", "false"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.%", "3"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.*", "image/web"), ), }, { @@ -2599,6 +2606,9 @@ func TestAccCloudflareRuleset_CacheSettingsHandleDefaultHeaderExcludeOrigin(t *t resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.include.0", "x-test"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.include.1", "x-test2"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.exclude_origin", "false"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.%", "3"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.*", "image/web"), ), }, { @@ -2618,6 +2628,9 @@ func TestAccCloudflareRuleset_CacheSettingsHandleDefaultHeaderExcludeOrigin(t *t resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.include.0", "x-test"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.include.1", "x-test2"), resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.exclude_origin", "true"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.%", "3"), + resource.TestCheckResourceAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "rules.0.action_parameters.0.cache_key.0.custom_key.0.header.0.contains.accept.*", "image/web"), ), }, }, @@ -4110,6 +4123,11 @@ func testAccCloudflareRulesetCacheSettingsAllEnabled(rnd, accountID, zoneID stri include = ["habc", "hdef"] check_presence = ["habc_t", "hdef_t"] exclude_origin = true + contains = { + "accept" = ["image/web", "image/png"] + "accept-encoding" = ["br", "zstd"] + "some-header" = ["some-value", "some-other-value"] + } } cookie { include = ["cabc", "cdef"] @@ -4786,6 +4804,11 @@ func testAccCloudflareRulesetCacheSettingsHandleDefaultHeaderExcludeOrigin(rnd, header { check_presence = ["x-forwarded-for"] include = ["x-test", "x-test2"] + contains = { + "accept" = ["image/web", "image/png"] + "accept-encoding" = ["br", "zstd"] + "some-header" = ["some-value", "some-other-value"] + } } } } @@ -4820,6 +4843,11 @@ func testAccCloudflareRulesetCacheSettingsHandleHeaderExcludeOriginSet(rnd, zone check_presence = ["x-forwarded-for"] include = ["x-test", "x-test2"] exclude_origin = true + contains = { + "accept" = ["image/web", "image/png"] + "accept-encoding" = ["br", "zstd"] + "some-header" = ["some-value", "some-other-value"] + } } } } @@ -4854,6 +4882,11 @@ func testAccCloudflareRulesetCacheSettingsHandleHeaderExcludeOriginFalse(rnd, zo check_presence = ["x-forwarded-for"] include = ["x-test", "x-test2"] exclude_origin = false + contains = { + "accept" = ["image/web", "image/png"] + "accept-encoding" = ["br", "zstd"] + "some-header" = ["some-value", "some-other-value"] + } } } } diff --git a/internal/framework/service/rulesets/schema.go b/internal/framework/service/rulesets/schema.go index f5e3114291..2b0712d7e6 100644 --- a/internal/framework/service/rulesets/schema.go +++ b/internal/framework/service/rulesets/schema.go @@ -643,6 +643,13 @@ func (r *RulesetResource) Schema(ctx context.Context, req resource.SchemaRequest defaults.DefaultBool(false), }, }, + "contains": schema.MapAttribute{ + ElementType: types.SetType{ + ElemType: types.StringType, + }, + Optional: true, + MarkdownDescription: "Dictionary of headers mapping to lists of values to check for presence in the custom key.", + }, }, }, Validators: []validator.List{ diff --git a/internal/sdkv2provider/data_source_rulesets.go b/internal/sdkv2provider/data_source_rulesets.go index 109948de0e..692a6d8f2a 100644 --- a/internal/sdkv2provider/data_source_rulesets.go +++ b/internal/sdkv2provider/data_source_rulesets.go @@ -766,6 +766,17 @@ func resourceCloudflareRulesetSchema() map[string]*schema.Schema { Optional: true, Description: "Exclude the origin header from the custom key.", }, + "contains": { + Type: schema.TypeMap, + Optional: true, + Description: "Dictionary of headers mapping to lists of values to check for presence in the custom key.", + Elem: &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, }, }, },