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

FLPROD-796: Register snippet and snippet_rules resources #4565

Merged
merged 5 commits into from
Dec 5, 2024
Merged
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
7 changes: 7 additions & 0 deletions .changelog/4565.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:new-resource
cloudflare_snippet
```

```release-note:new-resource
cloudflare_snippet_rules
```
4 changes: 4 additions & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/r2_bucket"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/risk_behavior"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/rulesets"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/snippet_rules"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/snippets"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/turnstile"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/user"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/workers_for_platforms_dispatch_namespace"
Expand Down Expand Up @@ -369,6 +371,8 @@ func (p *CloudflareProvider) Configure(ctx context.Context, req provider.Configu

func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
snippet_rules.NewResource,
snippets.NewResource,
cloud_connector_rules.NewResource,
d1.NewResource,
email_routing_address.NewResource,
Expand Down
15 changes: 15 additions & 0 deletions internal/framework/service/snippet_rules/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package snippet_rules

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

type SnippetRules struct {
ZoneID types.String `tfsdk:"zone_id"`
Rules []SnippetRule `tfsdk:"rules"`
}

type SnippetRule struct {
Enabled types.Bool `tfsdk:"enabled"`
Expression types.String `tfsdk:"expression"`
Description types.String `tfsdk:"description"`
SnippetName types.String `tfsdk:"snippet_name"`
}
166 changes: 166 additions & 0 deletions internal/framework/service/snippet_rules/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package snippet_rules

import (
"context"
"fmt"
"strings"

cfv1 "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient"
"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 = &SnippetRulesResource{}
var _ resource.ResourceWithImportState = &SnippetRulesResource{}

// SnippetRulesResource defines the resource implementation.
type SnippetRulesResource struct {
client *muxclient.Client
}

func NewResource() resource.Resource {
return &SnippetRulesResource{}
}

func (r *SnippetRulesResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_snippet_rules"
}

func (r *SnippetRulesResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*muxclient.Client)

if !ok {
resp.Diagnostics.AddError(
"unexpected resource configure type",
fmt.Sprintf("Expected *muxclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

func resourceFromAPIResponse(data *SnippetRules, rules []cfv1.SnippetRule) {
result := make([]SnippetRule, len(rules))
for i, rule := range rules {
result[i].Description = types.StringValue(rule.Description)
result[i].Expression = types.StringValue(rule.Expression)
result[i].Enabled = types.BoolPointerValue(rule.Enabled)
result[i].SnippetName = types.StringValue(rule.SnippetName)
}
data.Rules = result
}

func requestFromResource(data *SnippetRules) []cfv1.SnippetRule {
rules := make([]cfv1.SnippetRule, len(data.Rules))
for i, rule := range data.Rules {
rules[i] = cfv1.SnippetRule{
Enabled: rule.Enabled.ValueBoolPointer(),
Expression: rule.Expression.ValueString(),
SnippetName: rule.SnippetName.ValueString(),
Description: rule.Description.ValueString(),
}
}
return rules
}

func (r *SnippetRulesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *SnippetRules

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

rules, err := r.client.V1.UpdateZoneSnippetsRules(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()),
requestFromResource(data),
)
if err != nil {
resp.Diagnostics.AddError("failed to create snippet rules", err.Error())
return
}

resourceFromAPIResponse(data, rules)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *SnippetRulesResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *SnippetRules

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

rules, err := r.client.V1.ListZoneSnippetsRules(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()))
if err != nil {
resp.Diagnostics.AddError("failed reading snippet rules", err.Error())
return
}
resourceFromAPIResponse(data, rules)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *SnippetRulesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *SnippetRules

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

rules, err := r.client.V1.UpdateZoneSnippetsRules(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()),
requestFromResource(data),
)
if err != nil {
resp.Diagnostics.AddError("failed to create snippet rules", err.Error())
return
}

resourceFromAPIResponse(data, rules)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *SnippetRulesResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *SnippetRules

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

_, err := r.client.V1.UpdateZoneSnippetsRules(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()), []cfv1.SnippetRule{})

if err != nil {
resp.Diagnostics.AddError("failed to delete snippet rules", err.Error())
return
}
}

func (r *SnippetRulesResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idparts := strings.Split(req.ID, "/")
if len(idparts) != 2 {
resp.Diagnostics.AddError("error importing snippet rule", `invalid ID specified. Please specify the ID as "<zone_id>/<snippet_rule_id>"`)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("zone_id"), idparts[0],
)...)
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("id"), idparts[1],
)...)
}
125 changes: 125 additions & 0 deletions internal/framework/service/snippet_rules/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package snippet_rules_test

import (
"context"
"fmt"
"os"
"testing"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/pkg/errors"
)

func TestMain(m *testing.M) {
resource.TestMain(m)
}

func init() {
resource.AddTestSweepers("cloudflare_snippet_rules", &resource.Sweeper{
Name: "cloudflare_snippet_rules",
F: testSweepCloudflareSnippetRules,
})
}

func testSweepCloudflareSnippetRules(r string) error {
ctx := context.Background()
client, clientErr := acctest.SharedV1Client()
if clientErr != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to create Cloudflare client: %s", clientErr))
}

zone := os.Getenv("CLOUDFLARE_ZONE_ID")
if zone == "" {
return errors.New("CLOUDFLARE_ZONE_ID must be set")
}

_, err := client.UpdateZoneSnippetsRules(context.Background(), cloudflare.ZoneIdentifier(zone), []cloudflare.SnippetRule{})
if err != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to disable Cloudflare Zone Snippet Rules: %s", err))
}

return nil
}

func TestAccCloudflareSnippetRules(t *testing.T) {
t.Parallel()

rnd := utils.GenerateRandomResourceName()
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
resourceName := "cloudflare_snippet_rules." + rnd
resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareSnippetRules(rnd, zoneID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttr(resourceName, "rules.#", "3"),
resource.TestCheckResourceAttr(resourceName, "rules.0.%", "4"),
resource.TestCheckResourceAttr(resourceName, "rules.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "rules.0.expression", "true"),
resource.TestCheckResourceAttr(resourceName, "rules.0.description", "some description 1"),
resource.TestCheckResourceAttr(resourceName, "rules.0.snippet_name", "test_snippet_0"),

resource.TestCheckResourceAttr(resourceName, "rules.1.%", "4"),
resource.TestCheckResourceAttr(resourceName, "rules.1.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "rules.1.expression", "true"),
resource.TestCheckResourceAttr(resourceName, "rules.1.description", "some description 2"),
resource.TestCheckResourceAttr(resourceName, "rules.1.snippet_name", "test_snippet_1"),

resource.TestCheckResourceAttr(resourceName, "rules.2.%", "4"),
resource.TestCheckResourceAttr(resourceName, "rules.2.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "rules.2.expression", "true"),
resource.TestCheckResourceAttr(resourceName, "rules.2.description", "some description 3"),
resource.TestCheckResourceAttr(resourceName, "rules.2.snippet_name", "test_snippet_2"),
),
},
},
})
}

func testAccCheckCloudflareSnippetRules(rnd, zoneID string) string {
return fmt.Sprintf(`
resource "cloudflare_snippet" "%[1]s" {
count = 3
zone_id = "%[2]s"
name = "test_snippet_${count.index}"
main_module = "file1.js"
files {
name = "file1.js"
content = "export default {async fetch(request) {return fetch(request)}};"
}
}

