Skip to content

Commit 9cff07e

Browse files
committed
feat: new project spike protection resource
1 parent 479c55b commit 9cff07e

File tree

3 files changed

+307
-0
lines changed

3 files changed

+307
-0
lines changed

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func (p *SentryProvider) Configure(ctx context.Context, req provider.ConfigureRe
9595
func (p *SentryProvider) Resources(ctx context.Context) []func() resource.Resource {
9696
return []func() resource.Resource{
9797
NewProjectInboundDataFilterResource,
98+
NewProjectSpikeProtectionResource,
9899
NewTeamMemberResource,
99100
}
100101
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/path"
9+
"github.com/hashicorp/terraform-plugin-framework/resource"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
"github.com/jianyuan/go-sentry/v2/sentry"
15+
)
16+
17+
var _ resource.Resource = &ProjectSpikeProtectionResource{}
18+
var _ resource.ResourceWithImportState = &ProjectSpikeProtectionResource{}
19+
20+
func NewProjectSpikeProtectionResource() resource.Resource {
21+
return &ProjectSpikeProtectionResource{}
22+
}
23+
24+
type ProjectSpikeProtectionResource struct {
25+
client *sentry.Client
26+
}
27+
28+
type ProjectSpikeProtectionResourceModel struct {
29+
Id types.String `tfsdk:"id"`
30+
Organization types.String `tfsdk:"organization"`
31+
ProjectSlug types.String `tfsdk:"project_slug"`
32+
Enabled types.Bool `tfsdk:"enabled"`
33+
}
34+
35+
func (r *ProjectSpikeProtectionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
36+
resp.TypeName = req.ProviderTypeName + "_project_spike_protection"
37+
}
38+
39+
func (r *ProjectSpikeProtectionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
40+
resp.Schema = schema.Schema{
41+
MarkdownDescription: "Sentry Project Spike Protection resource. This resource is used to create and manage spike protection for a project.",
42+
43+
Attributes: map[string]schema.Attribute{
44+
"id": schema.StringAttribute{
45+
Description: "The ID of this resource.",
46+
Computed: true,
47+
PlanModifiers: []planmodifier.String{
48+
stringplanmodifier.UseStateForUnknown(),
49+
},
50+
},
51+
"organization": schema.StringAttribute{
52+
Description: "The slug of the organization the project belongs to.",
53+
Required: true,
54+
},
55+
"project_slug": schema.StringAttribute{
56+
Description: "The slug of the project to create the filter for.",
57+
Required: true,
58+
},
59+
"enabled": schema.BoolAttribute{
60+
Description: "Toggle the browser-extensions, localhost, filtered-transaction, or web-crawlers filter on or off.",
61+
Required: true,
62+
},
63+
},
64+
}
65+
}
66+
67+
func (r *ProjectSpikeProtectionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
68+
// Prevent panic if the provider has not been configured.
69+
if req.ProviderData == nil {
70+
return
71+
}
72+
73+
client, ok := req.ProviderData.(*sentry.Client)
74+
75+
if !ok {
76+
resp.Diagnostics.AddError(
77+
"Unexpected Resource Configure Type",
78+
fmt.Sprintf("Expected *sentry.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
79+
)
80+
81+
return
82+
}
83+
84+
r.client = client
85+
}
86+
87+
func (r *ProjectSpikeProtectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
88+
var data ProjectSpikeProtectionResourceModel
89+
90+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
91+
92+
if resp.Diagnostics.HasError() {
93+
return
94+
}
95+
96+
if data.Enabled.ValueBool() {
97+
_, err := r.client.SpikeProtections.Enable(
98+
ctx,
99+
data.Organization.ValueString(),
100+
&sentry.SpikeProtectionParams{
101+
Projects: []string{data.ProjectSlug.ValueString()},
102+
},
103+
)
104+
if err != nil {
105+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error enabling spike protection: %s", err.Error()))
106+
return
107+
}
108+
} else {
109+
_, err := r.client.SpikeProtections.Disable(
110+
ctx,
111+
data.Organization.ValueString(),
112+
&sentry.SpikeProtectionParams{
113+
Projects: []string{data.ProjectSlug.ValueString()},
114+
},
115+
)
116+
if err != nil {
117+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error disabling spike protection: %s", err.Error()))
118+
return
119+
}
120+
}
121+
122+
data.Id = types.StringValue(buildTwoPartID(data.Organization.ValueString(), data.ProjectSlug.ValueString()))
123+
124+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
125+
}
126+
127+
func (r *ProjectSpikeProtectionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
128+
var data ProjectSpikeProtectionResourceModel
129+
130+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
131+
132+
if resp.Diagnostics.HasError() {
133+
return
134+
}
135+
136+
project, apiResp, err := r.client.Projects.Get(
137+
ctx,
138+
data.Organization.ValueString(),
139+
data.ProjectSlug.ValueString(),
140+
)
141+
if apiResp.StatusCode == http.StatusNotFound {
142+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Project not found: %s", err.Error()))
143+
resp.State.RemoveResource(ctx)
144+
return
145+
}
146+
if err != nil {
147+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error reading project: %s", err.Error()))
148+
return
149+
}
150+
151+
data.Id = types.StringValue(buildTwoPartID(data.Organization.ValueString(), project.Slug))
152+
data.Organization = types.StringPointerValue(project.Organization.Slug)
153+
data.ProjectSlug = types.StringValue(project.Slug)
154+
if disabled, ok := project.Options["quotas:spike-protection-disabled"].(bool); ok {
155+
data.Enabled = types.BoolValue(!disabled)
156+
}
157+
158+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
159+
}
160+
161+
func (r *ProjectSpikeProtectionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
162+
var data ProjectSpikeProtectionResourceModel
163+
164+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
165+
166+
if resp.Diagnostics.HasError() {
167+
return
168+
}
169+
170+
if data.Enabled.ValueBool() {
171+
_, err := r.client.SpikeProtections.Enable(
172+
ctx,
173+
data.Organization.ValueString(),
174+
&sentry.SpikeProtectionParams{
175+
Projects: []string{data.ProjectSlug.ValueString()},
176+
},
177+
)
178+
if err != nil {
179+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error enabling spike protection: %s", err.Error()))
180+
return
181+
}
182+
} else {
183+
_, err := r.client.SpikeProtections.Disable(
184+
ctx,
185+
data.Organization.ValueString(),
186+
&sentry.SpikeProtectionParams{
187+
Projects: []string{data.ProjectSlug.ValueString()},
188+
},
189+
)
190+
if err != nil {
191+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error disabling spike protection: %s", err.Error()))
192+
return
193+
}
194+
}
195+
196+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
197+
}
198+
199+
func (r *ProjectSpikeProtectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
200+
var data ProjectSpikeProtectionResourceModel
201+
202+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
203+
204+
if resp.Diagnostics.HasError() {
205+
return
206+
}
207+
208+
apiResp, err := r.client.SpikeProtections.Disable(
209+
ctx,
210+
data.Organization.ValueString(),
211+
&sentry.SpikeProtectionParams{
212+
Projects: []string{data.ProjectSlug.ValueString()},
213+
},
214+
)
215+
if apiResp.StatusCode == http.StatusNotFound {
216+
return
217+
}
218+
219+
if err != nil {
220+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error disabling spike protection: %s", err.Error()))
221+
return
222+
}
223+
}
224+
225+
func (r *ProjectSpikeProtectionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
226+
org, projectSlug, err := splitTwoPartID(req.ID, "organization", "project-slug")
227+
if err != nil {
228+
resp.Diagnostics.AddError("Invalid ID", fmt.Sprintf("Error parsing ID: %s", err.Error()))
229+
return
230+
}
231+
resp.Diagnostics.Append(resp.State.SetAttribute(
232+
ctx, path.Root("organization"), org,
233+
)...)
234+
resp.Diagnostics.Append(resp.State.SetAttribute(
235+
ctx, path.Root("project_slug"), projectSlug,
236+
)...)
237+
resp.Diagnostics.Append(resp.State.SetAttribute(
238+
ctx, path.Root("id"), req.ID,
239+
)...)
240+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package provider
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
8+
"github.com/jianyuan/terraform-provider-sentry/internal/acctest"
9+
)
10+
11+
func TestAccProjectSpikeProtectionResource(t *testing.T) {
12+
rn := "sentry_project_spike_protection.test"
13+
teamSlug := acctest.RandomWithPrefix("tf-team")
14+
projectSlug := acctest.RandomWithPrefix("tf-project")
15+
16+
resource.Test(t, resource.TestCase{
17+
PreCheck: func() { acctest.PreCheck(t) },
18+
ProtoV5ProviderFactories: testAccProtoV5ProviderFactories,
19+
Steps: []resource.TestStep{
20+
{
21+
Config: testAccProjectSpikeProtectionConfig(teamSlug, projectSlug, true),
22+
Check: resource.ComposeTestCheckFunc(
23+
resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization),
24+
resource.TestCheckResourceAttr(rn, "project_slug", projectSlug),
25+
resource.TestCheckResourceAttr(rn, "enabled", "true"),
26+
),
27+
},
28+
{
29+
Config: testAccProjectSpikeProtectionConfig(teamSlug, projectSlug, false),
30+
Check: resource.ComposeTestCheckFunc(
31+
resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization),
32+
resource.TestCheckResourceAttr(rn, "project_slug", projectSlug),
33+
resource.TestCheckResourceAttr(rn, "enabled", "false"),
34+
),
35+
},
36+
{
37+
ResourceName: rn,
38+
ImportState: true,
39+
ImportStateVerify: true,
40+
},
41+
},
42+
})
43+
}
44+
45+
func testAccProjectSpikeProtectionConfig(teamName string, projectName string, enabled bool) string {
46+
return testAccOrganizationDataSourceConfig + fmt.Sprintf(`
47+
resource "sentry_team" "test" {
48+
organization = data.sentry_organization.test.id
49+
name = "%[1]s"
50+
slug = "%[1]s"
51+
}
52+
53+
resource "sentry_project" "test" {
54+
organization = sentry_team.test.organization
55+
teams = [sentry_team.test.id]
56+
name = "%[2]s"
57+
platform = "go"
58+
}
59+
60+
resource "sentry_project_spike_protection" "test" {
61+
organization = sentry_project.test.organization
62+
project_slug = sentry_project.test.slug
63+
enabled = %[3]t
64+
}
65+
`, teamName, projectName, enabled)
66+
}

0 commit comments

Comments
 (0)