From 6df352953b000d1d112b35abcd72d2498454f8e8 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sun, 5 May 2024 23:29:24 +0100 Subject: [PATCH] feat: new `sentry_all_projects_spike_protection` resource (#429) --- docs/data-sources/all_projects.md | 12 +- .../all_projects_spike_protection.md | 65 ++++ docs/resources/project_spike_protection.md | 2 +- .../resource.tf | 40 +++ internal/provider/data_source_all_projects.go | 61 ++-- .../provider/data_source_all_projects_test.go | 30 +- internal/provider/provider.go | 1 + .../resource_all_projects_spike_protection.go | 281 ++++++++++++++++++ ...urce_all_projects_spike_protection_test.go | 85 ++++++ .../resource_project_spike_protection.go | 16 +- .../resource_project_spike_protection_test.go | 33 +- 11 files changed, 537 insertions(+), 89 deletions(-) create mode 100644 docs/resources/all_projects_spike_protection.md create mode 100644 examples/resources/sentry_all_projects_spike_protection/resource.tf create mode 100644 internal/provider/resource_all_projects_spike_protection.go create mode 100644 internal/provider/resource_all_projects_spike_protection_test.go diff --git a/docs/data-sources/all_projects.md b/docs/data-sources/all_projects.md index 421e5cea..bafccfd3 100644 --- a/docs/data-sources/all_projects.md +++ b/docs/data-sources/all_projects.md @@ -28,6 +28,7 @@ data "sentry_projects" "default" { ### Read-Only +- `project_slugs` (Set of String) The slugs of the projects. - `projects` (Attributes Set) The list of projects. (see [below for nested schema](#nestedatt--projects)) @@ -40,16 +41,5 @@ Read-Only: - `features` (Set of String) The features of this project. - `id` (String) The ID of this project. - `name` (String) The name of this project. -- `organization` (Attributes) The organization associated with this project. (see [below for nested schema](#nestedatt--projects--organization)) - `platform` (String) The platform of this project. - `slug` (String) The slug of this project. -- `status` (String) The status of this project. - - -### Nested Schema for `projects.organization` - -Read-Only: - -- `id` (String) The ID of this organization. -- `name` (String) The name of this organization. -- `slug` (String) The slug of this organization. diff --git a/docs/resources/all_projects_spike_protection.md b/docs/resources/all_projects_spike_protection.md new file mode 100644 index 00000000..6a9a99cf --- /dev/null +++ b/docs/resources/all_projects_spike_protection.md @@ -0,0 +1,65 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "sentry_all_projects_spike_protection Resource - terraform-provider-sentry" +subcategory: "" +description: |- + Enable spike protection for all projects in an organization. +--- + +# sentry_all_projects_spike_protection (Resource) + +Enable spike protection for all projects in an organization. + +## Example Usage + +```terraform +# Enable spike protection for several projects in a Sentry organization. +resource "sentry_project" "web-app" { + organization = "my-organization" + + teams = ["my-first-team"] + name = "web-app" + slug = "web-app" + + platform = "go" +} + +resource "sentry_project" "mobile-app" { + organization = "my-organization" + + teams = ["my-second-team"] + name = "mobile-app" + slug = "mobile-app" + + platform = "android" +} + +resource "sentry_all_projects_spike_protection" "main" { + organization = "my-organization" + projects = [ + sentry_project.web-app.id, + sentry_project.mobile-app.id, + ] + enabled = true +} + +# Use the `sentry_all_projects` data source to get all projects in a Sentry organization and enable spike protection for all of them. +data "sentry_all_projects" "all" { + organization = "my-organization" +} + +resource "sentry_all_projects_spike_protection" "main" { + organization = data.sentry_all_projects.all.organization + projects = data.sentry_all_projects.all.project_slugs + enabled = true +} +``` + + +## Schema + +### Required + +- `enabled` (Boolean) Toggle the browser-extensions, localhost, filtered-transaction, or web-crawlers filter on or off for all projects. +- `organization` (String) The slug of the organization the resource belongs to. +- `projects` (Set of String) The slugs of the projects to enable or disable spike protection for. diff --git a/docs/resources/project_spike_protection.md b/docs/resources/project_spike_protection.md index 7d87f79d..63644c83 100644 --- a/docs/resources/project_spike_protection.md +++ b/docs/resources/project_spike_protection.md @@ -37,7 +37,7 @@ resource "sentry_project_spike_protection" "default" { - `enabled` (Boolean) Toggle the browser-extensions, localhost, filtered-transaction, or web-crawlers filter on or off. - `organization` (String) The slug of the organization the project belongs to. -- `project` (String) The slug of the project to create the filter for. +- `project` (String) The slug of the project to enable or disable spike protection for. ### Read-Only diff --git a/examples/resources/sentry_all_projects_spike_protection/resource.tf b/examples/resources/sentry_all_projects_spike_protection/resource.tf new file mode 100644 index 00000000..241957c0 --- /dev/null +++ b/examples/resources/sentry_all_projects_spike_protection/resource.tf @@ -0,0 +1,40 @@ +# Enable spike protection for several projects in a Sentry organization. +resource "sentry_project" "web-app" { + organization = "my-organization" + + teams = ["my-first-team"] + name = "web-app" + slug = "web-app" + + platform = "go" +} + +resource "sentry_project" "mobile-app" { + organization = "my-organization" + + teams = ["my-second-team"] + name = "mobile-app" + slug = "mobile-app" + + platform = "android" +} + +resource "sentry_all_projects_spike_protection" "main" { + organization = "my-organization" + projects = [ + sentry_project.web-app.id, + sentry_project.mobile-app.id, + ] + enabled = true +} + +# Use the `sentry_all_projects` data source to get all projects in a Sentry organization and enable spike protection for all of them. +data "sentry_all_projects" "all" { + organization = "my-organization" +} + +resource "sentry_all_projects_spike_protection" "main" { + organization = data.sentry_all_projects.all.organization + projects = data.sentry_all_projects.all.project_slugs + enabled = true +} diff --git a/internal/provider/data_source_all_projects.go b/internal/provider/data_source_all_projects.go index ac098d17..3103e557 100644 --- a/internal/provider/data_source_all_projects.go +++ b/internal/provider/data_source_all_projects.go @@ -23,15 +23,13 @@ type AllProjectsDataSource struct { } type AllProjectsDataSourceProjectModel struct { - Id types.String `tfsdk:"id"` - Slug types.String `tfsdk:"slug"` - Name types.String `tfsdk:"name"` - Platform types.String `tfsdk:"platform"` - DateCreated types.String `tfsdk:"date_created"` - Features types.Set `tfsdk:"features"` - Color types.String `tfsdk:"color"` - Status types.String `tfsdk:"status"` - Organization OrganizationModel `tfsdk:"organization"` + Id types.String `tfsdk:"id"` + Slug types.String `tfsdk:"slug"` + Name types.String `tfsdk:"name"` + Platform types.String `tfsdk:"platform"` + DateCreated types.String `tfsdk:"date_created"` + Features types.Set `tfsdk:"features"` + Color types.String `tfsdk:"color"` } func (m *AllProjectsDataSourceProjectModel) Fill(project sentry.Project) error { @@ -48,23 +46,25 @@ func (m *AllProjectsDataSourceProjectModel) Fill(project sentry.Project) error { m.Features = types.SetValueMust(types.StringType, featureElements) m.Color = types.StringValue(project.Color) - m.Status = types.StringValue(project.Status) - m.Organization = OrganizationModel{} - if err := m.Organization.Fill(project.Organization); err != nil { - return err - } return nil } type AllProjectsDataSourceModel struct { Organization types.String `tfsdk:"organization"` + ProjectSlugs types.Set `tfsdk:"project_slugs"` Projects []AllProjectsDataSourceProjectModel `tfsdk:"projects"` } func (m *AllProjectsDataSourceModel) Fill(organization string, projects []sentry.Project) error { m.Organization = types.StringValue(organization) + projectSlugElements := []attr.Value{} + for _, project := range projects { + projectSlugElements = append(projectSlugElements, types.StringValue(project.Slug)) + } + m.ProjectSlugs = types.SetValueMust(types.StringType, projectSlugElements) + for _, project := range projects { p := AllProjectsDataSourceProjectModel{} if err := p.Fill(project); err != nil { @@ -89,8 +89,14 @@ func (d *AllProjectsDataSource) Schema(ctx context.Context, req datasource.Schem MarkdownDescription: "The slug of the organization the resource belongs to.", Required: true, }, + "project_slugs": schema.SetAttribute{ + MarkdownDescription: "The slugs of the projects.", + Computed: true, + ElementType: types.StringType, + }, "projects": schema.SetNestedAttribute{ MarkdownDescription: "The list of projects.", + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -122,31 +128,8 @@ func (d *AllProjectsDataSource) Schema(ctx context.Context, req datasource.Schem MarkdownDescription: "The color of this project.", Computed: true, }, - "status": schema.StringAttribute{ - MarkdownDescription: "The status of this project.", - Computed: true, - }, - "organization": schema.SingleNestedAttribute{ - MarkdownDescription: "The organization associated with this project.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - MarkdownDescription: "The ID of this organization.", - Computed: true, - }, - "slug": schema.StringAttribute{ - MarkdownDescription: "The slug of this organization.", - Computed: true, - }, - "name": schema.StringAttribute{ - MarkdownDescription: "The name of this organization.", - Computed: true, - }, - }, - }, }, }, - Computed: true, }, }, } @@ -161,10 +144,10 @@ func (d *AllProjectsDataSource) Read(ctx context.Context, req datasource.ReadReq } var allProjects []sentry.Project - params := &sentry.ListProjectsParams{} + params := &sentry.ListOrganizationProjectsParams{} for { - projects, apiResp, err := d.client.Projects.List(ctx, params) + projects, apiResp, err := d.client.OrganizationProjects.List(ctx, data.Organization.ValueString(), params) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Read error: %s", err)) return diff --git a/internal/provider/data_source_all_projects_test.go b/internal/provider/data_source_all_projects_test.go index aca59ccb..0be82da8 100644 --- a/internal/provider/data_source_all_projects_test.go +++ b/internal/provider/data_source_all_projects_test.go @@ -12,32 +12,30 @@ import ( ) func TestAccAllProjectsDataSource(t *testing.T) { - dn := "data.sentry_all_projects.test" - team := acctest.RandomWithPrefix("tf-team") - project := acctest.RandomWithPrefix("tf-project") + teamName := acctest.RandomWithPrefix("tf-team") + projectName := acctest.RandomWithPrefix("tf-project") + rn := "data.sentry_all_projects.test" resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccAllProjectsDataSourceConfig(team, project), + Config: testAccAllProjectsDataSourceConfig(teamName, projectName), ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue(dn, tfjsonpath.New("projects"), knownvalue.ListPartial(map[int]knownvalue.Check{ - 0: knownvalue.ObjectExact(map[string]knownvalue.Check{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("project_slugs"), knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.StringExact(projectName), + })), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("projects"), knownvalue.SetPartial([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ "id": knownvalue.NotNull(), - "slug": knownvalue.NotNull(), - "name": knownvalue.NotNull(), - "platform": knownvalue.NotNull(), + "slug": knownvalue.StringExact(projectName), + "name": knownvalue.StringExact(projectName), + "platform": knownvalue.StringExact("go"), "date_created": knownvalue.NotNull(), "features": knownvalue.NotNull(), "color": knownvalue.NotNull(), - "status": knownvalue.NotNull(), - "organization": knownvalue.ObjectExact(map[string]knownvalue.Check{ - "id": knownvalue.NotNull(), - "slug": knownvalue.NotNull(), - "name": knownvalue.NotNull(), - }), }), })), }, @@ -63,6 +61,8 @@ resource "sentry_project" "test" { data "sentry_all_projects" "test" { organization = data.sentry_organization.test.slug + + depends_on = [sentry_project.test] } `, teamName, projectName) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 2c7c4365..77d65ea4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -94,6 +94,7 @@ func (p *SentryProvider) Configure(ctx context.Context, req provider.ConfigureRe func (p *SentryProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ + NewAllProjectsSpikeProtectionResource, NewClientKeyResource, NewIssueAlertResource, NewNotificationActionResource, diff --git a/internal/provider/resource_all_projects_spike_protection.go b/internal/provider/resource_all_projects_spike_protection.go new file mode 100644 index 00000000..be57818c --- /dev/null +++ b/internal/provider/resource_all_projects_spike_protection.go @@ -0,0 +1,281 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/jianyuan/go-sentry/v2/sentry" +) + +var _ resource.Resource = &AllProjectsSpikeProtectionResource{} +var _ resource.ResourceWithConfigure = &AllProjectsSpikeProtectionResource{} + +func NewAllProjectsSpikeProtectionResource() resource.Resource { + return &AllProjectsSpikeProtectionResource{} +} + +type AllProjectsSpikeProtectionResource struct { + baseResource +} + +type AllProjectsSpikeProtectionResourceModel struct { + Organization types.String `tfsdk:"organization"` + Enabled types.Bool `tfsdk:"enabled"` + Projects types.Set `tfsdk:"projects"` +} + +func (m *AllProjectsSpikeProtectionResourceModel) Fill(organization string, enabled bool, projects []sentry.Project) error { + m.Organization = types.StringValue(organization) + m.Enabled = types.BoolValue(enabled) + + projectElements := []attr.Value{} + for _, project := range projects { + projectElements = append(projectElements, types.StringValue(project.Slug)) + } + m.Projects = types.SetValueMust(types.StringType, projectElements) + + return nil +} + +func (r *AllProjectsSpikeProtectionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_all_projects_spike_protection" +} + +func (r *AllProjectsSpikeProtectionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Enable spike protection for all projects in an organization.", + + Attributes: map[string]schema.Attribute{ + "organization": schema.StringAttribute{ + MarkdownDescription: "The slug of the organization the resource belongs to.", + Required: true, + }, + "projects": schema.SetAttribute{ + MarkdownDescription: "The slugs of the projects to enable or disable spike protection for.", + Required: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Toggle the browser-extensions, localhost, filtered-transaction, or web-crawlers filter on or off for all projects.", + Required: true, + }, + }, + } +} + +func (r *AllProjectsSpikeProtectionResource) readProjects(ctx context.Context, organization string, enabled bool, projectSlugs []string) ([]sentry.Project, error) { + var allProjects []sentry.Project + params := &sentry.ListOrganizationProjectsParams{ + Options: "quotas:spike-protection-disabled", + } + + for { + projects, apiResp, err := r.client.OrganizationProjects.List(ctx, organization, params) + if err != nil { + return nil, err + } + + for _, project := range projects { + for _, projectSlug := range projectSlugs { + if projectSlug == project.Slug { + if projectDisabled, ok := project.Options["quotas:spike-protection-disabled"].(bool); ok && projectDisabled != enabled { + allProjects = append(allProjects, *project) + } + + break + } + } + } + + if apiResp.Cursor == "" { + break + } + params.Cursor = apiResp.Cursor + } + + return allProjects, nil +} + +func (r *AllProjectsSpikeProtectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data AllProjectsSpikeProtectionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + projects := []string{} + if !data.Projects.IsNull() { + resp.Diagnostics.Append(data.Projects.ElementsAs(ctx, &projects, false)...) + } + + if data.Enabled.ValueBool() { + _, err := r.client.SpikeProtections.Enable( + ctx, + data.Organization.ValueString(), + &sentry.SpikeProtectionParams{ + Projects: projects, + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error enabling spike protection: %s", err.Error())) + return + } + } else { + _, err := r.client.SpikeProtections.Disable( + ctx, + data.Organization.ValueString(), + &sentry.SpikeProtectionParams{ + Projects: projects, + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error disabling spike protection: %s", err.Error())) + return + } + } + + allProjects, err := r.readProjects(ctx, data.Organization.ValueString(), data.Enabled.ValueBool(), projects) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Read error: %s", err)) + return + } + + if err := data.Fill(data.Organization.ValueString(), data.Enabled.ValueBool(), allProjects); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Fill error: %s", err.Error())) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *AllProjectsSpikeProtectionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data AllProjectsSpikeProtectionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + projects := []string{} + if !data.Projects.IsNull() { + resp.Diagnostics.Append(data.Projects.ElementsAs(ctx, &projects, false)...) + } + + allProjects, err := r.readProjects(ctx, data.Organization.ValueString(), data.Enabled.ValueBool(), projects) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Read error: %s", err)) + return + } + + if err := data.Fill(data.Organization.ValueString(), data.Enabled.ValueBool(), allProjects); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Fill error: %s", err.Error())) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *AllProjectsSpikeProtectionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data AllProjectsSpikeProtectionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + projects := []string{} + if !data.Projects.IsNull() { + resp.Diagnostics.Append(data.Projects.ElementsAs(ctx, &projects, false)...) + } + + if data.Enabled.ValueBool() { + _, err := r.client.SpikeProtections.Enable( + ctx, + data.Organization.ValueString(), + &sentry.SpikeProtectionParams{ + Projects: projects, + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error enabling spike protection: %s", err.Error())) + return + } + } else { + _, err := r.client.SpikeProtections.Disable( + ctx, + data.Organization.ValueString(), + &sentry.SpikeProtectionParams{ + Projects: projects, + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error disabling spike protection: %s", err.Error())) + return + } + } + + allProjects, err := r.readProjects(ctx, data.Organization.ValueString(), data.Enabled.ValueBool(), projects) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Read error: %s", err)) + return + } + + if err := data.Fill(data.Organization.ValueString(), data.Enabled.ValueBool(), allProjects); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Fill error: %s", err.Error())) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *AllProjectsSpikeProtectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data AllProjectsSpikeProtectionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + projects := []string{} + if !data.Projects.IsNull() { + resp.Diagnostics.Append(data.Projects.ElementsAs(ctx, &projects, false)...) + } + + if data.Enabled.ValueBool() { + // We need to disable the spike protection if it was enabled. + _, err := r.client.SpikeProtections.Disable( + ctx, + data.Organization.ValueString(), + &sentry.SpikeProtectionParams{ + Projects: projects, + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error disabling spike protection: %s", err.Error())) + return + } + } else { + // We need to enable the spike protection if it was disabled. + _, err := r.client.SpikeProtections.Enable( + ctx, + data.Organization.ValueString(), + &sentry.SpikeProtectionParams{ + Projects: projects, + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error enabling spike protection: %s", err.Error())) + return + } + } +} diff --git a/internal/provider/resource_all_projects_spike_protection_test.go b/internal/provider/resource_all_projects_spike_protection_test.go new file mode 100644 index 00000000..b588548d --- /dev/null +++ b/internal/provider/resource_all_projects_spike_protection_test.go @@ -0,0 +1,85 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/jianyuan/terraform-provider-sentry/internal/acctest" +) + +func TestAccAllProjectsSpikeProtectionResource(t *testing.T) { + teamName := acctest.RandomWithPrefix("tf-team") + project1Name := acctest.RandomWithPrefix("tf-project") + project2Name := acctest.RandomWithPrefix("tf-project") + rn := "sentry_all_projects_spike_protection.test" + + checks := []statecheck.StateCheck{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccProjectResourceConfig(teamName, project1Name) + ` + resource "sentry_all_projects_spike_protection" "test" { + organization = sentry_team.test.organization + projects = [sentry_project.test.id] + enabled = true + } + `, + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("enabled"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("projects"), knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact(project1Name), + })), + ), + }, + { + Config: testAccProjectResourceConfig(teamName, project1Name) + ` + resource "sentry_all_projects_spike_protection" "test" { + organization = sentry_team.test.organization + projects = [sentry_project.test.id] + enabled = false + } + `, + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("enabled"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("projects"), knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact(project1Name), + })), + ), + }, + { + Config: testAccProjectResourceConfig(teamName, project1Name) + fmt.Sprintf(` + resource "sentry_project" "test2" { + organization = sentry_team.test.organization + teams = [sentry_team.test.id] + name = "%[1]s" + } + + resource "sentry_all_projects_spike_protection" "test" { + organization = sentry_team.test.organization + projects = [sentry_project.test.id, sentry_project.test2.id] + enabled = false + } + `, project2Name), + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("enabled"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("projects"), knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact(project1Name), + knownvalue.StringExact(project2Name), + })), + ), + }, + }, + }) +} diff --git a/internal/provider/resource_project_spike_protection.go b/internal/provider/resource_project_spike_protection.go index 6d908cd8..67af1347 100644 --- a/internal/provider/resource_project_spike_protection.go +++ b/internal/provider/resource_project_spike_protection.go @@ -54,23 +54,23 @@ func (r *ProjectSpikeProtectionResource) Schema(ctx context.Context, req resourc Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "The ID of this resource.", - Computed: true, + MarkdownDescription: "The ID of this resource.", + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "organization": schema.StringAttribute{ - Description: "The slug of the organization the project belongs to.", - Required: true, + MarkdownDescription: "The slug of the organization the project belongs to.", + Required: true, }, "project": schema.StringAttribute{ - Description: "The slug of the project to create the filter for.", - Required: true, + MarkdownDescription: "The slug of the project to enable or disable spike protection for.", + Required: true, }, "enabled": schema.BoolAttribute{ - Description: "Toggle the browser-extensions, localhost, filtered-transaction, or web-crawlers filter on or off.", - Required: true, + MarkdownDescription: "Toggle the browser-extensions, localhost, filtered-transaction, or web-crawlers filter on or off.", + Required: true, }, }, } diff --git a/internal/provider/resource_project_spike_protection_test.go b/internal/provider/resource_project_spike_protection_test.go index cddbda4f..c2967ee5 100644 --- a/internal/provider/resource_project_spike_protection_test.go +++ b/internal/provider/resource_project_spike_protection_test.go @@ -5,33 +5,36 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "github.com/jianyuan/terraform-provider-sentry/internal/acctest" ) func TestAccProjectSpikeProtectionResource(t *testing.T) { rn := "sentry_project_spike_protection.test" - team := acctest.RandomWithPrefix("tf-team") - project := acctest.RandomWithPrefix("tf-project") + teamName := acctest.RandomWithPrefix("tf-team") + projectName := acctest.RandomWithPrefix("tf-project") resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccProjectSpikeProtectionResourceConfig(team, project, true), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization), - resource.TestCheckResourceAttr(rn, "project", project), - resource.TestCheckResourceAttr(rn, "enabled", "true"), - ), + Config: testAccProjectSpikeProtectionResourceConfig(teamName, projectName, true), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("project"), knownvalue.StringExact(projectName)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("enabled"), knownvalue.Bool(true)), + }, }, { - Config: testAccProjectSpikeProtectionResourceConfig(team, project, false), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization), - resource.TestCheckResourceAttr(rn, "project", project), - resource.TestCheckResourceAttr(rn, "enabled", "false"), - ), + Config: testAccProjectSpikeProtectionResourceConfig(teamName, projectName, false), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("project"), knownvalue.StringExact(projectName)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("enabled"), knownvalue.Bool(false)), + }, }, { ResourceName: rn, @@ -42,7 +45,7 @@ func TestAccProjectSpikeProtectionResource(t *testing.T) { }) } -func testAccProjectSpikeProtectionResourceConfig(teamName string, projectName string, enabled bool) string { +func testAccProjectSpikeProtectionResourceConfig(teamName, projectName string, enabled bool) string { return testAccOrganizationDataSourceConfig + fmt.Sprintf(` resource "sentry_team" "test" { organization = data.sentry_organization.test.id