resource "cloudflare_snippet_rules" "%[1]s" {
zone_id = "%[2]s"
rules {
enabled = true
expression = "true"
description = "some description 1"
snippet_name = "test_snippet_0"
}

rules {
enabled = true
expression = "true"
description = "some description 2"
snippet_name = "test_snippet_1"
}

rules {
enabled = true
expression = "true"
description = "some description 3"
snippet_name = "test_snippet_2"
}

depends_on = ["cloudflare_snippet.%[1]s"]
}`, rnd, zoneID)
}
62 changes: 62 additions & 0 deletions internal/framework/service/snippet_rules/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package snippet_rules

import (
"context"

"github.com/MakeNowJust/heredoc/v2"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"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"
)

func (r *SnippetRulesResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: heredoc.Doc(`
The [Snippet Rules](https://developers.cloudflare.com/rules/snippets/) resource allows you to create and manage snippet rules for a zone.
`),
Version: 1,

Attributes: map[string]schema.Attribute{
consts.ZoneIDSchemaKey: schema.StringAttribute{
MarkdownDescription: consts.ZoneIDSchemaDescription,
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
Blocks: map[string]schema.Block{
"rules": schema.SetNestedBlock{
MarkdownDescription: "List of Snippet Rules",
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
setvalidator.IsRequired(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"enabled": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Whether the headers rule is active.",
},
"expression": schema.StringAttribute{
Required: true,
MarkdownDescription: "Criteria for an HTTP request to trigger the snippet rule. Uses the Firewall Rules expression language based on Wireshark display filters.",
},
"snippet_name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Name of the snippet invoked by this rule.",
},
"description": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Brief summary of the snippet rule and its intended use.",
},
},
},
},
},
}
}
Loading
Loading