From aa5c4ccef0ee90e1705748ee60ee2495f0a497c6 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Thu, 2 May 2024 16:28:39 +0100 Subject: [PATCH] Add support for Attack Challenge Mode --- client/attack_challenge_mode.go | 54 ++++ client/project.go | 5 + docs/data-sources/attack_challenge_mode.md | 38 +++ docs/resources/attack_challenge_mode.md | 54 ++++ .../data-source.tf | 3 + .../vercel_attack_challenge_mode/import.sh | 4 + .../vercel_attack_challenge_mode/resource.tf | 8 + vercel/data_source_attack_challenge_mode.go | 113 +++++++ .../data_source_attack_challenge_mode_test.go | 73 +++++ vercel/provider.go | 2 + vercel/resource_attack_challenge_mode.go | 296 ++++++++++++++++++ vercel/resource_attack_challenge_mode_test.go | 96 ++++++ vercel/resource_project_function_cpu_test.go | 119 +++++++ 13 files changed, 865 insertions(+) create mode 100644 client/attack_challenge_mode.go create mode 100644 docs/data-sources/attack_challenge_mode.md create mode 100644 docs/resources/attack_challenge_mode.md create mode 100644 examples/data-sources/vercel_attack_challenge_mode/data-source.tf create mode 100644 examples/resources/vercel_attack_challenge_mode/import.sh create mode 100644 examples/resources/vercel_attack_challenge_mode/resource.tf create mode 100644 vercel/data_source_attack_challenge_mode.go create mode 100644 vercel/data_source_attack_challenge_mode_test.go create mode 100644 vercel/resource_attack_challenge_mode.go create mode 100644 vercel/resource_attack_challenge_mode_test.go create mode 100644 vercel/resource_project_function_cpu_test.go diff --git a/client/attack_challenge_mode.go b/client/attack_challenge_mode.go new file mode 100644 index 00000000..df43d08b --- /dev/null +++ b/client/attack_challenge_mode.go @@ -0,0 +1,54 @@ +package client + +import ( + "context" + "fmt" +) + +type AttackChallengeMode struct { + ProjectID string `json:"projectId"` + TeamID string `json:"-"` + Enabled bool `json:"attackModeEnabled"` +} + +func (c *Client) GetAttackChallengeMode(ctx context.Context, projectID, teamID string) (a AttackChallengeMode, err error) { + project, err := c.GetProject(ctx, projectID, teamID) + if err != nil { + return a, err + } + var enabled bool + if project.Security != nil { + enabled = project.Security.AttackModeEnabled + } + return AttackChallengeMode{ + ProjectID: projectID, + TeamID: teamID, + Enabled: enabled, + }, err +} + +func (c *Client) UpdateAttackChallengeMode(ctx context.Context, request AttackChallengeMode) (a AttackChallengeMode, err error) { + url := fmt.Sprintf("%s/security/attack-mode", c.baseURL) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + + payload := string(mustMarshal(request)) + var res struct { + AttackModeEnabled bool `json:"attackModeEnabled"` + } + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &res) + if err != nil { + return a, err + } + return AttackChallengeMode{ + ProjectID: request.ProjectID, + TeamID: request.TeamID, + Enabled: res.AttackModeEnabled, + }, err +} diff --git a/client/project.go b/client/project.go index b39b860b..a2034b75 100644 --- a/client/project.go +++ b/client/project.go @@ -181,6 +181,7 @@ type ProjectResponse struct { DirectoryListing bool `json:"directoryListing"` SkewProtectionMaxAge int `json:"skewProtectionMaxAge"` GitComments *GitComments `json:"gitComments"` + Security *Security `json:"security"` } type GitComments struct { @@ -188,6 +189,10 @@ type GitComments struct { OnPullRequest bool `json:"onPullRequest"` } +type Security struct { + AttackModeEnabled bool `json:"attackModeEnabled"` +} + // GetProject retrieves information about an existing project from Vercel. func (c *Client) GetProject(ctx context.Context, projectID, teamID string) (r ProjectResponse, err error) { url := fmt.Sprintf("%s/v10/projects/%s", c.baseURL, projectID) diff --git a/docs/data-sources/attack_challenge_mode.md b/docs/data-sources/attack_challenge_mode.md new file mode 100644 index 00000000..69d972f7 --- /dev/null +++ b/docs/data-sources/attack_challenge_mode.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_attack_challenge_mode Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides an Attack Challenge Mode resource. + Attack Challenge Mode prevent malicious traffic by showing a verification challenge for every visitor. +--- + +# vercel_attack_challenge_mode (Data Source) + +Provides an Attack Challenge Mode resource. + +Attack Challenge Mode prevent malicious traffic by showing a verification challenge for every visitor. + +## Example Usage + +```terraform +data "vercel_attack_challenge_mode" "example" { + project_id = vercel_project.example.id +} +``` + + +## Schema + +### Required + +- `project_id` (String) The ID of the Project to adjust the CPU for. + +### Optional + +- `team_id` (String) The ID of the team the Project exists under. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `enabled` (Boolean) Whether Attack Challenge Mode is enabled or not. +- `id` (String) The resource identifier. diff --git a/docs/resources/attack_challenge_mode.md b/docs/resources/attack_challenge_mode.md new file mode 100644 index 00000000..c82cd74d --- /dev/null +++ b/docs/resources/attack_challenge_mode.md @@ -0,0 +1,54 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_attack_challenge_mode Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides an Attack Challenge Mode resource. + Attack Challenge Mode prevent malicious traffic by showing a verification challenge for every visitor. +--- + +# vercel_attack_challenge_mode (Resource) + +Provides an Attack Challenge Mode resource. + +Attack Challenge Mode prevent malicious traffic by showing a verification challenge for every visitor. + +## Example Usage + +```terraform +resource "vercel_project" "example" { + name = "example-project" +} + +resource "vercel_attack_challenge_mode" "example" { + project_id = vercel_project.example.id + enabled = true +} +``` + + +## Schema + +### Required + +- `enabled` (Boolean) Whether Attack Challenge Mode is enabled or not. +- `project_id` (String) The ID of the Project to adjust the CPU for. + +### Optional + +- `team_id` (String) The ID of the team the Project exists under. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `id` (String) The resource identifier. + +## Import + +Import is supported using the following syntax: + +```shell +# You can import via the team_id and project_id. +# - team_id can be found in the team `settings` tab in the Vercel UI. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_attack_challenge_mode.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` diff --git a/examples/data-sources/vercel_attack_challenge_mode/data-source.tf b/examples/data-sources/vercel_attack_challenge_mode/data-source.tf new file mode 100644 index 00000000..87ac3e5e --- /dev/null +++ b/examples/data-sources/vercel_attack_challenge_mode/data-source.tf @@ -0,0 +1,3 @@ +data "vercel_attack_challenge_mode" "example" { + project_id = vercel_project.example.id +} diff --git a/examples/resources/vercel_attack_challenge_mode/import.sh b/examples/resources/vercel_attack_challenge_mode/import.sh new file mode 100644 index 00000000..dd22cd33 --- /dev/null +++ b/examples/resources/vercel_attack_challenge_mode/import.sh @@ -0,0 +1,4 @@ +# You can import via the team_id and project_id. +# - team_id can be found in the team `settings` tab in the Vercel UI. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_attack_challenge_mode.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/examples/resources/vercel_attack_challenge_mode/resource.tf b/examples/resources/vercel_attack_challenge_mode/resource.tf new file mode 100644 index 00000000..c9122040 --- /dev/null +++ b/examples/resources/vercel_attack_challenge_mode/resource.tf @@ -0,0 +1,8 @@ +resource "vercel_project" "example" { + name = "example-project" +} + +resource "vercel_attack_challenge_mode" "example" { + project_id = vercel_project.example.id + enabled = true +} diff --git a/vercel/data_source_attack_challenge_mode.go b/vercel/data_source_attack_challenge_mode.go new file mode 100644 index 00000000..bfe1b6fa --- /dev/null +++ b/vercel/data_source_attack_challenge_mode.go @@ -0,0 +1,113 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &attackChallengeModeDataSource{} + _ datasource.DataSourceWithConfigure = &attackChallengeModeDataSource{} +) + +func newAttackChallengeModeDataSource() datasource.DataSource { + return &attackChallengeModeDataSource{} +} + +type attackChallengeModeDataSource struct { + client *client.Client +} + +func (d *attackChallengeModeDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_attack_challenge_mode" +} + +func (d *attackChallengeModeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client +} + +func (r *attackChallengeModeDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides an Attack Challenge Mode resource. + +Attack Challenge Mode prevent malicious traffic by showing a verification challenge for every visitor.`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The resource identifier.", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the Project to adjust the CPU for.", + Required: true, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the team the Project exists under. Required when configuring a team resource if a default team has not been set in the provider.", + }, + "enabled": schema.BoolAttribute{ + Computed: true, + Description: "Whether Attack Challenge Mode is enabled or not.", + }, + }, + } +} + +func (d *attackChallengeModeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config AttackChallengeMode + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := d.client.GetAttackChallengeMode(ctx, config.ProjectID.ValueString(), config.TeamID.ValueString()) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading Attack Challenge Mode", + fmt.Sprintf("Could not get Attack Challenge Mode %s %s, unexpected error: %s", + config.TeamID.ValueString(), + config.ProjectID.ValueString(), + err, + ), + ) + return + } + + result := responseToAttackChallengeMode(out) + tflog.Info(ctx, "read attack challenge mode", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_attack_challenge_mode_test.go b/vercel/data_source_attack_challenge_mode_test.go new file mode 100644 index 00000000..e9e8061d --- /dev/null +++ b/vercel/data_source_attack_challenge_mode_test.go @@ -0,0 +1,73 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_AttackChallengeModeDataSource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccAttackChallengeModeConfig(name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.vercel_attack_challenge_mode.never_enabled", "enabled", "false"), + resource.TestCheckResourceAttr("data.vercel_attack_challenge_mode.enabled", "enabled", "true"), + resource.TestCheckResourceAttr("data.vercel_attack_challenge_mode.disabled", "enabled", "false"), + ), + }, + }, + }) +} + +func testAccAttackChallengeModeConfig(name, teamID string) string { + return fmt.Sprintf(` +resource "vercel_project" "never_enabled" { + name = "test-acc-%[1]s" + %[2]s +} + +data "vercel_attack_challenge_mode" "never_enabled" { + project_id = vercel_project.never_enabled.id + %[2]s +} + +resource "vercel_project" "enabled" { + name = "test-acc-%[1]s-enabled" + %[2]s +} + +resource "vercel_attack_challenge_mode" "enabled" { + project_id = vercel_project.enabled.id + enabled = true + %[2]s +} + +data "vercel_attack_challenge_mode" "enabled" { + project_id = vercel_attack_challenge_mode.enabled.project_id + %[2]s +} + +resource "vercel_project" "disabled" { + name = "test-acc-%[1]s-disabled" + %[2]s +} + +resource "vercel_attack_challenge_mode" "disabled" { + project_id = vercel_project.disabled.id + enabled = false + %[2]s +} + +data "vercel_attack_challenge_mode" "disabled" { + project_id = vercel_attack_challenge_mode.disabled.project_id + %[2]s +} +`, name, teamID) +} diff --git a/vercel/provider.go b/vercel/provider.go index c4bf887b..8e6c2cb7 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -51,6 +51,7 @@ Use the navigation to the left to read about the available resources. func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ newAliasResource, + newAttackChallengeModeResource, newDNSRecordResource, newDeploymentResource, newEdgeConfigResource, @@ -69,6 +70,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ newAliasDataSource, + newAttackChallengeModeDataSource, newDeploymentDataSource, newEdgeConfigDataSource, newEdgeConfigSchemaDataSource, diff --git a/vercel/resource_attack_challenge_mode.go b/vercel/resource_attack_challenge_mode.go new file mode 100644 index 00000000..c3a7649a --- /dev/null +++ b/vercel/resource_attack_challenge_mode.go @@ -0,0 +1,296 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &attackChallengeModeResource{} + _ resource.ResourceWithConfigure = &attackChallengeModeResource{} + _ resource.ResourceWithImportState = &attackChallengeModeResource{} +) + +func newAttackChallengeModeResource() resource.Resource { + return &attackChallengeModeResource{} +} + +type attackChallengeModeResource struct { + client *client.Client +} + +func (r *attackChallengeModeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_attack_challenge_mode" +} + +func (r *attackChallengeModeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *attackChallengeModeResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides an Attack Challenge Mode resource. + +Attack Challenge Mode prevent malicious traffic by showing a verification challenge for every visitor.`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The resource identifier.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the Project to adjust the CPU for.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the team the Project exists under. Required when configuring a team resource if a default team has not been set in the provider.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + }, + "enabled": schema.BoolAttribute{ + Required: true, + Description: "Whether Attack Challenge Mode is enabled or not.", + }, + }, + } +} + +type AttackChallengeMode struct { + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` + Enabled types.Bool `tfsdk:"enabled"` +} + +func responseToAttackChallengeMode(out client.AttackChallengeMode) AttackChallengeMode { + return AttackChallengeMode{ + ID: types.StringValue(out.ProjectID), + ProjectID: types.StringValue(out.ProjectID), + TeamID: toTeamID(out.TeamID), + Enabled: types.BoolValue(out.Enabled), + } +} + +func (r *attackChallengeModeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan AttackChallengeMode + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.GetProject(ctx, plan.ProjectID.ValueString(), plan.TeamID.ValueString()) + if client.NotFound(err) { + resp.Diagnostics.AddError( + "Error creating Attack Challenge Mode", + "Could not find project, please make sure both the project_id and team_id match the project and team you wish to deploy to.", + ) + return + } + out, err := r.client.UpdateAttackChallengeMode(ctx, client.AttackChallengeMode{ + TeamID: plan.TeamID.ValueString(), + ProjectID: plan.ProjectID.ValueString(), + Enabled: plan.Enabled.ValueBool(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error creating Attack Challenge Mode", + "Could not create Attack Challenge Mode, unexpected error: "+err.Error(), + ) + return + } + + result := responseToAttackChallengeMode(out) + tflog.Info(ctx, "created attack challenge mode", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *attackChallengeModeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AttackChallengeMode + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetAttackChallengeMode(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading Attack Challenge Mode", + fmt.Sprintf("Could not get Attack Challenge Mode %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + result := responseToAttackChallengeMode(out) + tflog.Info(ctx, "read attack challenge mode", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update does nothing. +func (r *attackChallengeModeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan AttackChallengeMode + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.UpdateAttackChallengeMode(ctx, client.AttackChallengeMode{ + TeamID: plan.TeamID.ValueString(), + ProjectID: plan.ProjectID.ValueString(), + Enabled: plan.Enabled.ValueBool(), + }) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error updating Attack Challenge Mode", + fmt.Sprintf("Could not update Attack Challenge Mode %s %s, unexpected error: %s", + plan.TeamID.ValueString(), + plan.ID.ValueString(), + err, + ), + ) + return + } + + result := responseToAttackChallengeMode(out) + tflog.Trace(ctx, "update attack challenge mode", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *attackChallengeModeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AttackChallengeMode + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Disable on deletion + _, err := r.client.UpdateAttackChallengeMode(ctx, client.AttackChallengeMode{ + TeamID: state.TeamID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + Enabled: false, + }) + if client.NotFound(err) { + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error deleting Attack Challenge Mode", + fmt.Sprintf( + "Could not delete Attack Challenge Mode %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted attack challenge mode", map[string]interface{}{ + "team_id": state.TeamID.ValueString(), + "project_id": state.ProjectID.ValueString(), + }) +} + +func (r *attackChallengeModeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, projectID, ok := splitInto1Or2(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing Attack Challenge Mode", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id\" or \"project_id\"", req.ID), + ) + } + + out, err := r.client.GetAttackChallengeMode(ctx, projectID, teamID) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading Attack Challenge Mode", + fmt.Sprintf("Could not get Attack Challenge Mode %s %s, unexpected error: %s", + teamID, + projectID, + err, + ), + ) + return + } + + result := responseToAttackChallengeMode(out) + tflog.Info(ctx, "import attack challenge mode", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags := resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/resource_attack_challenge_mode_test.go b/vercel/resource_attack_challenge_mode_test.go new file mode 100644 index 00000000..0dd80cb2 --- /dev/null +++ b/vercel/resource_attack_challenge_mode_test.go @@ -0,0 +1,96 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAcc_AttackChallengeModeResource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccAttackChallengeModeConfigResource(name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("vercel_attack_challenge_mode.enabled", "enabled", "true"), + resource.TestCheckResourceAttr("vercel_attack_challenge_mode.disabled", "enabled", "false"), + ), + }, + { + ImportState: true, + ResourceName: "vercel_attack_challenge_mode.enabled", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_attack_challenge_mode.enabled"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + ImportState: true, + ResourceName: "vercel_attack_challenge_mode.disabled", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_attack_challenge_mode.disabled"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + Config: testAccAttackChallengeModeConfigResourceUpdated(name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("vercel_attack_challenge_mode.enabled", "enabled", "false"), + ), + }, + }, + }) +} + +func testAccAttackChallengeModeConfigResource(name, teamID string) string { + return fmt.Sprintf(` +resource "vercel_project" "enabled" { + name = "test-acc-%[1]s-enabled" + %[2]s +} + +resource "vercel_attack_challenge_mode" "enabled" { + project_id = vercel_project.enabled.id + enabled = true + %[2]s +} + +resource "vercel_project" "disabled" { + name = "test-acc-%[1]s-disabled" + %[2]s +} + +resource "vercel_attack_challenge_mode" "disabled" { + project_id = vercel_project.disabled.id + enabled = false + %[2]s +} +`, name, teamID) +} + +func testAccAttackChallengeModeConfigResourceUpdated(name, teamID string) string { + return fmt.Sprintf(` +resource "vercel_project" "enabled" { + name = "test-acc-%[1]s-enabled" + %[2]s +} + +resource "vercel_attack_challenge_mode" "enabled" { + project_id = vercel_project.enabled.id + enabled = false + %[2]s +} +`, name, teamID) +} diff --git a/vercel/resource_project_function_cpu_test.go b/vercel/resource_project_function_cpu_test.go new file mode 100644 index 00000000..c34c0c1b --- /dev/null +++ b/vercel/resource_project_function_cpu_test.go @@ -0,0 +1,119 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAcc_ProjectFunctionCPUResource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccProjectFunctionCPUResourceConfig(name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("vercel_project_function_cpu.basic", "cpu", "basic"), + resource.TestCheckResourceAttr("vercel_project_function_cpu.standard", "cpu", "standard"), + resource.TestCheckResourceAttr("vercel_project_function_cpu.performance", "cpu", "performance"), + ), + }, + { + ImportState: true, + ResourceName: "vercel_project_function_cpu.basic", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_project_function_cpu.basic"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + ImportState: true, + ResourceName: "vercel_project_function_cpu.standard", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_project_function_cpu.standard"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + ImportState: true, + ResourceName: "vercel_project_function_cpu.performance", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["vercel_project_function_cpu.performance"] + if !ok { + return "", fmt.Errorf("resource not found") + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.ID), nil + }, + }, + { + Config: testAccProjectFunctionCPUResourceConfigUpdated(name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("vercel_project_function_cpu.basic", "cpu", "performance"), + ), + }, + }, + }) +} + +func testAccProjectFunctionCPUResourceConfig(name, teamID string) string { + return fmt.Sprintf(` +resource "vercel_project" "basic" { + name = "test-acc-%[1]s" + %[2]s +} + +resource "vercel_project_function_cpu" "basic" { + project_id = vercel_project.basic.id + cpu = "basic" + %[2]s +} + +resource "vercel_project" "standard" { + name = "test-acc-%[1]s-standard" + %[2]s +} + +resource "vercel_project_function_cpu" "standard" { + project_id = vercel_project.standard.id + cpu = "standard" + %[2]s +} + +resource "vercel_project" "performance" { + name = "test-acc-%[1]s-performance" + %[2]s +} + +resource "vercel_project_function_cpu" "performance" { + project_id = vercel_project.performance.id + cpu = "performance" + %[2]s +} +`, name, teamID) +} + +func testAccProjectFunctionCPUResourceConfigUpdated(name, teamID string) string { + return fmt.Sprintf(` +resource "vercel_project" "basic" { + name = "test-acc-%[1]s" + %[2]s +} + +resource "vercel_project_function_cpu" "basic" { + project_id = vercel_project.basic.id + cpu = "performance" + %[2]s +} +`, name, teamID) +}