-
Notifications
You must be signed in to change notification settings - Fork 626
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for turnstile / challenge widgets
- Loading branch information
1 parent
1bd50b6
commit de84ba5
Showing
6 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:new-resource | ||
resource/challenge_widget: add support for challenge widgets / Turnstile | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package challenge_widget | ||
|
||
import "github.com/hashicorp/terraform-plugin-framework/types" | ||
|
||
type ChallengeWidgetModel 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:"off_label"` | ||
} |
188 changes: 188 additions & 0 deletions
188
internal/framework/service/challenge_widget/resource.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
package challenge_widget | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/expanders" | ||
"strings" | ||
|
||
"github.com/cloudflare/cloudflare-go" | ||
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/flatteners" | ||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-framework/resource" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
) | ||
|
||
// Ensure provider defined types fully satisfy framework interfaces. | ||
var _ resource.Resource = &ChallengeWidgetResource{} | ||
var _ resource.ResourceWithImportState = &ChallengeWidgetResource{} | ||
|
||
func NewResource() resource.Resource { | ||
return &ChallengeWidgetResource{} | ||
} | ||
|
||
// ChallengeWidgetResource defines the resource implementation for challenge widgets. | ||
type ChallengeWidgetResource struct { | ||
client *cloudflare.API | ||
} | ||
|
||
func (r *ChallengeWidgetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { | ||
resp.TypeName = req.ProviderTypeName + "_challenge_widget" | ||
} | ||
|
||
func (r *ChallengeWidgetResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { | ||
if req.ProviderData == nil { | ||
return | ||
} | ||
|
||
client, ok := req.ProviderData.(*cloudflare.API) | ||
|
||
if !ok { | ||
resp.Diagnostics.AddError( | ||
"Unexpected Resource Configure Type", | ||
fmt.Sprintf("Expected *cloudflare.API, got: %T. Please report this issue to the provider developers.", req.ProviderData), | ||
) | ||
|
||
return | ||
} | ||
|
||
r.client = client | ||
} | ||
|
||
func (r *ChallengeWidgetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { | ||
var data *ChallengeWidgetModel | ||
|
||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) | ||
|
||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
widget := buildChallengeWidgetFromModel(data) | ||
|
||
createWidget, err := r.client.CreateChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), | ||
cloudflare.CreateChallengeWidgetRequest{ | ||
OffLabel: data.OffLabel.ValueBool(), | ||
Name: widget.Name, | ||
Domains: widget.Domains, | ||
Mode: widget.Mode, | ||
BotFightMode: widget.BotFightMode, | ||
Region: widget.Region, | ||
}) | ||
if err != nil { | ||
resp.Diagnostics.AddError("Error creating challenge widget", err.Error()) | ||
} | ||
|
||
data = buildChallengeModelFromWidget( | ||
data.AccountID, | ||
createWidget, | ||
) | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) | ||
} | ||
|
||
func (r *ChallengeWidgetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { | ||
var data *ChallengeWidgetModel | ||
|
||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...) | ||
|
||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
widget, err := r.client.GetChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), data.ID.ValueString()) | ||
|
||
if err != nil { | ||
resp.Diagnostics.AddError("Error reading challenge widget", err.Error()) | ||
} | ||
|
||
data = buildChallengeModelFromWidget( | ||
data.AccountID, | ||
widget, | ||
) | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) | ||
} | ||
|
||
func (r *ChallengeWidgetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { | ||
var data *ChallengeWidgetModel | ||
|
||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) | ||
|
||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
widget := buildChallengeWidgetFromModel(data) | ||
|
||
updatedWidget, err := r.client.UpdateChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), widget) | ||
|
||
if err != nil { | ||
resp.Diagnostics.AddError("Error reading challenge widget", err.Error()) | ||
} | ||
|
||
data = buildChallengeModelFromWidget( | ||
data.AccountID, | ||
updatedWidget, | ||
) | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) | ||
} | ||
|
||
func (r *ChallengeWidgetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { | ||
var data *ChallengeWidgetModel | ||
|
||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...) | ||
|
||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
err := r.client.DeleteChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), data.ID.ValueString()) | ||
if err != nil { | ||
resp.Diagnostics.AddError("Error deleting challenge widget", err.Error()) | ||
} | ||
} | ||
|
||
func (r *ChallengeWidgetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { | ||
idParts := strings.Split(req.ID, "/") | ||
if len(idParts) != 2 { | ||
resp.Diagnostics.AddError("Error importing challenge widget", "Invalid ID specified. Please specify the ID as \"accounts_id/sitekey\"") | ||
} | ||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("account_id"), idParts[0])...) | ||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) | ||
} | ||
|
||
func buildChallengeWidgetFromModel(widget *ChallengeWidgetModel) cloudflare.ChallengeWidget { | ||
built := cloudflare.ChallengeWidget{ | ||
SiteKey: widget.ID.ValueString(), | ||
Name: widget.Name.ValueString(), | ||
BotFightMode: widget.BotFightMode.ValueBool(), | ||
Mode: widget.Mode.ValueString(), | ||
Region: widget.Region.ValueString(), | ||
Domains: expanders.StringSet(widget.Domains), | ||
} | ||
|
||
return built | ||
} | ||
|
||
func buildChallengeModelFromWidget(accountID types.String, widget cloudflare.ChallengeWidget) *ChallengeWidgetModel { | ||
built := ChallengeWidgetModel{ | ||
AccountID: accountID, | ||
ID: flatteners.String(widget.SiteKey), | ||
Secret: flatteners.String(widget.Secret), | ||
BotFightMode: types.BoolValue(widget.BotFightMode), | ||
Name: flatteners.String(widget.Name), | ||
Mode: flatteners.String(widget.Mode), | ||
Region: flatteners.String(widget.Region), | ||
} | ||
|
||
var domains []attr.Value | ||
for _, s := range widget.Domains { | ||
domains = append(domains, types.StringValue(s)) | ||
} | ||
built.Domains = flatteners.StringSet(domains) | ||
|
||
return &built | ||
} |
54 changes: 54 additions & 0 deletions
54
internal/framework/service/challenge_widget/resource_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package challenge_widget_test | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"testing" | ||
|
||
"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest" | ||
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils" | ||
"github.com/hashicorp/terraform-plugin-testing/helper/resource" | ||
) | ||
|
||
func TestAccCloudflareChallengeWidgetBasic(t *testing.T) { | ||
rnd := utils.GenerateRandomResourceName() | ||
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") | ||
resourceName := "cloudflare_challenge_widget." + rnd | ||
|
||
resource.ParallelTest(t, resource.TestCase{ | ||
PreCheck: func() { acctest.TestAccPreCheck(t) }, | ||
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: testAccCheckCloudflareChallengeWidgetBasic(rnd, accountID), | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr(resourceName, "name", rnd), | ||
resource.TestCheckResourceAttr(resourceName, "account_id", accountID), | ||
resource.TestCheckResourceAttr(resourceName, "bot_fight_mode", "false"), | ||
resource.TestCheckResourceAttr(resourceName, "domains.#", "1"), | ||
resource.TestCheckResourceAttr(resourceName, "domains.0", "example.com"), | ||
resource.TestCheckResourceAttr(resourceName, "mode", "invisible"), | ||
resource.TestCheckResourceAttr(resourceName, "region", "world"), | ||
), | ||
}, | ||
{ | ||
ResourceName: resourceName, | ||
ImportStateIdPrefix: fmt.Sprintf("%s/", accountID), | ||
ImportState: true, | ||
ImportStateVerify: true, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccCheckCloudflareChallengeWidgetBasic(rnd, accountID string) string { | ||
return fmt.Sprintf(` | ||
resource "cloudflare_challenge_widget" "%[1]s" { | ||
account_id = "%[2]s" | ||
name = "%[1]s" | ||
bot_fight_mode = false | ||
domains = [ "example.com" ] | ||
mode = "invisible" | ||
region = "world" | ||
}`, rnd, accountID) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package challenge_widget | ||
|
||
import ( | ||
"context" | ||
"github.com/MakeNowJust/heredoc/v2" | ||
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts" | ||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" | ||
"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" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
) | ||
|
||
func (r *ChallengeWidgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { | ||
resp.Schema = schema.Schema{ | ||
MarkdownDescription: heredoc.Doc(` | ||
The [Challenge Widget](https://developers.cloudflare.com/turnstile/) resource allows you to manage Cloudflare Turnstile Widgets. | ||
`), | ||
|
||
Attributes: map[string]schema.Attribute{ | ||
consts.IDSchemaKey: schema.StringAttribute{ | ||
Computed: true, | ||
Optional: true, | ||
MarkdownDescription: consts.IDSchemaDescription + " This is the site key value.", | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.UseStateForUnknown(), | ||
}, | ||
}, | ||
consts.AccountIDSchemaKey: schema.StringAttribute{ | ||
MarkdownDescription: "The account identifier to target for the resource.", | ||
Required: true, | ||
}, | ||
"secret": schema.StringAttribute{ | ||
MarkdownDescription: "Secret key for this widget.", | ||
Computed: true, | ||
Sensitive: true, | ||
}, | ||
"name": schema.StringAttribute{ | ||
MarkdownDescription: "Human readable widget name.", | ||
Required: true, | ||
}, | ||
"domains": schema.SetAttribute{ | ||
MarkdownDescription: "Domains where the widget is deployed", | ||
Required: true, | ||
ElementType: types.StringType, | ||
}, | ||
"mode": schema.StringAttribute{ | ||
MarkdownDescription: "Widget Mode", | ||
Optional: true, | ||
Validators: []validator.String{ | ||
stringvalidator.OneOf("non-interactive", "invisible", "managed"), | ||
}, | ||
}, | ||
"region": schema.StringAttribute{ | ||
MarkdownDescription: "Region where this widget can be used.", | ||
Optional: true, | ||
Validators: []validator.String{ | ||
stringvalidator.OneOf("world"), | ||
}, | ||
}, | ||
"bot_fight_mode": schema.BoolAttribute{ | ||
MarkdownDescription: "If bot_fight_mode is set to true, Cloudflare issues computationally expensive challenges in response to malicious bots (ENT only).", | ||
Optional: true, | ||
}, | ||
}, | ||
} | ||
} |