Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for turnstile / challenge widgets #2380

Merged
merged 14 commits into from
Apr 26, 2023
Merged
3 changes: 3 additions & 0 deletions .changelog/2380.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_turnstile_widget
```
5 changes: 0 additions & 5 deletions .golintci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ run:
issues:
max-per-linter: 0
max-same-issues: 0
exclude:
# no way to flag these paths as ignore in contextcheck
- "Function `toRuleset->toRulesetRule->StringSet` should pass the context parameter"
- "Function `toRulesetResourceModel` should pass the context parameter"
- "Function `remapPreservedRuleIDs->toRuleset->toRulesetRule->StringSet` should pass the context parameter"

linters:
disable-all: true
Expand Down
45 changes: 45 additions & 0 deletions docs/resources/turnstile_widget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
page_title: "cloudflare_turnstile_widget Resource - Cloudflare"
subcategory: ""
description: |-
The Challenge Widget https://developers.cloudflare.com/turnstile/ resource allows you to manage Cloudflare Turnstile Widgets.
---

# cloudflare_turnstile_widget (Resource)

The [Challenge Widget](https://developers.cloudflare.com/turnstile/) resource allows you to manage Cloudflare Turnstile Widgets.

## Example Usage

```terraform
resource "cloudflare_turnstile_widget" "example" {
account_id = "f037e56e89293a057740de681ac9abbe"
name = "example widget"
bot_fight_mode = false
domains = ["example.com"]
mode = "invisible"
region = "world"
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `account_id` (String) The account identifier to target for the resource.
- `domains` (Set of String) Domains where the widget is deployed
- `name` (String) Human readable widget name.

### Optional

- `bot_fight_mode` (Boolean) If bot_fight_mode is set to true, Cloudflare issues computationally expensive challenges in response to malicious bots (Enterprise only).
- `id` (String) The identifier of this resource. This is the site key value.
- `mode` (String) Widget Mode
- `offlabel` (Boolean) Do not show any Cloudflare branding on the widget (Enterprise only).
- `region` (String) Region where this widget can be used.

### Read-Only

- `secret` (String, Sensitive) Secret key for this widget.


8 changes: 8 additions & 0 deletions examples/resources/cloudflare_turnstile_widget/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "cloudflare_turnstile_widget" "example" {
account_id = "f037e56e89293a057740de681ac9abbe"
name = "example widget"
bot_fight_mode = false
domains = ["example.com"]
mode = "invisible"
region = "world"
}
4 changes: 2 additions & 2 deletions internal/framework/expanders/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

// StringList accepts a `types.List` and returns a slice of strings.
func StringList(in types.List) []string {
func StringList(ctx context.Context, in types.List) []string {
results := []string{}
_ = in.ElementsAs(context.Background(), &results, false)
_ = in.ElementsAs(ctx, &results, false)
return results
}
4 changes: 2 additions & 2 deletions internal/framework/expanders/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

// StringSet accepts a `types.Set` and returns a slice of strings.
func StringSet(in types.Set) []string {
func StringSet(ctx context.Context, in types.Set) []string {
results := []string{}
_ = in.ElementsAs(context.Background(), &results, false)
_ = in.ElementsAs(ctx, &results, false)
return results
}
2 changes: 2 additions & 0 deletions internal/framework/flatteners/bool.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
// } else {
// requestPayload.Enabled = types.BoolNull()
// }
//
// nolint: contextcheck
func Bool(in *bool) basetypes.BoolValue {
if reflect.ValueOf(in).IsNil() {
return types.BoolNull()
Expand Down
2 changes: 2 additions & 0 deletions internal/framework/flatteners/int64.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
// }
//
// Not recommended if you care about returning an empty string for the state.
//
// nolint: contextcheck
func Int64(in int64) basetypes.Int64Value {
if in == 0 {
return types.Int64Null()
Expand Down
2 changes: 2 additions & 0 deletions internal/framework/flatteners/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
// StringSet accepts a `[]attr.Value` and returns a `basetypes.SetValue`. The
// return type automatically handles `SetNull` for empty results and coercing
// all element values to a string if there are any elements.
//
// nolint: contextcheck
func StringSet(in []attr.Value) basetypes.SetValue {
if len(in) == 0 {
return types.SetNull(types.StringType)
Expand Down
2 changes: 2 additions & 0 deletions internal/framework/flatteners/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
// }
//
// Not recommended if you care about returning an empty string for the state.
//
// nolint: contextcheck
func String(in string) basetypes.StringValue {
if in == "" {
return types.StringNull()
Expand Down
2 changes: 2 additions & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/rulesets"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/turnstile"
"github.com/cloudflare/terraform-provider-cloudflare/internal/sdkv2provider"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
Expand Down Expand Up @@ -307,6 +308,7 @@ func (p *CloudflareProvider) Configure(ctx context.Context, req provider.Configu
func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
rulesets.NewResource,
turnstile.NewResource,
}
}

Expand Down
76 changes: 38 additions & 38 deletions internal/framework/service/rulesets/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (r *RulesetResource) Create(ctx context.Context, req resource.CreateRequest
Phase: rulesetPhase,
}

rulesetData := data.toRuleset()
rulesetData := data.toRuleset(ctx)

if len(rulesetData.Rules) > 0 {
rs.Rules = rulesetData.Rules
Expand Down Expand Up @@ -166,7 +166,7 @@ func (r *RulesetResource) Create(ctx context.Context, req resource.CreateRequest

data.ID = types.StringValue(ruleset.ID)

diags = resp.State.Set(ctx, toRulesetResourceModel(data.ZoneID, data.AccountID, ruleset))
diags = resp.State.Set(ctx, toRulesetResourceModel(ctx, data.ZoneID, data.AccountID, ruleset))
resp.Diagnostics.Append(diags...)
}

Expand Down Expand Up @@ -204,7 +204,7 @@ func (r *RulesetResource) Read(ctx context.Context, req resource.ReadRequest, re
return
}

resp.Diagnostics.Append(resp.State.Set(ctx, toRulesetResourceModel(zoneID, accountID, ruleset))...)
resp.Diagnostics.Append(resp.State.Set(ctx, toRulesetResourceModel(ctx, zoneID, accountID, ruleset))...)
}

func (r *RulesetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
Expand All @@ -223,7 +223,7 @@ func (r *RulesetResource) Update(ctx context.Context, req resource.UpdateRequest
accountID := plan.AccountID
zoneID := plan.ZoneID.ValueString()

remappedRules, e := remapPreservedRuleIDs(state, plan)
remappedRules, e := remapPreservedRuleIDs(ctx, state, plan)
if e != nil {
resp.Diagnostics.AddError("failed to remap rule IDs from state", e.Error())
return
Expand All @@ -245,7 +245,7 @@ func (r *RulesetResource) Update(ctx context.Context, req resource.UpdateRequest

plan.ID = types.StringValue(rs.ID)

resp.Diagnostics.Append(resp.State.Set(ctx, toRulesetResourceModel(state.ZoneID, state.AccountID, rs))...)
resp.Diagnostics.Append(resp.State.Set(ctx, toRulesetResourceModel(ctx, state.ZoneID, state.AccountID, rs))...)
}

func (r *RulesetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
Expand Down Expand Up @@ -297,7 +297,7 @@ func (r *RulesetResource) ImportState(ctx context.Context, req resource.ImportSt
//
// The reverse of this method is `toRuleset` which handles building an API
// representation using the proposed config.
func toRulesetResourceModel(zoneID, accountID basetypes.StringValue, in cloudflare.Ruleset) *RulesetResourceModel {
func toRulesetResourceModel(ctx context.Context, zoneID, accountID basetypes.StringValue, in cloudflare.Ruleset) *RulesetResourceModel {
data := RulesetResourceModel{
ID: types.StringValue(in.ID),
Description: types.StringValue(in.Description),
Expand Down Expand Up @@ -503,17 +503,17 @@ func toRulesetResourceModel(zoneID, accountID basetypes.StringValue, in cloudfla
}

if ruleResponse.ActionParameters.CacheKey.CustomKey.Cookie != nil {
include, _ := basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Cookie.Include)
checkPresence, _ := basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Cookie.CheckPresence)
include, _ := basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Cookie.Include)
checkPresence, _ := basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Cookie.CheckPresence)
key.Cookie = []*ActionParameterCacheKeyCustomKeyCookieModel{{
Include: include,
CheckPresence: checkPresence,
}}
}

if ruleResponse.ActionParameters.CacheKey.CustomKey.Header != nil {
include, _ := basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Header.Include)
checkPresence, _ := basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Header.CheckPresence)
include, _ := basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Header.Include)
checkPresence, _ := basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Header.CheckPresence)
if len(include.Elements()) > 0 || len(checkPresence.Elements()) > 0 {
key.Header = []*ActionParameterCacheKeyCustomKeyHeaderModel{{
Include: include,
Expand All @@ -524,22 +524,22 @@ func toRulesetResourceModel(zoneID, accountID basetypes.StringValue, in cloudfla
}

if ruleResponse.ActionParameters.CacheKey.CustomKey.Query != nil {
include, _ := basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Include)
exclude, _ := basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Exclude)
include, _ := basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Include)
exclude, _ := basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Exclude)

if ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Include != nil {
if ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Include.All {
include, _ = basetypes.NewSetValueFrom(context.Background(), types.StringType, []string{"*"})
include, _ = basetypes.NewSetValueFrom(ctx, types.StringType, []string{"*"})
} else {
include, _ = basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Include.List)
include, _ = basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Include.List)
}
}

if ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Exclude != nil {
if ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Exclude.All {
exclude, _ = basetypes.NewSetValueFrom(context.Background(), types.StringType, []string{"*"})
exclude, _ = basetypes.NewSetValueFrom(ctx, types.StringType, []string{"*"})
} else {
exclude, _ = basetypes.NewSetValueFrom(context.Background(), types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Exclude.List)
exclude, _ = basetypes.NewSetValueFrom(ctx, types.StringType, ruleResponse.ActionParameters.CacheKey.CustomKey.Query.Exclude.List)
}
}

Expand Down Expand Up @@ -716,13 +716,13 @@ func toRulesetResourceModel(zoneID, accountID basetypes.StringValue, in cloudfla
//
// The reverse of this method is `toRulesetResourceModel` which handles building
// a state representation using the API response.
func (r *RulesetResourceModel) toRuleset() cloudflare.Ruleset {
func (r *RulesetResourceModel) toRuleset(ctx context.Context) cloudflare.Ruleset {
var rs cloudflare.Ruleset
var rules []cloudflare.RulesetRule

rs.ID = r.ID.ValueString()
for _, rule := range r.Rules {
rules = append(rules, rule.toRulesetRule())
rules = append(rules, rule.toRulesetRule(ctx))
}

rs.Rules = rules
Expand All @@ -732,7 +732,7 @@ func (r *RulesetResourceModel) toRuleset() cloudflare.Ruleset {

// toRulesetRule takes a state representation of a Ruleset Rule and transforms
// it into an API representation.
func (r *RulesModel) toRulesetRule() cloudflare.RulesetRule {
func (r *RulesModel) toRulesetRule(ctx context.Context) cloudflare.RulesetRule {
rr := cloudflare.RulesetRule{
Action: r.Action.ValueString(),
Expression: r.Expression.ValueString(),
Expand Down Expand Up @@ -769,16 +769,16 @@ func (r *RulesModel) toRulesetRule() cloudflare.RulesetRule {
}
}

if len(expanders.StringSet(ap.Phases)) > 0 {
rr.ActionParameters.Phases = expanders.StringSet(ap.Phases)
if len(expanders.StringSet(ctx, ap.Phases)) > 0 {
rr.ActionParameters.Phases = expanders.StringSet(ctx, ap.Phases)
}

if len(expanders.StringSet(ap.Products)) > 0 {
rr.ActionParameters.Products = expanders.StringSet(ap.Products)
if len(expanders.StringSet(ctx, ap.Products)) > 0 {
rr.ActionParameters.Products = expanders.StringSet(ctx, ap.Products)
}

if len(expanders.StringSet(ap.Rulesets)) > 0 {
rr.ActionParameters.Rulesets = expanders.StringSet(ap.Rulesets)
if len(expanders.StringSet(ctx, ap.Rulesets)) > 0 {
rr.ActionParameters.Rulesets = expanders.StringSet(ctx, ap.Rulesets)
}

if !ap.ID.IsNull() {
Expand Down Expand Up @@ -1072,8 +1072,8 @@ func (r *RulesModel) toRulesetRule() cloudflare.RulesetRule {
customKey := cloudflare.RulesetRuleActionParametersCustomKey{}

if len(ap.CacheKey[0].CustomKey[0].QueryString) > 0 {
includeQueryList := expanders.StringSet(ap.CacheKey[0].CustomKey[0].QueryString[0].Include)
excludeQueryList := expanders.StringSet(ap.CacheKey[0].CustomKey[0].QueryString[0].Exclude)
includeQueryList := expanders.StringSet(ctx, ap.CacheKey[0].CustomKey[0].QueryString[0].Include)
excludeQueryList := expanders.StringSet(ctx, ap.CacheKey[0].CustomKey[0].QueryString[0].Exclude)

if len(includeQueryList) > 0 {
if len(includeQueryList) == 1 && includeQueryList[0] == "*" {
Expand Down Expand Up @@ -1109,8 +1109,8 @@ func (r *RulesModel) toRulesetRule() cloudflare.RulesetRule {
}

if len(ap.CacheKey[0].CustomKey[0].Header) > 0 {
includeQueryList := expanders.StringSet(ap.CacheKey[0].CustomKey[0].Header[0].Include)
checkPresenceList := expanders.StringSet(basetypes.SetValue(ap.CacheKey[0].CustomKey[0].Header[0].CheckPresence))
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))

customKey.Header = &cloudflare.RulesetRuleActionParametersCustomKeyHeader{
RulesetRuleActionParametersCustomKeyFields: cloudflare.RulesetRuleActionParametersCustomKeyFields{
Expand All @@ -1122,8 +1122,8 @@ func (r *RulesModel) toRulesetRule() cloudflare.RulesetRule {
}

if len(ap.CacheKey[0].CustomKey[0].Cookie) > 0 {
includeQueryList := expanders.StringSet(ap.CacheKey[0].CustomKey[0].Cookie[0].Include)
checkPresenceList := expanders.StringSet(basetypes.SetValue(ap.CacheKey[0].CustomKey[0].Cookie[0].CheckPresence))
includeQueryList := expanders.StringSet(ctx, ap.CacheKey[0].CustomKey[0].Cookie[0].Include)
checkPresenceList := expanders.StringSet(ctx, basetypes.SetValue(ap.CacheKey[0].CustomKey[0].Cookie[0].CheckPresence))

if len(includeQueryList) > 0 || len(checkPresenceList) > 0 {
customKey.Cookie = &cloudflare.RulesetRuleActionParametersCustomKeyCookie{
Expand Down Expand Up @@ -1218,21 +1218,21 @@ func (r *RulesModel) toRulesetRule() cloudflare.RulesetRule {
rr.ActionParameters.FromValue = from
}

apCookieFields := expanders.StringSet(ap.CookieFields)
apCookieFields := expanders.StringSet(ctx, ap.CookieFields)
if len(apCookieFields) > 0 {
for _, cookie := range apCookieFields {
rr.ActionParameters.CookieFields = append(rr.ActionParameters.CookieFields, cloudflare.RulesetActionParametersLogCustomField{Name: cookie})
}
}

apRequestFields := expanders.StringSet(ap.RequestFields)
apRequestFields := expanders.StringSet(ctx, ap.RequestFields)
if len(apRequestFields) > 0 {
for _, request := range apRequestFields {
rr.ActionParameters.RequestFields = append(rr.ActionParameters.RequestFields, cloudflare.RulesetActionParametersLogCustomField{Name: request})
}
}

apResponseFields := expanders.StringSet(ap.ResponseFields)
apResponseFields := expanders.StringSet(ctx, ap.ResponseFields)
if len(apResponseFields) > 0 {
for _, request := range apResponseFields {
rr.ActionParameters.ResponseFields = append(rr.ActionParameters.ResponseFields, cloudflare.RulesetActionParametersLogCustomField{Name: request})
Expand All @@ -1242,7 +1242,7 @@ func (r *RulesModel) toRulesetRule() cloudflare.RulesetRule {

for _, rl := range r.Ratelimit {
rr.RateLimit = &cloudflare.RulesetRuleRateLimit{
Characteristics: expanders.StringSet(rl.Characteristics),
Characteristics: expanders.StringSet(ctx, rl.Characteristics),
Period: int(rl.Period.ValueInt64()),
RequestsPerPeriod: int(rl.RequestsPerPeriod.ValueInt64()),
ScorePerPeriod: int(rl.ScorePerPeriod.ValueInt64()),
Expand Down Expand Up @@ -1353,9 +1353,9 @@ func newRuleIDs(rulesetRules []cloudflare.RulesetRule) (ruleIDs, error) {
return r, nil
}

func remapPreservedRuleIDs(state, plan *RulesetResourceModel) ([]cloudflare.RulesetRule, error) {
currentRuleset := state.toRuleset()
plannedRuleset := plan.toRuleset()
func remapPreservedRuleIDs(ctx context.Context, state, plan *RulesetResourceModel) ([]cloudflare.RulesetRule, error) {
currentRuleset := state.toRuleset(ctx)
plannedRuleset := plan.toRuleset(ctx)

ids, err := newRuleIDs(currentRuleset.Rules)
if err != nil {
Expand Down
15 changes: 15 additions & 0 deletions internal/framework/service/turnstile/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package turnstile

import "github.com/hashicorp/terraform-plugin-framework/types"

type TurnstileWidgetModel struct {
AccountID types.String `tfsdk:"account_id"`
ID types.String `tfsdk:"id"`
Domains types.Set `tfsdk:"domains"`
Name types.String `tfsdk:"name"`
Secret types.String `tfsdk:"secret"`
Region types.String `tfsdk:"region"`
Mode types.String `tfsdk:"mode"`
BotFightMode types.Bool `tfsdk:"bot_fight_mode"`
OffLabel types.Bool `tfsdk:"offlabel"`
}
Loading