diff --git a/docs/index.md b/docs/index.md index 2b674fd7..8951f58d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -107,7 +107,7 @@ output "team" { resource "sentry_project" "main" { organization = sentry_team.main.organization - team = sentry_team.main.id + teams = [sentry_team.main.id] name = "My project" platform = "python" } diff --git a/docs/resources/organization_code_mapping.md b/docs/resources/organization_code_mapping.md index c3529de6..9859a431 100644 --- a/docs/resources/organization_code_mapping.md +++ b/docs/resources/organization_code_mapping.md @@ -23,9 +23,9 @@ data "sentry_organization_integration" "github" { resource "sentry_project" "this" { organization = "my-organization" - team = "my-team" - name = "Web App" - slug = "web-app" + teams = ["my-team"] + name = "Web App" + slug = "web-app" platform = "javascript" resolve_age = 720 diff --git a/docs/resources/project.md b/docs/resources/project.md index b4ebc5b6..32ba32a2 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -35,6 +35,7 @@ resource "sentry_project" "default" { - `name` (String) The name for the project. - `organization` (String) The slug of the organization the project belongs to. +- `teams` (Set of String) The slugs of the teams to create the project for. ### Optional @@ -45,19 +46,12 @@ resource "sentry_project" "default" { - `platform` (String) The platform for this project. For a list of valid values, [see this page](https://github.com/jianyuan/terraform-provider-sentry/blob/main/internal/sentryplatforms/platforms.txt). Use `other` for platforms not listed. - `resolve_age` (Number) Hours in which an issue is automatically resolve if not seen after this amount of time. - `slug` (String) The optional slug for this project. -- `team` (String, Deprecated) The slug of the team to create the project for. **Deprecated** Use `teams` instead. -- `teams` (Set of String) The slugs of the teams to create the project for. ### Read-Only -- `color` (String) -- `features` (List of String) +- `features` (Set of String) - `id` (String) The ID of this resource. - `internal_id` (String) The internal ID for this project. -- `is_bookmarked` (Boolean, Deprecated) -- `is_public` (Boolean) -- `project_id` (String, Deprecated) Use `internal_id` instead. -- `status` (String) ## Import diff --git a/examples/kitchen-sink/demo.tf b/examples/kitchen-sink/demo.tf index 3d11032f..5abfa7a1 100644 --- a/examples/kitchen-sink/demo.tf +++ b/examples/kitchen-sink/demo.tf @@ -38,7 +38,7 @@ output "team" { resource "sentry_project" "main" { organization = sentry_team.main.organization - team = sentry_team.main.id + teams = [sentry_team.main.id] name = "My project" platform = "python" } diff --git a/examples/resources/sentry_organization_code_mapping/resource.tf b/examples/resources/sentry_organization_code_mapping/resource.tf index 0f4ad47d..f0f7b258 100644 --- a/examples/resources/sentry_organization_code_mapping/resource.tf +++ b/examples/resources/sentry_organization_code_mapping/resource.tf @@ -8,9 +8,9 @@ data "sentry_organization_integration" "github" { resource "sentry_project" "this" { organization = "my-organization" - team = "my-team" - name = "Web App" - slug = "web-app" + teams = ["my-team"] + name = "Web App" + slug = "web-app" platform = "javascript" resolve_age = 720 diff --git a/go.mod b/go.mod index 93c3b82e..51688aa5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/jianyuan/terraform-provider-sentry go 1.21 require ( - github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/terraform-plugin-docs v0.19.4 @@ -22,6 +21,7 @@ require ( github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/hashicorp/cli v1.1.6 // indirect + github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/yuin/goldmark v1.7.1 // indirect diff --git a/internal/provider/data_source_all_client_keys_test.go b/internal/provider/data_source_all_client_keys_test.go index cffdcc6b..bab8ee37 100644 --- a/internal/provider/data_source_all_client_keys_test.go +++ b/internal/provider/data_source_all_client_keys_test.go @@ -47,7 +47,10 @@ func TestAccAllClientKeysDataSource(t *testing.T) { } func testAccAllClientKeysDataSourceConfig(teamName, projectName string) string { - return testAccProjectResourceConfig(teamName, projectName) + ` + return testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + }) + ` data "sentry_all_keys" "test" { organization = sentry_project.test.organization project = sentry_project.test.id diff --git a/internal/provider/data_source_client_key_test.go b/internal/provider/data_source_client_key_test.go index 56012e51..5f10f0f5 100644 --- a/internal/provider/data_source_client_key_test.go +++ b/internal/provider/data_source_client_key_test.go @@ -163,7 +163,10 @@ func TestAccClientKeyDataSource_first(t *testing.T) { } func testAccClientKeyDataSourceConfig_bare(teamName, projectName string) string { - return testAccProjectResourceConfig(teamName, projectName) + ` + return testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + }) + ` data "sentry_key" "test" { organization = sentry_project.test.organization project = sentry_project.test.id diff --git a/internal/provider/data_source_project_test.go b/internal/provider/data_source_project_test.go index 696f5bd9..4eff62a0 100644 --- a/internal/provider/data_source_project_test.go +++ b/internal/provider/data_source_project_test.go @@ -54,7 +54,10 @@ func TestAccProjectDataSource_UpgradeFromVersion(t *testing.T) { } func testAccProjectDataSourceConfig(teamName, projectName string) string { - return testAccProjectResourceConfig(teamName, projectName) + ` + return testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + }) + ` data "sentry_project" "test" { organization = sentry_project.test.organization slug = sentry_project.test.id diff --git a/internal/provider/provider.go b/internal/provider/provider.go index de6ba9da..1c9df1e6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -101,6 +101,7 @@ func (p *SentryProvider) Resources(ctx context.Context) []func() resource.Resour NewIssueAlertResource, NewNotificationActionResource, NewProjectInboundDataFilterResource, + NewProjectResource, NewProjectSpikeProtectionResource, NewProjectSymbolSourcesResource, NewTeamMemberResource, diff --git a/internal/provider/resource_all_projects_spike_protection_test.go b/internal/provider/resource_all_projects_spike_protection_test.go index b588548d..81274318 100644 --- a/internal/provider/resource_all_projects_spike_protection_test.go +++ b/internal/provider/resource_all_projects_spike_protection_test.go @@ -26,7 +26,10 @@ func TestAccAllProjectsSpikeProtectionResource(t *testing.T) { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccProjectResourceConfig(teamName, project1Name) + ` + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: project1Name, + }) + ` resource "sentry_all_projects_spike_protection" "test" { organization = sentry_team.test.organization projects = [sentry_project.test.id] @@ -42,7 +45,10 @@ func TestAccAllProjectsSpikeProtectionResource(t *testing.T) { ), }, { - Config: testAccProjectResourceConfig(teamName, project1Name) + ` + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: project1Name, + }) + ` resource "sentry_all_projects_spike_protection" "test" { organization = sentry_team.test.organization projects = [sentry_project.test.id] @@ -58,7 +64,10 @@ func TestAccAllProjectsSpikeProtectionResource(t *testing.T) { ), }, { - Config: testAccProjectResourceConfig(teamName, project1Name) + fmt.Sprintf(` + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: project1Name, + }) + fmt.Sprintf(` resource "sentry_project" "test2" { organization = sentry_team.test.organization teams = [sentry_team.test.id] diff --git a/internal/provider/resource_client_key_test.go b/internal/provider/resource_client_key_test.go index f8723aae..3ef90173 100644 --- a/internal/provider/resource_client_key_test.go +++ b/internal/provider/resource_client_key_test.go @@ -148,7 +148,10 @@ func TestAccClientKeyResource(t *testing.T) { } func testAccClientKeyResourceConfig(teamName, projectName, keyName, extras string) string { - return testAccProjectResourceConfig(teamName, projectName) + fmt.Sprintf(` + return testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + }) + fmt.Sprintf(` resource "sentry_key" "test" { organization = sentry_project.test.organization project = sentry_project.test.id diff --git a/internal/provider/resource_project.go b/internal/provider/resource_project.go new file mode 100644 index 00000000..948b4db8 --- /dev/null +++ b/internal/provider/resource_project.go @@ -0,0 +1,488 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + "slices" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "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" + "github.com/jianyuan/go-sentry/v2/sentry" + "github.com/jianyuan/terraform-provider-sentry/internal/sentryplatforms" +) + +var _ resource.Resource = &ProjectResource{} +var _ resource.ResourceWithConfigure = &ProjectResource{} +var _ resource.ResourceWithImportState = &ProjectResource{} + +func NewProjectResource() resource.Resource { + return &ProjectResource{} +} + +type ProjectResource struct { + baseResource +} + +type ProjectResourceModel struct { + Id types.String `tfsdk:"id"` + Organization types.String `tfsdk:"organization"` + Teams types.Set `tfsdk:"teams"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` + Platform types.String `tfsdk:"platform"` + DefaultRules types.Bool `tfsdk:"default_rules"` + DefaultKey types.Bool `tfsdk:"default_key"` + InternalId types.String `tfsdk:"internal_id"` + Features types.Set `tfsdk:"features"` + DigestsMinDelay types.Int64 `tfsdk:"digests_min_delay"` + DigestsMaxDelay types.Int64 `tfsdk:"digests_max_delay"` + ResolveAge types.Int64 `tfsdk:"resolve_age"` +} + +func (data *ProjectResourceModel) Fill(organization string, project sentry.Project) error { + data.Id = types.StringValue(project.Slug) + data.Organization = types.StringValue(organization) + data.Name = types.StringValue(project.Name) + data.Slug = types.StringValue(project.Slug) + data.Platform = types.StringValue(project.Platform) + data.InternalId = types.StringValue(project.ID) + + if data.DigestsMinDelay.IsNull() { + data.DigestsMinDelay = types.Int64Null() + } else { + data.DigestsMinDelay = types.Int64Value(int64(project.DigestsMinDelay)) + } + if data.DigestsMaxDelay.IsNull() { + data.DigestsMaxDelay = types.Int64Null() + } else { + data.DigestsMaxDelay = types.Int64Value(int64(project.DigestsMaxDelay)) + } + if data.ResolveAge.IsNull() { + data.ResolveAge = types.Int64Null() + } else { + data.ResolveAge = types.Int64Value(int64(project.ResolveAge)) + } + + var teamElements []attr.Value + for _, team := range project.Teams { + teamElements = append(teamElements, types.StringPointerValue(team.Slug)) + } + data.Teams = types.SetValueMust(types.StringType, teamElements) + + var featureElements []attr.Value + for _, feature := range project.Features { + featureElements = append(featureElements, types.StringValue(feature)) + } + data.Features = types.SetValueMust(types.StringType, featureElements) + + return nil +} + +func (r *ProjectResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project" +} + +func (r *ProjectResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Sentry Project resource.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "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, + }, + "teams": schema.SetAttribute{ + Description: "The slugs of the teams to create the project for.", + Required: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + "name": schema.StringAttribute{ + Description: "The name for the project.", + Required: true, + }, + "slug": schema.StringAttribute{ + Description: "The optional slug for this project.", + Optional: true, + Computed: true, + }, + "platform": schema.StringAttribute{ + MarkdownDescription: "The platform for this project. For a list of valid values, [see this page](https://github.com/jianyuan/terraform-provider-sentry/blob/main/internal/sentryplatforms/platforms.txt). Use `other` for platforms not listed.", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf(sentryplatforms.Platforms...), + }, + }, + "default_rules": schema.BoolAttribute{ + Description: "Whether to create a default issue alert. Defaults to true where the behavior is to alert the user on every new issue.", + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "default_key": schema.BoolAttribute{ + Description: "Whether to create a default key. By default, Sentry will create a key for you. If you wish to manage keys manually, set this to false and create keys using the `sentry_key` resource.", + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "internal_id": schema.StringAttribute{ + Description: "The internal ID for this project.", + Computed: true, + }, + "features": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "digests_min_delay": schema.Int64Attribute{ + Description: "The minimum amount of time (in seconds) to wait between scheduling digests for delivery after the initial scheduling.", + Optional: true, + }, + "digests_max_delay": schema.Int64Attribute{ + Description: "The maximum amount of time (in seconds) to wait between scheduling digests for delivery.", + Optional: true, + }, + "resolve_age": schema.Int64Attribute{ + Description: "Hours in which an issue is automatically resolve if not seen after this amount of time.", + Optional: true, + }, + }, + } +} + +func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ProjectResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + var teams []string + resp.Diagnostics.Append(data.Teams.ElementsAs(ctx, &teams, false)...) + if resp.Diagnostics.HasError() { + return + } + + if len(teams) == 0 { + resp.Diagnostics.AddError("Client Error", "At least one team is required") + return + } + + // Create the project + project, _, err := r.client.Projects.Create( + ctx, + data.Organization.ValueString(), + teams[0], + &sentry.CreateProjectParams{ + Name: data.Name.ValueString(), + Slug: data.Slug.ValueString(), + Platform: data.Platform.ValueString(), + DefaultRules: data.DefaultRules.ValueBoolPointer(), + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error creating project: %s", err.Error())) + return + } + + // Update the project + updateParams := &sentry.UpdateProjectParams{ + Name: data.Name.ValueString(), + Slug: data.Slug.ValueString(), + Platform: data.Platform.ValueString(), + } + if !data.DigestsMinDelay.IsNull() { + updateParams.DigestsMinDelay = sentry.Int(int(data.DigestsMinDelay.ValueInt64())) + } + if !data.DigestsMaxDelay.IsNull() { + updateParams.DigestsMaxDelay = sentry.Int(int(data.DigestsMaxDelay.ValueInt64())) + } + if !data.ResolveAge.IsNull() { + updateParams.ResolveAge = sentry.Int(int(data.ResolveAge.ValueInt64())) + } + + project, apiResp, err := r.client.Projects.Update( + ctx, + data.Organization.ValueString(), + project.ID, + updateParams, + ) + if apiResp.StatusCode == http.StatusNotFound { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Project not found: %s", err.Error())) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error updating project: %s", err.Error())) + return + } + + // If the default key is set to false, remove the default key + if !data.DefaultKey.IsNull() && !data.DefaultKey.ValueBool() { + if err := r.removeDefaultKey(ctx, data.Organization.ValueString(), project); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error removing default key: %s", err.Error())) + return + } + } + + // Add additional teams + if len(teams) > 1 { + for _, team := range teams[1:] { + _, _, err := r.client.Projects.AddTeam(ctx, data.Organization.ValueString(), project.Slug, team) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error adding team to project: %s", err.Error())) + return + } + } + } + + project, apiResp, err = r.client.Projects.Get(ctx, data.Organization.ValueString(), project.Slug) + if apiResp.StatusCode == http.StatusNotFound { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Project not found: %s", err.Error())) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error reading project: %s", err.Error())) + return + } + + if err := data.Fill(data.Organization.ValueString(), *project); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error filling project: %s", err.Error())) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ProjectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ProjectResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + project, apiResp, err := r.client.Projects.Get( + ctx, + data.Organization.ValueString(), + data.Id.ValueString(), + ) + if apiResp.StatusCode == http.StatusNotFound { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Project not found: %s", err.Error())) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error reading project: %s", err.Error())) + return + } + + if err := data.Fill(data.Organization.ValueString(), *project); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error filling project: %s", err.Error())) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state ProjectResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + params := &sentry.UpdateProjectParams{ + Name: plan.Name.ValueString(), + Slug: plan.Slug.ValueString(), + Platform: plan.Platform.ValueString(), + } + if !plan.DigestsMinDelay.IsNull() { + params.DigestsMinDelay = sentry.Int(int(plan.DigestsMinDelay.ValueInt64())) + } + if !plan.DigestsMaxDelay.IsNull() { + params.DigestsMaxDelay = sentry.Int(int(plan.DigestsMaxDelay.ValueInt64())) + } + if !plan.ResolveAge.IsNull() { + params.ResolveAge = sentry.Int(int(plan.ResolveAge.ValueInt64())) + } + + project, apiResp, err := r.client.Projects.Update( + ctx, + plan.Organization.ValueString(), + plan.Id.ValueString(), + params, + ) + if apiResp.StatusCode == http.StatusNotFound { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Project not found: %s", err.Error())) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error updating project: %s", err.Error())) + return + } + + // If the default key is set to false, remove the default key + if !plan.DefaultKey.IsNull() && !plan.DefaultKey.ValueBool() { + if err := r.removeDefaultKey(ctx, plan.Organization.ValueString(), project); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error removing default key: %s", err.Error())) + return + } + } + + // Update teams + if !plan.Teams.Equal(state.Teams) { + var planTeams, stateTeams []string + + resp.Diagnostics.Append(plan.Teams.ElementsAs(ctx, &planTeams, false)...) + resp.Diagnostics.Append(state.Teams.ElementsAs(ctx, &stateTeams, false)...) + if resp.Diagnostics.HasError() { + return + } + + // Add teams + for _, team := range planTeams { + if !slices.Contains(stateTeams, team) { + _, _, err := r.client.Projects.AddTeam( + ctx, + plan.Organization.ValueString(), + project.Slug, + team, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error adding team to project: %s", err.Error())) + return + } + } + } + + // Remove teams + for _, team := range stateTeams { + if !slices.Contains(planTeams, team) { + _, err := r.client.Projects.RemoveTeam( + ctx, + plan.Organization.ValueString(), + project.Slug, + team, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error removing team from project: %s", err.Error())) + return + } + } + } + } + + project, apiResp, err = r.client.Projects.Get( + ctx, + plan.Organization.ValueString(), + plan.Id.ValueString(), + ) + if apiResp.StatusCode == http.StatusNotFound { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Project not found: %s", err.Error())) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error reading project: %s", err.Error())) + return + } + + if err := plan.Fill(plan.Organization.ValueString(), *project); err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error filling project: %s", err.Error())) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ProjectResource) removeDefaultKey(ctx context.Context, organization string, project *sentry.Project) error { + params := &sentry.ListProjectKeysParams{} + + for { + keys, apiResp, err := r.client.ProjectKeys.List(ctx, organization, project.Slug, params) + if err != nil { + return err + } + + for _, key := range keys { + if key.Name == "Default" { + _, err := r.client.ProjectKeys.Delete(ctx, organization, project.ID, key.ID) + if err != nil { + return err + } + + return nil + } + } + + if apiResp.Cursor == "" { + break + } + params.Cursor = apiResp.Cursor + } + + return nil +} + +func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data ProjectResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + apiResp, err := r.client.Projects.Delete( + ctx, + data.Organization.ValueString(), + data.Id.ValueString(), + ) + if apiResp.StatusCode == http.StatusNotFound { + return + } + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error deleting project: %s", err.Error())) + return + } +} + +func (r *ProjectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + organization, project, err := splitTwoPartID(req.ID, "organization", "project-slug") + if err != nil { + resp.Diagnostics.AddError("Invalid ID", fmt.Sprintf("Error parsing ID: %s", err.Error())) + return + } + resp.Diagnostics.Append(resp.State.SetAttribute( + ctx, path.Root("organization"), organization, + )...) + resp.Diagnostics.Append(resp.State.SetAttribute( + ctx, path.Root("id"), project, + )...) +} diff --git a/internal/provider/resource_project_test.go b/internal/provider/resource_project_test.go index b9b2c4f9..e39f3cd9 100644 --- a/internal/provider/resource_project_test.go +++ b/internal/provider/resource_project_test.go @@ -4,11 +4,19 @@ import ( "context" "fmt" "log" + "regexp" "strings" + "testing" + "text/template" "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/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "github.com/jianyuan/go-sentry/v2/sentry" "github.com/jianyuan/terraform-provider-sentry/internal/acctest" + "github.com/jianyuan/terraform-provider-sentry/internal/pkg/must" ) func init() { @@ -52,13 +60,390 @@ func init() { }) } -func testAccProjectResourceConfig(teamName, projectName string) string { - return testAccTeamResourceConfig(teamName) + fmt.Sprintf(` +func TestAccProjectResource_basic(t *testing.T) { + teamName1 := acctest.RandomWithPrefix("tf-team") + teamName2 := acctest.RandomWithPrefix("tf-team") + teamName3 := acctest.RandomWithPrefix("tf-team") + projectName := acctest.RandomWithPrefix("tf-project") + rn := "sentry_project.test" + + checks := []statecheck.StateCheck{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("slug"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_rules"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_key"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("internal_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("features"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_min_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_max_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("resolve_age"), knownvalue.Null()), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectResourceConfig_teams(testAccProjectResourceConfig_teamsData{ + AllTeamNames: []string{teamName1, teamName2, teamName3}, + TeamIds: []int{0, 1}, + ProjectName: projectName, + Platform: "go", + }), + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(projectName)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("teams"), knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact(teamName1), + knownvalue.StringExact(teamName2), + })), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("platform"), knownvalue.StringExact("go")), + ), + }, + { + Config: testAccProjectResourceConfig_teams(testAccProjectResourceConfig_teamsData{ + AllTeamNames: []string{teamName1, teamName2, teamName3}, + TeamIds: []int{0}, + ProjectName: projectName + "-renamed", + Platform: "python", + }), + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(projectName+"-renamed")), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("teams"), knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact(teamName1), + })), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("platform"), knownvalue.StringExact("python")), + ), + }, + { + Config: testAccProjectResourceConfig_teams(testAccProjectResourceConfig_teamsData{ + AllTeamNames: []string{teamName1, teamName2, teamName3}, + TeamIds: []int{2}, + ProjectName: projectName + "-renamed-again", + Platform: "python", + }), + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(projectName+"-renamed-again")), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("teams"), knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact(teamName3), + })), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("platform"), knownvalue.StringExact("python")), + ), + }, + { + ResourceName: rn, + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[rn] + if !ok { + return "", fmt.Errorf("not found: %s", rn) + } + organization := rs.Primary.Attributes["organization"] + project := rs.Primary.ID + return buildTwoPartID(organization, project), nil + }, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccProjectResource_noDefaultKeyOnCreate(t *testing.T) { + teamName := acctest.RandomWithPrefix("tf-team") + projectName := acctest.RandomWithPrefix("tf-project") + rn := "sentry_project.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + NoDefaultKey: true, + }) + ` + data "sentry_all_keys" "test" { + organization = sentry_project.test.organization + project = sentry_project.test.slug + } + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("teams"), knownvalue.SetExact([]knownvalue.Check{knownvalue.StringExact(teamName)})), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(projectName)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("slug"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("platform"), knownvalue.StringExact("go")), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_rules"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_key"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("internal_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("features"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_min_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_max_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("resolve_age"), knownvalue.Null()), + statecheck.ExpectKnownValue("data.sentry_all_keys.test", tfjsonpath.New("keys"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestAccProjectResource_noDefaultKeyOnUpdate(t *testing.T) { + teamName := acctest.RandomWithPrefix("tf-team") + projectName := acctest.RandomWithPrefix("tf-project") + rn := "sentry_project.test" + + checks := []statecheck.StateCheck{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("teams"), knownvalue.SetExact([]knownvalue.Check{knownvalue.StringExact(teamName)})), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(projectName)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("slug"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("platform"), knownvalue.StringExact("go")), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_rules"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("internal_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("features"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_min_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_max_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("resolve_age"), knownvalue.Null()), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + }) + ` + data "sentry_all_keys" "test" { + organization = sentry_project.test.organization + project = sentry_project.test.slug + } + `, + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_key"), knownvalue.Null()), + statecheck.ExpectKnownValue("data.sentry_all_keys.test", tfjsonpath.New("keys"), knownvalue.ListSizeExact(1)), + ), + }, + { + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + NoDefaultKey: true, + }) + ` + data "sentry_all_keys" "test" { + organization = sentry_project.test.organization + project = sentry_project.test.slug + } + `, + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_key"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue("data.sentry_all_keys.test", tfjsonpath.New("keys"), knownvalue.ListSizeExact(0)), + ), + }, + }, + }) +} + +func TestAccProjectResource_invalidPlatform(t *testing.T) { + teamName := acctest.RandomWithPrefix("tf-team") + projectName := acctest.RandomWithPrefix("tf-project") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + Platform: "invalid", + }), + ExpectError: regexp.MustCompile(`Attribute platform value must be one of`), + }, + }, + }) +} + +func TestAccProjectResource_noTeam(t *testing.T) { + projectName := acctest.RandomWithPrefix("tf-project") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectResourceConfig_teams(testAccProjectResourceConfig_teamsData{ + ProjectName: projectName, + }), + ExpectError: regexp.MustCompile(`Attribute teams set must contain at least 1 elements, got: 0`), + }, + }, + }) +} + +func TestAccProjectResource_UpgradeFromVersion(t *testing.T) { + teamName := acctest.RandomWithPrefix("tf-team") + projectName := acctest.RandomWithPrefix("tf-project") + rn := "sentry_project.test" + + checks := []statecheck.StateCheck{ + statecheck.ExpectKnownValue(rn, tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("organization"), knownvalue.StringExact(acctest.TestOrganization)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("teams"), knownvalue.SetExact([]knownvalue.Check{knownvalue.StringExact(teamName)})), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("name"), knownvalue.StringExact(projectName)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("slug"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("platform"), knownvalue.StringExact("go")), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("internal_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("features"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_min_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("digests_max_delay"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("resolve_age"), knownvalue.Null()), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: testAccCheckProjectDestroy, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + acctest.ProviderName: { + Source: "jianyuan/sentry", + VersionConstraint: "0.12.3", + }, + }, + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + }), + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_rules"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_key"), knownvalue.Bool(true)), + ), + }, + { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Config: testAccProjectResourceConfig(testAccProjectResourceConfigData{ + TeamName: teamName, + ProjectName: projectName, + }), + ConfigStateChecks: append( + checks, + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_rules"), knownvalue.Null()), + statecheck.ExpectKnownValue(rn, tfjsonpath.New("default_key"), knownvalue.Null()), + ), + }, + }, + }) +} + +func testAccCheckProjectDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "sentry_project" { + continue + } + + ctx := context.Background() + project, resp, err := acctest.SharedClient.Projects.Get( + ctx, + rs.Primary.Attributes["organization"], + rs.Primary.ID, + ) + if err == nil { + if project != nil { + return fmt.Errorf("project %q still exists", rs.Primary.ID) + } + } + if resp.StatusCode != 404 { + return err + } + return nil + } + + return nil +} + +var testAccProjectResourceConfigTemplate = template.Must(template.New("config").Parse(` resource "sentry_project" "test" { organization = sentry_team.test.organization teams = [sentry_team.test.id] - name = "%[1]s" - platform = "go" + name = "{{ .ProjectName }}" + platform = "{{ or .Platform "go" }}" + + {{ if .NoDefaultRules }} + default_rules = false + {{ end }} + + {{ if .NoDefaultKey }} + default_key = false + {{ end }} +} +`)) + +type testAccProjectResourceConfigData struct { + TeamName string + ProjectName string + Platform string + NoDefaultRules bool + NoDefaultKey bool +} + +func testAccProjectResourceConfig(data testAccProjectResourceConfigData) string { + var builder strings.Builder + + must.Get(builder.WriteString(testAccTeamResourceConfig(data.TeamName))) + must.Do(testAccProjectResourceConfigTemplate.Execute(&builder, data)) + + return builder.String() +} + +var testAccProjectResourceConfig_teamsTemplate = template.Must(template.New("config").Parse(` +{{ range $i, $teamName := .AllTeamNames }} +resource "sentry_team" "team_{{ $i }}" { + organization = data.sentry_organization.test.id + name = "{{ $teamName }}" + slug = "{{ $teamName }}" } -`, projectName) +{{ end }} + +resource "sentry_project" "test" { + organization = data.sentry_organization.test.id + teams = [ + {{ range $i, $TeamId := .TeamIds }} + sentry_team.team_{{ $TeamId }}.slug, + {{ end }} + ] + name = "{{ .ProjectName }}" + platform = "{{ or .Platform "go" }}" +} +`)) + +type testAccProjectResourceConfig_teamsData struct { + AllTeamNames []string + TeamIds []int + ProjectName string + Platform string +} + +func testAccProjectResourceConfig_teams(data testAccProjectResourceConfig_teamsData) string { + var builder strings.Builder + + must.Get(builder.WriteString(testAccOrganizationDataSourceConfig)) + must.Do(testAccProjectResourceConfig_teamsTemplate.Execute(&builder, data)) + + return builder.String() } diff --git a/internal/provider/resource_team_member.go b/internal/provider/resource_team_member.go index 6a4e72e9..65f06e19 100644 --- a/internal/provider/resource_team_member.go +++ b/internal/provider/resource_team_member.go @@ -324,7 +324,7 @@ func (r *TeamMemberResource) Update(ctx context.Context, req resource.UpdateRequ return } - if err := state.Fill( + if err := plan.Fill( plan.Organization.ValueString(), plan.Team.ValueString(), plan.MemberId.ValueString(), @@ -336,7 +336,7 @@ func (r *TeamMemberResource) Update(ctx context.Context, req resource.UpdateRequ } } - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } func (r *TeamMemberResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { diff --git a/internal/sentryplatforms/sentryplatforms.go b/internal/sentryplatforms/sentryplatforms.go index 3a2340f2..b8336b93 100644 --- a/internal/sentryplatforms/sentryplatforms.go +++ b/internal/sentryplatforms/sentryplatforms.go @@ -9,20 +9,6 @@ import ( //go:embed platforms.txt var rawPlatforms string -var platforms = strings.Split(strings.TrimSpace(rawPlatforms), "\n") -// Validate checks if a platform is valid from the list loaded from platforms.txt. -func Validate(platform string) bool { - // "other" is a special case that is always valid - if platform == "other" { - return true - } - - for _, p := range platforms { - if p == platform { - return true - } - } - - return false -} +// Platforms is a list of valid Sentry platforms. +var Platforms = strings.Split(strings.TrimSpace(rawPlatforms), "\n") diff --git a/internal/sentryplatforms/sentryplatforms_test.go b/internal/sentryplatforms/sentryplatforms_test.go deleted file mode 100644 index ba08e6cd..00000000 --- a/internal/sentryplatforms/sentryplatforms_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package sentryplatforms - -import ( - "fmt" - "testing" -) - -func TestValidate(t *testing.T) { - testCases := []struct { - platform string - want bool - }{ - {"android", true}, - {"python", true}, - {"javascript", true}, - {"other", true}, - {"bogus", false}, - } - for _, tc := range testCases { - t.Run(fmt.Sprintf("platform=%s", tc.platform), func(t *testing.T) { - got := Validate(tc.platform) - if got != tc.want { - t.Errorf("got %v; want %v", got, tc.want) - } - }) - } -} diff --git a/sentry/provider.go b/sentry/provider.go index a822fb4d..74fa9b92 100644 --- a/sentry/provider.go +++ b/sentry/provider.go @@ -45,7 +45,6 @@ func NewProvider(version string) func() *schema.Provider { "sentry_organization_repository_github": resourceSentryOrganizationRepositoryGithub(), "sentry_organization": resourceSentryOrganization(), "sentry_plugin": resourceSentryPlugin(), - "sentry_project": resourceSentryProject(), "sentry_team": resourceSentryTeam(), }, diff --git a/sentry/resource_sentry_project.go b/sentry/resource_sentry_project.go deleted file mode 100644 index ec5d123f..00000000 --- a/sentry/resource_sentry_project.go +++ /dev/null @@ -1,422 +0,0 @@ -package sentry - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/jianyuan/go-sentry/v2/sentry" - "github.com/jianyuan/terraform-provider-sentry/internal/sentryplatforms" -) - -func resourceSentryProject() *schema.Resource { - return &schema.Resource{ - Description: "Sentry Project resource.", - - CreateContext: resourceSentryProjectCreate, - ReadContext: resourceSentryProjectRead, - UpdateContext: resourceSentryProjectUpdate, - DeleteContext: resourceSentryProjectDelete, - - Importer: &schema.ResourceImporter{ - StateContext: importOrganizationAndID, - }, - - Schema: map[string]*schema.Schema{ - "organization": { - Description: "The slug of the organization the project belongs to.", - Type: schema.TypeString, - Required: true, - }, - "team": { - Description: "The slug of the team to create the project for. **Deprecated** Use `teams` instead.", - Type: schema.TypeString, - Deprecated: "Use `teams` instead.", - ConflictsWith: []string{"teams"}, - Optional: true, - }, - "teams": { - Description: "The slugs of the teams to create the project for.", - Type: schema.TypeSet, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - ConflictsWith: []string{"team"}, - Optional: true, - }, - "name": { - Description: "The name for the project.", - Type: schema.TypeString, - Required: true, - }, - "slug": { - Description: "The optional slug for this project.", - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "platform": { - Description: "The platform for this project. For a list of valid values, [see this page](https://github.com/jianyuan/terraform-provider-sentry/blob/main/internal/sentryplatforms/platforms.txt). Use `other` for platforms not listed.", - Type: schema.TypeString, - Optional: true, - Computed: true, - ValidateDiagFunc: validatePlatform, - }, - "default_rules": { - Description: "Whether to create a default issue alert. Defaults to true where the behavior is to alert the user on every new issue.", - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - "default_key": { - Description: "Whether to create a default key. By default, Sentry will create a key for you. If you wish to manage keys manually, set this to false and create keys using the `sentry_key` resource.", - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - "internal_id": { - Description: "The internal ID for this project.", - Type: schema.TypeString, - Computed: true, - }, - "is_public": { - Type: schema.TypeBool, - Computed: true, - }, - "is_bookmarked": { - Deprecated: "is_bookmarked is no longer used", - Type: schema.TypeBool, - Computed: true, - }, - "color": { - Type: schema.TypeString, - Computed: true, - }, - "features": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "status": { - Type: schema.TypeString, - Computed: true, - }, - "digests_min_delay": { - Description: "The minimum amount of time (in seconds) to wait between scheduling digests for delivery after the initial scheduling.", - Type: schema.TypeInt, - Computed: true, - Optional: true, - }, - "digests_max_delay": { - Description: "The maximum amount of time (in seconds) to wait between scheduling digests for delivery.", - Type: schema.TypeInt, - Computed: true, - Optional: true, - }, - "resolve_age": { - Description: "Hours in which an issue is automatically resolve if not seen after this amount of time.", - Type: schema.TypeInt, - Optional: true, - Computed: true, - }, - "project_id": { - Deprecated: "Use `internal_id` instead.", - Description: "Use `internal_id` instead.", - Type: schema.TypeString, - Computed: true, - }, - // TODO: Project options - }, - } -} - -func resourceSentryProjectCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*sentry.Client) - - org := d.Get("organization").(string) - - team, teamOk := d.GetOk("team") - teams, teamsOk := d.GetOk("teams") - if !teamOk && !teamsOk { - return diag.FromErr(errors.New("one of team or teams must be configured")) - } - - var initialTeam string - if teamOk { - initialTeam = team.(string) - } else { - // Since `Set.List()` produces deterministic ordering, `teams[0]` should always - // resolve to the same value given the same `teams`. - // Pick the first team when creating the project. - initialTeam = teams.(*schema.Set).List()[0].(string) - } - - params := &sentry.CreateProjectParams{ - Name: d.Get("name").(string), - Slug: d.Get("slug").(string), - } - - defaultRules, defaultRulesOk := d.GetOkExists("default_rules") - if defaultRulesOk { - params.DefaultRules = sentry.Bool(defaultRules.(bool)) - } - - tflog.Debug(ctx, "Creating Sentry project", map[string]interface{}{ - "team": team, - "teams": teams, - "org": org, - "initialTeam": initialTeam, - "defaultRules": params.DefaultRules, - }) - proj, _, err := client.Projects.Create(ctx, org, initialTeam, params) - if err != nil { - return diag.FromErr(err) - } - tflog.Debug(ctx, "Created Sentry project", map[string]interface{}{ - "projectSlug": proj.Slug, - "projectID": proj.ID, - "team": initialTeam, - "org": org, - }) - - defaultKey, defaultKeyOk := d.GetOkExists("default_key") - if defaultKeyOk && !defaultKey.(bool) { - err = removeDefaultKey(ctx, client, org, proj.Slug) - if err != nil { - return diag.FromErr(err) - } - } - - d.SetId(proj.Slug) - return resourceSentryProjectUpdate(ctx, d, meta) -} - -func resourceSentryProjectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*sentry.Client) - - slug := d.Id() - org := d.Get("organization").(string) - - tflog.Debug(ctx, "Reading Sentry project", map[string]interface{}{ - "projectSlug": slug, - "org": org, - }) - proj, resp, err := client.Projects.Get(ctx, org, slug) - if found, err := checkClientGet(resp, err, d); !found { - return diag.FromErr(err) - } - tflog.Debug(ctx, "Read Sentry project", map[string]interface{}{ - "projectSlug": proj.Slug, - "projectID": proj.ID, - "org": org, - }) - - d.SetId(proj.Slug) - retErr := multierror.Append( - d.Set("organization", proj.Organization.Slug), - d.Set("name", proj.Name), - d.Set("slug", proj.Slug), - d.Set("platform", proj.Platform), - d.Set("internal_id", proj.ID), - d.Set("is_public", proj.IsPublic), - d.Set("color", proj.Color), - d.Set("features", proj.Features), - d.Set("status", proj.Status), - d.Set("digests_min_delay", proj.DigestsMinDelay), - d.Set("digests_max_delay", proj.DigestsMaxDelay), - d.Set("resolve_age", proj.ResolveAge), - d.Set("project_id", proj.ID), // Deprecated - ) - if _, ok := d.GetOk("team"); ok { - retErr = multierror.Append(retErr, d.Set("team", proj.Team.Slug)) - } else { - teams := make([]string, 0, len(proj.Teams)) - for _, team := range proj.Teams { - teams = append(teams, *team.Slug) - } - retErr = multierror.Append(retErr, d.Set("teams", flattenStringSet(teams))) - } - - // TODO: Project options - - return diag.FromErr(retErr.ErrorOrNil()) -} - -func resourceSentryProjectUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*sentry.Client) - - project := d.Id() - org := d.Get("organization").(string) - params := &sentry.UpdateProjectParams{ - Name: d.Get("name").(string), - Slug: d.Get("slug").(string), - } - - platform := d.Get("platform").(string) - if platform != "" { - params.Platform = platform - } - - if v, ok := d.GetOk("digests_min_delay"); ok { - params.DigestsMinDelay = sentry.Int(v.(int)) - } - - if v, ok := d.GetOk("digests_max_delay"); ok { - params.DigestsMaxDelay = sentry.Int(v.(int)) - } - - if v, ok := d.GetOk("resolve_age"); ok { - params.ResolveAge = sentry.Int(v.(int)) - } - - tflog.Debug(ctx, "Updating project", map[string]interface{}{ - "org": org, - "project": project, - }) - proj, _, err := client.Projects.Update(ctx, org, project, params) - if err != nil { - return diag.FromErr(err) - } - - d.SetId(proj.Slug) - - oldTeams := map[string]struct{}{} - newTeams := map[string]struct{}{} - if d.HasChange("team") { - oldTeam, newTeam := d.GetChange("team") - if oldTeam.(string) != "" { - oldTeams[oldTeam.(string)] = struct{}{} - } - if newTeam.(string) != "" { - newTeams[newTeam.(string)] = struct{}{} - } - } - - if d.HasChange("teams") { - o, n := d.GetChange("teams") - for _, oldTeam := range o.(*schema.Set).List() { - if oldTeam.(string) != "" { - oldTeams[oldTeam.(string)] = struct{}{} - } - } - for _, newTeam := range n.(*schema.Set).List() { - if newTeam.(string) != "" { - newTeams[newTeam.(string)] = struct{}{} - } - } - } - - // Ensure old teams and new teams do not overlap. - for newTeam := range newTeams { - delete(oldTeams, newTeam) - } - - if len(newTeams) > 0 { - tflog.Debug(ctx, "Adding teams to project", map[string]interface{}{ - "org": org, - "project": project, - "teamsToAdd": newTeams, - }) - - for newTeam := range newTeams { - _, _, err = client.Projects.AddTeam(ctx, org, project, newTeam) - if err != nil { - return diag.FromErr(err) - } - } - } - - if len(oldTeams) > 0 { - tflog.Debug(ctx, "Removing teams from project", map[string]interface{}{ - "org": org, - "project": project, - "teamsToRemove": oldTeams, - }) - - for oldTeam := range oldTeams { - resp, err := client.Projects.RemoveTeam(ctx, org, project, oldTeam) - if err != nil { - if resp.Response.StatusCode != http.StatusNotFound { - return diag.FromErr(err) - } - } - } - } - - return resourceSentryProjectRead(ctx, d, meta) -} - -func resourceSentryProjectDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*sentry.Client) - - slug := d.Id() - org := d.Get("organization").(string) - - tflog.Debug(ctx, "Deleting Sentry project", map[string]interface{}{ - "projectSlug": slug, - "org": org, - }) - _, err := client.Projects.Delete(ctx, org, slug) - tflog.Debug(ctx, "Deleted Sentry project", map[string]interface{}{ - "projectSlug": slug, - "org": org, - }) - - return diag.FromErr(err) -} - -func validatePlatform(i interface{}, path cty.Path) diag.Diagnostics { - var diagnostics diag.Diagnostics - - v := i.(string) - if sentryplatforms.Validate(v) { - return nil - } - - msg := fmt.Sprintf("%s is not a valid platform", v) - diagnostics = append(diagnostics, diag.Diagnostic{ - Severity: diag.Error, - Summary: msg, - Detail: msg, - AttributePath: path, - }) - return diagnostics -} - -func removeDefaultKey(ctx context.Context, client *sentry.Client, organizationSlug string, projectSlug string) error { - listParams := &sentry.ListProjectKeysParams{} - - for { - keys, resp, err := client.ProjectKeys.List(ctx, organizationSlug, projectSlug, listParams) - if err != nil { - return err - } - - for _, key := range keys { - if key.Name == "Default" { - // Delete the default rule - _, err := client.ProjectKeys.Delete(ctx, organizationSlug, projectSlug, key.ID) - if err != nil { - return err - } - - return nil - } - } - - if resp.Cursor == "" { - break - } - listParams.Cursor = resp.Cursor - } - - return nil -} diff --git a/sentry/resource_sentry_project_test.go b/sentry/resource_sentry_project_test.go index 28b3a522..8f2c06c7 100644 --- a/sentry/resource_sentry_project_test.go +++ b/sentry/resource_sentry_project_test.go @@ -1,472 +1,16 @@ package sentry import ( - "context" - "errors" "fmt" - "regexp" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "github.com/jianyuan/terraform-provider-sentry/internal/acctest" ) -func TestAccSentryProject_basic(t *testing.T) { - teamName1 := acctest.RandomWithPrefix("tf-team") - teamName2 := acctest.RandomWithPrefix("tf-team") - teamName3 := acctest.RandomWithPrefix("tf-team") - projectName := acctest.RandomWithPrefix("tf-project") - rn := "sentry_project.test" - - check := func(projectName string, teamNames []string) resource.TestCheckFunc { - var projectID string - - fs := resource.ComposeTestCheckFunc( - testAccCheckSentryProjectExists(rn, &projectID), - resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization), - resource.TestCheckResourceAttr(rn, "teams.#", strconv.Itoa(len(teamNames))), - resource.TestCheckResourceAttr(rn, "name", projectName), - resource.TestCheckResourceAttrSet(rn, "slug"), - resource.TestCheckResourceAttr(rn, "platform", "go"), - resource.TestCheckResourceAttrSet(rn, "internal_id"), - resource.TestCheckResourceAttrPtr(rn, "internal_id", &projectID), - resource.TestCheckResourceAttrPair(rn, "project_id", rn, "internal_id"), - ) - for _, teamName := range teamNames { - fs = resource.ComposeTestCheckFunc(fs, resource.TestCheckTypeSetElemAttr(rn, "teams.*", teamName)) - } - return fs - } - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSentryProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccSentryProjectConfig_teams([]string{teamName1}, projectName), - Check: check(projectName, []string{teamName1}), - }, - { - Config: testAccSentryProjectConfig_teams([]string{teamName2}, projectName+"-renamed"), - Check: check(projectName+"-renamed", []string{teamName2}), - }, - { - Config: testAccSentryProjectConfig_teams([]string{teamName2, teamName3}, projectName+"-renamed"), - Check: check(projectName+"-renamed", []string{teamName2, teamName3}), - }, - { - ResourceName: rn, - ImportState: true, - ImportStateIdFunc: testAccSentryProjectImportStateIdFunc(rn), - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"default_key", "default_rules"}, - }, - }, - }) -} - -func TestAccSentryProject_teamMigration(t *testing.T) { - teams := []string{ - acctest.RandomWithPrefix("tf-team"), - acctest.RandomWithPrefix("tf-team"), - acctest.RandomWithPrefix("tf-team"), - } - projectName := acctest.RandomWithPrefix("tf-project") - rn := "sentry_project.test" - - check := func(team string, teams []string) resource.TestCheckFunc { - var projectID string - - fs := resource.ComposeTestCheckFunc( - testAccCheckSentryProjectExists(rn, &projectID), - resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization), - resource.TestCheckResourceAttr(rn, "name", projectName), - resource.TestCheckResourceAttrSet(rn, "slug"), - resource.TestCheckResourceAttr(rn, "platform", "go"), - resource.TestCheckResourceAttrSet(rn, "internal_id"), - resource.TestCheckResourceAttrPtr(rn, "internal_id", &projectID), - resource.TestCheckResourceAttrPair(rn, "project_id", rn, "internal_id"), - ) - if team != "" { - fs = resource.ComposeTestCheckFunc(fs, resource.TestCheckResourceAttr(rn, "team", team)) - } - for _, team := range teams { - fs = resource.ComposeTestCheckFunc(fs, resource.TestCheckTypeSetElemAttr(rn, "teams.*", team)) - } - - return fs - } - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSentryProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccSentryProjectConfig_teams_old(teams, projectName), - Check: check(teams[0], nil), - }, - { - Config: testAccSentryProjectConfig_teams(teams, projectName), - Check: check("", teams), - }, - { - ResourceName: rn, - ImportState: true, - ImportStateIdFunc: testAccSentryProjectImportStateIdFunc(rn), - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"team", "default_key", "default_rules"}, - }, - }, - }) -} - -func TestAccSentryProject_deprecatedTeam(t *testing.T) { - teamName := acctest.RandomWithPrefix("tf-team") - projectName := acctest.RandomWithPrefix("tf-project") - rn := "sentry_project.test" - - check := func(projectName string) resource.TestCheckFunc { - var projectID string - - return resource.ComposeTestCheckFunc( - testAccCheckSentryProjectExists(rn, &projectID), - resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization), - resource.TestCheckResourceAttr(rn, "team", teamName), - resource.TestCheckResourceAttr(rn, "name", projectName), - resource.TestCheckResourceAttrSet(rn, "slug"), - resource.TestCheckResourceAttr(rn, "platform", "go"), - resource.TestCheckResourceAttrSet(rn, "internal_id"), - resource.TestCheckResourceAttrPtr(rn, "internal_id", &projectID), - resource.TestCheckResourceAttrPair(rn, "project_id", rn, "internal_id"), - ) - } - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSentryProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccSentryProjectConfig_team(teamName, projectName), - Check: check(projectName), - }, - { - Config: testAccSentryProjectConfig_team(teamName, projectName+"-renamed"), - Check: check(projectName + "-renamed"), - }, - { - ResourceName: rn, - ImportState: true, - ImportStateIdFunc: testAccSentryProjectImportStateIdFunc(rn), - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"team", "default_key", "default_rules"}, - }, - }, - }) -} - -func TestAccSentryProject_noTeam(t *testing.T) { - teamName := acctest.RandomWithPrefix("tf-team") - projectName := acctest.RandomWithPrefix("tf-project") - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSentryProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccSentryProjectConfig_noTeam(teamName, projectName), - ExpectError: regexp.MustCompile("one of team or teams must be configured"), - }, - }, - }) -} - -func TestAccSentryProject_teamConflict(t *testing.T) { - teamName := acctest.RandomWithPrefix("tf-team") - projectName := acctest.RandomWithPrefix("tf-project") - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSentryProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccSentryProjectConfig_teamConflict(teamName, projectName), - ExpectError: regexp.MustCompile("\"team\": conflicts with teams"), - }, - { - Config: testAccSentryProjectConfig_teamConflict(teamName, projectName), - ExpectError: regexp.MustCompile("\"teams\": conflicts with team"), - }, - }, - }) -} - -func TestAccSentryProject_changeTeam(t *testing.T) { - teamName1 := acctest.RandomWithPrefix("tf-team") - teamName2 := acctest.RandomWithPrefix("tf-team") - projectName := acctest.RandomWithPrefix("tf-project") - rn := "sentry_project.test" - - check := func(teamName, projectName string) resource.TestCheckFunc { - var projectID string - - return resource.ComposeTestCheckFunc( - testAccCheckSentryProjectExists(rn, &projectID), - resource.TestCheckResourceAttr(rn, "organization", acctest.TestOrganization), - resource.TestCheckResourceAttr(rn, "team", teamName), - resource.TestCheckResourceAttr(rn, "name", projectName), - resource.TestCheckResourceAttrSet(rn, "slug"), - resource.TestCheckResourceAttr(rn, "platform", "go"), - resource.TestCheckResourceAttrSet(rn, "internal_id"), - resource.TestCheckResourceAttrPtr(rn, "internal_id", &projectID), - resource.TestCheckResourceAttrPair(rn, "project_id", rn, "internal_id"), - ) - } - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSentryProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccSentryProjectConfig_changeTeam(teamName1, teamName2, projectName, "test_1"), - Check: check(teamName1, projectName), - }, - { - Config: testAccSentryProjectConfig_changeTeam(teamName1, teamName2, projectName, "test_2"), - Check: check(teamName2, projectName), - }, - }, - }) -} - -func TestAccSentryProject_noDefaultKey(t *testing.T) { - teamName := acctest.RandomWithPrefix("tf-team") - projectName := acctest.RandomWithPrefix("tf-project") - rn := "sentry_project.test" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSentryProjectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccSentryProjectConfig_noDefaultKey(teamName, projectName), - Check: testAccCheckSentryProjectDefaultKeyRemoved(rn), - }, - }, - }) -} - -func testAccCheckSentryProjectDestroy(s *terraform.State) error { - for _, rs := range s.RootModule().Resources { - if rs.Type != "sentry_project" { - continue - } - - ctx := context.Background() - proj, resp, err := acctest.SharedClient.Projects.Get(ctx, acctest.TestOrganization, rs.Primary.ID) - if err == nil { - if proj != nil { - return errors.New("project still exists") - } - } - if resp.StatusCode != 403 && resp.StatusCode != 404 { - return err - } - return nil - } - return nil -} - -func testAccCheckSentryProjectExists(n string, projectID *string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("not found: %s", n) - } - - if rs.Primary.ID == "" { - return errors.New("no ID is set") - } - - ctx := context.Background() - gotProj, _, err := acctest.SharedClient.Projects.Get( - ctx, - rs.Primary.Attributes["organization"], - rs.Primary.ID, - ) - if err != nil { - return err - } - *projectID = gotProj.ID - return nil - } -} - -func testAccCheckSentryProjectDefaultKeyRemoved(n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs := s.RootModule().Resources[n] - ctx := context.Background() - - keys, _, err := acctest.SharedClient.ProjectKeys.List( - ctx, - rs.Primary.Attributes["organization"], - rs.Primary.ID, - nil, - ) - if err != nil { - return err - } - if len(keys) != 0 { - return fmt.Errorf("expected no keys, got %d", len(keys)) - } - return nil - } -} - -func testAccSentryProjectImportStateIdFunc(n string) resource.ImportStateIdFunc { - return func(s *terraform.State) (string, error) { - rs, ok := s.RootModule().Resources[n] - if !ok { - return "", fmt.Errorf("not found: %s", n) - } - org := rs.Primary.Attributes["organization"] - projectSlug := rs.Primary.ID - return buildTwoPartID(org, projectSlug), nil - } -} - func testAccSentryProjectConfig_team(teamName, projectName string) string { return testAccSentryTeamConfig(teamName) + fmt.Sprintf(` resource "sentry_project" "test" { organization = sentry_team.test.organization - team = sentry_team.test.slug - name = "%[1]s" - platform = "go" -} - `, projectName) -} - -func testAccSentryProjectConfig_noTeam(teamName, projectName string) string { - return testAccSentryTeamConfig(teamName) + fmt.Sprintf(` -resource "sentry_project" "test" { - organization = sentry_team.test.organization - name = "%[1]s" - platform = "go" -} - `, projectName) -} - -func testAccSentryProjectConfig_teamConflict(teamName, projectName string) string { - return testAccSentryTeamConfig(teamName) + fmt.Sprintf(` -resource "sentry_project" "test" { - organization = sentry_team.test.organization - team = sentry_team.test.slug teams = [sentry_team.test.slug] name = "%[1]s" platform = "go" } `, projectName) } - -func testAccSentryProjectConfig_changeTeam(teamName1, teamName2, projectName, teamResourceName string) string { - return testAccSentryOrganizationDataSourceConfig + fmt.Sprintf(` -resource "sentry_team" "test_1" { - organization = data.sentry_organization.test.id - name = "%[1]s" - slug = "%[1]s" -} - -resource "sentry_team" "test_2" { - organization = data.sentry_organization.test.id - name = "%[2]s" - slug = "%[2]s" -} - -resource "sentry_project" "test" { - organization = sentry_team.%[4]s.organization - team = sentry_team.%[4]s.slug - name = "%[3]s" - platform = "go" -} - `, teamName1, teamName2, projectName, teamResourceName) -} - -func testAccSentryProjectConfig_teams_old(teamNames []string, projectName string) string { - config := testAccSentryOrganizationDataSourceConfig - - teamSlugs := make([]string, 0, len(teamNames)) - for i, teamName := range teamNames { - config += fmt.Sprintf(` -resource "sentry_team" "test_%[1]d" { - organization = data.sentry_organization.test.id - name = "%[2]s" - slug = "%[2]s" -} - `, i, teamName) - teamSlugs = append(teamSlugs, fmt.Sprintf("sentry_team.test_%d.slug", i)) - } - - config += fmt.Sprintf(` -resource "sentry_project" "test" { - organization = sentry_team.test_0.organization - team = %[2]s - name = "%[1]s" - platform = "go" -} - `, projectName, teamSlugs[0]) - - return config -} - -func testAccSentryProjectConfig_teams(teamNames []string, projectName string) string { - config := testAccSentryOrganizationDataSourceConfig - - teamSlugs := make([]string, 0, len(teamNames)) - for i, teamName := range teamNames { - config += fmt.Sprintf(` -resource "sentry_team" "test_%[1]d" { - organization = data.sentry_organization.test.id - name = "%[2]s" - slug = "%[2]s" -} - `, i, teamName) - teamSlugs = append(teamSlugs, fmt.Sprintf("sentry_team.test_%d.slug", i)) - } - - config += fmt.Sprintf(` -resource "sentry_project" "test" { - organization = sentry_team.test_0.organization - teams = [%[2]s] - name = "%[1]s" - platform = "go" -} - `, projectName, strings.Join(teamSlugs, ", ")) - - return config -} - -func testAccSentryProjectConfig_noDefaultKey(teamName, projectName string) string { - return fmt.Sprintf(` -resource "sentry_team" "test" { - organization = "%[1]s" - name = "%[2]s" -} - -resource "sentry_project" "test" { - organization = sentry_team.test.organization - teams = [sentry_team.test.id] - name = "%[3]s" - platform = "go" - default_key = false -} -`, acctest.TestOrganization, teamName, projectName) -}