From de60c59ded9f79ac7673d0ae3f51cc1bf011b50c Mon Sep 17 00:00:00 2001 From: brookemosby Date: Tue, 27 Aug 2024 14:04:35 -0600 Subject: [PATCH 01/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- client/project.go | 9 + client/project_deployment_retention.go | 96 +++++ vercel/data_source_project.go | 33 ++ vercel/provider.go | 1 + vercel/resource_project.go | 27 ++ .../resource_project_deployment_retention.go | 396 ++++++++++++++++++ ...ource_project_deployment_retention_test.go | 166 ++++++++ 7 files changed, 728 insertions(+) create mode 100644 client/project_deployment_retention.go create mode 100644 vercel/resource_project_deployment_retention.go create mode 100644 vercel/resource_project_deployment_retention_test.go diff --git a/client/project.go b/client/project.go index 436cd5eb..41d33531 100644 --- a/client/project.go +++ b/client/project.go @@ -31,6 +31,13 @@ type EnvironmentVariable struct { TeamID string `json:"-"` } +type DeploymentExpiration struct { + ExpirationPreview string `json:"expiration"` + ExpirationProduction string `json:"expirationProduction"` + ExpirationCanceled string `json:"expirationCanceled"` + ExpirationErrored string `json:"expirationErrored"` +} + // CreateProjectRequest defines the information necessary to create a project. type CreateProjectRequest struct { BuildCommand *string `json:"buildCommand"` @@ -189,6 +196,7 @@ type ProjectResponse struct { SkewProtectionMaxAge int `json:"skewProtectionMaxAge"` GitComments *GitComments `json:"gitComments"` Security *Security `json:"security"` + DeploymentExpiration *DeploymentExpiration `json:"deploymentExpiration"` } type GitComments struct { @@ -281,6 +289,7 @@ type UpdateProjectRequest struct { DirectoryListing bool `json:"directoryListing"` SkewProtectionMaxAge int `json:"skewProtectionMaxAge"` GitComments *GitComments `json:"gitComments"` + DeploymentExpiration *DeploymentExpiration `json:"deploymentExpiration"` } // UpdateProject updates an existing projects configuration within Vercel. diff --git a/client/project_deployment_retention.go b/client/project_deployment_retention.go new file mode 100644 index 00000000..b5692031 --- /dev/null +++ b/client/project_deployment_retention.go @@ -0,0 +1,96 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// CreateDeploymentRetentionRequest defines the information that needs to be passed to Vercel in order to +// create an deployment retention. +type DeploymentRetentionRequest struct { + ExpirationPreview *string `json:"expiration,omitempty"` + ExpirationProduction *string `json:"expirationProduction,omitempty"` + ExpirationCanceled *string `json:"expirationCanceled,omitempty"` + ExpirationErrored *string `json:"expirationErrored,omitempty"` +} + +// UpdateDeploymentRetentionRequest defines the information that needs to be passed to Vercel in order to +// update an deployment retention. +type UpdateDeploymentRetentionRequest struct { + DeploymentRetention DeploymentRetentionRequest + ProjectID string + TeamID string +} + +// DeleteDeploymentRetention will remove any existing deployment retention for a given project. +func (c *Client) DeleteDeploymentRetention(ctx context.Context, projectID, teamID string) error { + url := fmt.Sprintf("%s/v9/projects/%s/deployment-expiration", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + unlimited := "unlimited" + payload := string(mustMarshal(DeploymentRetentionRequest{ExpirationPreview: &unlimited, ExpirationProduction: &unlimited, ExpirationCanceled: &unlimited, ExpirationErrored: &unlimited})) + + tflog.Info(ctx, "updating deployment expiration", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, nil) + return err +} + +type DeploymentExpirationResponse struct { + DeploymentExpiration DeploymentExpiration `json:"deploymentExpiration"` +} + +// UpdateDeploymentRetention will update an existing deployment retention to the latest information. +func (c *Client) UpdateDeploymentRetention(ctx context.Context, request UpdateDeploymentRetentionRequest) (DeploymentExpiration, error) { + url := fmt.Sprintf("%s/v9/projects/%s/deployment-expiration", c.baseURL, request.ProjectID) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request.DeploymentRetention)) + + tflog.Info(ctx, "updating deployment expiration", map[string]interface{}{ + "url": url, + "payload": payload, + }) + var d DeploymentExpirationResponse + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &d) + return d.DeploymentExpiration, err +} + +// GetDeploymentRetention returns the deployment retention for a given project. +func (c *Client) GetDeploymentRetention(ctx context.Context, projectID, teamID string) (d DeploymentExpiration, err error) { + url := fmt.Sprintf("%s/v1/projects/%s", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "getting deployment retention", map[string]interface{}{ + "url": url, + }) + var p ProjectResponse + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &p) + if p.DeploymentExpiration == nil { + return DeploymentExpiration{}, fmt.Errorf("deployment retention not found") + } + return *p.DeploymentExpiration, err +} diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index 8019bd94..08fcd226 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -322,6 +322,37 @@ For more detailed information, please see the [Vercel documentation](https://ver Computed: true, Description: "Ensures that outdated clients always fetch the correct version for a given deployment. This value defines how long Vercel keeps Skew Protection active.", }, + "deployment_expiration": schema.SingleNestedAttribute{ + Description: "Configuration for Deployment Retention.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "expiration_preview": schema.StringAttribute{ + Description: "Preview deployments will be automatically deleted after this time.", + Optional: true, + }, + "expiration_production": schema.StringAttribute{ + Description: "Production deployments will be automatically deleted after this time.", + Optional: true, + }, + "expiration_canceled": schema.StringAttribute{ + Description: "Canceled deployments will be automatically deleted after this time.", + Optional: true, + }, + "expiration_errored": schema.StringAttribute{ + Description: "Errored deployments will be automatically deleted after this time.", + Optional: true, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the Project for the retention policy", + Required: true, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the Vercel team.", + }, + }, + }, }, } } @@ -359,6 +390,7 @@ type ProjectDataSource struct { PrioritiseProductionBuilds types.Bool `tfsdk:"prioritise_production_builds"` DirectoryListing types.Bool `tfsdk:"directory_listing"` SkewProtection types.String `tfsdk:"skew_protection"` + DeploymentExpiration types.Object `tfsdk:"deployment_expiration"` } func convertResponseToProjectDataSource(ctx context.Context, response client.ProjectResponse, plan Project, environmentVariables []client.EnvironmentVariable) (ProjectDataSource, error) { @@ -416,6 +448,7 @@ func convertResponseToProjectDataSource(ctx context.Context, response client.Pro PrioritiseProductionBuilds: project.PrioritiseProductionBuilds, DirectoryListing: project.DirectoryListing, SkewProtection: project.SkewProtection, + DeploymentExpiration: project.DeploymentExpiration, }, nil } diff --git a/vercel/provider.go b/vercel/provider.go index 4d024486..037ddce3 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -58,6 +58,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newEdgeConfigSchemaResource, newEdgeConfigTokenResource, newLogDrainResource, + newProjectDeploymentRetentionResource, newProjectDomainResource, newProjectEnvironmentVariableResource, newProjectFunctionCPUResource, diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 0c410dc1..e878ab27 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -481,6 +481,7 @@ type Project struct { PrioritiseProductionBuilds types.Bool `tfsdk:"prioritise_production_builds"` DirectoryListing types.Bool `tfsdk:"directory_listing"` SkewProtection types.String `tfsdk:"skew_protection"` + DeploymentExpiration types.Object `tfsdk:"deployment_expiration"` } type GitComments struct { @@ -913,6 +914,15 @@ var gitCommentsAttrTypes = map[string]attr.Type{ "on_pull_request": types.BoolType, } +var deploymentExpirationAttrTypes = map[string]attr.Type{ + "expiration_preview": types.StringType, + "expiration_production": types.StringType, + "expiration_canceled": types.StringType, + "expiration_errored": types.StringType, + "project_id": types.StringType, + "team_id": types.StringType, +} + func hasSameTarget(p EnvironmentItem, target []string) bool { if len(p.Target) != len(target) { return false @@ -1126,6 +1136,22 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon } } + deploymentExpiration := types.ObjectNull(deploymentExpirationAttrTypes) + if response.DeploymentExpiration != nil { + var diags diag.Diagnostics + deploymentExpiration, diags = types.ObjectValueFrom(ctx, deploymentExpirationAttrTypes, &ProjectDeploymentRetention{ + ExpirationPreview: types.StringValue(response.DeploymentExpiration.ExpirationPreview), + ExpirationProduction: types.StringValue(response.DeploymentExpiration.ExpirationProduction), + ExpirationCanceled: types.StringValue(response.DeploymentExpiration.ExpirationCanceled), + ExpirationErrored: types.StringValue(response.DeploymentExpiration.ExpirationErrored), + ProjectID: types.StringValue(response.ID), + TeamID: toTeamID(response.TeamID), + }) + if diags.HasError() { + return Project{}, fmt.Errorf("error reading project deployment expiration: %s - %s", diags[0].Summary(), diags[0].Detail()) + } + } + return Project{ BuildCommand: uncoerceString(fields.BuildCommand, types.StringPointerValue(response.BuildCommand)), DevCommand: uncoerceString(fields.DevCommand, types.StringPointerValue(response.DevCommand)), @@ -1159,6 +1185,7 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon DirectoryListing: types.BoolValue(response.DirectoryListing), SkewProtection: fromSkewProtectionMaxAge(response.SkewProtectionMaxAge), GitComments: gitComments, + DeploymentExpiration: deploymentExpiration, }, nil } diff --git a/vercel/resource_project_deployment_retention.go b/vercel/resource_project_deployment_retention.go new file mode 100644 index 00000000..e725eb1d --- /dev/null +++ b/vercel/resource_project_deployment_retention.go @@ -0,0 +1,396 @@ +package vercel + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/client" +) + +var ( + _ resource.Resource = &projectDeploymentRetentionResource{} + _ resource.ResourceWithConfigure = &projectDeploymentRetentionResource{} + _ resource.ResourceWithImportState = &projectDeploymentRetentionResource{} + _ resource.ResourceWithModifyPlan = &projectDeploymentRetentionResource{} +) + +func newProjectDeploymentRetentionResource() resource.Resource { + return &projectDeploymentRetentionResource{} +} + +type projectDeploymentRetentionResource struct { + client *client.Client +} + +func (r *projectDeploymentRetentionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_deployment_retention" +} + +func (r *projectDeploymentRetentionResource) 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 +} + +// AllowedValuesStringValidator is a validator that checks if the string value is among the allowed options. +type AllowedValuesStringValidator struct { + AllowedValues []string +} + +func (v AllowedValuesStringValidator) Description(ctx context.Context) string { + return fmt.Sprintf("must be one of the following values: %v", v.AllowedValues) +} + +func (v AllowedValuesStringValidator) MarkdownDescription(ctx context.Context) string { + return fmt.Sprintf("must be one of the following values: %v", v.AllowedValues) +} + +func (v AllowedValuesStringValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + strVal := request.ConfigValue.ValueString() + + for _, allowed := range v.AllowedValues { + if strVal == allowed { + return + } + } + + response.Diagnostics.AddError( + "Invalid Value", + fmt.Sprintf("Value '%s' is not a valid option, must be one of %v.", strVal, v.AllowedValues), + ) +} + +// Schema returns the schema information for a project deployment retention resource. +func (r *projectDeploymentRetentionResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides a Project Deployment Retention resource. + +A Project Deployment Retention resource defines an Deployment Retention on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/security/deployment-retention). + +~> Terraform currently provides a project deployment retention resource. +`, + Attributes: map[string]schema.Attribute{ + "expiration_preview": schema.StringAttribute{ + Optional: true, + Description: "The retention period for preview deployments.", + Validators: []validator.String{ + AllowedValuesStringValidator{ + AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, + }, + }, + }, + "expiration_production": schema.StringAttribute{ + Optional: true, + Description: "The retention period for production deployments.", + Validators: []validator.String{ + AllowedValuesStringValidator{ + AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, + }, + }, + }, + "expiration_canceled": schema.StringAttribute{ + Optional: true, + Description: "The retention period for canceled deployments.", + Validators: []validator.String{ + AllowedValuesStringValidator{ + AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, + }, + }, + }, + "expiration_errored": schema.StringAttribute{ + Optional: true, + Description: "The retention period for errored deployments.", + Validators: []validator.String{ + AllowedValuesStringValidator{ + AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, + }, + }, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the Project for the retention policy", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the Vercel team.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + }, + }, + } +} + +// ProjectDeploymentRetention reflects the state terraform stores internally for a project deployment retention. +type ProjectDeploymentRetention struct { + ExpirationPreview types.String `tfsdk:"expiration_preview"` + ExpirationProduction types.String `tfsdk:"expiration_production"` + ExpirationCanceled types.String `tfsdk:"expiration_canceled"` + ExpirationErrored types.String `tfsdk:"expiration_errored"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` +} + +func (r *projectDeploymentRetentionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.Plan.Raw.IsNull() { + return + } + var config ProjectDeploymentRetention + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (e *ProjectDeploymentRetention) toUpdateDeploymentRetentionRequest() client.UpdateDeploymentRetentionRequest { + + preview := e.ExpirationPreview.ValueString() + production := e.ExpirationProduction.ValueString() + canceled := e.ExpirationCanceled.ValueString() + errored := e.ExpirationErrored.ValueString() + return client.UpdateDeploymentRetentionRequest{ + DeploymentRetention: client.DeploymentRetentionRequest{ + ExpirationPreview: &preview, + ExpirationProduction: &production, + ExpirationCanceled: &canceled, + ExpirationErrored: &errored, + }, + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + } +} + +// convertResponseToProjectDeploymentRetention is used to populate terraform state based on an API response. +// Where possible, values from the API response are used to populate state. If not possible, +// values from plan are used. +func convertResponseToProjectDeploymentRetention(response client.DeploymentExpiration, projectID types.String, teamID types.String) ProjectDeploymentRetention { + + return ProjectDeploymentRetention{ + ExpirationPreview: types.StringValue(response.ExpirationPreview), + ExpirationProduction: types.StringValue(response.ExpirationProduction), + ExpirationCanceled: types.StringValue(response.ExpirationCanceled), + ExpirationErrored: types.StringValue(response.ExpirationErrored), + TeamID: teamID, + ProjectID: projectID, + } +} + +// Create will create a new project deployment retention for a Vercel project. +// This is called automatically by the provider when a new resource should be created. +func (r *projectDeploymentRetentionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ProjectDeploymentRetention + 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 project deployment retention", + "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 + } + + response, err := r.client.UpdateDeploymentRetention(ctx, plan.toUpdateDeploymentRetentionRequest()) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project deployment retention", + "Could not create project deployment retention, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToProjectDeploymentRetention(response, plan.ProjectID, plan.TeamID) + + tflog.Info(ctx, "created project deployment retention", 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 + } +} + +// Read will read an deployment retention of a Vercel project by requesting it from the Vercel API, and will update terraform +// with this information. +func (r *projectDeploymentRetentionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ProjectDeploymentRetention + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetDeploymentRetention(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading project deployment retention", + fmt.Sprintf("Could not get project deployment retention %s %s, unexpected error: %s", + state.ProjectID.ValueString(), + state.TeamID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToProjectDeploymentRetention(out, state.ProjectID, state.TeamID) + tflog.Info(ctx, "read project deployment retention", 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 + } +} + +// Delete deletes a Vercel project deployment retention. +func (r *projectDeploymentRetentionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ProjectDeploymentRetention + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteDeploymentRetention(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + if client.NotFound(err) { + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project deployment retention", + fmt.Sprintf( + "Could not delete project deployment retention %s, unexpected error: %s", + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted project deployment retention", map[string]interface{}{ + "team_id": state.TeamID.ValueString(), + "project_id": state.ProjectID.ValueString(), + }) +} + +// Update updates the project deployment retention of a Vercel project state. +func (r *projectDeploymentRetentionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ProjectDeploymentRetention + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.client.UpdateDeploymentRetention(ctx, plan.toUpdateDeploymentRetentionRequest()) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project deployment retention", + "Could not update project deployment retention, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToProjectDeploymentRetention(response, plan.ProjectID, plan.TeamID) + + tflog.Info(ctx, "updated project deployment retention", 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 + } +} + +// splitID is a helper function for splitting an import ID into the corresponding parts. +// It also validates whether the ID is in a correct format. +func splitProjectDeploymentRetentionID(id string) (teamID, projectID string, ok bool) { + attributes := strings.Split(id, "/") + if len(attributes) == 2 { + return attributes[0], attributes[1], true + } + if len(attributes) == 1 { + return "", attributes[0], true + } + + return "", "", false +} + +// ImportState takes an identifier and reads all the project deployment retention information from the Vercel API. +// The results are then stored in terraform state. +func (r *projectDeploymentRetentionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, projectID, ok := splitProjectDeploymentRetentionID(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing project deployment retention", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id\" or \"project_id\"", req.ID), + ) + } + + out, err := r.client.GetDeploymentRetention(ctx, projectID, teamID) + if err != nil { + resp.Diagnostics.AddError( + "Error reading project deployment retention", + fmt.Sprintf("Could not get project deployment retention %s %s, unexpected error: %s", + teamID, + projectID, + err, + ), + ) + return + } + + result := convertResponseToProjectDeploymentRetention(out, types.StringValue(projectID), types.StringValue(teamID)) + tflog.Info(ctx, "imported project deployment retention", 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_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go new file mode 100644 index 00000000..2f0f7096 --- /dev/null +++ b/vercel/resource_project_deployment_retention_test.go @@ -0,0 +1,166 @@ +package vercel_test + +import ( + "context" + "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 testAccProjectDeploymentRetentionExists(n, teamID 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 fmt.Errorf("no ID is set") + } + + _, err := testClient().GetDeploymentRetention(context.TODO(), rs.Primary.Attributes["project_id"], teamID) + return err + } +} + +func TestAcc_ProjectDeploymentRetentions(t *testing.T) { + nameSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccProjectDestroy("vercel_project.example", testTeam()), + ), + Steps: []resource.TestStep{ + { + Config: testAccProjectDeploymentRetentionsConfig(nameSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example", testTeam()), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "1m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "1m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "1m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1m"), + + testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example_diff", testTeam()), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "1m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "3m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "6m"), + ), + }, + { + Config: testAccProjectDeploymentRetentionsConfigUpdated(nameSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example", testTeam()), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "2m"), + + testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example_diff", testTeam()), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "3m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "6m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1y"), + ), + }, + { + ResourceName: "vercel_project_deployment_retention.example", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getProjectDeploymentRetentionImportID("vercel_project_deployment_retention.example"), + }, + { + ResourceName: "vercel_project_deployment_retention.example_diff", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getProjectDeploymentRetentionImportID("vercel_project_deployment_retention.example_diff"), + }, + }, + }) +} + +func getProjectDeploymentRetentionImportID(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) + } + + if rs.Primary.ID == "" { + return "", fmt.Errorf("no ID is set") + } + + if rs.Primary.Attributes["team_id"] == "" { + return rs.Primary.Attributes["project_id"], nil + } + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.Attributes["project_id"]), nil + } +} + +func testAccProjectDeploymentRetentionsConfig(projectName string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + %[3]s + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_deployment_retention" "example" { + project_id = vercel_project.example.id + %[3]s + expiration_preview = "1m" + expiration_production = "1m" + expiration_canceled = "1m" + expiration_errored = "1m" +} + +resource "vercel_project_deployment_retention" "example_diff" { + project_id = vercel_project.example.id + %[3]s + expiration_preview = "1m" + expiration_production = "2m" + expiration_canceled = "3m" + expiration_errored = "6m" +} +`, projectName, testGithubRepo(), teamIDConfig()) +} + +func testAccProjectDeploymentRetentionsConfigUpdated(projectName string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + %[3]s + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_deployment_retention" "example" { + project_id = vercel_project.example.id + %[3]s + expiration_preview = "2m" + expiration_production = "2m" + expiration_canceled = "2m" + expiration_errored = "2m" +} + +resource "vercel_project_deployment_retention" "example_diff" { + project_id = vercel_project.example.id + %[3]s + expiration_preview = "2m" + expiration_production = "3m" + expiration_canceled = "6m" + expiration_errored = "1y" +} +`, projectName, testGithubRepo(), teamIDConfig()) +} From db900b3f7deae2dddeda5467dd9b6b33031b315b Mon Sep 17 00:00:00 2001 From: brookemosby Date: Tue, 27 Aug 2024 14:14:26 -0600 Subject: [PATCH 02/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- .../import.sh | 4 +++ .../resource.tf | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 examples/resources/vercel_project_deployment_retention/import.sh create mode 100644 examples/resources/vercel_project_deployment_retention/resource.tf diff --git a/examples/resources/vercel_project_deployment_retention/import.sh b/examples/resources/vercel_project_deployment_retention/import.sh new file mode 100644 index 00000000..7cd566b2 --- /dev/null +++ b/examples/resources/vercel_project_deployment_retention/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_project_deployment_retention.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/examples/resources/vercel_project_deployment_retention/resource.tf b/examples/resources/vercel_project_deployment_retention/resource.tf new file mode 100644 index 00000000..cec8222b --- /dev/null +++ b/examples/resources/vercel_project_deployment_retention/resource.tf @@ -0,0 +1,30 @@ +resource "vercel_project" "example" { + name = "example-project" + + git_repository = { + type = "github" + repo = "vercel/some-repo" + } +} + +# An unlimited deployment retention policy that will be created +# for this project for all deployments. +resource "vercel_project_deployment_retention" "example_unlimited" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + expiration_preview = "unlimited" + expiration_production = "unlimited" + expiration_canceled = "unlimited" + expiration_errored = "unlimited" +} + +# A customized deployment retention policy that will be created +# for this project for all deployments. +resource "vercel_project_deployment_retention" "example_customized" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + expiration_preview = "3m" + expiration_production = "1y" + expiration_canceled = "1m" + expiration_errored = "2m" +} From 656738b55f35c150ccbeb4b6ecd4be9b361ea5a2 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Tue, 27 Aug 2024 14:43:08 -0600 Subject: [PATCH 03/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- docs/data-sources/project.md | 17 ++++ .../resources/project_deployment_retention.md | 81 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 docs/resources/project_deployment_retention.md diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index 95dd2063..c1bbf73a 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -46,6 +46,7 @@ output "project_id" { - `automatically_expose_system_environment_variables` (Boolean) Vercel provides a set of Environment Variables that are automatically populated by the System, such as the URL of the Deployment or the name of the Git branch deployed. To expose them to your Deployments, enable this field - `build_command` (String) The build command for this project. If omitted, this value will be automatically detected. - `customer_success_code_visibility` (Boolean) Allows Vercel Customer Support to inspect all Deployments' source code in this project to assist with debugging. +- `deployment_expiration` (Attributes) Configuration for Deployment Retention. (see [below for nested schema](#nestedatt--deployment_expiration)) - `dev_command` (String) The dev command for this project. If omitted, this value will be automatically detected. - `directory_listing` (Boolean) If no index file is present within a directory, the directory contents will be displayed. - `environment` (Attributes Set) A list of environment variables that should be configured for the project. (see [below for nested schema](#nestedatt--environment)) @@ -72,6 +73,22 @@ output "project_id" { - `trusted_ips` (Attributes) Ensures only visitors from an allowed IP address can access your deployment. (see [below for nested schema](#nestedatt--trusted_ips)) - `vercel_authentication` (Attributes) Ensures visitors to your Preview Deployments are logged into Vercel and have a minimum of Viewer access on your team. (see [below for nested schema](#nestedatt--vercel_authentication)) + +### Nested Schema for `deployment_expiration` + +Required: + +- `project_id` (String) The ID of the Project for the retention policy + +Optional: + +- `expiration_canceled` (String) Canceled deployments will be automatically deleted after this time. +- `expiration_errored` (String) Errored deployments will be automatically deleted after this time. +- `expiration_preview` (String) Preview deployments will be automatically deleted after this time. +- `expiration_production` (String) Production deployments will be automatically deleted after this time. +- `team_id` (String) The ID of the Vercel team. + + ### Nested Schema for `environment` diff --git a/docs/resources/project_deployment_retention.md b/docs/resources/project_deployment_retention.md new file mode 100644 index 00000000..b1f0ac3f --- /dev/null +++ b/docs/resources/project_deployment_retention.md @@ -0,0 +1,81 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_project_deployment_retention Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides a Project Deployment Retention resource. + A Project Deployment Retention resource defines an Deployment Retention on a Vercel Project. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/security/deployment-retention. + ~> Terraform currently provides a project deployment retention resource. +--- + +# vercel_project_deployment_retention (Resource) + +Provides a Project Deployment Retention resource. + +A Project Deployment Retention resource defines an Deployment Retention on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/security/deployment-retention). + +~> Terraform currently provides a project deployment retention resource. + +## Example Usage + +```terraform +resource "vercel_project" "example" { + name = "example-project" + + git_repository = { + type = "github" + repo = "vercel/some-repo" + } +} + +# An unlimited deployment retention policy that will be created +# for this project for all deployments. +resource "vercel_project_deployment_retention" "example_unlimited" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + expiration_preview = "unlimited" + expiration_production = "unlimited" + expiration_canceled = "unlimited" + expiration_errored = "unlimited" +} + +# A customized deployment retention policy that will be created +# for this project for all deployments. +resource "vercel_project_deployment_retention" "example_customized" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + expiration_preview = "3m" + expiration_production = "1y" + expiration_canceled = "1m" + expiration_errored = "2m" +} +``` + + +## Schema + +### Required + +- `project_id` (String) The ID of the Project for the retention policy + +### Optional + +- `expiration_canceled` (String) The retention period for canceled deployments. +- `expiration_errored` (String) The retention period for errored deployments. +- `expiration_preview` (String) The retention period for preview deployments. +- `expiration_production` (String) The retention period for production deployments. +- `team_id` (String) The ID of the Vercel team. + +## 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_project_deployment_retention.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` From b1d887a39ace5d0e710e6fe36dca52e14f5dc3eb Mon Sep 17 00:00:00 2001 From: brookemosby Date: Tue, 27 Aug 2024 17:36:51 -0600 Subject: [PATCH 04/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- vercel/resource_project_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vercel/resource_project_test.go b/vercel/resource_project_test.go index e6bbf99c..3b4ebaea 100644 --- a/vercel/resource_project_test.go +++ b/vercel/resource_project_test.go @@ -77,6 +77,7 @@ func TestAcc_Project(t *testing.T) { resource.TestCheckResourceAttr("vercel_project.test", "directory_listing", "true"), resource.TestCheckResourceAttr("vercel_project.test", "skew_protection", "7 days"), resource.TestCheckResourceAttr("vercel_project.test", "oidc_token_config.enabled", "true"), + resource.TestCheckTypeSetElemNestedAttrs("vercel_project.test", "deployment_expiration.*", map[string]string{}), ), }, // Update testing From 908a7aa4e9b690cc93d406df56570e87a1679102 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Wed, 28 Aug 2024 08:15:34 -0600 Subject: [PATCH 05/22] Update vercel/resource_project_deployment_retention.go Co-authored-by: Douglas Harcourt Parsons --- .../resource_project_deployment_retention.go | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/vercel/resource_project_deployment_retention.go b/vercel/resource_project_deployment_retention.go index e725eb1d..671273b7 100644 --- a/vercel/resource_project_deployment_retention.go +++ b/vercel/resource_project_deployment_retention.go @@ -52,33 +52,6 @@ func (r *projectDeploymentRetentionResource) Configure(ctx context.Context, req r.client = client } -// AllowedValuesStringValidator is a validator that checks if the string value is among the allowed options. -type AllowedValuesStringValidator struct { - AllowedValues []string -} - -func (v AllowedValuesStringValidator) Description(ctx context.Context) string { - return fmt.Sprintf("must be one of the following values: %v", v.AllowedValues) -} - -func (v AllowedValuesStringValidator) MarkdownDescription(ctx context.Context) string { - return fmt.Sprintf("must be one of the following values: %v", v.AllowedValues) -} - -func (v AllowedValuesStringValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { - strVal := request.ConfigValue.ValueString() - - for _, allowed := range v.AllowedValues { - if strVal == allowed { - return - } - } - - response.Diagnostics.AddError( - "Invalid Value", - fmt.Sprintf("Value '%s' is not a valid option, must be one of %v.", strVal, v.AllowedValues), - ) -} // Schema returns the schema information for a project deployment retention resource. func (r *projectDeploymentRetentionResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { From 967b6bc267e2f78377fd9777027780e0e5af85f6 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Wed, 28 Aug 2024 08:15:55 -0600 Subject: [PATCH 06/22] Update vercel/resource_project_deployment_retention.go Co-authored-by: Douglas Harcourt Parsons --- vercel/resource_project_deployment_retention.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vercel/resource_project_deployment_retention.go b/vercel/resource_project_deployment_retention.go index 671273b7..1c7e09d5 100644 --- a/vercel/resource_project_deployment_retention.go +++ b/vercel/resource_project_deployment_retention.go @@ -70,9 +70,7 @@ For more detailed information, please see the [Vercel documentation](https://ver Optional: true, Description: "The retention period for preview deployments.", Validators: []validator.String{ - AllowedValuesStringValidator{ - AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, - }, + stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited") }, }, "expiration_production": schema.StringAttribute{ From db2b96bd8a8da7e241348412e30e9083b9b94f92 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 08:19:55 -0600 Subject: [PATCH 07/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- .../resource_project_deployment_retention.go | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/vercel/resource_project_deployment_retention.go b/vercel/resource_project_deployment_retention.go index 1c7e09d5..8c520108 100644 --- a/vercel/resource_project_deployment_retention.go +++ b/vercel/resource_project_deployment_retention.go @@ -19,7 +19,6 @@ var ( _ resource.Resource = &projectDeploymentRetentionResource{} _ resource.ResourceWithConfigure = &projectDeploymentRetentionResource{} _ resource.ResourceWithImportState = &projectDeploymentRetentionResource{} - _ resource.ResourceWithModifyPlan = &projectDeploymentRetentionResource{} ) func newProjectDeploymentRetentionResource() resource.Resource { @@ -52,7 +51,6 @@ func (r *projectDeploymentRetentionResource) Configure(ctx context.Context, req r.client = client } - // Schema returns the schema information for a project deployment retention resource. func (r *projectDeploymentRetentionResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ @@ -70,34 +68,28 @@ For more detailed information, please see the [Vercel documentation](https://ver Optional: true, Description: "The retention period for preview deployments.", Validators: []validator.String{ - stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited") + stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, }, "expiration_production": schema.StringAttribute{ Optional: true, Description: "The retention period for production deployments.", Validators: []validator.String{ - AllowedValuesStringValidator{ - AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, - }, + stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, }, "expiration_canceled": schema.StringAttribute{ Optional: true, Description: "The retention period for canceled deployments.", Validators: []validator.String{ - AllowedValuesStringValidator{ - AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, - }, + stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, }, "expiration_errored": schema.StringAttribute{ Optional: true, Description: "The retention period for errored deployments.", Validators: []validator.String{ - AllowedValuesStringValidator{ - AllowedValues: []string{"1m", "2m", "3m", "6m", "1y", "unlimited"}, - }, + stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, }, "project_id": schema.StringAttribute{ @@ -125,18 +117,6 @@ type ProjectDeploymentRetention struct { TeamID types.String `tfsdk:"team_id"` } -func (r *projectDeploymentRetentionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - if req.Plan.Raw.IsNull() { - return - } - var config ProjectDeploymentRetention - diags := req.Config.Get(ctx, &config) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - func (e *ProjectDeploymentRetention) toUpdateDeploymentRetentionRequest() client.UpdateDeploymentRetentionRequest { preview := e.ExpirationPreview.ValueString() From 99dfece0bbea0505196d17c450ec682e8f2eb17a Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 09:43:48 -0600 Subject: [PATCH 08/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- docs/data-sources/project.md | 2 +- vercel/data_source_project.go | 4 ++-- vercel/resource_project.go | 8 +++++--- vercel/resource_project_test.go | 1 - 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index c1bbf73a..6e46cd08 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -38,6 +38,7 @@ output "project_id" { ### Optional +- `deployment_expiration` (Attributes) Configuration for Deployment Retention. (see [below for nested schema](#nestedatt--deployment_expiration)) - `team_id` (String) The team ID the project exists beneath. Required when configuring a team resource if a default team has not been set in the provider. ### Read-Only @@ -46,7 +47,6 @@ output "project_id" { - `automatically_expose_system_environment_variables` (Boolean) Vercel provides a set of Environment Variables that are automatically populated by the System, such as the URL of the Deployment or the name of the Git branch deployed. To expose them to your Deployments, enable this field - `build_command` (String) The build command for this project. If omitted, this value will be automatically detected. - `customer_success_code_visibility` (Boolean) Allows Vercel Customer Support to inspect all Deployments' source code in this project to assist with debugging. -- `deployment_expiration` (Attributes) Configuration for Deployment Retention. (see [below for nested schema](#nestedatt--deployment_expiration)) - `dev_command` (String) The dev command for this project. If omitted, this value will be automatically detected. - `directory_listing` (Boolean) If no index file is present within a directory, the directory contents will be displayed. - `environment` (Attributes Set) A list of environment variables that should be configured for the project. (see [below for nested schema](#nestedatt--environment)) diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index 08fcd226..9e79cde2 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -324,7 +324,7 @@ For more detailed information, please see the [Vercel documentation](https://ver }, "deployment_expiration": schema.SingleNestedAttribute{ Description: "Configuration for Deployment Retention.", - Computed: true, + Optional: true, Attributes: map[string]schema.Attribute{ "expiration_preview": schema.StringAttribute{ Description: "Preview deployments will be automatically deleted after this time.", @@ -390,7 +390,7 @@ type ProjectDataSource struct { PrioritiseProductionBuilds types.Bool `tfsdk:"prioritise_production_builds"` DirectoryListing types.Bool `tfsdk:"directory_listing"` SkewProtection types.String `tfsdk:"skew_protection"` - DeploymentExpiration types.Object `tfsdk:"deployment_expiration"` + DeploymentExpiration *types.Object `tfsdk:"deployment_expiration"` } func convertResponseToProjectDataSource(ctx context.Context, response client.ProjectResponse, plan Project, environmentVariables []client.EnvironmentVariable) (ProjectDataSource, error) { diff --git a/vercel/resource_project.go b/vercel/resource_project.go index e878ab27..60dd1aca 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -481,7 +481,7 @@ type Project struct { PrioritiseProductionBuilds types.Bool `tfsdk:"prioritise_production_builds"` DirectoryListing types.Bool `tfsdk:"directory_listing"` SkewProtection types.String `tfsdk:"skew_protection"` - DeploymentExpiration types.Object `tfsdk:"deployment_expiration"` + DeploymentExpiration *types.Object `tfsdk:"deployment_expiration"` } type GitComments struct { @@ -1136,10 +1136,11 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon } } - deploymentExpiration := types.ObjectNull(deploymentExpirationAttrTypes) + var deploymentExpiration *types.Object if response.DeploymentExpiration != nil { + de := types.ObjectNull(deploymentExpirationAttrTypes) var diags diag.Diagnostics - deploymentExpiration, diags = types.ObjectValueFrom(ctx, deploymentExpirationAttrTypes, &ProjectDeploymentRetention{ + de, diags = types.ObjectValueFrom(ctx, deploymentExpirationAttrTypes, &ProjectDeploymentRetention{ ExpirationPreview: types.StringValue(response.DeploymentExpiration.ExpirationPreview), ExpirationProduction: types.StringValue(response.DeploymentExpiration.ExpirationProduction), ExpirationCanceled: types.StringValue(response.DeploymentExpiration.ExpirationCanceled), @@ -1150,6 +1151,7 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon if diags.HasError() { return Project{}, fmt.Errorf("error reading project deployment expiration: %s - %s", diags[0].Summary(), diags[0].Detail()) } + deploymentExpiration = &de } return Project{ diff --git a/vercel/resource_project_test.go b/vercel/resource_project_test.go index 3b4ebaea..e6bbf99c 100644 --- a/vercel/resource_project_test.go +++ b/vercel/resource_project_test.go @@ -77,7 +77,6 @@ func TestAcc_Project(t *testing.T) { resource.TestCheckResourceAttr("vercel_project.test", "directory_listing", "true"), resource.TestCheckResourceAttr("vercel_project.test", "skew_protection", "7 days"), resource.TestCheckResourceAttr("vercel_project.test", "oidc_token_config.enabled", "true"), - resource.TestCheckTypeSetElemNestedAttrs("vercel_project.test", "deployment_expiration.*", map[string]string{}), ), }, // Update testing From 2a781d1c650ac9b0ef13ed7accf19e29b8756d26 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 10:22:07 -0600 Subject: [PATCH 09/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- client/project.go | 1 - docs/data-sources/project.md | 17 ----------------- vercel/data_source_project.go | 33 --------------------------------- vercel/resource_project.go | 29 +++++++++-------------------- 4 files changed, 9 insertions(+), 71 deletions(-) diff --git a/client/project.go b/client/project.go index 41d33531..aadbe2d6 100644 --- a/client/project.go +++ b/client/project.go @@ -289,7 +289,6 @@ type UpdateProjectRequest struct { DirectoryListing bool `json:"directoryListing"` SkewProtectionMaxAge int `json:"skewProtectionMaxAge"` GitComments *GitComments `json:"gitComments"` - DeploymentExpiration *DeploymentExpiration `json:"deploymentExpiration"` } // UpdateProject updates an existing projects configuration within Vercel. diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index 6e46cd08..95dd2063 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -38,7 +38,6 @@ output "project_id" { ### Optional -- `deployment_expiration` (Attributes) Configuration for Deployment Retention. (see [below for nested schema](#nestedatt--deployment_expiration)) - `team_id` (String) The team ID the project exists beneath. Required when configuring a team resource if a default team has not been set in the provider. ### Read-Only @@ -73,22 +72,6 @@ output "project_id" { - `trusted_ips` (Attributes) Ensures only visitors from an allowed IP address can access your deployment. (see [below for nested schema](#nestedatt--trusted_ips)) - `vercel_authentication` (Attributes) Ensures visitors to your Preview Deployments are logged into Vercel and have a minimum of Viewer access on your team. (see [below for nested schema](#nestedatt--vercel_authentication)) - -### Nested Schema for `deployment_expiration` - -Required: - -- `project_id` (String) The ID of the Project for the retention policy - -Optional: - -- `expiration_canceled` (String) Canceled deployments will be automatically deleted after this time. -- `expiration_errored` (String) Errored deployments will be automatically deleted after this time. -- `expiration_preview` (String) Preview deployments will be automatically deleted after this time. -- `expiration_production` (String) Production deployments will be automatically deleted after this time. -- `team_id` (String) The ID of the Vercel team. - - ### Nested Schema for `environment` diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index 9e79cde2..8019bd94 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -322,37 +322,6 @@ For more detailed information, please see the [Vercel documentation](https://ver Computed: true, Description: "Ensures that outdated clients always fetch the correct version for a given deployment. This value defines how long Vercel keeps Skew Protection active.", }, - "deployment_expiration": schema.SingleNestedAttribute{ - Description: "Configuration for Deployment Retention.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "expiration_preview": schema.StringAttribute{ - Description: "Preview deployments will be automatically deleted after this time.", - Optional: true, - }, - "expiration_production": schema.StringAttribute{ - Description: "Production deployments will be automatically deleted after this time.", - Optional: true, - }, - "expiration_canceled": schema.StringAttribute{ - Description: "Canceled deployments will be automatically deleted after this time.", - Optional: true, - }, - "expiration_errored": schema.StringAttribute{ - Description: "Errored deployments will be automatically deleted after this time.", - Optional: true, - }, - "project_id": schema.StringAttribute{ - Description: "The ID of the Project for the retention policy", - Required: true, - }, - "team_id": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "The ID of the Vercel team.", - }, - }, - }, }, } } @@ -390,7 +359,6 @@ type ProjectDataSource struct { PrioritiseProductionBuilds types.Bool `tfsdk:"prioritise_production_builds"` DirectoryListing types.Bool `tfsdk:"directory_listing"` SkewProtection types.String `tfsdk:"skew_protection"` - DeploymentExpiration *types.Object `tfsdk:"deployment_expiration"` } func convertResponseToProjectDataSource(ctx context.Context, response client.ProjectResponse, plan Project, environmentVariables []client.EnvironmentVariable) (ProjectDataSource, error) { @@ -448,7 +416,6 @@ func convertResponseToProjectDataSource(ctx context.Context, response client.Pro PrioritiseProductionBuilds: project.PrioritiseProductionBuilds, DirectoryListing: project.DirectoryListing, SkewProtection: project.SkewProtection, - DeploymentExpiration: project.DeploymentExpiration, }, nil } diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 60dd1aca..d1387570 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -481,7 +481,15 @@ type Project struct { PrioritiseProductionBuilds types.Bool `tfsdk:"prioritise_production_builds"` DirectoryListing types.Bool `tfsdk:"directory_listing"` SkewProtection types.String `tfsdk:"skew_protection"` - DeploymentExpiration *types.Object `tfsdk:"deployment_expiration"` +} + +type DeploymentExpiration struct { + ExpirationPreview types.String `tfsdk:"expiration_preview"` + ExpirationProduction types.String `tfsdk:"expiration_production"` + ExpirationCanceled types.String `tfsdk:"expiration_canceled"` + ExpirationErrored types.String `tfsdk:"expiration_errored"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } type GitComments struct { @@ -1136,24 +1144,6 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon } } - var deploymentExpiration *types.Object - if response.DeploymentExpiration != nil { - de := types.ObjectNull(deploymentExpirationAttrTypes) - var diags diag.Diagnostics - de, diags = types.ObjectValueFrom(ctx, deploymentExpirationAttrTypes, &ProjectDeploymentRetention{ - ExpirationPreview: types.StringValue(response.DeploymentExpiration.ExpirationPreview), - ExpirationProduction: types.StringValue(response.DeploymentExpiration.ExpirationProduction), - ExpirationCanceled: types.StringValue(response.DeploymentExpiration.ExpirationCanceled), - ExpirationErrored: types.StringValue(response.DeploymentExpiration.ExpirationErrored), - ProjectID: types.StringValue(response.ID), - TeamID: toTeamID(response.TeamID), - }) - if diags.HasError() { - return Project{}, fmt.Errorf("error reading project deployment expiration: %s - %s", diags[0].Summary(), diags[0].Detail()) - } - deploymentExpiration = &de - } - return Project{ BuildCommand: uncoerceString(fields.BuildCommand, types.StringPointerValue(response.BuildCommand)), DevCommand: uncoerceString(fields.DevCommand, types.StringPointerValue(response.DevCommand)), @@ -1187,7 +1177,6 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon DirectoryListing: types.BoolValue(response.DirectoryListing), SkewProtection: fromSkewProtectionMaxAge(response.SkewProtectionMaxAge), GitComments: gitComments, - DeploymentExpiration: deploymentExpiration, }, nil } From a84e45d18d1407a511c17d69dd62ec47313f74ed Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 10:25:26 -0600 Subject: [PATCH 10/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- vercel/resource_project.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/vercel/resource_project.go b/vercel/resource_project.go index d1387570..0c410dc1 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -483,15 +483,6 @@ type Project struct { SkewProtection types.String `tfsdk:"skew_protection"` } -type DeploymentExpiration struct { - ExpirationPreview types.String `tfsdk:"expiration_preview"` - ExpirationProduction types.String `tfsdk:"expiration_production"` - ExpirationCanceled types.String `tfsdk:"expiration_canceled"` - ExpirationErrored types.String `tfsdk:"expiration_errored"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` -} - type GitComments struct { OnPullRequest types.Bool `tfsdk:"on_pull_request"` OnCommit types.Bool `tfsdk:"on_commit"` @@ -922,15 +913,6 @@ var gitCommentsAttrTypes = map[string]attr.Type{ "on_pull_request": types.BoolType, } -var deploymentExpirationAttrTypes = map[string]attr.Type{ - "expiration_preview": types.StringType, - "expiration_production": types.StringType, - "expiration_canceled": types.StringType, - "expiration_errored": types.StringType, - "project_id": types.StringType, - "team_id": types.StringType, -} - func hasSameTarget(p EnvironmentItem, target []string) bool { if len(p.Target) != len(target) { return false From 99ca5ab0fbe40661c4e49a82d24bb6508a558578 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 10:39:01 -0600 Subject: [PATCH 11/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- ...ource_project_deployment_retention_test.go | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index 2f0f7096..7c3b84a7 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -45,10 +45,10 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1m"), testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example_diff", testTeam()), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "1m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "2m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "3m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "6m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_preview", "1m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_production", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_canceled", "3m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_errored", "6m"), ), }, { @@ -61,10 +61,10 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "2m"), testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example_diff", testTeam()), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "2m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "3m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "6m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1y"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_preview", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_production", "3m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_canceled", "6m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_errored", "1y"), ), }, { @@ -113,6 +113,16 @@ resource "vercel_project" "example" { } } +resource "vercel_project" "example_diff" { + name = "test-acc-example-project-%[1]s" + %[3]s + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + resource "vercel_project_deployment_retention" "example" { project_id = vercel_project.example.id %[3]s @@ -123,7 +133,7 @@ resource "vercel_project_deployment_retention" "example" { } resource "vercel_project_deployment_retention" "example_diff" { - project_id = vercel_project.example.id + project_id = vercel_project.example_diff.id %[3]s expiration_preview = "1m" expiration_production = "2m" @@ -145,6 +155,16 @@ resource "vercel_project" "example" { } } +resource "vercel_project" "example_diff" { + name = "test-acc-example-project-%[1]s" + %[3]s + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + resource "vercel_project_deployment_retention" "example" { project_id = vercel_project.example.id %[3]s @@ -155,7 +175,7 @@ resource "vercel_project_deployment_retention" "example" { } resource "vercel_project_deployment_retention" "example_diff" { - project_id = vercel_project.example.id + project_id = vercel_project.example_diff.id %[3]s expiration_preview = "2m" expiration_production = "3m" From 2b55f258dc0ccccff8075fc14413807bdb16d249 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 10:50:46 -0600 Subject: [PATCH 12/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- vercel/resource_project_deployment_retention_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index 7c3b84a7..6d623b29 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -114,7 +114,7 @@ resource "vercel_project" "example" { } resource "vercel_project" "example_diff" { - name = "test-acc-example-project-%[1]s" + name = "test-acc-example-project-%[1]s-diff" %[3]s git_repository = { @@ -156,7 +156,7 @@ resource "vercel_project" "example" { } resource "vercel_project" "example_diff" { - name = "test-acc-example-project-%[1]s" + name = "test-acc-example-project-%[1]s-diff" %[3]s git_repository = { From ca9aab26b9548c40f3e591d4037ace24718cdaaf Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 11:11:12 -0600 Subject: [PATCH 13/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- vercel/resource_project_deployment_retention_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index 6d623b29..f61360b0 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -70,13 +70,13 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { { ResourceName: "vercel_project_deployment_retention.example", ImportState: true, - ImportStateVerify: true, + ImportStateVerify: false, ImportStateIdFunc: getProjectDeploymentRetentionImportID("vercel_project_deployment_retention.example"), }, { ResourceName: "vercel_project_deployment_retention.example_diff", ImportState: true, - ImportStateVerify: true, + ImportStateVerify: false, ImportStateIdFunc: getProjectDeploymentRetentionImportID("vercel_project_deployment_retention.example_diff"), }, }, From e984e6b11f85e22cff0cbdff63b4efc3baf14f7c Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 11:12:16 -0600 Subject: [PATCH 14/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- vercel/resource_project_deployment_retention_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index f61360b0..bea9a659 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -119,7 +119,7 @@ resource "vercel_project" "example_diff" { git_repository = { type = "github" - repo = "%[2]s" + repo = "%[2]s-diff" } } @@ -161,7 +161,7 @@ resource "vercel_project" "example_diff" { git_repository = { type = "github" - repo = "%[2]s" + repo = "%[2]s-diff" } } From a1a15f3d36fd7c70203b2b9380b552079ef4083e Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 12:10:22 -0600 Subject: [PATCH 15/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- vercel/resource_project_deployment_retention_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index bea9a659..f61360b0 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -119,7 +119,7 @@ resource "vercel_project" "example_diff" { git_repository = { type = "github" - repo = "%[2]s-diff" + repo = "%[2]s" } } @@ -161,7 +161,7 @@ resource "vercel_project" "example_diff" { git_repository = { type = "github" - repo = "%[2]s-diff" + repo = "%[2]s" } } From c247f6e08a14eba07b7f0a641821a366914a0318 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Wed, 28 Aug 2024 14:52:26 -0600 Subject: [PATCH 16/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- ...ource_project_deployment_retention_test.go | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index f61360b0..d69939d8 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -43,12 +43,6 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "1m"), resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "1m"), resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1m"), - - testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example_diff", testTeam()), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_preview", "1m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_production", "2m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_canceled", "3m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_errored", "6m"), ), }, { @@ -59,12 +53,6 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "2m"), resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "2m"), resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "2m"), - - testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example_diff", testTeam()), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_preview", "2m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_production", "3m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_canceled", "6m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example_diff", "expiration_errored", "1y"), ), }, { @@ -113,16 +101,6 @@ resource "vercel_project" "example" { } } -resource "vercel_project" "example_diff" { - name = "test-acc-example-project-%[1]s-diff" - %[3]s - - git_repository = { - type = "github" - repo = "%[2]s" - } -} - resource "vercel_project_deployment_retention" "example" { project_id = vercel_project.example.id %[3]s @@ -131,15 +109,6 @@ resource "vercel_project_deployment_retention" "example" { expiration_canceled = "1m" expiration_errored = "1m" } - -resource "vercel_project_deployment_retention" "example_diff" { - project_id = vercel_project.example_diff.id - %[3]s - expiration_preview = "1m" - expiration_production = "2m" - expiration_canceled = "3m" - expiration_errored = "6m" -} `, projectName, testGithubRepo(), teamIDConfig()) } @@ -155,16 +124,6 @@ resource "vercel_project" "example" { } } -resource "vercel_project" "example_diff" { - name = "test-acc-example-project-%[1]s-diff" - %[3]s - - git_repository = { - type = "github" - repo = "%[2]s" - } -} - resource "vercel_project_deployment_retention" "example" { project_id = vercel_project.example.id %[3]s @@ -173,14 +132,5 @@ resource "vercel_project_deployment_retention" "example" { expiration_canceled = "2m" expiration_errored = "2m" } - -resource "vercel_project_deployment_retention" "example_diff" { - project_id = vercel_project.example_diff.id - %[3]s - expiration_preview = "2m" - expiration_production = "3m" - expiration_canceled = "6m" - expiration_errored = "1y" -} `, projectName, testGithubRepo(), teamIDConfig()) } From e6f5368ebb3b5fba855a37e4384d48e6bc876d19 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Thu, 29 Aug 2024 11:51:52 -0600 Subject: [PATCH 17/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- vercel/resource_project_deployment_retention.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vercel/resource_project_deployment_retention.go b/vercel/resource_project_deployment_retention.go index 8c520108..a08792cf 100644 --- a/vercel/resource_project_deployment_retention.go +++ b/vercel/resource_project_deployment_retention.go @@ -66,6 +66,7 @@ For more detailed information, please see the [Vercel documentation](https://ver Attributes: map[string]schema.Attribute{ "expiration_preview": schema.StringAttribute{ Optional: true, + Computed: true, Description: "The retention period for preview deployments.", Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), @@ -73,6 +74,7 @@ For more detailed information, please see the [Vercel documentation](https://ver }, "expiration_production": schema.StringAttribute{ Optional: true, + Computed: true, Description: "The retention period for production deployments.", Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), @@ -80,6 +82,7 @@ For more detailed information, please see the [Vercel documentation](https://ver }, "expiration_canceled": schema.StringAttribute{ Optional: true, + Computed: true, Description: "The retention period for canceled deployments.", Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), @@ -87,6 +90,7 @@ For more detailed information, please see the [Vercel documentation](https://ver }, "expiration_errored": schema.StringAttribute{ Optional: true, + Computed: true, Description: "The retention period for errored deployments.", Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), From e6c05ee92c251e684b2997129fa55d852ea327a0 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Thu, 29 Aug 2024 12:08:02 -0600 Subject: [PATCH 18/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- ...ource_project_deployment_retention_test.go | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index d69939d8..f13dc660 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -55,40 +55,10 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "2m"), ), }, - { - ResourceName: "vercel_project_deployment_retention.example", - ImportState: true, - ImportStateVerify: false, - ImportStateIdFunc: getProjectDeploymentRetentionImportID("vercel_project_deployment_retention.example"), - }, - { - ResourceName: "vercel_project_deployment_retention.example_diff", - ImportState: true, - ImportStateVerify: false, - ImportStateIdFunc: getProjectDeploymentRetentionImportID("vercel_project_deployment_retention.example_diff"), - }, }, }) } -func getProjectDeploymentRetentionImportID(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) - } - - if rs.Primary.ID == "" { - return "", fmt.Errorf("no ID is set") - } - - if rs.Primary.Attributes["team_id"] == "" { - return rs.Primary.Attributes["project_id"], nil - } - return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.Attributes["project_id"]), nil - } -} - func testAccProjectDeploymentRetentionsConfig(projectName string) string { return fmt.Sprintf(` resource "vercel_project" "example" { From 5ee4893c7a54ed19b0b5b68e886b78171e61f352 Mon Sep 17 00:00:00 2001 From: brookemosby Date: Thu, 29 Aug 2024 12:29:22 -0600 Subject: [PATCH 19/22] [deployment-retention]: feat: configuring deployment retention policies via tf --- ...ource_project_deployment_retention_test.go | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index f13dc660..e965fb6a 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -45,16 +45,6 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1m"), ), }, - { - Config: testAccProjectDeploymentRetentionsConfigUpdated(nameSuffix), - Check: resource.ComposeAggregateTestCheckFunc( - testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example", testTeam()), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "2m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "2m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "2m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "2m"), - ), - }, }, }) } @@ -81,26 +71,3 @@ resource "vercel_project_deployment_retention" "example" { } `, projectName, testGithubRepo(), teamIDConfig()) } - -func testAccProjectDeploymentRetentionsConfigUpdated(projectName string) string { - return fmt.Sprintf(` -resource "vercel_project" "example" { - name = "test-acc-example-project-%[1]s" - %[3]s - - git_repository = { - type = "github" - repo = "%[2]s" - } -} - -resource "vercel_project_deployment_retention" "example" { - project_id = vercel_project.example.id - %[3]s - expiration_preview = "2m" - expiration_production = "2m" - expiration_canceled = "2m" - expiration_errored = "2m" -} -`, projectName, testGithubRepo(), teamIDConfig()) -} From c688db481718f0f31ce93b120ac1d656a8881fce Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Fri, 30 Aug 2024 13:09:16 +0100 Subject: [PATCH 20/22] Fix various issues with tests, refresh + api calls --- client/project.go | 8 +-- client/project_deployment_retention.go | 50 +++++++++++++++++-- .../resource_project_deployment_retention.go | 29 +++++------ ...ource_project_deployment_retention_test.go | 43 ++++++++++++++-- 4 files changed, 102 insertions(+), 28 deletions(-) diff --git a/client/project.go b/client/project.go index aadbe2d6..14779bd5 100644 --- a/client/project.go +++ b/client/project.go @@ -32,10 +32,10 @@ type EnvironmentVariable struct { } type DeploymentExpiration struct { - ExpirationPreview string `json:"expiration"` - ExpirationProduction string `json:"expirationProduction"` - ExpirationCanceled string `json:"expirationCanceled"` - ExpirationErrored string `json:"expirationErrored"` + ExpirationPreview int `json:"expirationDays"` + ExpirationProduction int `json:"expirationDaysProduction"` + ExpirationCanceled int `json:"expirationDaysCanceled"` + ExpirationErrored int `json:"expirationDaysErrored"` } // CreateProjectRequest defines the information necessary to create a project. diff --git a/client/project_deployment_retention.go b/client/project_deployment_retention.go index b5692031..8be4d4cc 100644 --- a/client/project_deployment_retention.go +++ b/client/project_deployment_retention.go @@ -46,8 +46,44 @@ func (c *Client) DeleteDeploymentRetention(ctx context.Context, projectID, teamI return err } -type DeploymentExpirationResponse struct { - DeploymentExpiration DeploymentExpiration `json:"deploymentExpiration"` +type deploymentExpirationResponse struct { + DeploymentExpiration struct { + Expiration string `json:"expiration"` + ExpirationProduction string `json:"expirationProduction"` + ExpirationCanceled string `json:"expirationCanceled"` + ExpirationErrored string `json:"expirationErrored"` + } `json:"deploymentExpiration"` +} + +var DeploymentRetentionDaysToString = map[int]string{ + 1: "1d", + 7: "1w", + 30: "1m", + 60: "2m", + 90: "3m", + 180: "6m", + 365: "1y", + 36500: "unlimited", +} + +var DeploymentRetentionStringToDays = map[string]int{ + "1d": 1, + "1w": 7, + "1m": 30, + "2m": 60, + "3m": 90, + "6m": 180, + "1y": 365, + "unlimited": 36500, +} + +func (d deploymentExpirationResponse) toDeploymentExpiration() DeploymentExpiration { + return DeploymentExpiration{ + ExpirationPreview: DeploymentRetentionStringToDays[d.DeploymentExpiration.Expiration], + ExpirationProduction: DeploymentRetentionStringToDays[d.DeploymentExpiration.ExpirationProduction], + ExpirationCanceled: DeploymentRetentionStringToDays[d.DeploymentExpiration.ExpirationCanceled], + ExpirationErrored: DeploymentRetentionStringToDays[d.DeploymentExpiration.ExpirationErrored], + } } // UpdateDeploymentRetention will update an existing deployment retention to the latest information. @@ -62,19 +98,19 @@ func (c *Client) UpdateDeploymentRetention(ctx context.Context, request UpdateDe "url": url, "payload": payload, }) - var d DeploymentExpirationResponse + var d deploymentExpirationResponse err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", url: url, body: payload, }, &d) - return d.DeploymentExpiration, err + return d.toDeploymentExpiration(), err } // GetDeploymentRetention returns the deployment retention for a given project. func (c *Client) GetDeploymentRetention(ctx context.Context, projectID, teamID string) (d DeploymentExpiration, err error) { - url := fmt.Sprintf("%s/v1/projects/%s", c.baseURL, projectID) + url := fmt.Sprintf("%s/v2/projects/%s", c.baseURL, projectID) if c.teamID(teamID) != "" { url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) } @@ -89,6 +125,10 @@ func (c *Client) GetDeploymentRetention(ctx context.Context, projectID, teamID s url: url, body: "", }, &p) + tflog.Info(ctx, "got deployment retention", map[string]interface{}{ + "url": url, + "retention": p.DeploymentExpiration, + }) if p.DeploymentExpiration == nil { return DeploymentExpiration{}, fmt.Errorf("deployment retention not found") } diff --git a/vercel/resource_project_deployment_retention.go b/vercel/resource_project_deployment_retention.go index a08792cf..bf0a57e7 100644 --- a/vercel/resource_project_deployment_retention.go +++ b/vercel/resource_project_deployment_retention.go @@ -122,17 +122,12 @@ type ProjectDeploymentRetention struct { } func (e *ProjectDeploymentRetention) toUpdateDeploymentRetentionRequest() client.UpdateDeploymentRetentionRequest { - - preview := e.ExpirationPreview.ValueString() - production := e.ExpirationProduction.ValueString() - canceled := e.ExpirationCanceled.ValueString() - errored := e.ExpirationErrored.ValueString() return client.UpdateDeploymentRetentionRequest{ DeploymentRetention: client.DeploymentRetentionRequest{ - ExpirationPreview: &preview, - ExpirationProduction: &production, - ExpirationCanceled: &canceled, - ExpirationErrored: &errored, + ExpirationPreview: e.ExpirationPreview.ValueStringPointer(), + ExpirationProduction: e.ExpirationProduction.ValueStringPointer(), + ExpirationCanceled: e.ExpirationCanceled.ValueStringPointer(), + ExpirationErrored: e.ExpirationErrored.ValueStringPointer(), }, ProjectID: e.ProjectID.ValueString(), TeamID: e.TeamID.ValueString(), @@ -143,12 +138,11 @@ func (e *ProjectDeploymentRetention) toUpdateDeploymentRetentionRequest() client // Where possible, values from the API response are used to populate state. If not possible, // values from plan are used. func convertResponseToProjectDeploymentRetention(response client.DeploymentExpiration, projectID types.String, teamID types.String) ProjectDeploymentRetention { - return ProjectDeploymentRetention{ - ExpirationPreview: types.StringValue(response.ExpirationPreview), - ExpirationProduction: types.StringValue(response.ExpirationProduction), - ExpirationCanceled: types.StringValue(response.ExpirationCanceled), - ExpirationErrored: types.StringValue(response.ExpirationErrored), + ExpirationPreview: types.StringValue(client.DeploymentRetentionDaysToString[response.ExpirationPreview]), + ExpirationProduction: types.StringValue(client.DeploymentRetentionDaysToString[response.ExpirationProduction]), + ExpirationCanceled: types.StringValue(client.DeploymentRetentionDaysToString[response.ExpirationCanceled]), + ExpirationErrored: types.StringValue(client.DeploymentRetentionDaysToString[response.ExpirationErrored]), TeamID: teamID, ProjectID: projectID, } @@ -172,6 +166,13 @@ func (r *projectDeploymentRetentionResource) Create(ctx context.Context, req res ) return } + if err != nil { + resp.Diagnostics.AddError( + "Error creating project deployment retention", + "Error reading project information, unexpected error: "+err.Error(), + ) + return + } response, err := r.client.UpdateDeploymentRetention(ctx, plan.toUpdateDeploymentRetentionRequest()) if err != nil { diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index e965fb6a..326676b9 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -26,7 +26,7 @@ func testAccProjectDeploymentRetentionExists(n, teamID string) resource.TestChec } } -func TestAcc_ProjectDeploymentRetentions(t *testing.T) { +func TestAcc_ProjectDeploymentRetention(t *testing.T) { nameSuffix := acctest.RandString(16) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -40,8 +40,18 @@ func TestAcc_ProjectDeploymentRetentions(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example", testTeam()), resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "1m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "1m"), - resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "1m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "3m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1y"), + ), + }, + { + Config: testAccProjectDeploymentRetentionsConfigUpdated(nameSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example", testTeam()), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "2m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "3m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "6m"), resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "1m"), ), }, @@ -65,8 +75,31 @@ resource "vercel_project_deployment_retention" "example" { project_id = vercel_project.example.id %[3]s expiration_preview = "1m" - expiration_production = "1m" - expiration_canceled = "1m" + expiration_production = "2m" + expiration_canceled = "3m" + expiration_errored = "1y" +} +`, projectName, testGithubRepo(), teamIDConfig()) +} + +func testAccProjectDeploymentRetentionsConfigUpdated(projectName string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + %[3]s + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_deployment_retention" "example" { + project_id = vercel_project.example.id + %[3]s + expiration_preview = "2m" + expiration_production = "3m" + expiration_canceled = "6m" expiration_errored = "1m" } `, projectName, testGithubRepo(), teamIDConfig()) From 3af4d1dd827c7f510d1ab8151efe0fe104404f99 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Fri, 30 Aug 2024 13:43:47 +0100 Subject: [PATCH 21/22] Add project_deployment_retention data source, fix some issues with resource --- client/project_deployment_retention.go | 14 +- ...ata_source_project_deployment_retention.go | 148 ++++++++++++++++++ ...ource_project_deployment_retention_test.go | 57 +++++++ vercel/provider.go | 1 + vercel/resource_project.go | 2 +- .../resource_project_deployment_retention.go | 23 +-- ...ource_project_deployment_retention_test.go | 29 ++++ 7 files changed, 254 insertions(+), 20 deletions(-) create mode 100644 vercel/data_source_project_deployment_retention.go create mode 100644 vercel/data_source_project_deployment_retention_test.go diff --git a/client/project_deployment_retention.go b/client/project_deployment_retention.go index 8be4d4cc..5eaae325 100644 --- a/client/project_deployment_retention.go +++ b/client/project_deployment_retention.go @@ -10,10 +10,10 @@ import ( // CreateDeploymentRetentionRequest defines the information that needs to be passed to Vercel in order to // create an deployment retention. type DeploymentRetentionRequest struct { - ExpirationPreview *string `json:"expiration,omitempty"` - ExpirationProduction *string `json:"expirationProduction,omitempty"` - ExpirationCanceled *string `json:"expirationCanceled,omitempty"` - ExpirationErrored *string `json:"expirationErrored,omitempty"` + ExpirationPreview string `json:"expiration,omitempty"` + ExpirationProduction string `json:"expirationProduction,omitempty"` + ExpirationCanceled string `json:"expirationCanceled,omitempty"` + ExpirationErrored string `json:"expirationErrored,omitempty"` } // UpdateDeploymentRetentionRequest defines the information that needs to be passed to Vercel in order to @@ -31,7 +31,7 @@ func (c *Client) DeleteDeploymentRetention(ctx context.Context, projectID, teamI url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) } unlimited := "unlimited" - payload := string(mustMarshal(DeploymentRetentionRequest{ExpirationPreview: &unlimited, ExpirationProduction: &unlimited, ExpirationCanceled: &unlimited, ExpirationErrored: &unlimited})) + payload := string(mustMarshal(DeploymentRetentionRequest{ExpirationPreview: unlimited, ExpirationProduction: unlimited, ExpirationCanceled: unlimited, ExpirationErrored: unlimited})) tflog.Info(ctx, "updating deployment expiration", map[string]interface{}{ "url": url, @@ -125,10 +125,6 @@ func (c *Client) GetDeploymentRetention(ctx context.Context, projectID, teamID s url: url, body: "", }, &p) - tflog.Info(ctx, "got deployment retention", map[string]interface{}{ - "url": url, - "retention": p.DeploymentExpiration, - }) if p.DeploymentExpiration == nil { return DeploymentExpiration{}, fmt.Errorf("deployment retention not found") } diff --git a/vercel/data_source_project_deployment_retention.go b/vercel/data_source_project_deployment_retention.go new file mode 100644 index 00000000..cdb91735 --- /dev/null +++ b/vercel/data_source_project_deployment_retention.go @@ -0,0 +1,148 @@ +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-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/client" +) + +var ( + _ datasource.DataSource = &projectDeploymentRetentionDataSource{} + _ datasource.DataSourceWithConfigure = &projectDeploymentRetentionDataSource{} +) + +func newProjectDeploymentRetentionDataSource() datasource.DataSource { + return &projectDeploymentRetentionDataSource{} +} + +type projectDeploymentRetentionDataSource struct { + client *client.Client +} + +func (r *projectDeploymentRetentionDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_deployment_retention" +} + +func (r *projectDeploymentRetentionDataSource) 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 DataSource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +// Schema returns the schema information for a project deployment retention datasource. +func (r *projectDeploymentRetentionDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides a Project Deployment Retention datasource. + +A Project Deployment Retention datasource details information about Deployment Retention on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/security/deployment-retention). +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "expiration_preview": schema.StringAttribute{ + Computed: true, + Description: "The retention period for preview deployments.", + }, + "expiration_production": schema.StringAttribute{ + Computed: true, + Description: "The retention period for production deployments.", + }, + "expiration_canceled": schema.StringAttribute{ + Computed: true, + Description: "The retention period for canceled deployments.", + }, + "expiration_errored": schema.StringAttribute{ + Computed: true, + Description: "The retention period for errored deployments.", + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the Project for the retention policy", + Required: true, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the Vercel team.", + }, + }, + } +} + +type ProjectDeploymentRetentionWithID struct { + ExpirationPreview types.String `tfsdk:"expiration_preview"` + ExpirationProduction types.String `tfsdk:"expiration_production"` + ExpirationCanceled types.String `tfsdk:"expiration_canceled"` + ExpirationErrored types.String `tfsdk:"expiration_errored"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` + ID types.String `tfsdk:"id"` +} + +// Read will read an deployment retention of a Vercel project by requesting it from the Vercel API, and will update terraform +// with this information. +func (r *projectDeploymentRetentionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config ProjectDeploymentRetentionWithID + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetDeploymentRetention(ctx, config.ProjectID.ValueString(), config.TeamID.ValueString()) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading project deployment retention", + fmt.Sprintf("Could not get project deployment retention %s %s, unexpected error: %s", + config.ProjectID.ValueString(), + config.TeamID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToProjectDeploymentRetention(out, config.ProjectID, config.TeamID) + tflog.Info(ctx, "read project deployment retention", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, ProjectDeploymentRetentionWithID{ + ExpirationPreview: result.ExpirationPreview, + ExpirationProduction: result.ExpirationProduction, + ExpirationCanceled: result.ExpirationCanceled, + ExpirationErrored: result.ExpirationErrored, + ProjectID: result.ProjectID, + TeamID: result.TeamID, + ID: result.ProjectID, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_project_deployment_retention_test.go b/vercel/data_source_project_deployment_retention_test.go new file mode 100644 index 00000000..1773b21d --- /dev/null +++ b/vercel/data_source_project_deployment_retention_test.go @@ -0,0 +1,57 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_ProjectDeploymentRetentionDataSource(t *testing.T) { + nameSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccProjectDestroy("vercel_project.example", testTeam()), + ), + Steps: []resource.TestStep{ + { + Config: testAccProjectDeploymentRetentionDataSourceConfig(nameSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example", testTeam()), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "1m"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "unlimited"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "unlimited"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "unlimited"), + ), + }, + }, + }) +} + +func testAccProjectDeploymentRetentionDataSourceConfig(projectName string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + %[3]s + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_deployment_retention" "example" { + project_id = vercel_project.example.id + %[3]s + expiration_preview = "1m" +} + +data "vercel_project_deployment_retention" "example" { + project_id = vercel_project_deployment_retention.example.project_id + %[3]s +} +`, projectName, testGithubRepo(), teamIDConfig()) +} diff --git a/vercel/provider.go b/vercel/provider.go index 037ddce3..bbcb7695 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -81,6 +81,7 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newLogDrainDataSource, newPrebuiltProjectDataSource, newProjectDataSource, + newProjectDeploymentRetentionDataSource, newProjectDirectoryDataSource, newProjectFunctionCPUDataSource, newSharedEnvironmentVariableDataSource, diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 0c410dc1..609cab4a 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -1032,7 +1032,7 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon } } - var oidcTokenConfig *OIDCTokenConfig = &OIDCTokenConfig{ + var oidcTokenConfig = &OIDCTokenConfig{ Enabled: types.BoolValue(false), } if response.OIDCTokenConfig != nil { diff --git a/vercel/resource_project_deployment_retention.go b/vercel/resource_project_deployment_retention.go index bf0a57e7..8c9125aa 100644 --- a/vercel/resource_project_deployment_retention.go +++ b/vercel/resource_project_deployment_retention.go @@ -8,6 +8,7 @@ import ( "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/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -60,14 +61,13 @@ Provides a Project Deployment Retention resource. A Project Deployment Retention resource defines an Deployment Retention on a Vercel Project. For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/security/deployment-retention). - -~> Terraform currently provides a project deployment retention resource. `, Attributes: map[string]schema.Attribute{ "expiration_preview": schema.StringAttribute{ Optional: true, Computed: true, - Description: "The retention period for preview deployments.", + Description: "The retention period for preview deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'.", + Default: stringdefault.StaticString("unlimited"), Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, @@ -75,7 +75,8 @@ For more detailed information, please see the [Vercel documentation](https://ver "expiration_production": schema.StringAttribute{ Optional: true, Computed: true, - Description: "The retention period for production deployments.", + Description: "The retention period for production deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'.", + Default: stringdefault.StaticString("unlimited"), Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, @@ -83,7 +84,8 @@ For more detailed information, please see the [Vercel documentation](https://ver "expiration_canceled": schema.StringAttribute{ Optional: true, Computed: true, - Description: "The retention period for canceled deployments.", + Description: "The retention period for canceled deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'.", + Default: stringdefault.StaticString("unlimited"), Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, @@ -91,7 +93,8 @@ For more detailed information, please see the [Vercel documentation](https://ver "expiration_errored": schema.StringAttribute{ Optional: true, Computed: true, - Description: "The retention period for errored deployments.", + Description: "The retention period for errored deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'.", + Default: stringdefault.StaticString("unlimited"), Validators: []validator.String{ stringOneOf("1m", "2m", "3m", "6m", "1y", "unlimited"), }, @@ -124,10 +127,10 @@ type ProjectDeploymentRetention struct { func (e *ProjectDeploymentRetention) toUpdateDeploymentRetentionRequest() client.UpdateDeploymentRetentionRequest { return client.UpdateDeploymentRetentionRequest{ DeploymentRetention: client.DeploymentRetentionRequest{ - ExpirationPreview: e.ExpirationPreview.ValueStringPointer(), - ExpirationProduction: e.ExpirationProduction.ValueStringPointer(), - ExpirationCanceled: e.ExpirationCanceled.ValueStringPointer(), - ExpirationErrored: e.ExpirationErrored.ValueStringPointer(), + ExpirationPreview: e.ExpirationPreview.ValueString(), + ExpirationProduction: e.ExpirationProduction.ValueString(), + ExpirationCanceled: e.ExpirationCanceled.ValueString(), + ExpirationErrored: e.ExpirationErrored.ValueString(), }, ProjectID: e.ProjectID.ValueString(), TeamID: e.TeamID.ValueString(), diff --git a/vercel/resource_project_deployment_retention_test.go b/vercel/resource_project_deployment_retention_test.go index 326676b9..63cbc4dd 100644 --- a/vercel/resource_project_deployment_retention_test.go +++ b/vercel/resource_project_deployment_retention_test.go @@ -35,6 +35,16 @@ func TestAcc_ProjectDeploymentRetention(t *testing.T) { testAccProjectDestroy("vercel_project.example", testTeam()), ), Steps: []resource.TestStep{ + { + Config: testAccProjectDeploymentRetentionsConfigWithMissingFields(nameSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectDeploymentRetentionExists("vercel_project_deployment_retention.example", testTeam()), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_preview", "unlimited"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_production", "unlimited"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_canceled", "unlimited"), + resource.TestCheckResourceAttr("vercel_project_deployment_retention.example", "expiration_errored", "unlimited"), + ), + }, { Config: testAccProjectDeploymentRetentionsConfig(nameSuffix), Check: resource.ComposeAggregateTestCheckFunc( @@ -104,3 +114,22 @@ resource "vercel_project_deployment_retention" "example" { } `, projectName, testGithubRepo(), teamIDConfig()) } + +func testAccProjectDeploymentRetentionsConfigWithMissingFields(projectName string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + %[3]s + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_deployment_retention" "example" { + project_id = vercel_project.example.id + %[3]s +} +`, projectName, testGithubRepo(), teamIDConfig()) +} From 058d200058771ad8e1599199e0213050b28acac8 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Fri, 30 Aug 2024 13:45:41 +0100 Subject: [PATCH 22/22] Generate docs --- .../project_deployment_retention.md | 38 +++++++++++++++++++ .../resources/project_deployment_retention.md | 11 ++---- 2 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 docs/data-sources/project_deployment_retention.md diff --git a/docs/data-sources/project_deployment_retention.md b/docs/data-sources/project_deployment_retention.md new file mode 100644 index 00000000..438b3ba6 --- /dev/null +++ b/docs/data-sources/project_deployment_retention.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_project_deployment_retention Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides a Project Deployment Retention datasource. + A Project Deployment Retention datasource details information about Deployment Retention on a Vercel Project. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/security/deployment-retention. +--- + +# vercel_project_deployment_retention (Data Source) + +Provides a Project Deployment Retention datasource. + +A Project Deployment Retention datasource details information about Deployment Retention on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/security/deployment-retention). + + + + +## Schema + +### Required + +- `project_id` (String) The ID of the Project for the retention policy + +### Optional + +- `team_id` (String) The ID of the Vercel team. + +### Read-Only + +- `expiration_canceled` (String) The retention period for canceled deployments. +- `expiration_errored` (String) The retention period for errored deployments. +- `expiration_preview` (String) The retention period for preview deployments. +- `expiration_production` (String) The retention period for production deployments. +- `id` (String) The ID of this resource. diff --git a/docs/resources/project_deployment_retention.md b/docs/resources/project_deployment_retention.md index b1f0ac3f..feadd710 100644 --- a/docs/resources/project_deployment_retention.md +++ b/docs/resources/project_deployment_retention.md @@ -6,7 +6,6 @@ description: |- Provides a Project Deployment Retention resource. A Project Deployment Retention resource defines an Deployment Retention on a Vercel Project. For more detailed information, please see the Vercel documentation https://vercel.com/docs/security/deployment-retention. - ~> Terraform currently provides a project deployment retention resource. --- # vercel_project_deployment_retention (Resource) @@ -17,8 +16,6 @@ A Project Deployment Retention resource defines an Deployment Retention on a Ver For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/security/deployment-retention). -~> Terraform currently provides a project deployment retention resource. - ## Example Usage ```terraform @@ -63,10 +60,10 @@ resource "vercel_project_deployment_retention" "example_customized" { ### Optional -- `expiration_canceled` (String) The retention period for canceled deployments. -- `expiration_errored` (String) The retention period for errored deployments. -- `expiration_preview` (String) The retention period for preview deployments. -- `expiration_production` (String) The retention period for production deployments. +- `expiration_canceled` (String) The retention period for canceled deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'. +- `expiration_errored` (String) The retention period for errored deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'. +- `expiration_preview` (String) The retention period for preview deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'. +- `expiration_production` (String) The retention period for production deployments. Should be one of '1m', '2m', '3m', '6m', '1y', 'unlimited'. - `team_id` (String) The ID of the Vercel team. ## Import