From 276bc812749b08058ea41a9211b3759245326503 Mon Sep 17 00:00:00 2001 From: Denis Davydov Date: Tue, 12 Nov 2024 20:35:38 +0000 Subject: [PATCH 1/3] FLPROD-796: Register snippet and snippet_rules resources --- .changelog/4565.txt | 3 + internal/framework/provider/provider.go | 4 + .../framework/service/snippet_rules/model.go | 15 ++ .../service/snippet_rules/resource.go | 166 ++++++++++++++++++ .../service/snippet_rules/resource_test.go | 151 ++++++++++++++++ .../framework/service/snippet_rules/schema.go | 62 +++++++ internal/framework/service/snippets/model.go | 15 ++ .../framework/service/snippets/resource.go | 161 +++++++++++++++++ .../service/snippets/resource_test.go | 86 +++++++++ internal/framework/service/snippets/schema.go | 65 +++++++ 10 files changed, 728 insertions(+) create mode 100644 .changelog/4565.txt create mode 100644 internal/framework/service/snippet_rules/model.go create mode 100644 internal/framework/service/snippet_rules/resource.go create mode 100644 internal/framework/service/snippet_rules/resource_test.go create mode 100644 internal/framework/service/snippet_rules/schema.go create mode 100644 internal/framework/service/snippets/model.go create mode 100644 internal/framework/service/snippets/resource.go create mode 100644 internal/framework/service/snippets/resource_test.go create mode 100644 internal/framework/service/snippets/schema.go diff --git a/.changelog/4565.txt b/.changelog/4565.txt new file mode 100644 index 0000000000..1c9341943e --- /dev/null +++ b/.changelog/4565.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +cloudflare_snippet_rules and cloudflare_snippet +``` diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index 7e87a8d657..8472da7c0b 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -33,6 +33,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" @@ -368,6 +370,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, diff --git a/internal/framework/service/snippet_rules/model.go b/internal/framework/service/snippet_rules/model.go new file mode 100644 index 0000000000..22012eb076 --- /dev/null +++ b/internal/framework/service/snippet_rules/model.go @@ -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"` +} diff --git a/internal/framework/service/snippet_rules/resource.go b/internal/framework/service/snippet_rules/resource.go new file mode 100644 index 0000000000..cbe43a3d61 --- /dev/null +++ b/internal/framework/service/snippet_rules/resource.go @@ -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 "/"`) + 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], + )...) +} diff --git a/internal/framework/service/snippet_rules/resource_test.go b/internal/framework/service/snippet_rules/resource_test.go new file mode 100644 index 0000000000..d1c2f31aee --- /dev/null +++ b/internal/framework/service/snippet_rules/resource_test.go @@ -0,0 +1,151 @@ +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_1"), + + 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_snippet2"), + + 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_3"), + ), + }, + { + Config: testAccCheckCloudflareSnippetRulesRemovedRule(rnd, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.ZoneIDSchemaKey, zoneID), + resource.TestCheckResourceAttr(resourceName, "rules.#", "2"), + + 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 2"), + resource.TestCheckResourceAttr(resourceName, "rules.0.snippet_name", "test_snippet_2"), + + 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 3"), + resource.TestCheckResourceAttr(resourceName, "rules.1.snippet_name", "test_snippet_3"), + ), + }, + }, + }) +} + +func testAccCheckCloudflareSnippetRules(rnd, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_snippet_rules" "%[1]s" { + zone_id = "%[2]s" + rules { + enabled = true + expression = "true" + description = "some description 1" + snippet_name = "test_snippet_1" + } + + rules { + enabled = true + expression = "true" + description = "some description 2" + snippet_name = "test_snippet_2" + } + + rules { + enabled = true + expression = "true" + description = "some description 3" + snippet_name = "test_snippet_3" + } + }`, rnd, zoneID) +} + +func testAccCheckCloudflareSnippetRulesRemovedRule(rnd, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_snippet_rules" "%[1]s" { + zone_id = "%[2]s" + rules { + enabled = true + expression = "true" + description = "some description 2" + snippet_name = "test_snippet_2" + } + + rules { + enabled = true + expression = "true" + description = "some description 3" + snippet_name = "test_snippet_3" + } + }`, rnd, zoneID) +} diff --git a/internal/framework/service/snippet_rules/schema.go b/internal/framework/service/snippet_rules/schema.go new file mode 100644 index 0000000000..12039c82b6 --- /dev/null +++ b/internal/framework/service/snippet_rules/schema.go @@ -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.", + }, + }, + }, + }, + }, + } +} diff --git a/internal/framework/service/snippets/model.go b/internal/framework/service/snippets/model.go new file mode 100644 index 0000000000..049340a18c --- /dev/null +++ b/internal/framework/service/snippets/model.go @@ -0,0 +1,15 @@ +package snippets + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type Snippet struct { + ZoneID types.String `tfsdk:"zone_id"` + SnippetFile []SnippetFile `tfsdk:"files"` + SnippetName types.String `tfsdk:"name"` + MainModule types.String `tfsdk:"main_module"` +} + +type SnippetFile struct { + FileName types.String `tfsdk:"name"` + Content types.String `tfsdk:"content"` +} diff --git a/internal/framework/service/snippets/resource.go b/internal/framework/service/snippets/resource.go new file mode 100644 index 0000000000..58cd160c50 --- /dev/null +++ b/internal/framework/service/snippets/resource.go @@ -0,0 +1,161 @@ +package snippets + +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 = &SnippetResource{} +var _ resource.ResourceWithImportState = &SnippetResource{} + +// SnippetResource defines the resource implementation. +type SnippetResource struct { + client *muxclient.Client +} + +func NewResource() resource.Resource { + return &SnippetResource{} +} + +func (r *SnippetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_snippet" +} + +func (r *SnippetResource) 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 *Snippet, snippet cfv1.Snippet) { + data.SnippetName = types.StringValue(snippet.SnippetName) +} + +func requestFromResource(data *Snippet) cfv1.SnippetRequest { + files := make([]cfv1.SnippetFile, 0, len(data.SnippetFile)) + for _, file := range data.SnippetFile { + files = append(files, cfv1.SnippetFile{ + FileName: file.FileName.ValueString(), + Content: file.Content.ValueString(), + }) + } + return cfv1.SnippetRequest{ + SnippetName: data.SnippetName.ValueString(), + MainFile: data.MainModule.ValueString(), + Files: files, + } +} + +func (r *SnippetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *Snippet + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + snippet, err := r.client.V1.UpdateZoneSnippet(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()), + requestFromResource(data), + ) + if err != nil { + resp.Diagnostics.AddError("failed to create snippet", err.Error()) + return + } + + resourceFromAPIResponse(data, *snippet) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SnippetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *Snippet + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + snippet, err := r.client.V1.GetZoneSnippet(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()), data.SnippetName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("failed reading snippet", err.Error()) + return + } + resourceFromAPIResponse(data, *snippet) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SnippetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *Snippet + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + snippet, err := r.client.V1.UpdateZoneSnippet(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()), + requestFromResource(data), + ) + if err != nil { + resp.Diagnostics.AddError("failed to create snippet", err.Error()) + return + } + + resourceFromAPIResponse(data, *snippet) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SnippetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *Snippet + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.V1.DeleteZoneSnippet(ctx, cfv1.ZoneIdentifier(data.ZoneID.ValueString()), data.SnippetName.ValueString()) + + if err != nil { + resp.Diagnostics.AddError("failed to delete snippet", err.Error()) + return + } +} + +func (r *SnippetResource) 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", `invalid ID specified. Please specify the ID as "/"`) + 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], + )...) +} diff --git a/internal/framework/service/snippets/resource_test.go b/internal/framework/service/snippets/resource_test.go new file mode 100644 index 0000000000..9db3021301 --- /dev/null +++ b/internal/framework/service/snippets/resource_test.go @@ -0,0 +1,86 @@ +package snippets_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", &resource.Sweeper{ + Name: "cloudflare_snippet", + F: testSweepCloudflareSnippet, + }) +} + +func testSweepCloudflareSnippet(_ 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.DeleteZoneSnippet(context.Background(), cloudflare.ZoneIdentifier(zone), "test_snippet") + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to disable Cloudflare Zone Snippet: %s", err)) + } + + return nil +} + +func TestAccCloudflareSnippet(t *testing.T) { + t.Parallel() + + rnd := utils.GenerateRandomResourceName() + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + resourceName := "cloudflare_snippet." + rnd + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareSnippet(rnd, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.ZoneIDSchemaKey, zoneID), + resource.TestCheckResourceAttr(resourceName, "name", "test_snippet"), + ), + }, + }, + }) +} + +func testAccCheckCloudflareSnippet(rnd, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_snippet" "%[1]s" { + zone_id = "%[2]s" + name = "test_snippet" + main_module = "file1.js" + files { + name = "file1.js" + content = "export default {async fetch(request) {return fetch(request)}};" + } + + files { + name = "file2.js" + content = "export default {async fetch(request) {return fetch(request)}};" + } + }`, rnd, zoneID) +} diff --git a/internal/framework/service/snippets/schema.go b/internal/framework/service/snippets/schema.go new file mode 100644 index 0000000000..b929e7a833 --- /dev/null +++ b/internal/framework/service/snippets/schema.go @@ -0,0 +1,65 @@ +package snippets + +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 *SnippetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: heredoc.Doc(` + The [Snippet](https://developers.cloudflare.com/rules/snippets/) resource allows you to create and manage snippet for a zone. + `), + Version: 1, + Attributes: map[string]schema.Attribute{ + consts.ZoneIDSchemaKey: schema.StringAttribute{ + MarkdownDescription: consts.ZoneIDSchemaDescription, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the snippet.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "main_module": schema.StringAttribute{ + MarkdownDescription: "Main module file name of the snippet.", + Required: true, + PlanModifiers: []planmodifier.String{}, + }, + }, + Blocks: map[string]schema.Block{ + "files": schema.SetNestedBlock{ + MarkdownDescription: "List of Snippet Files", + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.IsRequired(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Name of the snippet file.", + }, + "content": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Content of the snippet file.", + }, + }, + }, + }, + }, + } +} From c331d352799faac9c2a723f2c58ce69c7cedb9ba Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Mon, 25 Nov 2024 15:10:01 +1100 Subject: [PATCH 2/3] update changelog format --- .changelog/4565.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.changelog/4565.txt b/.changelog/4565.txt index 1c9341943e..667c22bc4c 100644 --- a/.changelog/4565.txt +++ b/.changelog/4565.txt @@ -1,3 +1,7 @@ + ```release-note:new-resource +cloudflare_snippet +``` + ```release-note:new-resource -cloudflare_snippet_rules and cloudflare_snippet +cloudflare_snippet_rules ``` From 25778f71d812058a9b8587d01767c3e9eea84575 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Thu, 5 Dec 2024 12:47:16 +1100 Subject: [PATCH 3/3] fix test assertions --- .../service/snippet_rules/resource_test.go | 96 +++++++------------ 1 file changed, 35 insertions(+), 61 deletions(-) diff --git a/internal/framework/service/snippet_rules/resource_test.go b/internal/framework/service/snippet_rules/resource_test.go index d1c2f31aee..9f80ef0e66 100644 --- a/internal/framework/service/snippet_rules/resource_test.go +++ b/internal/framework/service/snippet_rules/resource_test.go @@ -65,38 +65,19 @@ func TestAccCloudflareSnippetRules(t *testing.T) { 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_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_snippet2"), + 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_3"), - ), - }, - { - Config: testAccCheckCloudflareSnippetRulesRemovedRule(rnd, zoneID), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, consts.ZoneIDSchemaKey, zoneID), - resource.TestCheckResourceAttr(resourceName, "rules.#", "2"), - - 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 2"), - resource.TestCheckResourceAttr(resourceName, "rules.0.snippet_name", "test_snippet_2"), - - 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 3"), - resource.TestCheckResourceAttr(resourceName, "rules.1.snippet_name", "test_snippet_3"), + resource.TestCheckResourceAttr(resourceName, "rules.2.snippet_name", "test_snippet_2"), ), }, }, @@ -105,47 +86,40 @@ func TestAccCloudflareSnippetRules(t *testing.T) { func testAccCheckCloudflareSnippetRules(rnd, zoneID string) string { return fmt.Sprintf(` - resource "cloudflare_snippet_rules" "%[1]s" { - zone_id = "%[2]s" - rules { - enabled = true - expression = "true" - description = "some description 1" - snippet_name = "test_snippet_1" - } - - rules { - enabled = true - expression = "true" - description = "some description 2" - snippet_name = "test_snippet_2" - } - - rules { - enabled = true - expression = "true" - description = "some description 3" - snippet_name = "test_snippet_3" + 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)}};" + } } - }`, rnd, zoneID) -} -func testAccCheckCloudflareSnippetRulesRemovedRule(rnd, zoneID string) string { - return fmt.Sprintf(` resource "cloudflare_snippet_rules" "%[1]s" { - zone_id = "%[2]s" - rules { - enabled = true - expression = "true" - description = "some description 2" - snippet_name = "test_snippet_2" - } - - rules { - enabled = true - expression = "true" - description = "some description 3" - snippet_name = "test_snippet_3" - } + 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) }