From 797b9cda8673e08e9033f017706c42430eb68dd9 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Tue, 3 Jun 2025 07:09:02 -0400 Subject: [PATCH 001/133] start terraform provider --- client/project.go | 1 + client/project_rolling_release.go | 87 +++++ vercel/data_source_project_rolling_release.go | 135 +++++++ vercel/provider.go | 2 + vercel/resource_project_rolling_release.go | 349 ++++++++++++++++++ 5 files changed, 574 insertions(+) create mode 100644 client/project_rolling_release.go create mode 100644 vercel/data_source_project_rolling_release.go create mode 100644 vercel/resource_project_rolling_release.go diff --git a/client/project.go b/client/project.go index 9c84a9be..ffc68ff8 100644 --- a/client/project.go +++ b/client/project.go @@ -210,6 +210,7 @@ type ProjectResponse struct { ResourceConfig *ResourceConfigResponse `json:"resourceConfig"` NodeVersion string `json:"nodeVersion"` Crons *ProjectCronsResponse `json:"crons"` + RollingRelease *ProjectRollingRelease `json:"rollingRelease,omitempty"` } type ProjectCronsResponse struct { diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go new file mode 100644 index 00000000..5c9a4ae9 --- /dev/null +++ b/client/project_rolling_release.go @@ -0,0 +1,87 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type Stage struct { + TargetPercentage float64 `json:"targetPercentage,omitempty"` + Duration float64 `json:"duration,omitempty"` + RequireApproval bool `json:"requireApproval,omitempty"` +} + +// CreateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to +// create a rolling release. +type RollingReleaseRequest struct { + Enabled bool `json:"enabled,omitempty"` + AdvancementType string `json:"advancementType,omitempty"` + CanaryResponseHeader string `json:"canaryResponseHeader,omitempty"` + Stages []Stage `json:"stages,omitempty"` +} + +// UpdateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to +// update a rolling release. +type UpdateRollingReleaseRequest struct { + RollingRelease RollingReleaseRequest + ProjectID string + TeamID string +} + +// ProjectRollingRelease defines the rolling release configuration on the Project document. +type ProjectRollingRelease struct { + Target string `json:"target"` + Stages []Stage `json:"stages,omitempty"` + CanaryResponseHeader bool `json:"canaryResponseHeader,omitempty"` +} + +type RollingReleaseResponse struct { + TeamID string `json:"-"` +} + +func (d RollingReleaseResponse) toRollingReleaseResponse(teamID string) RollingReleaseResponse { + return RollingReleaseResponse{ + TeamID: teamID, + } +} + +// UpdateRollingRelease will update an existing rolling release to the latest information. +func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseResponse, error) { + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config", c.baseURL, request.ProjectID) + + payload := string(mustMarshal(request.RollingRelease)) + + tflog.Info(ctx, "updating rolling-release", map[string]any{ + "url": url, + "payload": payload, + }) + var d RollingReleaseResponse + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &d) + return d.toRollingReleaseResponse(c.TeamID(request.TeamID)), err +} + +// GetRollingRelease returns the rolling release for a given project. +func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (d RollingReleaseResponse, err error) { + project, err := c.GetProject(ctx, projectID, teamID) + if err != nil { + return RollingReleaseResponse{}, fmt.Errorf("error getting project %s: %w", projectID, err) + } + + rollingRelease := project.RollingRelease + if rollingRelease == nil { + return RollingReleaseResponse{ + TeamID: c.TeamID(teamID), + }, nil + } + + return RollingReleaseResponse{ + TeamID: c.TeamID(teamID), + }, err +} diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go new file mode 100644 index 00000000..9ab9e0de --- /dev/null +++ b/vercel/data_source_project_rolling_release.go @@ -0,0 +1,135 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/v3/client" +) + +var ( + _ datasource.DataSource = &projectRollingReleaseDataSource{} + _ datasource.DataSourceWithConfigure = &projectRollingReleaseDataSource{} +) + +func newProjectRollingReleaseDataSource() datasource.DataSource { + return &projectRollingReleaseDataSource{} +} + +type projectRollingReleaseDataSource struct { + client *client.Client +} + +func (r *projectRollingReleaseDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_rolling_release" +} + +func (r *projectRollingReleaseDataSource) 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 Rolling Release datasource. +func (r *projectRollingReleaseDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides a Project Rolling Release datasource. + +A Project Rolling Release datasource details information about a Rolling Release on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the Project for the rolling release", + Required: true, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the Vercel team.", + }, + }, + } +} + +type ProjectRollingReleaseWithID struct { + Enabled types.Bool `tfsdk:"enabled"` + AdvancementType types.String `tfsdk:"advancement_type"` + CanaryResponseHeader types.Bool `tfsdk:"canary_response_header"` + Stages types.List `tfsdk:"stages"` + + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` + ID types.String `tfsdk:"id"` +} + +// Read will read the rolling release configuration of a Vercel project by requesting it from the Vercel API, and will update terraform +// with this information. +func (r *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config ProjectRollingReleaseWithID + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetRollingRelease(ctx, config.ProjectID.ValueString(), config.TeamID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error reading project rolling release", + fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", + config.ProjectID.ValueString(), + config.TeamID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToProjectRollingRelease(out, config.ProjectID) + tflog.Info(ctx, "read project rolling release", map[string]any{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + var stages []attr.Value + for _, s := range result.Stages { + stages = append(stages, types.Float64Value(s.TargetPercentage)) + } + + diags = resp.State.Set(ctx, ProjectRollingReleaseWithID{ + Enabled: types.BoolValue(result.Enabled), + AdvancementType: types.StringValue(result.AdvancementType), + CanaryResponseHeader: types.BoolValue(result.CanaryResponseHeader), + Stages: types.ListValueMust(types.StringType, stages), + ProjectID: result.ProjectID, + TeamID: result.TeamID, + ID: result.ProjectID, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/provider.go b/vercel/provider.go index a7cc8637..7893eeab 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -81,6 +81,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newTeamConfigResource, newTeamMemberResource, newWebhookResource, + newProjectRollingReleaseResource, } } @@ -109,6 +110,7 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newTeamMemberDataSource, newMicrofrontendGroupDataSource, newMicrofrontendGroupMembershipDataSource, + newProjectRollingReleaseDataSource, } } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go new file mode 100644 index 00000000..f793b201 --- /dev/null +++ b/vercel/resource_project_rolling_release.go @@ -0,0 +1,349 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +var ( + _ resource.Resource = &projectRollingReleaseResource{} + _ resource.ResourceWithConfigure = &projectRollingReleaseResource{} + _ resource.ResourceWithImportState = &projectRollingReleaseResource{} +) + +func newProjectRollingReleaseResource() resource.Resource { + return &projectRollingReleaseResource{} +} + +type projectRollingReleaseResource struct { + client *client.Client +} + +func (r *projectRollingReleaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_rolling_release" +} + +func (r *projectRollingReleaseResource) 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 +} + +// Schema returns the schema information for a project rolling release resource. +func (r *projectRollingReleaseResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides a Project Rolling release resource. + +A Project Rolling release resource defines an Rolling release on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). +`, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Description: "Whether the rolling release is enabled.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. + boolplanmodifier.RequiresReplace(), + }, + }, + "advancement_type": schema.StringAttribute{ + Description: "The advancement type of the rolling release. Can be 'automatic' or 'manual-approve'.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("manual-approve"), + + PlanModifiers: []planmodifier.String{ + // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. + stringplanmodifier.RequiresReplace(), + }, + }, + "canary_response_header": schema.BoolAttribute{ + Description: "Whether the canary response header is enabled. This header is used to identify canary deployments.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. + boolplanmodifier.RequiresReplace(), + }, + }, + "stages": schema.ListAttribute{ + Description: "A list of stages for the rolling release. Each stage has a target percentage and duration.", + Optional: true, + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "targetPercentage": types.Float64Type, + "duration": types.Float64Type, + }, + }, + PlanModifiers: []planmodifier.List{ + // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. + listplanmodifier.RequiresReplace(), + }, + }, + + "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()}, + }, + }, + } +} + +type Stage struct { + TargetPercentage float64 `tfsdk:"targetPercentage,omitempty"` + Duration float64 `tfsdk:"duration,omitempty"` +} + +// ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. +type ProjectRollingRelease struct { + Enabled bool `tfsdk:"enabled,omitempty"` + AdvancementType string `tfsdk:"advancementType,omitempty"` + CanaryResponseHeader bool `tfsdk:"canaryResponseHeader,omitempty"` + Stages []Stage `tfsdk:"stages,omitempty"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` +} + +func (e *ProjectRollingRelease) toUpdateRollingReleaseRequest() client.UpdateRollingReleaseRequest { + return client.UpdateRollingReleaseRequest{ + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + } +} + +// convertResponseToProjectRollingRelease 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 convertResponseToProjectRollingRelease(response client.RollingReleaseResponse, projectID types.String) ProjectRollingRelease { + return ProjectRollingRelease{ + TeamID: types.StringValue(response.TeamID), + ProjectID: projectID, + } +} + +// Create will create a new rolling release config on a Vercel project. +// This is called automatically by the provider when a new resource should be created. +func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ProjectRollingRelease + 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 rolling release", + "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 + } + if err != nil { + resp.Diagnostics.AddError( + "Error creating project rolling release", + "Error reading project information, unexpected error: "+err.Error(), + ) + return + } + + response, err := r.client.UpdateRollingRelease(ctx, plan.toUpdateRollingReleaseRequest()) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project rolling release", + "Could not create project rolling release, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToProjectRollingRelease(response, plan.ProjectID) + + tflog.Info(ctx, "created project rolling release", map[string]any{ + "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 rolling release of a Vercel project by requesting it from the Vercel API, and will update terraform +// with this information. +func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ProjectRollingRelease + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetRollingRelease(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 rolling release", + fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", + state.ProjectID.ValueString(), + state.TeamID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToProjectRollingRelease(out, state.ProjectID) + tflog.Info(ctx, "read project rolling release", map[string]any{ + "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 rolling release. +func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // var state ProjectRollingRelease + // diags := req.State.Get(ctx, &state) + // resp.Diagnostics.Append(diags...) + // if resp.Diagnostics.HasError() { + // return + // } + + // err := r.client.DeleteRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + // if client.NotFound(err) { + // return + // } + // if err != nil { + // resp.Diagnostics.AddError( + // "Error deleting project rolling release", + // fmt.Sprintf( + // "Could not delete project rolling release %s, unexpected error: %s", + // state.ProjectID.ValueString(), + // err, + // ), + // ) + // return + // } + + // tflog.Info(ctx, "deleted project rolling release", map[string]any{ + // "team_id": state.TeamID.ValueString(), + // "project_id": state.ProjectID.ValueString(), + // }) +} + +// Update updates the project rolling release of a Vercel project state. +func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ProjectRollingRelease + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.client.UpdateRollingRelease(ctx, plan.toUpdateRollingReleaseRequest()) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project rolling release", + "Could not update project rolling release, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToProjectRollingRelease(response, plan.ProjectID) + + tflog.Info(ctx, "updated project rolling release", map[string]any{ + "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 + } +} + +// ImportState takes an identifier and reads all the project rolling release information from the Vercel API. +// The results are then stored in terraform state. +func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, projectID, ok := splitInto1Or2(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing project rolling release", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id\" or \"project_id\"", req.ID), + ) + } + + out, err := r.client.GetRollingRelease(ctx, projectID, teamID) + if err != nil { + resp.Diagnostics.AddError( + "Error reading project rolling release", + fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", + teamID, + projectID, + err, + ), + ) + return + } + + result := convertResponseToProjectRollingRelease(out, types.StringValue(projectID)) + tflog.Info(ctx, "imported project rolling release", map[string]any{ + "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 + } +} From 6e9343b6cf2afb12e17099e4b3d665915c5b9620 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 5 Jun 2025 13:12:09 -0400 Subject: [PATCH 002/133] wip: adding get delete --- client/project_rolling_release.go | 43 +++++++++++------ vercel/resource_project_rolling_release.go | 54 +++++++++++----------- 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 5c9a4ae9..0e13c81e 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -49,7 +49,7 @@ func (d RollingReleaseResponse) toRollingReleaseResponse(teamID string) RollingR // UpdateRollingRelease will update an existing rolling release to the latest information. func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseResponse, error) { - url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config", c.baseURL, request.ProjectID) + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamid=%s", c.baseURL, request.ProjectID, request.TeamID) payload := string(mustMarshal(request.RollingRelease)) @@ -67,21 +67,36 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling return d.toRollingReleaseResponse(c.TeamID(request.TeamID)), err } +// DeleteRollingRelease will delete the rolling release for a given project. +func (c *Client) DeleteRollingRelease(ctx context.Context, projectID, teamID string) error { + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) + + tflog.Info(ctx, "deleting rolling-release", map[string]any{ + "url": url, + }) + + var d RollingReleaseResponse + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + }, &d) + return err +} + // GetRollingRelease returns the rolling release for a given project. func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (d RollingReleaseResponse, err error) { - project, err := c.GetProject(ctx, projectID, teamID) - if err != nil { - return RollingReleaseResponse{}, fmt.Errorf("error getting project %s: %w", projectID, err) - } + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) - rollingRelease := project.RollingRelease - if rollingRelease == nil { - return RollingReleaseResponse{ - TeamID: c.TeamID(teamID), - }, nil - } + tflog.Info(ctx, "deleting rolling-release", map[string]any{ + "url": url, + }) - return RollingReleaseResponse{ - TeamID: c.TeamID(teamID), - }, err + var d RollingReleaseResponse + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + }, &d) + return err } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index f793b201..4d8319d2 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -250,33 +250,33 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R // Delete deletes a Vercel project rolling release. func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - // var state ProjectRollingRelease - // diags := req.State.Get(ctx, &state) - // resp.Diagnostics.Append(diags...) - // if resp.Diagnostics.HasError() { - // return - // } - - // err := r.client.DeleteRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) - // if client.NotFound(err) { - // return - // } - // if err != nil { - // resp.Diagnostics.AddError( - // "Error deleting project rolling release", - // fmt.Sprintf( - // "Could not delete project rolling release %s, unexpected error: %s", - // state.ProjectID.ValueString(), - // err, - // ), - // ) - // return - // } - - // tflog.Info(ctx, "deleted project rolling release", map[string]any{ - // "team_id": state.TeamID.ValueString(), - // "project_id": state.ProjectID.ValueString(), - // }) + var state ProjectRollingRelease + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + if client.NotFound(err) { + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project rolling release", + fmt.Sprintf( + "Could not delete project rolling release %s, unexpected error: %s", + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted project rolling release", map[string]any{ + "team_id": state.TeamID.ValueString(), + "project_id": state.ProjectID.ValueString(), + }) } // Update updates the project rolling release of a Vercel project state. From 9be0fbe47cb8511f90684ab57322483dc4e434d3 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 5 Jun 2025 13:57:40 -0400 Subject: [PATCH 003/133] it compiles --- client/project.go | 2 +- client/project_rolling_release.go | 85 +++++++------- vercel/data_source_project_rolling_release.go | 41 ++++--- vercel/resource_project_rolling_release.go | 108 +++++++++++------- 4 files changed, 137 insertions(+), 99 deletions(-) diff --git a/client/project.go b/client/project.go index ffc68ff8..0d48cbc2 100644 --- a/client/project.go +++ b/client/project.go @@ -210,7 +210,7 @@ type ProjectResponse struct { ResourceConfig *ResourceConfigResponse `json:"resourceConfig"` NodeVersion string `json:"nodeVersion"` Crons *ProjectCronsResponse `json:"crons"` - RollingRelease *ProjectRollingRelease `json:"rollingRelease,omitempty"` + RollingRelease *RollingRelease `json:"rollingRelease,omitempty"` } type ProjectCronsResponse struct { diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 0e13c81e..5e2a68b7 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) -type Stage struct { +type RollingReleaseStage struct { TargetPercentage float64 `json:"targetPercentage,omitempty"` Duration float64 `json:"duration,omitempty"` RequireApproval bool `json:"requireApproval,omitempty"` @@ -15,40 +15,48 @@ type Stage struct { // CreateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to // create a rolling release. -type RollingReleaseRequest struct { - Enabled bool `json:"enabled,omitempty"` - AdvancementType string `json:"advancementType,omitempty"` - CanaryResponseHeader string `json:"canaryResponseHeader,omitempty"` - Stages []Stage `json:"stages,omitempty"` +type RollingRelease struct { + Enabled bool `json:"enabled,omitempty"` + AdvancementType string `json:"advancementType,omitempty"` + CanaryResponseHeader bool `json:"canaryResponseHeader,omitempty"` + Stages []RollingReleaseStage `json:"stages,omitempty"` } -// UpdateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to -// update a rolling release. -type UpdateRollingReleaseRequest struct { - RollingRelease RollingReleaseRequest - ProjectID string - TeamID string +type RollingReleaseInfo struct { + RollingRelease RollingRelease `json:"rollingRelease,omitempty"` + ProjectID string `json:"projectId"` + TeamID string `json:"teamId"` } -// ProjectRollingRelease defines the rolling release configuration on the Project document. -type ProjectRollingRelease struct { - Target string `json:"target"` - Stages []Stage `json:"stages,omitempty"` - CanaryResponseHeader bool `json:"canaryResponseHeader,omitempty"` -} +// GetRollingRelease returns the rolling release for a given project. +func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (RollingReleaseInfo, error) { + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) -type RollingReleaseResponse struct { - TeamID string `json:"-"` + tflog.Info(ctx, "deleting rolling-release", map[string]any{ + "url": url, + }) + + d := RollingReleaseInfo{} + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + }, &d) + d.ProjectID = projectID + d.TeamID = teamID + return d, err } -func (d RollingReleaseResponse) toRollingReleaseResponse(teamID string) RollingReleaseResponse { - return RollingReleaseResponse{ - TeamID: teamID, - } +// UpdateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to +// update a rolling release. +type UpdateRollingReleaseRequest struct { + RollingRelease RollingRelease `json:"rollingRelease,omitempty"` + ProjectID string `json:"projectId,omitempty"` + TeamID string `json:"teamId,omitempty"` } // UpdateRollingRelease will update an existing rolling release to the latest information. -func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseResponse, error) { +func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseInfo, error) { url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamid=%s", c.baseURL, request.ProjectID, request.TeamID) payload := string(mustMarshal(request.RollingRelease)) @@ -57,14 +65,16 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling "url": url, "payload": payload, }) - var d RollingReleaseResponse + var d RollingReleaseInfo err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", url: url, body: payload, }, &d) - return d.toRollingReleaseResponse(c.TeamID(request.TeamID)), err + d.ProjectID = request.ProjectID + d.TeamID = request.TeamID + return d, err } // DeleteRollingRelease will delete the rolling release for a given project. @@ -75,28 +85,13 @@ func (c *Client) DeleteRollingRelease(ctx context.Context, projectID, teamID str "url": url, }) - var d RollingReleaseResponse + var d RollingReleaseInfo err := c.doRequest(clientRequest{ ctx: ctx, method: "DELETE", url: url, }, &d) - return err -} - -// GetRollingRelease returns the rolling release for a given project. -func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (d RollingReleaseResponse, err error) { - url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) - - tflog.Info(ctx, "deleting rolling-release", map[string]any{ - "url": url, - }) - - var d RollingReleaseResponse - err := c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - }, &d) + d.ProjectID = projectID + d.TeamID = teamID return err } diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 9ab9e0de..69bd7d0d 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -108,25 +108,40 @@ func (r *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour return } - result := convertResponseToProjectRollingRelease(out, config.ProjectID) + result := convertResponseToTFRollingRelease(out) tflog.Info(ctx, "read project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + "team_id": result.TeamID, + "project_id": result.ProjectID, }) - var stages []attr.Value - for _, s := range result.Stages { - stages = append(stages, types.Float64Value(s.TargetPercentage)) + // Convert []TFRollingReleaseStage to []attr.Value (as []types.String) + stageValues := make([]attr.Value, len(result.RollingRelease.Stages)) + + var stageAttrType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "duration": types.Float64Type, + "target_percentage": types.Float64Type, + "require_approval": types.BoolType, + }, + } + + for i, stage := range result.RollingRelease.Stages { + newStage := types.ObjectValueMust(stageAttrType.AttrTypes, map[string]attr.Value{ + "duration": types.Float64Value(stage.Duration), + "target_percentage": types.Float64Value(stage.TargetPercentage), + "require_approval": types.BoolValue(stage.RequireApproval), + }) + stageValues[i] = newStage } diags = resp.State.Set(ctx, ProjectRollingReleaseWithID{ - Enabled: types.BoolValue(result.Enabled), - AdvancementType: types.StringValue(result.AdvancementType), - CanaryResponseHeader: types.BoolValue(result.CanaryResponseHeader), - Stages: types.ListValueMust(types.StringType, stages), - ProjectID: result.ProjectID, - TeamID: result.TeamID, - ID: result.ProjectID, + Enabled: types.BoolValue(result.RollingRelease.Enabled), + AdvancementType: types.StringValue(result.RollingRelease.AdvancementType), + CanaryResponseHeader: types.BoolValue(result.RollingRelease.CanaryResponseHeader), + Stages: types.ListValueMust(types.StringType, stageValues), + ProjectID: types.StringValue(result.ProjectID), + TeamID: types.StringValue(result.TeamID), + ID: types.StringValue(result.ProjectID), }) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 4d8319d2..c9e9df2d 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -127,49 +127,79 @@ For more detailed information, please see the [Vercel documentation](https://ver } } -type Stage struct { +type TFRollingReleaseStage struct { TargetPercentage float64 `tfsdk:"targetPercentage,omitempty"` Duration float64 `tfsdk:"duration,omitempty"` + RequireApproval bool `tfsdk:"requireApproval,omitempty"` +} + +// TFRollingRelease reflects the state terraform stores internally for a project rolling release. +type TFRollingRelease struct { + Enabled bool `tfsdk:"enabled,omitempty"` + AdvancementType string `tfsdk:"advancementType,omitempty"` + CanaryResponseHeader bool `tfsdk:"canaryResponseHeader,omitempty"` + Stages []TFRollingReleaseStage `tfsdk:"stages,omitempty"` } // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. -type ProjectRollingRelease struct { - Enabled bool `tfsdk:"enabled,omitempty"` - AdvancementType string `tfsdk:"advancementType,omitempty"` - CanaryResponseHeader bool `tfsdk:"canaryResponseHeader,omitempty"` - Stages []Stage `tfsdk:"stages,omitempty"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` +type TFRollingReleaseInfo struct { + RollingRelease TFRollingRelease `tfsdk:"rollingRelease,omitempty"` + ProjectID string `tfsdk:"project_id"` + TeamID string `tfsdk:"team_id"` } -func (e *ProjectRollingRelease) toUpdateRollingReleaseRequest() client.UpdateRollingReleaseRequest { +func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() client.UpdateRollingReleaseRequest { return client.UpdateRollingReleaseRequest{ - ProjectID: e.ProjectID.ValueString(), - TeamID: e.TeamID.ValueString(), + RollingRelease: client.RollingRelease{ + Enabled: e.RollingRelease.Enabled, + AdvancementType: e.RollingRelease.AdvancementType, + CanaryResponseHeader: e.RollingRelease.CanaryResponseHeader, + Stages: make([]client.RollingReleaseStage, len(e.RollingRelease.Stages)), + }, + ProjectID: e.ProjectID, + TeamID: e.TeamID, } } -// convertResponseToProjectRollingRelease is used to populate terraform state based on an API response. +func convertStages(stages []client.RollingReleaseStage) []TFRollingReleaseStage { + result := make([]TFRollingReleaseStage, len(stages)) + for i, stage := range stages { + result[i] = TFRollingReleaseStage{ + TargetPercentage: stage.TargetPercentage, + Duration: stage.Duration, + RequireApproval: stage.RequireApproval, + } + } + return result +} + +// convertResponseToTFRollingRelease 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 convertResponseToProjectRollingRelease(response client.RollingReleaseResponse, projectID types.String) ProjectRollingRelease { - return ProjectRollingRelease{ - TeamID: types.StringValue(response.TeamID), - ProjectID: projectID, +func convertResponseToTFRollingRelease(response client.RollingReleaseInfo) TFRollingReleaseInfo { + return TFRollingReleaseInfo{ + RollingRelease: TFRollingRelease{ + Enabled: response.RollingRelease.Enabled, + AdvancementType: response.RollingRelease.AdvancementType, + CanaryResponseHeader: response.RollingRelease.CanaryResponseHeader, + Stages: convertStages(response.RollingRelease.Stages), + }, + ProjectID: response.ProjectID, + TeamID: response.TeamID, } } // Create will create a new rolling release config on a Vercel project. // This is called automatically by the provider when a new resource should be created. func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan ProjectRollingRelease + var plan TFRollingReleaseInfo 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()) + _, err := r.client.GetProject(ctx, plan.ProjectID, plan.TeamID) if client.NotFound(err) { resp.Diagnostics.AddError( "Error creating project rolling release", @@ -194,11 +224,11 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } - result := convertResponseToProjectRollingRelease(response, plan.ProjectID) + result := convertResponseToTFRollingRelease(response) tflog.Info(ctx, "created project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + "team_id": result.TeamID, + "project_id": result.ProjectID, }) diags = resp.State.Set(ctx, result) @@ -211,14 +241,14 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource // Read will read an rolling release of a Vercel project by requesting it from the Vercel API, and will update terraform // with this information. func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state ProjectRollingRelease + var state TFRollingReleaseInfo diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - out, err := r.client.GetRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + out, err := r.client.GetRollingRelease(ctx, state.ProjectID, state.TeamID) if client.NotFound(err) { resp.State.RemoveResource(ctx) return @@ -227,18 +257,16 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R resp.Diagnostics.AddError( "Error reading project rolling release", fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", - state.ProjectID.ValueString(), - state.TeamID.ValueString(), err, ), ) return } - result := convertResponseToProjectRollingRelease(out, state.ProjectID) + result := convertResponseToTFRollingRelease(out) tflog.Info(ctx, "read project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + "team_id": result.TeamID, + "project_id": result.ProjectID, }) diags = resp.State.Set(ctx, result) @@ -250,14 +278,14 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R // Delete deletes a Vercel project rolling release. func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state ProjectRollingRelease + var state TFRollingReleaseInfo diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - err := r.client.DeleteRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + err := r.client.DeleteRollingRelease(ctx, state.ProjectID, state.TeamID) if client.NotFound(err) { return } @@ -266,7 +294,7 @@ func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource "Error deleting project rolling release", fmt.Sprintf( "Could not delete project rolling release %s, unexpected error: %s", - state.ProjectID.ValueString(), + state.ProjectID, err, ), ) @@ -274,14 +302,14 @@ func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource } tflog.Info(ctx, "deleted project rolling release", map[string]any{ - "team_id": state.TeamID.ValueString(), - "project_id": state.ProjectID.ValueString(), + "team_id": state.TeamID, + "project_id": state.ProjectID, }) } // Update updates the project rolling release of a Vercel project state. func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan ProjectRollingRelease + var plan TFRollingReleaseInfo diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -297,11 +325,11 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource return } - result := convertResponseToProjectRollingRelease(response, plan.ProjectID) + result := convertResponseToTFRollingRelease(response) tflog.Info(ctx, "updated project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + "team_id": result.TeamID, + "project_id": result.ProjectID, }) diags = resp.State.Set(ctx, result) @@ -335,10 +363,10 @@ func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req res return } - result := convertResponseToProjectRollingRelease(out, types.StringValue(projectID)) + result := convertResponseToTFRollingRelease(out) tflog.Info(ctx, "imported project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + "team_id": result.TeamID, + "project_id": result.ProjectID, }) diags := resp.State.Set(ctx, result) From 757452ae8cfc8a51c159034a0875a723676ddcb0 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 5 Jun 2025 14:20:14 -0400 Subject: [PATCH 004/133] rename --- ...rce_project_rolling_release.go => resource_rolling_release.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vercel/{resource_project_rolling_release.go => resource_rolling_release.go} (100%) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_rolling_release.go similarity index 100% rename from vercel/resource_project_rolling_release.go rename to vercel/resource_rolling_release.go From 11ea65007304ff3662f23fb0b24e98ed8eb05766 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Thu, 5 Jun 2025 16:44:58 -0600 Subject: [PATCH 005/133] [tf] feat: support for rolling release --- client/project_rolling_release.go | 222 ++++++- client/request.go | 54 ++ vercel/data_source_project_rolling_release.go | 187 ++++-- vercel/resource_project_rolling_release.go | 606 ++++++++++++++++++ vercel/resource_rolling_release.go | 377 ----------- 5 files changed, 975 insertions(+), 471 deletions(-) create mode 100644 vercel/resource_project_rolling_release.go delete mode 100644 vercel/resource_rolling_release.go diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 5e2a68b7..6662a70d 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -2,28 +2,89 @@ package client import ( "context" + "encoding/json" "fmt" "github.com/hashicorp/terraform-plugin-log/tflog" ) +// RollingReleaseStage represents a stage in a rolling release type RollingReleaseStage struct { - TargetPercentage float64 `json:"targetPercentage,omitempty"` - Duration float64 `json:"duration,omitempty"` - RequireApproval bool `json:"requireApproval,omitempty"` + TargetPercentage int `json:"targetPercentage"` // Required: 0-100 + Duration *int `json:"duration,omitempty"` // Required for automatic advancement: 1-10000 minutes + RequireApproval bool `json:"requireApproval,omitempty"` // Only in response for manual-approval type } -// CreateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to -// create a rolling release. +// RollingRelease represents the rolling release configuration type RollingRelease struct { - Enabled bool `json:"enabled,omitempty"` - AdvancementType string `json:"advancementType,omitempty"` - CanaryResponseHeader bool `json:"canaryResponseHeader,omitempty"` - Stages []RollingReleaseStage `json:"stages,omitempty"` + Enabled bool `json:"enabled"` // Required + AdvancementType string `json:"advancementType"` // Required when enabled=true: 'automatic' or 'manual-approval' + Stages []RollingReleaseStage `json:"stages"` // Required when enabled=true: 2-10 stages +} + +// ErrorResponse represents the error response from the Vercel API +type ErrorResponse struct { + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +// Validate checks if the rolling release configuration is valid according to API requirements +func (r *RollingRelease) Validate() error { + if !r.Enabled { + return nil // No validation needed when disabled + } + + // Validate advancement type + if r.AdvancementType == "" { + return fmt.Errorf("advancement_type is required when enabled is true") + } + if r.AdvancementType != "automatic" && r.AdvancementType != "manual-approval" { + return fmt.Errorf("advancement_type must be 'automatic' or 'manual-approval' when enabled is true, got: %s", r.AdvancementType) + } + + // Validate stages + if len(r.Stages) == 0 { + return fmt.Errorf("stages are required when enabled is true") + } + if len(r.Stages) < 2 || len(r.Stages) > 10 { + return fmt.Errorf("must have between 2 and 10 stages when enabled is true, got: %d", len(r.Stages)) + } + + // Validate last stage is 100% + lastStage := r.Stages[len(r.Stages)-1] + if lastStage.TargetPercentage != 100 { + return fmt.Errorf("last stage must have target_percentage=100, got: %d", lastStage.TargetPercentage) + } + + // Validate stages are in ascending order and within bounds + prevPercentage := 0 + for i, stage := range r.Stages { + // Validate percentage bounds + if stage.TargetPercentage < 1 || stage.TargetPercentage > 100 { + return fmt.Errorf("stage %d: target_percentage must be between 1 and 100, got: %d", i, stage.TargetPercentage) + } + + // Validate ascending order + if stage.TargetPercentage <= prevPercentage { + return fmt.Errorf("stage %d: target_percentage must be greater than previous stage (%d), got: %d", i, prevPercentage, stage.TargetPercentage) + } + prevPercentage = stage.TargetPercentage + + // Validate duration for automatic advancement + if r.AdvancementType == "automatic" { + if stage.Duration == nil || *stage.Duration < 1 || *stage.Duration > 10000 { + return fmt.Errorf("stage %d: duration must be between 1 and 10000 minutes for automatic advancement, got: %d", i, *stage.Duration) + } + } + } + + return nil } type RollingReleaseInfo struct { - RollingRelease RollingRelease `json:"rollingRelease,omitempty"` + RollingRelease RollingRelease `json:"rollingRelease"` ProjectID string `json:"projectId"` TeamID string `json:"teamId"` } @@ -32,8 +93,11 @@ type RollingReleaseInfo struct { func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (RollingReleaseInfo, error) { url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) - tflog.Info(ctx, "deleting rolling-release", map[string]any{ + tflog.Debug(ctx, "getting rolling-release configuration", map[string]any{ "url": url, + "method": "GET", + "project_id": projectID, + "team_id": teamID, }) d := RollingReleaseInfo{} @@ -50,39 +114,149 @@ func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string // UpdateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to // update a rolling release. type UpdateRollingReleaseRequest struct { - RollingRelease RollingRelease `json:"rollingRelease,omitempty"` - ProjectID string `json:"projectId,omitempty"` - TeamID string `json:"teamId,omitempty"` + RollingRelease RollingRelease `json:"rollingRelease"` + ProjectID string `json:"projectId"` + TeamID string `json:"teamId"` } // UpdateRollingRelease will update an existing rolling release to the latest information. func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseInfo, error) { - url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamid=%s", c.baseURL, request.ProjectID, request.TeamID) + // Validate the request + if err := request.RollingRelease.Validate(); err != nil { + return RollingReleaseInfo{}, fmt.Errorf("invalid rolling release configuration: %w", err) + } + + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID) - payload := string(mustMarshal(request.RollingRelease)) + // Process stages to ensure final stage only has targetPercentage + stages := make([]map[string]any, len(request.RollingRelease.Stages)) + for i, stage := range request.RollingRelease.Stages { + if i == len(request.RollingRelease.Stages)-1 { + // Final stage should only have targetPercentage + stages[i] = map[string]any{ + "targetPercentage": stage.TargetPercentage, + } + } else { + // Other stages can have all properties + stageMap := map[string]any{ + "targetPercentage": stage.TargetPercentage, + "requireApproval": stage.RequireApproval, + } + // Only include duration if it's set + if stage.Duration != nil { + stageMap["duration"] = *stage.Duration + } + stages[i] = stageMap + } + } - tflog.Info(ctx, "updating rolling-release", map[string]any{ - "url": url, + // Send just the rolling release configuration, not the whole request + payload := string(mustMarshal(map[string]any{ + "enabled": request.RollingRelease.Enabled, + "advancementType": request.RollingRelease.AdvancementType, + "stages": stages, + })) + + tflog.Debug(ctx, "updating rolling-release configuration", map[string]any{ + "url": url, + "method": "PATCH", + "project_id": request.ProjectID, + "team_id": request.TeamID, "payload": payload, + "base_url": c.baseURL, + "enabled": request.RollingRelease.Enabled, + "advancement_type": request.RollingRelease.AdvancementType, + "stages_count": len(request.RollingRelease.Stages), }) + + // Log each stage for debugging + for i, stage := range stages { + tflog.Debug(ctx, fmt.Sprintf("stage %d configuration", i), map[string]any{ + "stage": stage, + }) + } + var d RollingReleaseInfo - err := c.doRequest(clientRequest{ + resp, err := c.doRequestWithResponse(clientRequest{ ctx: ctx, method: "PATCH", url: url, body: payload, - }, &d) - d.ProjectID = request.ProjectID - d.TeamID = request.TeamID - return d, err + }) + + // Always log the raw response for debugging + tflog.Debug(ctx, "received raw response", map[string]any{ + "response": resp, + }) + + if err != nil { + // Try to parse error response + var errResp ErrorResponse + if resp != "" && json.Unmarshal([]byte(resp), &errResp) == nil { + tflog.Error(ctx, "error updating rolling-release", map[string]any{ + "error_code": errResp.Error.Code, + "error_message": errResp.Error.Message, + "url": url, + "payload": payload, + "response": resp, + }) + return d, fmt.Errorf("failed to update rolling release: %s - %s", errResp.Error.Code, errResp.Error.Message) + } + + tflog.Error(ctx, "error updating rolling-release", map[string]any{ + "error": err.Error(), + "url": url, + "payload": payload, + "response": resp, + }) + return d, fmt.Errorf("failed to update rolling release: %w", err) + } + + // Return the request state since we know it's valid + result := RollingReleaseInfo{ + ProjectID: request.ProjectID, + TeamID: request.TeamID, + RollingRelease: RollingRelease{ + Enabled: request.RollingRelease.Enabled, + AdvancementType: request.RollingRelease.AdvancementType, + Stages: make([]RollingReleaseStage, len(request.RollingRelease.Stages)), + }, + } + + // Copy stages, preserving the duration and requireApproval for non-final stages + for i, stage := range request.RollingRelease.Stages { + if i == len(request.RollingRelease.Stages)-1 { + // For the final stage, only include targetPercentage + result.RollingRelease.Stages[i] = RollingReleaseStage{ + TargetPercentage: stage.TargetPercentage, + // Do not include Duration or RequireApproval for final stage + } + } else { + // For other stages, include all properties + result.RollingRelease.Stages[i] = stage + } + } + + tflog.Debug(ctx, "returning rolling release configuration", map[string]any{ + "project_id": result.ProjectID, + "team_id": result.TeamID, + "enabled": result.RollingRelease.Enabled, + "advancement_type": result.RollingRelease.AdvancementType, + "stages": result.RollingRelease.Stages, + }) + + return result, nil } // DeleteRollingRelease will delete the rolling release for a given project. func (c *Client) DeleteRollingRelease(ctx context.Context, projectID, teamID string) error { url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) - tflog.Info(ctx, "deleting rolling-release", map[string]any{ + tflog.Debug(ctx, "deleting rolling-release configuration", map[string]any{ "url": url, + "method": "DELETE", + "project_id": projectID, + "team_id": teamID, }) var d RollingReleaseInfo diff --git a/client/request.go b/client/request.go index 75a58720..798cb78b 100644 --- a/client/request.go +++ b/client/request.go @@ -166,3 +166,57 @@ func (c *Client) _doRequest(req *http.Request, v any, errorOnNoContent bool) err return nil } + +// doRequestWithResponse is similar to doRequest but returns the raw response body as a string +func (c *Client) doRequestWithResponse(req clientRequest) (string, error) { + r, err := req.toHTTPRequest() + if err != nil { + return "", err + } + + r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token)) + resp, err := c.http().Do(r) + if err != nil { + return "", fmt.Errorf("error doing http request: %w", err) + } + + defer resp.Body.Close() + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response body: %w", err) + } + + if resp.StatusCode >= 300 { + var errorResponse APIError + if string(responseBody) == "" { + errorResponse.StatusCode = resp.StatusCode + return string(responseBody), errorResponse + } + err = json.Unmarshal(responseBody, &struct { + Error *APIError `json:"error"` + }{ + Error: &errorResponse, + }) + if errorResponse.Code == "" && errorResponse.Message == "" { + return string(responseBody), fmt.Errorf("error performing API request: %d %s", resp.StatusCode, string(responseBody)) + } + if err != nil { + return string(responseBody), fmt.Errorf("error unmarshaling response for status code %d: %w: %s", resp.StatusCode, err, string(responseBody)) + } + errorResponse.StatusCode = resp.StatusCode + errorResponse.RawMessage = responseBody + errorResponse.retryAfter = 1000 // set a sensible default for retrying. This is in milliseconds. + if resp.StatusCode == 429 { + retryAfterRaw := resp.Header.Get("Retry-After") + if retryAfterRaw != "" { + retryAfter, err := strconv.Atoi(retryAfterRaw) + if err == nil && retryAfter > 0 { + errorResponse.retryAfter = retryAfter + } + } + } + return string(responseBody), errorResponse + } + + return string(responseBody), nil +} diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 69bd7d0d..93e99066 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -13,8 +12,7 @@ import ( ) var ( - _ datasource.DataSource = &projectRollingReleaseDataSource{} - _ datasource.DataSourceWithConfigure = &projectRollingReleaseDataSource{} + _ datasource.DataSource = &projectRollingReleaseDataSource{} ) func newProjectRollingReleaseDataSource() datasource.DataSource { @@ -25,11 +23,11 @@ type projectRollingReleaseDataSource struct { client *client.Client } -func (r *projectRollingReleaseDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *projectRollingReleaseDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_project_rolling_release" } -func (r *projectRollingReleaseDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (d *projectRollingReleaseDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -38,111 +36,160 @@ func (r *projectRollingReleaseDataSource) Configure(ctx context.Context, req dat client, ok := req.ProviderData.(*client.Client) if !ok { resp.Diagnostics.AddError( - "Unexpected DataSource Configure Type", + "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 + d.client = client } -// Schema returns the schema information for a project Rolling Release datasource. -func (r *projectRollingReleaseDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: ` -Provides a Project Rolling Release datasource. - -A Project Rolling Release datasource details information about a Rolling Release on a Vercel Project. - -For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). -`, + MarkdownDescription: "Data source for a Vercel project rolling release configuration.", Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - }, "project_id": schema.StringAttribute{ - Description: "The ID of the Project for the rolling release", - Required: true, + MarkdownDescription: "The ID of the project.", + Required: true, }, "team_id": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "The ID of the Vercel team.", + MarkdownDescription: "The ID of the team the project exists in.", + Required: true, + }, + "rolling_release": schema.SingleNestedAttribute{ + MarkdownDescription: "The rolling release configuration.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether rolling releases are enabled.", + Computed: true, + }, + "advancement_type": schema.StringAttribute{ + MarkdownDescription: "The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true.", + Computed: true, + }, + "stages": schema.ListNestedAttribute{ + MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Computed: true, + }, + "duration": schema.Int64Attribute{ + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", + Computed: true, + }, + "require_approval": schema.BoolAttribute{ + MarkdownDescription: "Whether approval is required before advancing to the next stage.", + Computed: true, + }, + }, + }, + }, + }, }, }, } } -type ProjectRollingReleaseWithID struct { - Enabled types.Bool `tfsdk:"enabled"` - AdvancementType types.String `tfsdk:"advancement_type"` - CanaryResponseHeader types.Bool `tfsdk:"canary_response_header"` - Stages types.List `tfsdk:"stages"` +type TFRollingReleaseStageDataSource struct { + TargetPercentage types.Int64 `tfsdk:"target_percentage"` + Duration types.Int64 `tfsdk:"duration"` + RequireApproval types.Bool `tfsdk:"require_approval"` +} - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` - ID types.String `tfsdk:"id"` +type TFRollingReleaseDataSource struct { + Enabled types.Bool `tfsdk:"enabled"` + AdvancementType types.String `tfsdk:"advancement_type"` + Stages []TFRollingReleaseStageDataSource `tfsdk:"stages"` } -// Read will read the rolling release configuration of a Vercel project by requesting it from the Vercel API, and will update terraform -// with this information. -func (r *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config ProjectRollingReleaseWithID +type TFRollingReleaseInfoDataSource struct { + RollingRelease TFRollingReleaseDataSource `tfsdk:"rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` +} + +func convertStagesDataSource(stages []client.RollingReleaseStage) []TFRollingReleaseStageDataSource { + if len(stages) == 0 { + return make([]TFRollingReleaseStageDataSource, 0) + } + + result := make([]TFRollingReleaseStageDataSource, len(stages)) + for i, stage := range stages { + var duration types.Int64 + if stage.Duration != nil { + duration = types.Int64Value(int64(*stage.Duration)) + } else { + duration = types.Int64Null() + } + + result[i] = TFRollingReleaseStageDataSource{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), + Duration: duration, + RequireApproval: types.BoolValue(stage.RequireApproval), + } + } + return result +} + +func convertResponseToTFRollingReleaseDataSource(response client.RollingReleaseInfo) TFRollingReleaseInfoDataSource { + result := TFRollingReleaseInfoDataSource{ + RollingRelease: TFRollingReleaseDataSource{ + Enabled: types.BoolValue(response.RollingRelease.Enabled), + AdvancementType: types.StringNull(), + Stages: make([]TFRollingReleaseStageDataSource, 0), + }, + ProjectID: types.StringValue(response.ProjectID), + TeamID: types.StringValue(response.TeamID), + } + + if response.RollingRelease.Enabled { + result.RollingRelease.AdvancementType = types.StringValue(response.RollingRelease.AdvancementType) + result.RollingRelease.Stages = convertStagesDataSource(response.RollingRelease.Stages) + } + + return result +} + +func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config TFRollingReleaseInfoDataSource diags := req.Config.Get(ctx, &config) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - out, err := r.client.GetRollingRelease(ctx, config.ProjectID.ValueString(), config.TeamID.ValueString()) + out, err := d.client.GetRollingRelease(ctx, config.ProjectID.ValueString(), config.TeamID.ValueString()) + if client.NotFound(err) { + resp.Diagnostics.AddError( + "Error reading project rolling release", + fmt.Sprintf("No project rolling release found with id %s %s", config.TeamID.ValueString(), config.ProjectID.ValueString()), + ) + return + } if err != nil { resp.Diagnostics.AddError( "Error reading project rolling release", fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", - config.ProjectID.ValueString(), config.TeamID.ValueString(), + config.ProjectID.ValueString(), err, ), ) return } - result := convertResponseToTFRollingRelease(out) + result := convertResponseToTFRollingReleaseDataSource(out) tflog.Info(ctx, "read project rolling release", map[string]any{ - "team_id": result.TeamID, - "project_id": result.ProjectID, + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), }) - // Convert []TFRollingReleaseStage to []attr.Value (as []types.String) - stageValues := make([]attr.Value, len(result.RollingRelease.Stages)) - - var stageAttrType = types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "duration": types.Float64Type, - "target_percentage": types.Float64Type, - "require_approval": types.BoolType, - }, - } - - for i, stage := range result.RollingRelease.Stages { - newStage := types.ObjectValueMust(stageAttrType.AttrTypes, map[string]attr.Value{ - "duration": types.Float64Value(stage.Duration), - "target_percentage": types.Float64Value(stage.TargetPercentage), - "require_approval": types.BoolValue(stage.RequireApproval), - }) - stageValues[i] = newStage - } - - diags = resp.State.Set(ctx, ProjectRollingReleaseWithID{ - Enabled: types.BoolValue(result.RollingRelease.Enabled), - AdvancementType: types.StringValue(result.RollingRelease.AdvancementType), - CanaryResponseHeader: types.BoolValue(result.RollingRelease.CanaryResponseHeader), - Stages: types.ListValueMust(types.StringType, stageValues), - ProjectID: types.StringValue(result.ProjectID), - TeamID: types.StringValue(result.TeamID), - ID: types.StringValue(result.ProjectID), - }) + diags = resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go new file mode 100644 index 00000000..f427e184 --- /dev/null +++ b/vercel/resource_project_rolling_release.go @@ -0,0 +1,606 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v3/client" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/attr" +) + +var ( + _ resource.Resource = &projectRollingReleaseResource{} + _ resource.ResourceWithConfigure = &projectRollingReleaseResource{} + _ resource.ResourceWithImportState = &projectRollingReleaseResource{} +) + +func newProjectRollingReleaseResource() resource.Resource { + return &projectRollingReleaseResource{} +} + +type projectRollingReleaseResource struct { + client *client.Client +} + +func (r *projectRollingReleaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_rolling_release" +} + +func (r *projectRollingReleaseResource) 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 +} + +// Custom validator for advancement_type +type advancementTypeValidator struct{} + +func (v advancementTypeValidator) Description(ctx context.Context) string { + return "advancement_type must be either 'automatic' or 'manual-approval'" +} + +func (v advancementTypeValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v advancementTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + value := req.ConfigValue.ValueString() + if value != "automatic" && value != "manual-approval" { + resp.Diagnostics.AddError( + "Invalid advancement_type", + fmt.Sprintf("advancement_type must be either 'automatic' or 'manual-approval', got: %s", value), + ) + } +} + +// Schema returns the schema information for a project rolling release resource. +func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manages rolling release configuration for a Vercel project.", + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + MarkdownDescription: "The ID of the project.", + Required: true, + }, + "team_id": schema.StringAttribute{ + MarkdownDescription: "The ID of the team the project exists in.", + Required: true, + }, + "rolling_release": schema.SingleNestedAttribute{ + MarkdownDescription: "The rolling release configuration.", + Required: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether rolling releases are enabled.", + Required: true, + }, + "advancement_type": schema.StringAttribute{ + MarkdownDescription: "The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true.", + Optional: true, + Computed: true, + Validators: []validator.String{ + advancementTypeValidator{}, + }, + }, + "stages": schema.ListNestedAttribute{ + MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", + Optional: true, + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Required: true, + }, + "duration": schema.Int64Attribute{ + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", + Optional: true, + Computed: true, + }, + "require_approval": schema.BoolAttribute{ + MarkdownDescription: "Whether approval is required before advancing to the next stage.", + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + } +} + +type TFRollingReleaseStage struct { + TargetPercentage types.Int64 `tfsdk:"target_percentage"` + Duration types.Int64 `tfsdk:"duration"` + RequireApproval types.Bool `tfsdk:"require_approval"` +} + +// TFRollingRelease reflects the state terraform stores internally for a project rolling release. +type TFRollingRelease struct { + Enabled types.Bool `tfsdk:"enabled"` + AdvancementType types.String `tfsdk:"advancement_type"` + Stages types.List `tfsdk:"stages"` +} + +// ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. +type TFRollingReleaseInfo struct { + RollingRelease TFRollingRelease `tfsdk:"rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` +} + +type RollingReleaseStage struct { + TargetPercentage int `json:"targetPercentage"` + Duration *int `json:"duration,omitempty"` + RequireApproval bool `json:"requireApproval"` +} + +type RollingRelease struct { + Enabled bool `json:"enabled"` + AdvancementType string `json:"advancementType"` + Stages []RollingReleaseStage `json:"stages"` +} + +type UpdateRollingReleaseRequest struct { + RollingRelease RollingRelease `json:"rollingRelease"` + ProjectID string `json:"-"` + TeamID string `json:"-"` +} + +func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { + var stages []client.RollingReleaseStage + var advancementType string + var diags diag.Diagnostics + + if e.RollingRelease.Enabled.ValueBool() { + if !e.RollingRelease.AdvancementType.IsNull() { + advancementType = e.RollingRelease.AdvancementType.ValueString() + } else { + advancementType = "manual-approval" // Default to manual-approval if not specified + } + + // Convert stages from types.List to []client.RollingReleaseStage + var tfStages []TFRollingReleaseStage + if !e.RollingRelease.Stages.IsNull() && !e.RollingRelease.Stages.IsUnknown() { + diags = e.RollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) + if diags.HasError() { + return client.UpdateRollingReleaseRequest{}, diags + } + stages = make([]client.RollingReleaseStage, len(tfStages)) + for i, stage := range tfStages { + // For automatic advancement, set a default duration if not provided + if advancementType == "automatic" { + var duration int = 60 // Default duration in minutes + if !stage.Duration.IsNull() { + duration = int(stage.Duration.ValueInt64()) + } + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + Duration: &duration, + RequireApproval: stage.RequireApproval.ValueBool(), + } + } else { + // For manual approval, omit duration field completely + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + RequireApproval: stage.RequireApproval.ValueBool(), + } + } + } + } + } else { + // When disabled, don't send any stages to the API + stages = []client.RollingReleaseStage{} + } + + return client.UpdateRollingReleaseRequest{ + RollingRelease: client.RollingRelease{ + Enabled: e.RollingRelease.Enabled.ValueBool(), + AdvancementType: advancementType, + Stages: stages, + }, + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + }, diags +} + +func convertStages(stages []client.RollingReleaseStage, advancementType string, planStages []TFRollingReleaseStage, enabled bool, ctx context.Context) (types.List, diag.Diagnostics) { + // If disabled, always return plan stages to preserve state + if !enabled && len(planStages) > 0 { + elements := make([]attr.Value, len(planStages)) + for i, stage := range planStages { + // For disabled state, ensure duration is known + var duration types.Int64 + if stage.Duration.IsUnknown() { + duration = types.Int64Null() + } else { + duration = stage.Duration + } + + elements[i] = types.ObjectValueMust( + map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + "require_approval": types.BoolType, + }, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + "duration": duration, + "require_approval": stage.RequireApproval, + }, + ) + } + return types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + "require_approval": types.BoolType, + }, + }, elements) + } + + // If no stages from API and no plan stages, return empty list + if len(stages) == 0 { + return types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + "require_approval": types.BoolType, + }, + }, []attr.Value{}) + } + + elements := make([]attr.Value, len(stages)) + for i, stage := range stages { + targetPercentage := types.Int64Value(int64(stage.TargetPercentage)) + requireApproval := types.BoolValue(stage.RequireApproval) + var duration types.Int64 + + // If we have plan stages, preserve the values but ensure they're known + if i < len(planStages) { + targetPercentage = planStages[i].TargetPercentage + requireApproval = planStages[i].RequireApproval + + // Handle duration based on advancement type + if advancementType == "automatic" { + if planStages[i].Duration.IsUnknown() { + // For unknown values, use API value or default + if stage.Duration != nil { + duration = types.Int64Value(int64(*stage.Duration)) + } else { + duration = types.Int64Value(60) // Default duration in minutes + } + } else { + duration = planStages[i].Duration + } + } else { + duration = types.Int64Null() // Manual approval doesn't use duration + } + } else { + // Only set duration for automatic advancement + if advancementType == "automatic" { + if stage.Duration != nil { + duration = types.Int64Value(int64(*stage.Duration)) + } else { + duration = types.Int64Value(60) // Default duration in minutes + } + } else { + duration = types.Int64Null() + } + } + + elements[i] = types.ObjectValueMust( + map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + "require_approval": types.BoolType, + }, + map[string]attr.Value{ + "target_percentage": targetPercentage, + "duration": duration, + "require_approval": requireApproval, + }, + ) + } + + return types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + "require_approval": types.BoolType, + }, + }, elements) +} + +func convertResponseToTFRollingRelease(response client.RollingReleaseInfo, plan *TFRollingReleaseInfo, ctx context.Context) (TFRollingReleaseInfo, diag.Diagnostics) { + var diags diag.Diagnostics + + result := TFRollingReleaseInfo{ + RollingRelease: TFRollingRelease{ + Enabled: types.BoolValue(response.RollingRelease.Enabled), + }, + ProjectID: types.StringValue(response.ProjectID), + TeamID: types.StringValue(response.TeamID), + } + + // Get plan stages if available + var planStages []TFRollingReleaseStage + if plan != nil && !plan.RollingRelease.Stages.IsNull() && !plan.RollingRelease.Stages.IsUnknown() { + diags.Append(plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false)...) + if diags.HasError() { + return result, diags + } + } + + if response.RollingRelease.Enabled { + result.RollingRelease.AdvancementType = types.StringValue(response.RollingRelease.AdvancementType) + } else { + result.RollingRelease.AdvancementType = types.StringNull() + } + + // Convert stages, passing enabled state to ensure proper preservation + stages, stagesDiags := convertStages( + response.RollingRelease.Stages, + response.RollingRelease.AdvancementType, + planStages, + response.RollingRelease.Enabled, + ctx, + ) + diags.Append(stagesDiags...) + if diags.HasError() { + return result, diags + } + result.RollingRelease.Stages = stages + + return result, diags +} + +// Create will create a new rolling release config on a Vercel project. +// This is called automatically by the provider when a new resource should be created. +func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Debug(ctx, "Starting rolling release creation") + + var plan TFRollingReleaseInfo + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Got plan from request", map[string]any{ + "project_id": plan.ProjectID.ValueString(), + "team_id": plan.TeamID.ValueString(), + "enabled": plan.RollingRelease.Enabled.ValueBool(), + "advancement_type": plan.RollingRelease.AdvancementType.ValueString(), + "stages": plan.RollingRelease.Stages, + }) + + _, err := r.client.GetProject(ctx, plan.ProjectID.ValueString(), plan.TeamID.ValueString()) + if client.NotFound(err) { + resp.Diagnostics.AddError( + "Error creating project rolling release", + "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 + } + if err != nil { + resp.Diagnostics.AddError( + "Error creating project rolling release", + "Error reading project information, unexpected error: "+err.Error(), + ) + return + } + + tflog.Debug(ctx, "Project exists, creating rolling release") + + updateRequest, diags := plan.toUpdateRollingReleaseRequest() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.client.UpdateRollingRelease(ctx, updateRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project rolling release", + "Could not create project rolling release, unexpected error: "+err.Error(), + ) + return + } + + result, diags := convertResponseToTFRollingRelease(response, &plan, ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Log the values for debugging + tflog.Debug(ctx, "created project rolling release", map[string]any{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "enabled": result.RollingRelease.Enabled.ValueBool(), + "advancement_type": result.RollingRelease.AdvancementType.ValueString(), + "stages": result.RollingRelease.Stages, + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read will read an rolling release of a Vercel project by requesting it from the Vercel API, and will update terraform +// with this information. +func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TFRollingReleaseInfo + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetRollingRelease(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 rolling release", + fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", + err, + ), + ) + return + } + + result, diags := convertResponseToTFRollingRelease(out, nil, ctx) + tflog.Info(ctx, "read project rolling release", map[string]any{ + "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 rolling release. +func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TFRollingReleaseInfo + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + if client.NotFound(err) { + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project rolling release", + fmt.Sprintf( + "Could not delete project rolling release %s, unexpected error: %s", + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted project rolling release", map[string]any{ + "team_id": state.TeamID.ValueString(), + "project_id": state.ProjectID.ValueString(), + }) +} + +// Update updates the project rolling release of a Vercel project state. +func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TFRollingReleaseInfo + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + updateRequest, diags := plan.toUpdateRollingReleaseRequest() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.client.UpdateRollingRelease(ctx, updateRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project rolling release", + "Could not update project rolling release, unexpected error: "+err.Error(), + ) + return + } + + result, diags := convertResponseToTFRollingRelease(response, &plan, ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Log the values for debugging + tflog.Debug(ctx, "updated project rolling release", map[string]any{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "enabled": result.RollingRelease.Enabled.ValueBool(), + "advancement_type": result.RollingRelease.AdvancementType.ValueString(), + "stages": result.RollingRelease.Stages, + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// ImportState takes an identifier and reads all the project rolling release information from the Vercel API. +// The results are then stored in terraform state. +func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, projectID, ok := splitInto1Or2(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing project rolling release", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id\" or \"project_id\"", req.ID), + ) + } + + out, err := r.client.GetRollingRelease(ctx, projectID, teamID) + if err != nil { + resp.Diagnostics.AddError( + "Error reading project rolling release", + fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", + teamID, + projectID, + err, + ), + ) + return + } + + result, diags := convertResponseToTFRollingRelease(out, nil, ctx) + tflog.Info(ctx, "imported project rolling release", map[string]any{ + "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_rolling_release.go b/vercel/resource_rolling_release.go deleted file mode 100644 index c9e9df2d..00000000 --- a/vercel/resource_rolling_release.go +++ /dev/null @@ -1,377 +0,0 @@ -package vercel - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "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/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/vercel/terraform-provider-vercel/v3/client" -) - -var ( - _ resource.Resource = &projectRollingReleaseResource{} - _ resource.ResourceWithConfigure = &projectRollingReleaseResource{} - _ resource.ResourceWithImportState = &projectRollingReleaseResource{} -) - -func newProjectRollingReleaseResource() resource.Resource { - return &projectRollingReleaseResource{} -} - -type projectRollingReleaseResource struct { - client *client.Client -} - -func (r *projectRollingReleaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_project_rolling_release" -} - -func (r *projectRollingReleaseResource) 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 -} - -// Schema returns the schema information for a project rolling release resource. -func (r *projectRollingReleaseResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: ` -Provides a Project Rolling release resource. - -A Project Rolling release resource defines an Rolling release on a Vercel Project. - -For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). -`, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Whether the rolling release is enabled.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - PlanModifiers: []planmodifier.Bool{ - // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. - boolplanmodifier.RequiresReplace(), - }, - }, - "advancement_type": schema.StringAttribute{ - Description: "The advancement type of the rolling release. Can be 'automatic' or 'manual-approve'.", - Optional: true, - Computed: true, - Default: stringdefault.StaticString("manual-approve"), - - PlanModifiers: []planmodifier.String{ - // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. - stringplanmodifier.RequiresReplace(), - }, - }, - "canary_response_header": schema.BoolAttribute{ - Description: "Whether the canary response header is enabled. This header is used to identify canary deployments.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - PlanModifiers: []planmodifier.Bool{ - // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. - boolplanmodifier.RequiresReplace(), - }, - }, - "stages": schema.ListAttribute{ - Description: "A list of stages for the rolling release. Each stage has a target percentage and duration.", - Optional: true, - Computed: true, - ElementType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "targetPercentage": types.Float64Type, - "duration": types.Float64Type, - }, - }, - PlanModifiers: []planmodifier.List{ - // RequiresReplace is used to ensure that if the value changes, the resource will be replaced. - listplanmodifier.RequiresReplace(), - }, - }, - - "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()}, - }, - }, - } -} - -type TFRollingReleaseStage struct { - TargetPercentage float64 `tfsdk:"targetPercentage,omitempty"` - Duration float64 `tfsdk:"duration,omitempty"` - RequireApproval bool `tfsdk:"requireApproval,omitempty"` -} - -// TFRollingRelease reflects the state terraform stores internally for a project rolling release. -type TFRollingRelease struct { - Enabled bool `tfsdk:"enabled,omitempty"` - AdvancementType string `tfsdk:"advancementType,omitempty"` - CanaryResponseHeader bool `tfsdk:"canaryResponseHeader,omitempty"` - Stages []TFRollingReleaseStage `tfsdk:"stages,omitempty"` -} - -// ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. -type TFRollingReleaseInfo struct { - RollingRelease TFRollingRelease `tfsdk:"rollingRelease,omitempty"` - ProjectID string `tfsdk:"project_id"` - TeamID string `tfsdk:"team_id"` -} - -func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() client.UpdateRollingReleaseRequest { - return client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ - Enabled: e.RollingRelease.Enabled, - AdvancementType: e.RollingRelease.AdvancementType, - CanaryResponseHeader: e.RollingRelease.CanaryResponseHeader, - Stages: make([]client.RollingReleaseStage, len(e.RollingRelease.Stages)), - }, - ProjectID: e.ProjectID, - TeamID: e.TeamID, - } -} - -func convertStages(stages []client.RollingReleaseStage) []TFRollingReleaseStage { - result := make([]TFRollingReleaseStage, len(stages)) - for i, stage := range stages { - result[i] = TFRollingReleaseStage{ - TargetPercentage: stage.TargetPercentage, - Duration: stage.Duration, - RequireApproval: stage.RequireApproval, - } - } - return result -} - -// convertResponseToTFRollingRelease 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 convertResponseToTFRollingRelease(response client.RollingReleaseInfo) TFRollingReleaseInfo { - return TFRollingReleaseInfo{ - RollingRelease: TFRollingRelease{ - Enabled: response.RollingRelease.Enabled, - AdvancementType: response.RollingRelease.AdvancementType, - CanaryResponseHeader: response.RollingRelease.CanaryResponseHeader, - Stages: convertStages(response.RollingRelease.Stages), - }, - ProjectID: response.ProjectID, - TeamID: response.TeamID, - } -} - -// Create will create a new rolling release config on a Vercel project. -// This is called automatically by the provider when a new resource should be created. -func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan TFRollingReleaseInfo - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - _, err := r.client.GetProject(ctx, plan.ProjectID, plan.TeamID) - if client.NotFound(err) { - resp.Diagnostics.AddError( - "Error creating project rolling release", - "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 - } - if err != nil { - resp.Diagnostics.AddError( - "Error creating project rolling release", - "Error reading project information, unexpected error: "+err.Error(), - ) - return - } - - response, err := r.client.UpdateRollingRelease(ctx, plan.toUpdateRollingReleaseRequest()) - if err != nil { - resp.Diagnostics.AddError( - "Error creating project rolling release", - "Could not create project rolling release, unexpected error: "+err.Error(), - ) - return - } - - result := convertResponseToTFRollingRelease(response) - - tflog.Info(ctx, "created project rolling release", map[string]any{ - "team_id": result.TeamID, - "project_id": result.ProjectID, - }) - - diags = resp.State.Set(ctx, result) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -// Read will read an rolling release of a Vercel project by requesting it from the Vercel API, and will update terraform -// with this information. -func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state TFRollingReleaseInfo - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - out, err := r.client.GetRollingRelease(ctx, state.ProjectID, state.TeamID) - if client.NotFound(err) { - resp.State.RemoveResource(ctx) - return - } - if err != nil { - resp.Diagnostics.AddError( - "Error reading project rolling release", - fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", - err, - ), - ) - return - } - - result := convertResponseToTFRollingRelease(out) - tflog.Info(ctx, "read project rolling release", map[string]any{ - "team_id": result.TeamID, - "project_id": result.ProjectID, - }) - - diags = resp.State.Set(ctx, result) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -// Delete deletes a Vercel project rolling release. -func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state TFRollingReleaseInfo - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err := r.client.DeleteRollingRelease(ctx, state.ProjectID, state.TeamID) - if client.NotFound(err) { - return - } - if err != nil { - resp.Diagnostics.AddError( - "Error deleting project rolling release", - fmt.Sprintf( - "Could not delete project rolling release %s, unexpected error: %s", - state.ProjectID, - err, - ), - ) - return - } - - tflog.Info(ctx, "deleted project rolling release", map[string]any{ - "team_id": state.TeamID, - "project_id": state.ProjectID, - }) -} - -// Update updates the project rolling release of a Vercel project state. -func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan TFRollingReleaseInfo - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - response, err := r.client.UpdateRollingRelease(ctx, plan.toUpdateRollingReleaseRequest()) - if err != nil { - resp.Diagnostics.AddError( - "Error updating project rolling release", - "Could not update project rolling release, unexpected error: "+err.Error(), - ) - return - } - - result := convertResponseToTFRollingRelease(response) - - tflog.Info(ctx, "updated project rolling release", map[string]any{ - "team_id": result.TeamID, - "project_id": result.ProjectID, - }) - - diags = resp.State.Set(ctx, result) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -// ImportState takes an identifier and reads all the project rolling release information from the Vercel API. -// The results are then stored in terraform state. -func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - teamID, projectID, ok := splitInto1Or2(req.ID) - if !ok { - resp.Diagnostics.AddError( - "Error importing project rolling release", - fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id\" or \"project_id\"", req.ID), - ) - } - - out, err := r.client.GetRollingRelease(ctx, projectID, teamID) - if err != nil { - resp.Diagnostics.AddError( - "Error reading project rolling release", - fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", - teamID, - projectID, - err, - ), - ) - return - } - - result := convertResponseToTFRollingRelease(out) - tflog.Info(ctx, "imported project rolling release", map[string]any{ - "team_id": result.TeamID, - "project_id": result.ProjectID, - }) - - diags := resp.State.Set(ctx, result) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} From ae54c84a58bd6f56189c694d007e59855b1f54cd Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 6 Jun 2025 14:23:32 -0600 Subject: [PATCH 006/133] [rolling-release] add in docs --- docs/data-sources/project_rolling_release.md | 34 ++++++ docs/resources/project_rolling_release.md | 42 +++++++ terraform.tfstate | 45 ++++++++ terraform.tfstate.backup | 45 ++++++++ test/api_test.go | 114 +++++++++++++++++++ 5 files changed, 280 insertions(+) create mode 100644 docs/data-sources/project_rolling_release.md create mode 100644 docs/resources/project_rolling_release.md create mode 100644 terraform.tfstate create mode 100644 terraform.tfstate.backup create mode 100644 test/api_test.go diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md new file mode 100644 index 00000000..e99a1b54 --- /dev/null +++ b/docs/data-sources/project_rolling_release.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_project_rolling_release Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides a Project Rolling Release datasource. + A Project Rolling Release datasource details information about a Rolling Release on a Vercel Project. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/rolling-releases. +--- + +# vercel_project_rolling_release (Data Source) + +Provides a Project Rolling Release datasource. + +A Project Rolling Release datasource details information about a Rolling Release on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). + + + + +## Schema + +### Required + +- `project_id` (String) The ID of the Project for the rolling release + +### Optional + +- `team_id` (String) The ID of the Vercel team. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md new file mode 100644 index 00000000..657b78cb --- /dev/null +++ b/docs/resources/project_rolling_release.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_project_rolling_release Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides a Project Rolling release resource. + A Project Rolling release resource defines an Rolling release on a Vercel Project. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/rolling-releases. +--- + +# vercel_project_rolling_release (Resource) + +Provides a Project Rolling release resource. + +A Project Rolling release resource defines an Rolling release on a Vercel Project. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). + + + + +## Schema + +### Required + +- `project_id` (String) The ID of the Project for the retention policy + +### Optional + +- `advancement_type` (String) The advancement type of the rolling release. Can be 'automatic' or 'manual-approve'. +- `canary_response_header` (Boolean) Whether the canary response header is enabled. This header is used to identify canary deployments. +- `enabled` (Boolean) Whether the rolling release is enabled. +- `stages` (List of Object) A list of stages for the rolling release. Each stage has a target percentage and duration. (see [below for nested schema](#nestedatt--stages)) +- `team_id` (String) The ID of the Vercel team. + + +### Nested Schema for `stages` + +Optional: + +- `duration` (Number) +- `targetPercentage` (Number) diff --git a/terraform.tfstate b/terraform.tfstate new file mode 100644 index 00000000..758586b6 --- /dev/null +++ b/terraform.tfstate @@ -0,0 +1,45 @@ +{ + "version": 4, + "terraform_version": "1.12.1", + "serial": 15, + "lineage": "38bfdd5e-a58a-77ba-9d1b-cbd3f7147262", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "vercel_project_rolling_release", + "name": "example", + "provider": "provider[\"registry.terraform.io/vercel/vercel\"]", + "instances": [ + { + "status": "tainted", + "schema_version": 0, + "attributes": { + "project_id": "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86", + "rolling_release": { + "advancement_type": "", + "canary_response_header": false, + "enabled": false, + "stages": [ + { + "duration": 300, + "require_approval": false, + "target_percentage": 10 + }, + { + "duration": 0, + "require_approval": false, + "target_percentage": 100 + } + ] + }, + "team_id": "team_4FWx5KQoszRi0ZmM9q9IBoKG" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + } + ], + "check_results": null +} diff --git a/terraform.tfstate.backup b/terraform.tfstate.backup new file mode 100644 index 00000000..8fb194e5 --- /dev/null +++ b/terraform.tfstate.backup @@ -0,0 +1,45 @@ +{ + "version": 4, + "terraform_version": "1.12.1", + "serial": 14, + "lineage": "38bfdd5e-a58a-77ba-9d1b-cbd3f7147262", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "vercel_project_rolling_release", + "name": "example", + "provider": "provider[\"registry.terraform.io/vercel/vercel\"]", + "instances": [ + { + "status": "tainted", + "schema_version": 0, + "attributes": { + "project_id": "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86", + "rolling_release": { + "advancement_type": "", + "canary_response_header": false, + "enabled": false, + "stages": [ + { + "duration": 300, + "require_approval": false, + "target_percentage": 10 + }, + { + "duration": 0, + "require_approval": false, + "target_percentage": 100 + } + ] + }, + "team_id": "team_4FWx5KQoszRi0ZmM9q9IBoKG" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + } + ], + "check_results": null +} diff --git a/test/api_test.go b/test/api_test.go new file mode 100644 index 00000000..e1af2128 --- /dev/null +++ b/test/api_test.go @@ -0,0 +1,114 @@ +package test + +import ( + "context" + "os" + "testing" + + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +func TestRollingReleaseAPI(t *testing.T) { + token := os.Getenv("VERCEL_API_TOKEN") + if token == "" { + t.Skip("VERCEL_API_TOKEN not set") + } + + c := client.New(token) + ctx := context.Background() + + projectID := "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86" + teamID := "team_4FWx5KQoszRi0ZmM9q9IBoKG" + + // First, get the current state + current, err := c.GetRollingRelease(ctx, projectID, teamID) + if err != nil { + t.Fatalf("Failed to get current state: %v", err) + } + + t.Logf("Current state: %+v", current) + + // Define default stages that meet API requirements + defaultStages := []client.RollingReleaseStage{ + { + TargetPercentage: 10, + Duration: 60, // 1 hour in minutes + }, + { + TargetPercentage: 50, + Duration: 120, // 2 hours in minutes + }, + { + TargetPercentage: 100, // Final stage must be 100% + }, + } + + // Try different combinations + tests := []struct { + name string + config client.RollingRelease + }{ + { + name: "disable rolling release", + config: client.RollingRelease{ + Enabled: false, + }, + }, + { + name: "enable with automatic advancement", + config: client.RollingRelease{ + Enabled: true, + AdvancementType: "automatic", + Stages: defaultStages, + }, + }, + { + name: "enable with manual approval", + config: client.RollingRelease{ + Enabled: true, + AdvancementType: "manual-approval", + Stages: []client.RollingReleaseStage{ + { + TargetPercentage: 5, + }, + { + TargetPercentage: 25, + }, + { + TargetPercentage: 60, + }, + { + TargetPercentage: 100, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := client.UpdateRollingReleaseRequest{ + RollingRelease: tc.config, + ProjectID: projectID, + TeamID: teamID, + } + + // Update the configuration + t.Logf("Sending request: %+v", request) + updated, err := c.UpdateRollingRelease(ctx, request) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + + t.Logf("Update response: %+v", updated) + + // Get the state again to verify + final, err := c.GetRollingRelease(ctx, projectID, teamID) + if err != nil { + t.Fatalf("Failed to get final state: %v", err) + } + + t.Logf("Final state: %+v", final) + }) + } +} \ No newline at end of file From ba41d20d3f1e07b7412dad0afa679bfbde71dbe1 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 6 Jun 2025 14:35:10 -0600 Subject: [PATCH 007/133] [rolling-release] add in docs --- terraform.tfstate | 45 ---------------- terraform.tfstate.backup | 45 ---------------- test/api_test.go | 114 --------------------------------------- 3 files changed, 204 deletions(-) delete mode 100644 terraform.tfstate delete mode 100644 terraform.tfstate.backup delete mode 100644 test/api_test.go diff --git a/terraform.tfstate b/terraform.tfstate deleted file mode 100644 index 758586b6..00000000 --- a/terraform.tfstate +++ /dev/null @@ -1,45 +0,0 @@ -{ - "version": 4, - "terraform_version": "1.12.1", - "serial": 15, - "lineage": "38bfdd5e-a58a-77ba-9d1b-cbd3f7147262", - "outputs": {}, - "resources": [ - { - "mode": "managed", - "type": "vercel_project_rolling_release", - "name": "example", - "provider": "provider[\"registry.terraform.io/vercel/vercel\"]", - "instances": [ - { - "status": "tainted", - "schema_version": 0, - "attributes": { - "project_id": "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86", - "rolling_release": { - "advancement_type": "", - "canary_response_header": false, - "enabled": false, - "stages": [ - { - "duration": 300, - "require_approval": false, - "target_percentage": 10 - }, - { - "duration": 0, - "require_approval": false, - "target_percentage": 100 - } - ] - }, - "team_id": "team_4FWx5KQoszRi0ZmM9q9IBoKG" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - } - ], - "check_results": null -} diff --git a/terraform.tfstate.backup b/terraform.tfstate.backup deleted file mode 100644 index 8fb194e5..00000000 --- a/terraform.tfstate.backup +++ /dev/null @@ -1,45 +0,0 @@ -{ - "version": 4, - "terraform_version": "1.12.1", - "serial": 14, - "lineage": "38bfdd5e-a58a-77ba-9d1b-cbd3f7147262", - "outputs": {}, - "resources": [ - { - "mode": "managed", - "type": "vercel_project_rolling_release", - "name": "example", - "provider": "provider[\"registry.terraform.io/vercel/vercel\"]", - "instances": [ - { - "status": "tainted", - "schema_version": 0, - "attributes": { - "project_id": "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86", - "rolling_release": { - "advancement_type": "", - "canary_response_header": false, - "enabled": false, - "stages": [ - { - "duration": 300, - "require_approval": false, - "target_percentage": 10 - }, - { - "duration": 0, - "require_approval": false, - "target_percentage": 100 - } - ] - }, - "team_id": "team_4FWx5KQoszRi0ZmM9q9IBoKG" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - } - ], - "check_results": null -} diff --git a/test/api_test.go b/test/api_test.go deleted file mode 100644 index e1af2128..00000000 --- a/test/api_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package test - -import ( - "context" - "os" - "testing" - - "github.com/vercel/terraform-provider-vercel/v3/client" -) - -func TestRollingReleaseAPI(t *testing.T) { - token := os.Getenv("VERCEL_API_TOKEN") - if token == "" { - t.Skip("VERCEL_API_TOKEN not set") - } - - c := client.New(token) - ctx := context.Background() - - projectID := "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86" - teamID := "team_4FWx5KQoszRi0ZmM9q9IBoKG" - - // First, get the current state - current, err := c.GetRollingRelease(ctx, projectID, teamID) - if err != nil { - t.Fatalf("Failed to get current state: %v", err) - } - - t.Logf("Current state: %+v", current) - - // Define default stages that meet API requirements - defaultStages := []client.RollingReleaseStage{ - { - TargetPercentage: 10, - Duration: 60, // 1 hour in minutes - }, - { - TargetPercentage: 50, - Duration: 120, // 2 hours in minutes - }, - { - TargetPercentage: 100, // Final stage must be 100% - }, - } - - // Try different combinations - tests := []struct { - name string - config client.RollingRelease - }{ - { - name: "disable rolling release", - config: client.RollingRelease{ - Enabled: false, - }, - }, - { - name: "enable with automatic advancement", - config: client.RollingRelease{ - Enabled: true, - AdvancementType: "automatic", - Stages: defaultStages, - }, - }, - { - name: "enable with manual approval", - config: client.RollingRelease{ - Enabled: true, - AdvancementType: "manual-approval", - Stages: []client.RollingReleaseStage{ - { - TargetPercentage: 5, - }, - { - TargetPercentage: 25, - }, - { - TargetPercentage: 60, - }, - { - TargetPercentage: 100, - }, - }, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - request := client.UpdateRollingReleaseRequest{ - RollingRelease: tc.config, - ProjectID: projectID, - TeamID: teamID, - } - - // Update the configuration - t.Logf("Sending request: %+v", request) - updated, err := c.UpdateRollingRelease(ctx, request) - if err != nil { - t.Fatalf("Update failed: %v", err) - } - - t.Logf("Update response: %+v", updated) - - // Get the state again to verify - final, err := c.GetRollingRelease(ctx, projectID, teamID) - if err != nil { - t.Fatalf("Failed to get final state: %v", err) - } - - t.Logf("Final state: %+v", final) - }) - } -} \ No newline at end of file From c1370d6e723e3588a8a764093f5a2e8d477cf114 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 6 Jun 2025 14:37:36 -0600 Subject: [PATCH 008/133] [rolling-release] add in docs --- docs/data-sources/project_rolling_release.md | 35 ++++++++++------ docs/resources/project_rolling_release.md | 42 +++++++++++--------- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index e99a1b54..971f58c2 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -3,18 +3,12 @@ page_title: "vercel_project_rolling_release Data Source - terraform-provider-vercel" subcategory: "" description: |- - Provides a Project Rolling Release datasource. - A Project Rolling Release datasource details information about a Rolling Release on a Vercel Project. - For more detailed information, please see the Vercel documentation https://vercel.com/docs/rolling-releases. + Data source for a Vercel project rolling release configuration. --- # vercel_project_rolling_release (Data Source) -Provides a Project Rolling Release datasource. - -A Project Rolling Release datasource details information about a Rolling Release on a Vercel Project. - -For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). +Data source for a Vercel project rolling release configuration. @@ -23,12 +17,27 @@ For more detailed information, please see the [Vercel documentation](https://ver ### Required -- `project_id` (String) The ID of the Project for the rolling release +- `project_id` (String) The ID of the project. +- `team_id` (String) The ID of the team the project exists in. -### Optional +### Read-Only -- `team_id` (String) The ID of the Vercel team. +- `rolling_release` (Attributes) The rolling release configuration. (see [below for nested schema](#nestedatt--rolling_release)) -### Read-Only + +### Nested Schema for `rolling_release` + +Read-Only: + +- `advancement_type` (String) The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true. +- `enabled` (Boolean) Whether rolling releases are enabled. +- `stages` (Attributes List) The stages of the rolling release. Required when enabled is true. (see [below for nested schema](#nestedatt--rolling_release--stages)) + + +### Nested Schema for `rolling_release.stages` + +Read-Only: -- `id` (String) The ID of this resource. +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. +- `require_approval` (Boolean) Whether approval is required before advancing to the next stage. +- `target_percentage` (Number) The percentage of traffic to route to this stage. diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 657b78cb..17690c45 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -3,18 +3,12 @@ page_title: "vercel_project_rolling_release Resource - terraform-provider-vercel" subcategory: "" description: |- - Provides a Project Rolling release resource. - A Project Rolling release resource defines an Rolling release on a Vercel Project. - For more detailed information, please see the Vercel documentation https://vercel.com/docs/rolling-releases. + Manages rolling release configuration for a Vercel project. --- # vercel_project_rolling_release (Resource) -Provides a Project Rolling release resource. - -A Project Rolling release resource defines an Rolling release on a Vercel Project. - -For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/rolling-releases). +Manages rolling release configuration for a Vercel project. @@ -23,20 +17,30 @@ For more detailed information, please see the [Vercel documentation](https://ver ### Required -- `project_id` (String) The ID of the Project for the retention policy +- `project_id` (String) The ID of the project. +- `rolling_release` (Attributes) The rolling release configuration. (see [below for nested schema](#nestedatt--rolling_release)) +- `team_id` (String) The ID of the team the project exists in. + + +### Nested Schema for `rolling_release` + +Required: + +- `enabled` (Boolean) Whether rolling releases are enabled. + +Optional: + +- `advancement_type` (String) The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true. +- `stages` (Attributes List) The stages of the rolling release. Required when enabled is true. (see [below for nested schema](#nestedatt--rolling_release--stages)) -### Optional + +### Nested Schema for `rolling_release.stages` -- `advancement_type` (String) The advancement type of the rolling release. Can be 'automatic' or 'manual-approve'. -- `canary_response_header` (Boolean) Whether the canary response header is enabled. This header is used to identify canary deployments. -- `enabled` (Boolean) Whether the rolling release is enabled. -- `stages` (List of Object) A list of stages for the rolling release. Each stage has a target percentage and duration. (see [below for nested schema](#nestedatt--stages)) -- `team_id` (String) The ID of the Vercel team. +Required: - -### Nested Schema for `stages` +- `target_percentage` (Number) The percentage of traffic to route to this stage. Optional: -- `duration` (Number) -- `targetPercentage` (Number) +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. +- `require_approval` (Boolean) Whether approval is required before advancing to the next stage. From 53f04f3a842351b79fac9ca05eb3c5d931918a2b Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 6 Jun 2025 14:45:28 -0600 Subject: [PATCH 009/133] Update resource_project_rolling_release.go --- vercel/resource_project_rolling_release.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index f427e184..aeccd98a 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -471,14 +471,14 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R if err != nil { resp.Diagnostics.AddError( "Error reading project rolling release", - fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", + fmt.Sprintf("Could not get project rolling release, unexpected error: %s", err, ), ) return } - result, diags := convertResponseToTFRollingRelease(out, nil, ctx) + result, _ := convertResponseToTFRollingRelease(out, nil, ctx) tflog.Info(ctx, "read project rolling release", map[string]any{ "team_id": result.TeamID.ValueString(), "project_id": result.ProjectID.ValueString(), @@ -592,7 +592,7 @@ func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req res return } - result, diags := convertResponseToTFRollingRelease(out, nil, ctx) + result, _ := convertResponseToTFRollingRelease(out, nil, ctx) tflog.Info(ctx, "imported project rolling release", map[string]any{ "team_id": result.TeamID.ValueString(), "project_id": result.ProjectID.ValueString(), From 2796b4b0a675ff60b46d710e1761759a007f7553 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 9 Jun 2025 15:36:31 -0400 Subject: [PATCH 010/133] fix error --- vercel/resource_project_rolling_release.go | 106 ++++++++++----------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index aeccd98a..f58a14bf 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -4,14 +4,14 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/vercel/terraform-provider-vercel/v3/client" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/attr" ) var ( @@ -81,47 +81,47 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S Attributes: map[string]schema.Attribute{ "project_id": schema.StringAttribute{ MarkdownDescription: "The ID of the project.", - Required: true, + Required: true, }, "team_id": schema.StringAttribute{ MarkdownDescription: "The ID of the team the project exists in.", - Required: true, + Required: true, }, "rolling_release": schema.SingleNestedAttribute{ MarkdownDescription: "The rolling release configuration.", - Required: true, + Required: true, Attributes: map[string]schema.Attribute{ "enabled": schema.BoolAttribute{ MarkdownDescription: "Whether rolling releases are enabled.", - Required: true, + Required: true, }, "advancement_type": schema.StringAttribute{ MarkdownDescription: "The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true.", - Optional: true, - Computed: true, + Optional: true, + Computed: true, Validators: []validator.String{ advancementTypeValidator{}, }, }, "stages": schema.ListNestedAttribute{ MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", - Optional: true, - Computed: true, + Optional: true, + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, + Required: true, }, "duration": schema.Int64Attribute{ MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", - Optional: true, - Computed: true, + Optional: true, + Computed: true, }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", - Optional: true, - Computed: true, + Optional: true, + Computed: true, }, }, }, @@ -133,16 +133,16 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S } type TFRollingReleaseStage struct { - TargetPercentage types.Int64 `tfsdk:"target_percentage"` - Duration types.Int64 `tfsdk:"duration"` - RequireApproval types.Bool `tfsdk:"require_approval"` + TargetPercentage types.Int64 `tfsdk:"target_percentage"` + Duration types.Int64 `tfsdk:"duration"` + RequireApproval types.Bool `tfsdk:"require_approval"` } // TFRollingRelease reflects the state terraform stores internally for a project rolling release. type TFRollingRelease struct { - Enabled types.Bool `tfsdk:"enabled"` - AdvancementType types.String `tfsdk:"advancement_type"` - Stages types.List `tfsdk:"stages"` + Enabled types.Bool `tfsdk:"enabled"` + AdvancementType types.String `tfsdk:"advancement_type"` + Stages types.List `tfsdk:"stages"` } // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. @@ -154,13 +154,13 @@ type TFRollingReleaseInfo struct { type RollingReleaseStage struct { TargetPercentage int `json:"targetPercentage"` - Duration *int `json:"duration,omitempty"` - RequireApproval bool `json:"requireApproval"` + Duration *int `json:"duration,omitempty"` + RequireApproval bool `json:"requireApproval"` } type RollingRelease struct { - Enabled bool `json:"enabled"` - AdvancementType string `json:"advancementType"` + Enabled bool `json:"enabled"` + AdvancementType string `json:"advancementType"` Stages []RollingReleaseStage `json:"stages"` } @@ -243,21 +243,21 @@ func convertStages(stages []client.RollingReleaseStage, advancementType string, elements[i] = types.ObjectValueMust( map[string]attr.Type{ "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, + "duration": types.Int64Type, + "require_approval": types.BoolType, }, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, - "duration": duration, - "require_approval": stage.RequireApproval, + "duration": duration, + "require_approval": stage.RequireApproval, }, ) } return types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, + "duration": types.Int64Type, + "require_approval": types.BoolType, }, }, elements) } @@ -267,8 +267,8 @@ func convertStages(stages []client.RollingReleaseStage, advancementType string, return types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, + "duration": types.Int64Type, + "require_approval": types.BoolType, }, }, []attr.Value{}) } @@ -283,7 +283,7 @@ func convertStages(stages []client.RollingReleaseStage, advancementType string, if i < len(planStages) { targetPercentage = planStages[i].TargetPercentage requireApproval = planStages[i].RequireApproval - + // Handle duration based on advancement type if advancementType == "automatic" { if planStages[i].Duration.IsUnknown() { @@ -315,13 +315,13 @@ func convertStages(stages []client.RollingReleaseStage, advancementType string, elements[i] = types.ObjectValueMust( map[string]attr.Type{ "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, + "duration": types.Int64Type, + "require_approval": types.BoolType, }, map[string]attr.Value{ "target_percentage": targetPercentage, - "duration": duration, - "require_approval": requireApproval, + "duration": duration, + "require_approval": requireApproval, }, ) } @@ -329,8 +329,8 @@ func convertStages(stages []client.RollingReleaseStage, advancementType string, return types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, + "duration": types.Int64Type, + "require_approval": types.BoolType, }, }, elements) } @@ -391,11 +391,11 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource } tflog.Debug(ctx, "Got plan from request", map[string]any{ - "project_id": plan.ProjectID.ValueString(), - "team_id": plan.TeamID.ValueString(), - "enabled": plan.RollingRelease.Enabled.ValueBool(), + "project_id": plan.ProjectID.ValueString(), + "team_id": plan.TeamID.ValueString(), + "enabled": plan.RollingRelease.Enabled.ValueBool(), "advancement_type": plan.RollingRelease.AdvancementType.ValueString(), - "stages": plan.RollingRelease.Stages, + "stages": plan.RollingRelease.Stages, }) _, err := r.client.GetProject(ctx, plan.ProjectID.ValueString(), plan.TeamID.ValueString()) @@ -439,11 +439,11 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource // Log the values for debugging tflog.Debug(ctx, "created project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), - "enabled": result.RollingRelease.Enabled.ValueBool(), + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "enabled": result.RollingRelease.Enabled.ValueBool(), "advancement_type": result.RollingRelease.AdvancementType.ValueString(), - "stages": result.RollingRelease.Stages, + "stages": result.RollingRelease.Stages, }) diags = resp.State.Set(ctx, result) @@ -554,11 +554,11 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource // Log the values for debugging tflog.Debug(ctx, "updated project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), - "enabled": result.RollingRelease.Enabled.ValueBool(), + "team_id": result.TeamID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "enabled": result.RollingRelease.Enabled.ValueBool(), "advancement_type": result.RollingRelease.AdvancementType.ValueString(), - "stages": result.RollingRelease.Stages, + "stages": result.RollingRelease.Stages, }) diags = resp.State.Set(ctx, result) @@ -598,7 +598,7 @@ func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req res "project_id": result.ProjectID.ValueString(), }) - diags = resp.State.Set(ctx, result) + diags := resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From 717d6de5cc3729c240ba77bf84c9ba8ca90a84f9 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 9 Jun 2025 15:48:55 -0400 Subject: [PATCH 011/133] gofmt --- vercel/data_source_project_rolling_release.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 93e99066..4523c412 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -51,40 +51,40 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour Attributes: map[string]schema.Attribute{ "project_id": schema.StringAttribute{ MarkdownDescription: "The ID of the project.", - Required: true, + Required: true, }, "team_id": schema.StringAttribute{ MarkdownDescription: "The ID of the team the project exists in.", - Required: true, + Required: true, }, "rolling_release": schema.SingleNestedAttribute{ MarkdownDescription: "The rolling release configuration.", - Computed: true, + Computed: true, Attributes: map[string]schema.Attribute{ "enabled": schema.BoolAttribute{ MarkdownDescription: "Whether rolling releases are enabled.", - Computed: true, + Computed: true, }, "advancement_type": schema.StringAttribute{ MarkdownDescription: "The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true.", - Computed: true, + Computed: true, }, "stages": schema.ListNestedAttribute{ MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", - Computed: true, + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ MarkdownDescription: "The percentage of traffic to route to this stage.", - Computed: true, + Computed: true, }, "duration": schema.Int64Attribute{ MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", - Computed: true, + Computed: true, }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", - Computed: true, + Computed: true, }, }, }, From dab88e5f554a326d0fbf99f142e8fcc5c4b25459 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 9 Jun 2025 15:53:58 -0400 Subject: [PATCH 012/133] gofmt --- client/project_rolling_release.go | 68 +++++++++++++++---------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 6662a70d..36640361 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -10,16 +10,16 @@ import ( // RollingReleaseStage represents a stage in a rolling release type RollingReleaseStage struct { - TargetPercentage int `json:"targetPercentage"` // Required: 0-100 - Duration *int `json:"duration,omitempty"` // Required for automatic advancement: 1-10000 minutes + TargetPercentage int `json:"targetPercentage"` // Required: 0-100 + Duration *int `json:"duration,omitempty"` // Required for automatic advancement: 1-10000 minutes RequireApproval bool `json:"requireApproval,omitempty"` // Only in response for manual-approval type } // RollingRelease represents the rolling release configuration type RollingRelease struct { - Enabled bool `json:"enabled"` // Required - AdvancementType string `json:"advancementType"` // Required when enabled=true: 'automatic' or 'manual-approval' - Stages []RollingReleaseStage `json:"stages"` // Required when enabled=true: 2-10 stages + Enabled bool `json:"enabled"` // Required + AdvancementType string `json:"advancementType"` // Required when enabled=true: 'automatic' or 'manual-approval' + Stages []RollingReleaseStage `json:"stages"` // Required when enabled=true: 2-10 stages } // ErrorResponse represents the error response from the Vercel API @@ -94,10 +94,10 @@ func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) tflog.Debug(ctx, "getting rolling-release configuration", map[string]any{ - "url": url, - "method": "GET", + "url": url, + "method": "GET", "project_id": projectID, - "team_id": teamID, + "team_id": teamID, }) d := RollingReleaseInfo{} @@ -140,7 +140,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling // Other stages can have all properties stageMap := map[string]any{ "targetPercentage": stage.TargetPercentage, - "requireApproval": stage.RequireApproval, + "requireApproval": stage.RequireApproval, } // Only include duration if it's set if stage.Duration != nil { @@ -158,15 +158,15 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling })) tflog.Debug(ctx, "updating rolling-release configuration", map[string]any{ - "url": url, - "method": "PATCH", - "project_id": request.ProjectID, - "team_id": request.TeamID, - "payload": payload, - "base_url": c.baseURL, - "enabled": request.RollingRelease.Enabled, + "url": url, + "method": "PATCH", + "project_id": request.ProjectID, + "team_id": request.TeamID, + "payload": payload, + "base_url": c.baseURL, + "enabled": request.RollingRelease.Enabled, "advancement_type": request.RollingRelease.AdvancementType, - "stages_count": len(request.RollingRelease.Stages), + "stages_count": len(request.RollingRelease.Stages), }) // Log each stage for debugging @@ -194,19 +194,19 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling var errResp ErrorResponse if resp != "" && json.Unmarshal([]byte(resp), &errResp) == nil { tflog.Error(ctx, "error updating rolling-release", map[string]any{ - "error_code": errResp.Error.Code, + "error_code": errResp.Error.Code, "error_message": errResp.Error.Message, - "url": url, - "payload": payload, - "response": resp, + "url": url, + "payload": payload, + "response": resp, }) return d, fmt.Errorf("failed to update rolling release: %s - %s", errResp.Error.Code, errResp.Error.Message) } tflog.Error(ctx, "error updating rolling-release", map[string]any{ - "error": err.Error(), - "url": url, - "payload": payload, + "error": err.Error(), + "url": url, + "payload": payload, "response": resp, }) return d, fmt.Errorf("failed to update rolling release: %w", err) @@ -215,11 +215,11 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling // Return the request state since we know it's valid result := RollingReleaseInfo{ ProjectID: request.ProjectID, - TeamID: request.TeamID, + TeamID: request.TeamID, RollingRelease: RollingRelease{ - Enabled: request.RollingRelease.Enabled, + Enabled: request.RollingRelease.Enabled, AdvancementType: request.RollingRelease.AdvancementType, - Stages: make([]RollingReleaseStage, len(request.RollingRelease.Stages)), + Stages: make([]RollingReleaseStage, len(request.RollingRelease.Stages)), }, } @@ -238,11 +238,11 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } tflog.Debug(ctx, "returning rolling release configuration", map[string]any{ - "project_id": result.ProjectID, - "team_id": result.TeamID, - "enabled": result.RollingRelease.Enabled, + "project_id": result.ProjectID, + "team_id": result.TeamID, + "enabled": result.RollingRelease.Enabled, "advancement_type": result.RollingRelease.AdvancementType, - "stages": result.RollingRelease.Stages, + "stages": result.RollingRelease.Stages, }) return result, nil @@ -253,10 +253,10 @@ func (c *Client) DeleteRollingRelease(ctx context.Context, projectID, teamID str url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) tflog.Debug(ctx, "deleting rolling-release configuration", map[string]any{ - "url": url, - "method": "DELETE", + "url": url, + "method": "DELETE", "project_id": projectID, - "team_id": teamID, + "team_id": teamID, }) var d RollingReleaseInfo From d4f277cecc5c97213ec768ce0f76779fbcfe7898 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 9 Jun 2025 21:14:15 -0400 Subject: [PATCH 013/133] rr test --- .../resource_project_rolling_release_test.go | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 vercel/resource_project_rolling_release_test.go diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go new file mode 100644 index 00000000..02c3f689 --- /dev/null +++ b/vercel/resource_project_rolling_release_test.go @@ -0,0 +1,214 @@ +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" + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +func testAccProjectRollingReleaseExists(testClient *client.Client, 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.GetRollingRelease(context.TODO(), rs.Primary.Attributes["project_id"], teamID) + return err + } +} + +func TestAcc_ProjectRollingRelease(t *testing.T) { + resourceName := "vercel_project_rolling_release.example" + nameSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), + ), + Steps: []resource.TestStep{ + { + Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix, testGithubRepo(t))), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), + + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "duration": "1", + "require_approval": "false", + "target_percentage": "15", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "duration": "1", + "require_approval": "false", + "target_percentage": "50", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "duration": "1", + "require_approval": "false", + "target_percentage": "100", + }), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), + ), + }, + { + Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix, testGithubRepo(t))), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), + + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "duration": "10", + "require_approval": "false", + "target_percentage": "15", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "duration": "1", + "require_approval": "false", + "target_percentage": "55", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "duration": "100", + "require_approval": "false", + "target_percentage": "80", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "duration": "1", + "require_approval": "false", + "target_percentage": "100", + }), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.3.id"), + ), + }, + { + Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), + ), + }, + }, + }) +} + +func testAccProjectRollingReleasesConfig(projectName string, githubRepo string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + rolling_release = { + enabled = true + advancement_type = "automatic" + stages = [ + { + duration = 1 + require_approval = false + target_percentage = 15 + }, + { + duration = 1 + require_approval = false + target_percentage = 50 + }, + { + duration = 1 + require_approval = false + target_percentage = 100 + } + ] + } +} +`, projectName, githubRepo) +} + +func testAccProjectRollingReleasesConfigUpdate(projectName string, githubRepo string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + rolling_release = { + enabled = true + advancement_type = "automatic" + stages = [ + { + duration = 10 + require_approval = false + target_percentage = 15 + }, + { + duration = 1 + require_approval = false + target_percentage = 55 + }, + { + duration = 100 + require_approval = false + target_percentage = 85 + }, + { + duration = 1 + require_approval = false + target_percentage = 100 + } + ] + } +} +`, projectName, githubRepo) +} + +func testAccProjectRollingReleasesConfigOff(projectName string, githubRepo string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" + + git_repository = { + type = "github" + repo = "%[2]s" + } +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + rolling_release = { + enabled = false + } +} +`, projectName, githubRepo) +} From e4172a67a54817839513a73fd2a74c29522eaf17 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Tue, 10 Jun 2025 10:18:26 -0400 Subject: [PATCH 014/133] fixes resource name --- .../resource_project_rolling_release_test.go | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 02c3f689..b8e4922d 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -40,69 +40,69 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix, testGithubRepo(t))), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "15", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "50", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "100", }), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), + resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.0.id"), + resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.1.id"), + resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.2.id"), ), }, { Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix, testGithubRepo(t))), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.stages.#", "4"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ "duration": "10", "require_approval": "false", "target_percentage": "15", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "55", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ "duration": "100", "require_approval": "false", "target_percentage": "80", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "100", }), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.3.id"), + resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.0.id"), + resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.1.id"), + resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.2.id"), + resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.3.id"), ), }, { Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.enabled", "false"), ), }, }, From 2708ab118eb5a76fe501dba53382c102368f429a Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Tue, 10 Jun 2025 11:29:42 -0400 Subject: [PATCH 015/133] resource name --- .../resource_project_rolling_release_test.go | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index b8e4922d..cb9e98bb 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -40,69 +40,69 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix, testGithubRepo(t))), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.stages.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "15", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "50", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "100", }), - resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.0.id"), - resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.1.id"), - resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.2.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), ), }, { Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix, testGithubRepo(t))), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.stages.#", "4"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "duration": "10", "require_approval": "false", "target_percentage": "15", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "55", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "duration": "100", "require_approval": "false", "target_percentage": "80", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "vercel_project_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "duration": "1", "require_approval": "false", "target_percentage": "100", }), - resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.0.id"), - resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.1.id"), - resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.2.id"), - resource.TestCheckResourceAttrSet(resourceName, "vercel_project_rolling_release.stages.3.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), + resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.3.id"), ), }, { Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "vercel_project_rolling_release.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), ), }, }, From 71edceff86f1561bb9dc175c8e4498cab842030b Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Wed, 11 Jun 2025 08:20:22 -0400 Subject: [PATCH 016/133] removes test --- .../resource_project_rolling_release_test.go | 214 ------------------ 1 file changed, 214 deletions(-) delete mode 100644 vercel/resource_project_rolling_release_test.go diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go deleted file mode 100644 index cb9e98bb..00000000 --- a/vercel/resource_project_rolling_release_test.go +++ /dev/null @@ -1,214 +0,0 @@ -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" - "github.com/vercel/terraform-provider-vercel/v3/client" -) - -func testAccProjectRollingReleaseExists(testClient *client.Client, 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.GetRollingRelease(context.TODO(), rs.Primary.Attributes["project_id"], teamID) - return err - } -} - -func TestAcc_ProjectRollingRelease(t *testing.T) { - resourceName := "vercel_project_rolling_release.example" - nameSuffix := acctest.RandString(16) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: resource.ComposeAggregateTestCheckFunc( - testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), - ), - Steps: []resource.TestStep{ - { - Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix, testGithubRepo(t))), - Check: resource.ComposeAggregateTestCheckFunc( - testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "advancement_type", "automatic"), - - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "duration": "1", - "require_approval": "false", - "target_percentage": "15", - }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "duration": "1", - "require_approval": "false", - "target_percentage": "50", - }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "duration": "1", - "require_approval": "false", - "target_percentage": "100", - }), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), - ), - }, - { - Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix, testGithubRepo(t))), - Check: resource.ComposeAggregateTestCheckFunc( - testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "advancement_type", "automatic"), - - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "duration": "10", - "require_approval": "false", - "target_percentage": "15", - }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "duration": "1", - "require_approval": "false", - "target_percentage": "55", - }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "duration": "100", - "require_approval": "false", - "target_percentage": "80", - }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "duration": "1", - "require_approval": "false", - "target_percentage": "100", - }), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.0.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.1.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.2.id"), - resource.TestCheckResourceAttrSet(resourceName, "rolling_release.stages.3.id"), - ), - }, - { - Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), - Check: resource.ComposeAggregateTestCheckFunc( - testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "enabled", "false"), - ), - }, - }, - }) -} - -func testAccProjectRollingReleasesConfig(projectName string, githubRepo string) string { - return fmt.Sprintf(` -resource "vercel_project" "example" { - name = "test-acc-example-project-%[1]s" - - git_repository = { - type = "github" - repo = "%[2]s" - } -} - -resource "vercel_project_rolling_release" "example" { - project_id = vercel_project.example.id - team_id = vercel_project.example.team_id - rolling_release = { - enabled = true - advancement_type = "automatic" - stages = [ - { - duration = 1 - require_approval = false - target_percentage = 15 - }, - { - duration = 1 - require_approval = false - target_percentage = 50 - }, - { - duration = 1 - require_approval = false - target_percentage = 100 - } - ] - } -} -`, projectName, githubRepo) -} - -func testAccProjectRollingReleasesConfigUpdate(projectName string, githubRepo string) string { - return fmt.Sprintf(` -resource "vercel_project" "example" { - name = "test-acc-example-project-%[1]s" - - git_repository = { - type = "github" - repo = "%[2]s" - } -} - -resource "vercel_project_rolling_release" "example" { - project_id = vercel_project.example.id - team_id = vercel_project.example.team_id - rolling_release = { - enabled = true - advancement_type = "automatic" - stages = [ - { - duration = 10 - require_approval = false - target_percentage = 15 - }, - { - duration = 1 - require_approval = false - target_percentage = 55 - }, - { - duration = 100 - require_approval = false - target_percentage = 85 - }, - { - duration = 1 - require_approval = false - target_percentage = 100 - } - ] - } -} -`, projectName, githubRepo) -} - -func testAccProjectRollingReleasesConfigOff(projectName string, githubRepo string) string { - return fmt.Sprintf(` -resource "vercel_project" "example" { - name = "test-acc-example-project-%[1]s" - - git_repository = { - type = "github" - repo = "%[2]s" - } -} - -resource "vercel_project_rolling_release" "example" { - project_id = vercel_project.example.id - team_id = vercel_project.example.team_id - rolling_release = { - enabled = false - } -} -`, projectName, githubRepo) -} From 374a3724fb24d1676326c4dca2927b02d7f14089 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Wed, 11 Jun 2025 15:39:37 -0600 Subject: [PATCH 017/133] [rolling-release] --- client/project_rolling_release.go | 285 ++++---- main.tf | 32 + vercel/data_source_project_rolling_release.go | 10 +- vercel/resource_project_rolling_release.go | 617 ++++++++++++------ .../resource_project_rolling_release_test.go | 214 ++++++ 5 files changed, 806 insertions(+), 352 deletions(-) create mode 100644 main.tf create mode 100644 vercel/resource_project_rolling_release_test.go diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 36640361..be1ce90c 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" + "sort" + "time" ) // RollingReleaseStage represents a stage in a rolling release @@ -52,18 +52,17 @@ func (r *RollingRelease) Validate() error { return fmt.Errorf("must have between 2 and 10 stages when enabled is true, got: %d", len(r.Stages)) } - // Validate last stage is 100% - lastStage := r.Stages[len(r.Stages)-1] - if lastStage.TargetPercentage != 100 { - return fmt.Errorf("last stage must have target_percentage=100, got: %d", lastStage.TargetPercentage) - } + // Sort stages by target percentage to ensure correct order + sort.Slice(r.Stages, func(i, j int) bool { + return r.Stages[i].TargetPercentage < r.Stages[j].TargetPercentage + }) // Validate stages are in ascending order and within bounds prevPercentage := 0 for i, stage := range r.Stages { // Validate percentage bounds - if stage.TargetPercentage < 1 || stage.TargetPercentage > 100 { - return fmt.Errorf("stage %d: target_percentage must be between 1 and 100, got: %d", i, stage.TargetPercentage) + if stage.TargetPercentage < 0 || stage.TargetPercentage > 100 { + return fmt.Errorf("stage %d: target_percentage must be between 0 and 100, got: %d", i, stage.TargetPercentage) } // Validate ascending order @@ -74,12 +73,32 @@ func (r *RollingRelease) Validate() error { // Validate duration for automatic advancement if r.AdvancementType == "automatic" { - if stage.Duration == nil || *stage.Duration < 1 || *stage.Duration > 10000 { - return fmt.Errorf("stage %d: duration must be between 1 and 10000 minutes for automatic advancement, got: %d", i, *stage.Duration) + if i < len(r.Stages)-1 { // All stages except last need duration + if stage.Duration == nil { + return fmt.Errorf("stage %d: duration is required for automatic advancement (except for the last stage)", i) + } + if *stage.Duration < 1 || *stage.Duration > 10000 { + return fmt.Errorf("stage %d: duration must be between 1 and 10000 minutes for automatic advancement, got: %d", i, *stage.Duration) + } + } else { // Last stage should not have duration + if stage.Duration != nil { + return fmt.Errorf("stage %d: last stage should not have duration for automatic advancement", i) + } + } + } else { + // For manual approval, no stages should have duration + if stage.Duration != nil { + return fmt.Errorf("stage %d: duration should not be set for manual-approval advancement type", i) } } } + // Validate last stage is 100% + lastStage := r.Stages[len(r.Stages)-1] + if lastStage.TargetPercentage != 100 { + return fmt.Errorf("last stage must have target_percentage=100, got: %d", lastStage.TargetPercentage) + } + return nil } @@ -93,22 +112,25 @@ type RollingReleaseInfo struct { func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (RollingReleaseInfo, error) { url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) - tflog.Debug(ctx, "getting rolling-release configuration", map[string]any{ - "url": url, - "method": "GET", - "project_id": projectID, - "team_id": teamID, - }) - - d := RollingReleaseInfo{} - err := c.doRequest(clientRequest{ + resp, err := c.doRequestWithResponse(clientRequest{ ctx: ctx, method: "GET", url: url, - }, &d) + }) + + if err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error getting rolling-release: %w", err) + } + + // Parse the response + var d RollingReleaseInfo + if err := json.Unmarshal([]byte(resp), &d); err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error parsing response: %w", err) + } d.ProjectID = projectID d.TeamID = teamID - return d, err + + return d, nil } // UpdateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to @@ -121,144 +143,131 @@ type UpdateRollingReleaseRequest struct { // UpdateRollingRelease will update an existing rolling release to the latest information. func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseInfo, error) { - // Validate the request - if err := request.RollingRelease.Validate(); err != nil { - return RollingReleaseInfo{}, fmt.Errorf("invalid rolling release configuration: %w", err) - } - - url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID) + // If we're enabling, we need to do it in steps + if request.RollingRelease.Enabled { + // First ensure it's disabled + disabledRequest := UpdateRollingReleaseRequest{ + RollingRelease: RollingRelease{ + Enabled: false, + AdvancementType: "", + Stages: []RollingReleaseStage{}, + }, + ProjectID: request.ProjectID, + TeamID: request.TeamID, + } - // Process stages to ensure final stage only has targetPercentage - stages := make([]map[string]any, len(request.RollingRelease.Stages)) - for i, stage := range request.RollingRelease.Stages { - if i == len(request.RollingRelease.Stages)-1 { - // Final stage should only have targetPercentage - stages[i] = map[string]any{ - "targetPercentage": stage.TargetPercentage, - } - } else { - // Other stages can have all properties - stageMap := map[string]any{ - "targetPercentage": stage.TargetPercentage, - "requireApproval": stage.RequireApproval, - } - // Only include duration if it's set - if stage.Duration != nil { - stageMap["duration"] = *stage.Duration - } - stages[i] = stageMap + _, err := c.doRequestWithResponse(clientRequest{ + ctx: ctx, + method: "PATCH", + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + body: string(mustMarshal(disabledRequest.RollingRelease)), + }) + if err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error disabling rolling release: %w", err) } - } - // Send just the rolling release configuration, not the whole request - payload := string(mustMarshal(map[string]any{ - "enabled": request.RollingRelease.Enabled, - "advancementType": request.RollingRelease.AdvancementType, - "stages": stages, - })) - - tflog.Debug(ctx, "updating rolling-release configuration", map[string]any{ - "url": url, - "method": "PATCH", - "project_id": request.ProjectID, - "team_id": request.TeamID, - "payload": payload, - "base_url": c.baseURL, - "enabled": request.RollingRelease.Enabled, - "advancement_type": request.RollingRelease.AdvancementType, - "stages_count": len(request.RollingRelease.Stages), - }) + // Wait a bit before proceeding + time.Sleep(2 * time.Second) - // Log each stage for debugging - for i, stage := range stages { - tflog.Debug(ctx, fmt.Sprintf("stage %d configuration", i), map[string]any{ - "stage": stage, + // Now validate the request + if err := request.RollingRelease.Validate(); err != nil { + return RollingReleaseInfo{}, fmt.Errorf("invalid rolling release configuration: %w", err) + } + + // Sort stages by target percentage + sort.Slice(request.RollingRelease.Stages, func(i, j int) bool { + return request.RollingRelease.Stages[i].TargetPercentage < request.RollingRelease.Stages[j].TargetPercentage }) - } - var d RollingReleaseInfo - resp, err := c.doRequestWithResponse(clientRequest{ - ctx: ctx, - method: "PATCH", - url: url, - body: payload, - }) + // First set up the stages + stagesRequest := map[string]any{ + "enabled": false, + "advancementType": request.RollingRelease.AdvancementType, + "stages": request.RollingRelease.Stages, + } - // Always log the raw response for debugging - tflog.Debug(ctx, "received raw response", map[string]any{ - "response": resp, - }) + _, err = c.doRequestWithResponse(clientRequest{ + ctx: ctx, + method: "PATCH", + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + body: string(mustMarshal(stagesRequest)), + }) + if err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error configuring stages: %w", err) + } - if err != nil { - // Try to parse error response - var errResp ErrorResponse - if resp != "" && json.Unmarshal([]byte(resp), &errResp) == nil { - tflog.Error(ctx, "error updating rolling-release", map[string]any{ - "error_code": errResp.Error.Code, - "error_message": errResp.Error.Message, - "url": url, - "payload": payload, - "response": resp, - }) - return d, fmt.Errorf("failed to update rolling release: %s - %s", errResp.Error.Code, errResp.Error.Message) + // Wait a bit before enabling + time.Sleep(2 * time.Second) + + // Finally enable it + enableRequest := map[string]any{ + "enabled": true, + "advancementType": request.RollingRelease.AdvancementType, + "stages": request.RollingRelease.Stages, } - tflog.Error(ctx, "error updating rolling-release", map[string]any{ - "error": err.Error(), - "url": url, - "payload": payload, - "response": resp, + resp, err := c.doRequestWithResponse(clientRequest{ + ctx: ctx, + method: "PATCH", + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + body: string(mustMarshal(enableRequest)), }) - return d, fmt.Errorf("failed to update rolling release: %w", err) - } + if err != nil { + // Try to parse error response + var errResp ErrorResponse + if jsonErr := json.Unmarshal([]byte(resp), &errResp); jsonErr == nil && errResp.Error.Message != "" { + return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %s - %s", errResp.Error.Code, errResp.Error.Message) + } + return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %w", err) + } - // Return the request state since we know it's valid - result := RollingReleaseInfo{ - ProjectID: request.ProjectID, - TeamID: request.TeamID, - RollingRelease: RollingRelease{ - Enabled: request.RollingRelease.Enabled, - AdvancementType: request.RollingRelease.AdvancementType, - Stages: make([]RollingReleaseStage, len(request.RollingRelease.Stages)), - }, - } + // Parse the response + var result RollingReleaseInfo + if err := json.Unmarshal([]byte(resp), &result); err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error parsing response: %w", err) + } + result.ProjectID = request.ProjectID + result.TeamID = request.TeamID + + return result, nil + } else { + // For disabling, just send the request as is + disabledRequest := UpdateRollingReleaseRequest{ + RollingRelease: RollingRelease{ + Enabled: false, + AdvancementType: "", + Stages: []RollingReleaseStage{}, + }, + ProjectID: request.ProjectID, + TeamID: request.TeamID, + } - // Copy stages, preserving the duration and requireApproval for non-final stages - for i, stage := range request.RollingRelease.Stages { - if i == len(request.RollingRelease.Stages)-1 { - // For the final stage, only include targetPercentage - result.RollingRelease.Stages[i] = RollingReleaseStage{ - TargetPercentage: stage.TargetPercentage, - // Do not include Duration or RequireApproval for final stage - } - } else { - // For other stages, include all properties - result.RollingRelease.Stages[i] = stage + resp, err := c.doRequestWithResponse(clientRequest{ + ctx: ctx, + method: "PATCH", + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + body: string(mustMarshal(disabledRequest.RollingRelease)), + }) + if err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error disabling rolling release: %w", err) } - } - tflog.Debug(ctx, "returning rolling release configuration", map[string]any{ - "project_id": result.ProjectID, - "team_id": result.TeamID, - "enabled": result.RollingRelease.Enabled, - "advancement_type": result.RollingRelease.AdvancementType, - "stages": result.RollingRelease.Stages, - }) + // Parse the response + var result RollingReleaseInfo + if err := json.Unmarshal([]byte(resp), &result); err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error parsing response: %w", err) + } + result.ProjectID = request.ProjectID + result.TeamID = request.TeamID - return result, nil + return result, nil + } } // DeleteRollingRelease will delete the rolling release for a given project. func (c *Client) DeleteRollingRelease(ctx context.Context, projectID, teamID string) error { url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) - tflog.Debug(ctx, "deleting rolling-release configuration", map[string]any{ - "url": url, - "method": "DELETE", - "project_id": projectID, - "team_id": teamID, - }) - var d RollingReleaseInfo err := c.doRequest(clientRequest{ ctx: ctx, diff --git a/main.tf b/main.tf new file mode 100644 index 00000000..4be7e248 --- /dev/null +++ b/main.tf @@ -0,0 +1,32 @@ +terraform { + required_providers { + vercel = { + source = "vercel/vercel" + } + } +} + +resource "vercel_project_rolling_release" "example" { + project_id = "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86" + team_id = "team_4FWx5KQoszRi0ZmM9q9IBoKG" + rolling_release = { + enabled = true + advancement_type = "automatic" + stages = [ + { + duration = 1 + require_approval = false + target_percentage = 5 # Start with 5% + }, + { + duration = 1 + require_approval = false + target_percentage = 50 # Then 50% + }, + { + require_approval = false # No duration for last stage + target_percentage = 100 # Finally 100% + } + ] + } +} \ No newline at end of file diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 4523c412..f648deef 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -140,16 +140,16 @@ func convertResponseToTFRollingReleaseDataSource(response client.RollingReleaseI result := TFRollingReleaseInfoDataSource{ RollingRelease: TFRollingReleaseDataSource{ Enabled: types.BoolValue(response.RollingRelease.Enabled), - AdvancementType: types.StringNull(), - Stages: make([]TFRollingReleaseStageDataSource, 0), + AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), + Stages: convertStagesDataSource(response.RollingRelease.Stages), }, ProjectID: types.StringValue(response.ProjectID), TeamID: types.StringValue(response.TeamID), } - if response.RollingRelease.Enabled { - result.RollingRelease.AdvancementType = types.StringValue(response.RollingRelease.AdvancementType) - result.RollingRelease.Stages = convertStagesDataSource(response.RollingRelease.Stages) + if !response.RollingRelease.Enabled { + result.RollingRelease.AdvancementType = types.StringValue("") + result.RollingRelease.Stages = make([]TFRollingReleaseStageDataSource, 0) } return result diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index f58a14bf..3622d9a3 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -3,9 +3,12 @@ package vercel import ( "context" "fmt" + "sort" + "time" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -62,15 +65,35 @@ func (v advancementTypeValidator) MarkdownDescription(ctx context.Context) strin } func (v advancementTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - value := req.ConfigValue.ValueString() - if value != "automatic" && value != "manual-approval" { + // Get the value of enabled from the parent object + var enabled types.Bool + diags := req.Config.GetAttribute(ctx, path.Root("rolling_release").AtName("enabled"), &enabled) + if diags.HasError() { resp.Diagnostics.AddError( - "Invalid advancement_type", - fmt.Sprintf("advancement_type must be either 'automatic' or 'manual-approval', got: %s", value), + "Error validating advancement_type", + "Could not get enabled value from configuration", ) + return + } + + // Only validate when enabled is true + if enabled.ValueBool() { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + resp.Diagnostics.AddError( + "Invalid advancement_type", + "advancement_type is required when enabled is true", + ) + return + } + + value := req.ConfigValue.ValueString() + if value != "automatic" && value != "manual-approval" { + resp.Diagnostics.AddError( + "Invalid advancement_type", + fmt.Sprintf("advancement_type must be either 'automatic' or 'manual-approval', got: %s", value), + ) + return + } } } @@ -116,7 +139,6 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S "duration": schema.Int64Attribute{ MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", Optional: true, - Computed: true, }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", @@ -176,46 +198,137 @@ func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRol var diags diag.Diagnostics if e.RollingRelease.Enabled.ValueBool() { - if !e.RollingRelease.AdvancementType.IsNull() { - advancementType = e.RollingRelease.AdvancementType.ValueString() - } else { - advancementType = "manual-approval" // Default to manual-approval if not specified + // When enabled, both advancement_type and stages are required + if e.RollingRelease.AdvancementType.IsNull() || e.RollingRelease.AdvancementType.IsUnknown() { + diags.AddError( + "Error creating rolling release", + "advancement_type is required when enabled is true", + ) + return client.UpdateRollingReleaseRequest{}, diags + } + advancementType = e.RollingRelease.AdvancementType.ValueString() + + if e.RollingRelease.Stages.IsNull() || e.RollingRelease.Stages.IsUnknown() { + diags.AddError( + "Error creating rolling release", + "stages are required when enabled is true", + ) + return client.UpdateRollingReleaseRequest{}, diags } // Convert stages from types.List to []client.RollingReleaseStage var tfStages []TFRollingReleaseStage - if !e.RollingRelease.Stages.IsNull() && !e.RollingRelease.Stages.IsUnknown() { - diags = e.RollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) - if diags.HasError() { + diags = e.RollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) + if diags.HasError() { + return client.UpdateRollingReleaseRequest{}, diags + } + + // Validate stages + if len(tfStages) < 2 || len(tfStages) > 10 { + diags.AddError( + "Error creating rolling release", + fmt.Sprintf("must have between 2 and 10 stages when enabled is true, got: %d", len(tfStages)), + ) + return client.UpdateRollingReleaseRequest{}, diags + } + + // Sort stages by target percentage to ensure correct order + sort.Slice(tfStages, func(i, j int) bool { + return tfStages[i].TargetPercentage.ValueInt64() < tfStages[j].TargetPercentage.ValueInt64() + }) + + // Validate stages are in ascending order and within bounds + prevPercentage := int64(0) + for i, stage := range tfStages { + percentage := stage.TargetPercentage.ValueInt64() + + // Validate percentage bounds + if percentage < 0 || percentage > 100 { + diags.AddError( + "Error creating rolling release", + fmt.Sprintf("stage %d: target_percentage must be between 0 and 100, got: %d", i, percentage), + ) return client.UpdateRollingReleaseRequest{}, diags } - stages = make([]client.RollingReleaseStage, len(tfStages)) - for i, stage := range tfStages { - // For automatic advancement, set a default duration if not provided - if advancementType == "automatic" { - var duration int = 60 // Default duration in minutes - if !stage.Duration.IsNull() { - duration = int(stage.Duration.ValueInt64()) - } - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - Duration: &duration, - RequireApproval: stage.RequireApproval.ValueBool(), + + // Validate ascending order + if percentage <= prevPercentage { + diags.AddError( + "Error creating rolling release", + fmt.Sprintf("stage %d: target_percentage must be greater than previous stage (%d), got: %d", i, prevPercentage, percentage), + ) + return client.UpdateRollingReleaseRequest{}, diags + } + prevPercentage = percentage + } + + // Validate last stage is 100% + lastStage := tfStages[len(tfStages)-1] + if lastStage.TargetPercentage.ValueInt64() != 100 { + diags.AddError( + "Error creating rolling release", + fmt.Sprintf("last stage must have target_percentage=100, got: %d", lastStage.TargetPercentage.ValueInt64()), + ) + return client.UpdateRollingReleaseRequest{}, diags + } + + stages = make([]client.RollingReleaseStage, len(tfStages)) + for i, stage := range tfStages { + if advancementType == "automatic" { + // For automatic advancement, duration is required except for last stage + if i < len(tfStages)-1 { + // Non-last stage needs duration + if stage.Duration.IsNull() { + // Default duration for non-last stages + duration := 60 + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + Duration: &duration, + RequireApproval: stage.RequireApproval.ValueBool(), + } + } else { + duration := int(stage.Duration.ValueInt64()) + if duration < 1 || duration > 10000 { + diags.AddError( + "Error creating rolling release", + fmt.Sprintf("stage %d: duration must be between 1 and 10000 minutes for automatic advancement, got: %d", i, duration), + ) + return client.UpdateRollingReleaseRequest{}, diags + } + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + Duration: &duration, + RequireApproval: stage.RequireApproval.ValueBool(), + } } } else { - // For manual approval, omit duration field completely + // Last stage should not have duration stages[i] = client.RollingReleaseStage{ TargetPercentage: int(stage.TargetPercentage.ValueInt64()), RequireApproval: stage.RequireApproval.ValueBool(), } } + } else { + // For manual approval, omit duration field completely + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + RequireApproval: stage.RequireApproval.ValueBool(), + } } } } else { - // When disabled, don't send any stages to the API + // When disabled, don't send any stages or advancement type to the API stages = []client.RollingReleaseStage{} + advancementType = "" } + // Log the request for debugging + tflog.Debug(context.Background(), "converting to update request", map[string]any{ + "enabled": e.RollingRelease.Enabled.ValueBool(), + "advancement_type": advancementType, + "stages_count": len(stages), + }) + return client.UpdateRollingReleaseRequest{ RollingRelease: client.RollingRelease{ Enabled: e.RollingRelease.Enabled.ValueBool(), @@ -227,89 +340,109 @@ func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRol }, diags } -func convertStages(stages []client.RollingReleaseStage, advancementType string, planStages []TFRollingReleaseStage, enabled bool, ctx context.Context) (types.List, diag.Diagnostics) { - // If disabled, always return plan stages to preserve state - if !enabled && len(planStages) > 0 { - elements := make([]attr.Value, len(planStages)) - for i, stage := range planStages { - // For disabled state, ensure duration is known - var duration types.Int64 - if stage.Duration.IsUnknown() { - duration = types.Int64Null() - } else { - duration = stage.Duration - } +func convertResponseToTFRollingRelease(response client.RollingReleaseInfo, plan *TFRollingReleaseInfo, ctx context.Context) (TFRollingReleaseInfo, diag.Diagnostics) { + var diags diag.Diagnostics - elements[i] = types.ObjectValueMust( - map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, - }, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - "duration": duration, - "require_approval": stage.RequireApproval, - }, - ) + // If we have a plan and the response doesn't match what we expect, + // this is likely a race condition and we should preserve the plan values + if plan != nil && plan.RollingRelease.Enabled.ValueBool() { + // If the plan is enabled but response shows disabled or missing fields, + // this is likely a race condition + if !response.RollingRelease.Enabled || + response.RollingRelease.AdvancementType == "" || + len(response.RollingRelease.Stages) == 0 { + tflog.Debug(ctx, "detected race condition, preserving plan values", map[string]any{ + "plan_enabled": plan.RollingRelease.Enabled.ValueBool(), + "plan_advancement_type": plan.RollingRelease.AdvancementType.ValueString(), + "plan_stages_count": len(plan.RollingRelease.Stages.Elements()), + "response_enabled": response.RollingRelease.Enabled, + "response_advancement_type": response.RollingRelease.AdvancementType, + "response_stages_count": len(response.RollingRelease.Stages), + }) + return *plan, diags } - return types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, - }, - }, elements) } - // If no stages from API and no plan stages, return empty list - if len(stages) == 0 { - return types.ListValueFrom(ctx, types.ObjectType{ + result := TFRollingReleaseInfo{ + RollingRelease: TFRollingRelease{ + Enabled: types.BoolValue(response.RollingRelease.Enabled), + AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), + }, + ProjectID: types.StringValue(response.ProjectID), + TeamID: types.StringValue(response.TeamID), + } + + // If disabled, return empty values + if !response.RollingRelease.Enabled { + result.RollingRelease.AdvancementType = types.StringValue("") + // Create an empty list instead of null + emptyStages, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, "duration": types.Int64Type, "require_approval": types.BoolType, }, }, []attr.Value{}) + diags.Append(stagesDiags...) + if diags.HasError() { + return result, diags + } + result.RollingRelease.Stages = emptyStages + return result, diags } - elements := make([]attr.Value, len(stages)) - for i, stage := range stages { - targetPercentage := types.Int64Value(int64(stage.TargetPercentage)) - requireApproval := types.BoolValue(stage.RequireApproval) - var duration types.Int64 + // If we have a plan, try to match stages by target percentage to preserve order + var orderedStages []client.RollingReleaseStage + if plan != nil && !plan.RollingRelease.Stages.IsNull() && !plan.RollingRelease.Stages.IsUnknown() { + var planStages []TFRollingReleaseStage + diags.Append(plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false)...) + if diags.HasError() { + return result, diags + } - // If we have plan stages, preserve the values but ensure they're known - if i < len(planStages) { - targetPercentage = planStages[i].TargetPercentage - requireApproval = planStages[i].RequireApproval + // Create a map of target percentage to stage for quick lookup + stageMap := make(map[int]client.RollingReleaseStage) + for _, stage := range response.RollingRelease.Stages { + stageMap[stage.TargetPercentage] = stage + } - // Handle duration based on advancement type - if advancementType == "automatic" { - if planStages[i].Duration.IsUnknown() { - // For unknown values, use API value or default - if stage.Duration != nil { - duration = types.Int64Value(int64(*stage.Duration)) - } else { - duration = types.Int64Value(60) // Default duration in minutes - } - } else { - duration = planStages[i].Duration - } - } else { - duration = types.Int64Null() // Manual approval doesn't use duration + // Try to preserve the order from the plan + orderedStages = make([]client.RollingReleaseStage, 0, len(response.RollingRelease.Stages)) + for _, planStage := range planStages { + if stage, ok := stageMap[int(planStage.TargetPercentage.ValueInt64())]; ok { + orderedStages = append(orderedStages, stage) + delete(stageMap, stage.TargetPercentage) } - } else { - // Only set duration for automatic advancement - if advancementType == "automatic" { + } + + // Add any remaining stages that weren't in the plan + for _, stage := range response.RollingRelease.Stages { + if _, ok := stageMap[stage.TargetPercentage]; ok { + orderedStages = append(orderedStages, stage) + } + } + } else { + orderedStages = response.RollingRelease.Stages + } + + // Convert stages from response + elements := make([]attr.Value, len(orderedStages)) + for i, stage := range orderedStages { + var duration types.Int64 + if response.RollingRelease.AdvancementType == "automatic" { + // For automatic advancement, duration is required except for the last stage + if i < len(orderedStages)-1 { if stage.Duration != nil { duration = types.Int64Value(int64(*stage.Duration)) } else { duration = types.Int64Value(60) // Default duration in minutes } } else { - duration = types.Int64Null() + duration = types.Int64Value(0) // Last stage doesn't need duration } + } else { + // For manual approval, duration is not used + duration = types.Int64Value(0) } elements[i] = types.ObjectValueMust( @@ -319,70 +452,38 @@ func convertStages(stages []client.RollingReleaseStage, advancementType string, "require_approval": types.BoolType, }, map[string]attr.Value{ - "target_percentage": targetPercentage, + "target_percentage": types.Int64Value(int64(stage.TargetPercentage)), "duration": duration, - "require_approval": requireApproval, + "require_approval": types.BoolValue(stage.RequireApproval), }, ) } - return types.ListValueFrom(ctx, types.ObjectType{ + stages, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, "duration": types.Int64Type, "require_approval": types.BoolType, }, }, elements) -} - -func convertResponseToTFRollingRelease(response client.RollingReleaseInfo, plan *TFRollingReleaseInfo, ctx context.Context) (TFRollingReleaseInfo, diag.Diagnostics) { - var diags diag.Diagnostics - - result := TFRollingReleaseInfo{ - RollingRelease: TFRollingRelease{ - Enabled: types.BoolValue(response.RollingRelease.Enabled), - }, - ProjectID: types.StringValue(response.ProjectID), - TeamID: types.StringValue(response.TeamID), - } - - // Get plan stages if available - var planStages []TFRollingReleaseStage - if plan != nil && !plan.RollingRelease.Stages.IsNull() && !plan.RollingRelease.Stages.IsUnknown() { - diags.Append(plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false)...) - if diags.HasError() { - return result, diags - } - } - - if response.RollingRelease.Enabled { - result.RollingRelease.AdvancementType = types.StringValue(response.RollingRelease.AdvancementType) - } else { - result.RollingRelease.AdvancementType = types.StringNull() - } - - // Convert stages, passing enabled state to ensure proper preservation - stages, stagesDiags := convertStages( - response.RollingRelease.Stages, - response.RollingRelease.AdvancementType, - planStages, - response.RollingRelease.Enabled, - ctx, - ) diags.Append(stagesDiags...) if diags.HasError() { return result, diags } result.RollingRelease.Stages = stages + // Log the conversion result for debugging + tflog.Debug(ctx, "converted rolling release response", map[string]any{ + "enabled": result.RollingRelease.Enabled.ValueBool(), + "advancement_type": result.RollingRelease.AdvancementType.ValueString(), + "stages_count": len(elements), + }) + return result, diags } -// Create will create a new rolling release config on a Vercel project. -// This is called automatically by the provider when a new resource should be created. +// Create will create a rolling release for a Vercel project by sending a request to the Vercel API. func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - tflog.Debug(ctx, "Starting rolling release creation") - var plan TFRollingReleaseInfo diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) @@ -390,62 +491,74 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } - tflog.Debug(ctx, "Got plan from request", map[string]any{ - "project_id": plan.ProjectID.ValueString(), - "team_id": plan.TeamID.ValueString(), - "enabled": plan.RollingRelease.Enabled.ValueBool(), - "advancement_type": plan.RollingRelease.AdvancementType.ValueString(), - "stages": plan.RollingRelease.Stages, - }) - - _, err := r.client.GetProject(ctx, plan.ProjectID.ValueString(), plan.TeamID.ValueString()) - if client.NotFound(err) { - resp.Diagnostics.AddError( - "Error creating project rolling release", - "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 - } - if err != nil { - resp.Diagnostics.AddError( - "Error creating project rolling release", - "Error reading project information, unexpected error: "+err.Error(), - ) + // Convert plan to client request + request, diags := plan.toUpdateRollingReleaseRequest() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } - tflog.Debug(ctx, "Project exists, creating rolling release") + // Log the request for debugging + tflog.Debug(ctx, "creating rolling release", map[string]any{ + "enabled": request.RollingRelease.Enabled, + "advancement_type": request.RollingRelease.AdvancementType, + "stages": request.RollingRelease.Stages, + }) - updateRequest, diags := plan.toUpdateRollingReleaseRequest() - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + // If we're enabling, first create in disabled state then enable + if request.RollingRelease.Enabled { + // First create in disabled state + disabledRequest := client.UpdateRollingReleaseRequest{ + RollingRelease: client.RollingRelease{ + Enabled: false, + AdvancementType: "", + Stages: []client.RollingReleaseStage{}, + }, + ProjectID: request.ProjectID, + TeamID: request.TeamID, + } + + _, err := r.client.UpdateRollingRelease(ctx, disabledRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project rolling release", + fmt.Sprintf("Could not create project rolling release in disabled state, unexpected error: %s", + err, + ), + ) + return + } + + // Wait a bit before enabling + time.Sleep(2 * time.Second) } - response, err := r.client.UpdateRollingRelease(ctx, updateRequest) + out, err := r.client.UpdateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( "Error creating project rolling release", - "Could not create project rolling release, unexpected error: "+err.Error(), + fmt.Sprintf("Could not create project rolling release, unexpected error: %s", + err, + ), ) return } - result, diags := convertResponseToTFRollingRelease(response, &plan, ctx) + // Convert response to state + result, diags := convertResponseToTFRollingRelease(out, &plan, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - // Log the values for debugging - tflog.Debug(ctx, "created project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + // Log the result for debugging + tflog.Debug(ctx, "created rolling release", map[string]any{ "enabled": result.RollingRelease.Enabled.ValueBool(), "advancement_type": result.RollingRelease.AdvancementType.ValueString(), "stages": result.RollingRelease.Stages, }) + // Set state diags = resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -453,7 +566,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource } } -// Read will read an rolling release of a Vercel project by requesting it from the Vercel API, and will update terraform +// Read will read a rolling release of a Vercel project by requesting it from the Vercel API, and will update terraform // with this information. func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state TFRollingReleaseInfo @@ -478,51 +591,34 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R return } - result, _ := convertResponseToTFRollingRelease(out, nil, ctx) - tflog.Info(ctx, "read project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + // Log the response for debugging + tflog.Debug(ctx, "got rolling release from API", map[string]any{ + "enabled": out.RollingRelease.Enabled, + "advancement_type": out.RollingRelease.AdvancementType, + "stages": out.RollingRelease.Stages, }) - diags = resp.State.Set(ctx, result) + result, diags := convertResponseToTFRollingRelease(out, &state, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } -} -// Delete deletes a Vercel project rolling release. -func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state TFRollingReleaseInfo - diags := req.State.Get(ctx, &state) + // Log the result for debugging + tflog.Debug(ctx, "converted rolling release", map[string]any{ + "enabled": result.RollingRelease.Enabled.ValueBool(), + "advancement_type": result.RollingRelease.AdvancementType.ValueString(), + "stages": result.RollingRelease.Stages, + }) + + diags = resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - err := r.client.DeleteRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) - if client.NotFound(err) { - return - } - if err != nil { - resp.Diagnostics.AddError( - "Error deleting project rolling release", - fmt.Sprintf( - "Could not delete project rolling release %s, unexpected error: %s", - state.ProjectID.ValueString(), - err, - ), - ) - return - } - - tflog.Info(ctx, "deleted project rolling release", map[string]any{ - "team_id": state.TeamID.ValueString(), - "project_id": state.ProjectID.ValueString(), - }) } -// Update updates the project rolling release of a Vercel project state. +// Update will update an existing rolling release to the latest information. func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan TFRollingReleaseInfo diags := req.Plan.Get(ctx, &plan) @@ -531,31 +627,101 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource return } - updateRequest, diags := plan.toUpdateRollingReleaseRequest() + var state TFRollingReleaseInfo + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Convert plan to client request + request, diags := plan.toUpdateRollingReleaseRequest() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - response, err := r.client.UpdateRollingRelease(ctx, updateRequest) + // Log the request for debugging + tflog.Debug(ctx, "updating rolling release", map[string]any{ + "enabled": request.RollingRelease.Enabled, + "advancement_type": request.RollingRelease.AdvancementType, + "stages": request.RollingRelease.Stages, + }) + + // If we're transitioning from enabled to disabled, first disable + if state.RollingRelease.Enabled.ValueBool() && !request.RollingRelease.Enabled { + disabledRequest := client.UpdateRollingReleaseRequest{ + RollingRelease: client.RollingRelease{ + Enabled: false, + AdvancementType: "", + Stages: []client.RollingReleaseStage{}, + }, + ProjectID: request.ProjectID, + TeamID: request.TeamID, + } + + _, err := r.client.UpdateRollingRelease(ctx, disabledRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project rolling release", + fmt.Sprintf("Could not disable project rolling release, unexpected error: %s", + err, + ), + ) + return + } + + // Wait a bit before proceeding + time.Sleep(2 * time.Second) + } + + // If we're transitioning from disabled to enabled, first create in disabled state + if !state.RollingRelease.Enabled.ValueBool() && request.RollingRelease.Enabled { + disabledRequest := client.UpdateRollingReleaseRequest{ + RollingRelease: client.RollingRelease{ + Enabled: false, + AdvancementType: "", + Stages: []client.RollingReleaseStage{}, + }, + ProjectID: request.ProjectID, + TeamID: request.TeamID, + } + + _, err := r.client.UpdateRollingRelease(ctx, disabledRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project rolling release", + fmt.Sprintf("Could not create project rolling release in disabled state, unexpected error: %s", + err, + ), + ) + return + } + + // Wait a bit before enabling + time.Sleep(2 * time.Second) + } + + out, err := r.client.UpdateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( "Error updating project rolling release", - "Could not update project rolling release, unexpected error: "+err.Error(), + fmt.Sprintf("Could not update project rolling release, unexpected error: %s", + err, + ), ) return } - result, diags := convertResponseToTFRollingRelease(response, &plan, ctx) + // Convert response to state + result, diags := convertResponseToTFRollingRelease(out, &plan, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - // Log the values for debugging - tflog.Debug(ctx, "updated project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), + // Log the result for debugging + tflog.Debug(ctx, "updated rolling release", map[string]any{ "enabled": result.RollingRelease.Enabled.ValueBool(), "advancement_type": result.RollingRelease.AdvancementType.ValueString(), "stages": result.RollingRelease.Stages, @@ -568,6 +734,38 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource } } +// Delete will delete an existing rolling release by disabling it. +func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TFRollingReleaseInfo + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Disable rolling release + request := client.UpdateRollingReleaseRequest{ + RollingRelease: client.RollingRelease{ + Enabled: false, + AdvancementType: "", + Stages: []client.RollingReleaseStage{}, + }, + ProjectID: state.ProjectID.ValueString(), + TeamID: state.TeamID.ValueString(), + } + + _, err := r.client.UpdateRollingRelease(ctx, request) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project rolling release", + fmt.Sprintf("Could not delete project rolling release, unexpected error: %s", + err, + ), + ) + return + } +} + // ImportState takes an identifier and reads all the project rolling release information from the Vercel API. // The results are then stored in terraform state. func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -592,13 +790,14 @@ func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req res return } - result, _ := convertResponseToTFRollingRelease(out, nil, ctx) + // For import, we don't have any state to preserve + result, diags := convertResponseToTFRollingRelease(out, nil, ctx) tflog.Info(ctx, "imported project rolling release", map[string]any{ "team_id": result.TeamID.ValueString(), "project_id": result.ProjectID.ValueString(), }) - diags := resp.State.Set(ctx, result) + diags = resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go new file mode 100644 index 00000000..a8ab023a --- /dev/null +++ b/vercel/resource_project_rolling_release_test.go @@ -0,0 +1,214 @@ +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" + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +func testAccProjectRollingReleaseExists(testClient *client.Client, 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.GetRollingRelease(context.TODO(), rs.Primary.Attributes["project_id"], teamID) + return err + } +} + +func TestAcc_ProjectRollingRelease(t *testing.T) { + resourceName := "vercel_project_rolling_release.example" + nameSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), + ), + Steps: []resource.TestStep{ + // First create the resource in a disabled state + { + Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", ""), + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "0"), + ), + }, + // Then enable with initial configuration + { + Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix, testGithubRepo(t))), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "manual-approval"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "true", + "target_percentage": "20", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "true", + "target_percentage": "50", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "true", + "target_percentage": "100", + }), + ), + }, + // Then update to new configuration + { + Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix, testGithubRepo(t))), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "manual-approval"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "true", + "target_percentage": "20", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "true", + "target_percentage": "50", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "true", + "target_percentage": "80", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "true", + "target_percentage": "100", + }), + ), + }, + // Finally disable again + { + Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", ""), + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "0"), + ), + }, + }, + }) +} + +func testAccProjectRollingReleasesConfig(nameSuffix string, githubRepo string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%s" + git_repository = { + type = "github" + repo = "%s" + } + enable_preview_feedback = false + skew_protection = "12 hours" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + depends_on = [vercel_project.example] + rolling_release = { + enabled = true + advancement_type = "manual-approval" + stages = [ + { + require_approval = true + target_percentage = 20 + }, + { + require_approval = true + target_percentage = 50 + }, + { + require_approval = true + target_percentage = 100 + } + ] + } +} +`, nameSuffix, githubRepo) +} + +func testAccProjectRollingReleasesConfigUpdate(nameSuffix string, githubRepo string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%s" + git_repository = { + type = "github" + repo = "%s" + } + enable_preview_feedback = false + skew_protection = "12 hours" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + depends_on = [vercel_project.example] + rolling_release = { + enabled = true + advancement_type = "manual-approval" + stages = [ + { + require_approval = true + target_percentage = 20 + }, + { + require_approval = true + target_percentage = 50 + }, + { + require_approval = true + target_percentage = 80 + }, + { + require_approval = true + target_percentage = 100 + } + ] + } +} +`, nameSuffix, githubRepo) +} + +func testAccProjectRollingReleasesConfigOff(nameSuffix string, githubRepo string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%s" + git_repository = { + type = "github" + repo = "%s" + } + enable_preview_feedback = false + skew_protection = "12 hours" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + team_id = vercel_project.example.team_id + depends_on = [vercel_project.example] + rolling_release = { + enabled = false + advancement_type = "" + stages = [] + } +} +`, nameSuffix, githubRepo) +} From 5792285d98d1972e516a7fca168110e9769ea1d5 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 10:13:01 -0400 Subject: [PATCH 018/133] removes doRequestWithResponse --- client/project_rolling_release.go | 46 +++++++++----------------- client/request.go | 54 ------------------------------- 2 files changed, 15 insertions(+), 85 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index be1ce90c..b7617665 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -2,7 +2,6 @@ package client import ( "context" - "encoding/json" "fmt" "sort" "time" @@ -112,21 +111,17 @@ type RollingReleaseInfo struct { func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (RollingReleaseInfo, error) { url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) - resp, err := c.doRequestWithResponse(clientRequest{ + var d RollingReleaseInfo + err := c.doRequest(clientRequest{ ctx: ctx, method: "GET", url: url, - }) + }, &d) if err != nil { return RollingReleaseInfo{}, fmt.Errorf("error getting rolling-release: %w", err) } - // Parse the response - var d RollingReleaseInfo - if err := json.Unmarshal([]byte(resp), &d); err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error parsing response: %w", err) - } d.ProjectID = projectID d.TeamID = teamID @@ -156,12 +151,12 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling TeamID: request.TeamID, } - _, err := c.doRequestWithResponse(clientRequest{ + err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), body: string(mustMarshal(disabledRequest.RollingRelease)), - }) + }, nil) if err != nil { return RollingReleaseInfo{}, fmt.Errorf("error disabling rolling release: %w", err) } @@ -186,12 +181,12 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling "stages": request.RollingRelease.Stages, } - _, err = c.doRequestWithResponse(clientRequest{ + err = c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), body: string(mustMarshal(stagesRequest)), - }) + }, nil) if err != nil { return RollingReleaseInfo{}, fmt.Errorf("error configuring stages: %w", err) } @@ -206,26 +201,18 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling "stages": request.RollingRelease.Stages, } - resp, err := c.doRequestWithResponse(clientRequest{ + var result RollingReleaseInfo + + err = c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), body: string(mustMarshal(enableRequest)), - }) + }, &result) if err != nil { - // Try to parse error response - var errResp ErrorResponse - if jsonErr := json.Unmarshal([]byte(resp), &errResp); jsonErr == nil && errResp.Error.Message != "" { - return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %s - %s", errResp.Error.Code, errResp.Error.Message) - } return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %w", err) } - // Parse the response - var result RollingReleaseInfo - if err := json.Unmarshal([]byte(resp), &result); err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error parsing response: %w", err) - } result.ProjectID = request.ProjectID result.TeamID = request.TeamID @@ -242,21 +229,18 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling TeamID: request.TeamID, } - resp, err := c.doRequestWithResponse(clientRequest{ + var result RollingReleaseInfo + + err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), body: string(mustMarshal(disabledRequest.RollingRelease)), - }) + }, &result) if err != nil { return RollingReleaseInfo{}, fmt.Errorf("error disabling rolling release: %w", err) } - // Parse the response - var result RollingReleaseInfo - if err := json.Unmarshal([]byte(resp), &result); err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error parsing response: %w", err) - } result.ProjectID = request.ProjectID result.TeamID = request.TeamID diff --git a/client/request.go b/client/request.go index 798cb78b..75a58720 100644 --- a/client/request.go +++ b/client/request.go @@ -166,57 +166,3 @@ func (c *Client) _doRequest(req *http.Request, v any, errorOnNoContent bool) err return nil } - -// doRequestWithResponse is similar to doRequest but returns the raw response body as a string -func (c *Client) doRequestWithResponse(req clientRequest) (string, error) { - r, err := req.toHTTPRequest() - if err != nil { - return "", err - } - - r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token)) - resp, err := c.http().Do(r) - if err != nil { - return "", fmt.Errorf("error doing http request: %w", err) - } - - defer resp.Body.Close() - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("error reading response body: %w", err) - } - - if resp.StatusCode >= 300 { - var errorResponse APIError - if string(responseBody) == "" { - errorResponse.StatusCode = resp.StatusCode - return string(responseBody), errorResponse - } - err = json.Unmarshal(responseBody, &struct { - Error *APIError `json:"error"` - }{ - Error: &errorResponse, - }) - if errorResponse.Code == "" && errorResponse.Message == "" { - return string(responseBody), fmt.Errorf("error performing API request: %d %s", resp.StatusCode, string(responseBody)) - } - if err != nil { - return string(responseBody), fmt.Errorf("error unmarshaling response for status code %d: %w: %s", resp.StatusCode, err, string(responseBody)) - } - errorResponse.StatusCode = resp.StatusCode - errorResponse.RawMessage = responseBody - errorResponse.retryAfter = 1000 // set a sensible default for retrying. This is in milliseconds. - if resp.StatusCode == 429 { - retryAfterRaw := resp.Header.Get("Retry-After") - if retryAfterRaw != "" { - retryAfter, err := strconv.Atoi(retryAfterRaw) - if err == nil && retryAfter > 0 { - errorResponse.retryAfter = retryAfter - } - } - } - return string(responseBody), errorResponse - } - - return string(responseBody), nil -} From 0b9b0d76dfe093be55d73715bdd61f75f32470b5 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 10:37:32 -0400 Subject: [PATCH 019/133] review --- client/project_rolling_release.go | 30 ++++---- vercel/data_source_project_rolling_release.go | 9 +-- vercel/resource_project_rolling_release.go | 75 +++---------------- .../resource_project_rolling_release_test.go | 3 - vercel/validator_advancement_type.go | 54 +++++++++++++ 5 files changed, 81 insertions(+), 90 deletions(-) create mode 100644 vercel/validator_advancement_type.go diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index b7617665..86e2532d 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -109,7 +109,8 @@ type RollingReleaseInfo struct { // GetRollingRelease returns the rolling release for a given project. func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (RollingReleaseInfo, error) { - url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) + teamId := c.TeamID(teamID) + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamId) var d RollingReleaseInfo err := c.doRequest(clientRequest{ @@ -123,7 +124,7 @@ func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string } d.ProjectID = projectID - d.TeamID = teamID + d.TeamID = teamId return d, nil } @@ -138,6 +139,7 @@ type UpdateRollingReleaseRequest struct { // UpdateRollingRelease will update an existing rolling release to the latest information. func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseInfo, error) { + teamId := c.TeamID(request.TeamID) // If we're enabling, we need to do it in steps if request.RollingRelease.Enabled { // First ensure it's disabled @@ -147,14 +149,12 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling AdvancementType: "", Stages: []RollingReleaseStage{}, }, - ProjectID: request.ProjectID, - TeamID: request.TeamID, } err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), body: string(mustMarshal(disabledRequest.RollingRelease)), }, nil) if err != nil { @@ -184,7 +184,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling err = c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), body: string(mustMarshal(stagesRequest)), }, nil) if err != nil { @@ -206,7 +206,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling err = c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), body: string(mustMarshal(enableRequest)), }, &result) if err != nil { @@ -214,7 +214,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } result.ProjectID = request.ProjectID - result.TeamID = request.TeamID + result.TeamID = teamId return result, nil } else { @@ -225,8 +225,6 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling AdvancementType: "", Stages: []RollingReleaseStage{}, }, - ProjectID: request.ProjectID, - TeamID: request.TeamID, } var result RollingReleaseInfo @@ -234,7 +232,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), body: string(mustMarshal(disabledRequest.RollingRelease)), }, &result) if err != nil { @@ -242,7 +240,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } result.ProjectID = request.ProjectID - result.TeamID = request.TeamID + result.TeamID = teamId return result, nil } @@ -250,15 +248,13 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling // DeleteRollingRelease will delete the rolling release for a given project. func (c *Client) DeleteRollingRelease(ctx context.Context, projectID, teamID string) error { - url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamID) + teamId := c.TeamID(teamID) + url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamId) - var d RollingReleaseInfo err := c.doRequest(clientRequest{ ctx: ctx, method: "DELETE", url: url, - }, &d) - d.ProjectID = projectID - d.TeamID = teamID + }, nil) return err } diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index f648deef..b655aa99 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -54,8 +54,9 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour Required: true, }, "team_id": schema.StringAttribute{ - MarkdownDescription: "The ID of the team the project exists in.", - Required: true, + Optional: true, + Computed: true, + Description: "The ID of the Vercel team.", }, "rolling_release": schema.SingleNestedAttribute{ MarkdownDescription: "The rolling release configuration.", @@ -120,11 +121,9 @@ func convertStagesDataSource(stages []client.RollingReleaseStage) []TFRollingRel result := make([]TFRollingReleaseStageDataSource, len(stages)) for i, stage := range stages { - var duration types.Int64 + duration := types.Int64Null() if stage.Duration != nil { duration = types.Int64Value(int64(*stage.Duration)) - } else { - duration = types.Int64Null() } result[i] = TFRollingReleaseStageDataSource{ diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 3622d9a3..0c6c59a3 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -6,11 +6,13 @@ import ( "sort" "time" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/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" @@ -53,50 +55,6 @@ func (r *projectRollingReleaseResource) Configure(ctx context.Context, req resou r.client = client } -// Custom validator for advancement_type -type advancementTypeValidator struct{} - -func (v advancementTypeValidator) Description(ctx context.Context) string { - return "advancement_type must be either 'automatic' or 'manual-approval'" -} - -func (v advancementTypeValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -func (v advancementTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - // Get the value of enabled from the parent object - var enabled types.Bool - diags := req.Config.GetAttribute(ctx, path.Root("rolling_release").AtName("enabled"), &enabled) - if diags.HasError() { - resp.Diagnostics.AddError( - "Error validating advancement_type", - "Could not get enabled value from configuration", - ) - return - } - - // Only validate when enabled is true - if enabled.ValueBool() { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - resp.Diagnostics.AddError( - "Invalid advancement_type", - "advancement_type is required when enabled is true", - ) - return - } - - value := req.ConfigValue.ValueString() - if value != "automatic" && value != "manual-approval" { - resp.Diagnostics.AddError( - "Invalid advancement_type", - fmt.Sprintf("advancement_type must be either 'automatic' or 'manual-approval', got: %s", value), - ) - return - } - } -} - // Schema returns the schema information for a project rolling release resource. func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ @@ -107,8 +65,10 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S Required: true, }, "team_id": schema.StringAttribute{ - MarkdownDescription: "The ID of the team the project exists in.", - Required: true, + Optional: true, + Computed: true, + Description: "The ID of the Vercel team.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, }, "rolling_release": schema.SingleNestedAttribute{ MarkdownDescription: "The rolling release configuration.", @@ -135,6 +95,9 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S "target_percentage": schema.Int64Attribute{ MarkdownDescription: "The percentage of traffic to route to this stage.", Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, }, "duration": schema.Int64Attribute{ MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", @@ -174,24 +137,6 @@ type TFRollingReleaseInfo struct { TeamID types.String `tfsdk:"team_id"` } -type RollingReleaseStage struct { - TargetPercentage int `json:"targetPercentage"` - Duration *int `json:"duration,omitempty"` - RequireApproval bool `json:"requireApproval"` -} - -type RollingRelease struct { - Enabled bool `json:"enabled"` - AdvancementType string `json:"advancementType"` - Stages []RollingReleaseStage `json:"stages"` -} - -type UpdateRollingReleaseRequest struct { - RollingRelease RollingRelease `json:"rollingRelease"` - ProjectID string `json:"-"` - TeamID string `json:"-"` -} - func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { var stages []client.RollingReleaseStage var advancementType string diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index a8ab023a..f4c9270a 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -122,7 +122,6 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - team_id = vercel_project.example.team_id depends_on = [vercel_project.example] rolling_release = { enabled = true @@ -160,7 +159,6 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - team_id = vercel_project.example.team_id depends_on = [vercel_project.example] rolling_release = { enabled = true @@ -202,7 +200,6 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - team_id = vercel_project.example.team_id depends_on = [vercel_project.example] rolling_release = { enabled = false diff --git a/vercel/validator_advancement_type.go b/vercel/validator_advancement_type.go new file mode 100644 index 00000000..062846d2 --- /dev/null +++ b/vercel/validator_advancement_type.go @@ -0,0 +1,54 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Custom validator for advancement_type +type advancementTypeValidator struct{} + +func (v advancementTypeValidator) Description(ctx context.Context) string { + return "advancement_type must be either 'automatic' or 'manual-approval'" +} + +func (v advancementTypeValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v advancementTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + // Get the value of enabled from the parent object + var enabled types.Bool + diags := req.Config.GetAttribute(ctx, path.Root("rolling_release").AtName("enabled"), &enabled) + if diags.HasError() { + resp.Diagnostics.AddError( + "Error validating advancement_type", + "Could not get enabled value from configuration", + ) + return + } + + // Only validate when enabled is true + if enabled.ValueBool() { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + resp.Diagnostics.AddError( + "Invalid advancement_type", + "advancement_type is required when enabled is true", + ) + return + } + + value := req.ConfigValue.ValueString() + if value != "automatic" && value != "manual-approval" { + resp.Diagnostics.AddError( + "Invalid advancement_type", + fmt.Sprintf("advancement_type must be either 'automatic' or 'manual-approval', got: %s", value), + ) + return + } + } +} From 3647bd7db07284dd77177e95e4d7d618bca55054 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 10:47:07 -0400 Subject: [PATCH 020/133] more review --- client/project_rolling_release.go | 85 ------------------- .../resource.tf | 27 ++++++ main.tf | 32 ------- vercel/resource_project_rolling_release.go | 4 + .../resource_project_rolling_release_test.go | 35 +++----- 5 files changed, 41 insertions(+), 142 deletions(-) create mode 100644 examples/resources/vercel_project_rolling_release/resource.tf delete mode 100644 main.tf diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 86e2532d..ff80dae8 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -21,86 +21,6 @@ type RollingRelease struct { Stages []RollingReleaseStage `json:"stages"` // Required when enabled=true: 2-10 stages } -// ErrorResponse represents the error response from the Vercel API -type ErrorResponse struct { - Error struct { - Code string `json:"code"` - Message string `json:"message"` - } `json:"error"` -} - -// Validate checks if the rolling release configuration is valid according to API requirements -func (r *RollingRelease) Validate() error { - if !r.Enabled { - return nil // No validation needed when disabled - } - - // Validate advancement type - if r.AdvancementType == "" { - return fmt.Errorf("advancement_type is required when enabled is true") - } - if r.AdvancementType != "automatic" && r.AdvancementType != "manual-approval" { - return fmt.Errorf("advancement_type must be 'automatic' or 'manual-approval' when enabled is true, got: %s", r.AdvancementType) - } - - // Validate stages - if len(r.Stages) == 0 { - return fmt.Errorf("stages are required when enabled is true") - } - if len(r.Stages) < 2 || len(r.Stages) > 10 { - return fmt.Errorf("must have between 2 and 10 stages when enabled is true, got: %d", len(r.Stages)) - } - - // Sort stages by target percentage to ensure correct order - sort.Slice(r.Stages, func(i, j int) bool { - return r.Stages[i].TargetPercentage < r.Stages[j].TargetPercentage - }) - - // Validate stages are in ascending order and within bounds - prevPercentage := 0 - for i, stage := range r.Stages { - // Validate percentage bounds - if stage.TargetPercentage < 0 || stage.TargetPercentage > 100 { - return fmt.Errorf("stage %d: target_percentage must be between 0 and 100, got: %d", i, stage.TargetPercentage) - } - - // Validate ascending order - if stage.TargetPercentage <= prevPercentage { - return fmt.Errorf("stage %d: target_percentage must be greater than previous stage (%d), got: %d", i, prevPercentage, stage.TargetPercentage) - } - prevPercentage = stage.TargetPercentage - - // Validate duration for automatic advancement - if r.AdvancementType == "automatic" { - if i < len(r.Stages)-1 { // All stages except last need duration - if stage.Duration == nil { - return fmt.Errorf("stage %d: duration is required for automatic advancement (except for the last stage)", i) - } - if *stage.Duration < 1 || *stage.Duration > 10000 { - return fmt.Errorf("stage %d: duration must be between 1 and 10000 minutes for automatic advancement, got: %d", i, *stage.Duration) - } - } else { // Last stage should not have duration - if stage.Duration != nil { - return fmt.Errorf("stage %d: last stage should not have duration for automatic advancement", i) - } - } - } else { - // For manual approval, no stages should have duration - if stage.Duration != nil { - return fmt.Errorf("stage %d: duration should not be set for manual-approval advancement type", i) - } - } - } - - // Validate last stage is 100% - lastStage := r.Stages[len(r.Stages)-1] - if lastStage.TargetPercentage != 100 { - return fmt.Errorf("last stage must have target_percentage=100, got: %d", lastStage.TargetPercentage) - } - - return nil -} - type RollingReleaseInfo struct { RollingRelease RollingRelease `json:"rollingRelease"` ProjectID string `json:"projectId"` @@ -164,11 +84,6 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling // Wait a bit before proceeding time.Sleep(2 * time.Second) - // Now validate the request - if err := request.RollingRelease.Validate(); err != nil { - return RollingReleaseInfo{}, fmt.Errorf("invalid rolling release configuration: %w", err) - } - // Sort stages by target percentage sort.Slice(request.RollingRelease.Stages, func(i, j int) bool { return request.RollingRelease.Stages[i].TargetPercentage < request.RollingRelease.Stages[j].TargetPercentage diff --git a/examples/resources/vercel_project_rolling_release/resource.tf b/examples/resources/vercel_project_rolling_release/resource.tf new file mode 100644 index 00000000..73605256 --- /dev/null +++ b/examples/resources/vercel_project_rolling_release/resource.tf @@ -0,0 +1,27 @@ +resource "vercel_project" "example" { + name = "test-acc-example-project-%s" + skew_protection = "12 hours" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + depends_on = [vercel_project.example] + rolling_release = { + enabled = true + advancement_type = "manual-approval" + stages = [ + { + require_approval = true + target_percentage = 20 + }, + { + require_approval = true + target_percentage = 50 + }, + { + require_approval = true + target_percentage = 100 + } + ] + } +} \ No newline at end of file diff --git a/main.tf b/main.tf deleted file mode 100644 index 4be7e248..00000000 --- a/main.tf +++ /dev/null @@ -1,32 +0,0 @@ -terraform { - required_providers { - vercel = { - source = "vercel/vercel" - } - } -} - -resource "vercel_project_rolling_release" "example" { - project_id = "prj_9lRsbRoK8DCtxa4CmUu5rWfSaS86" - team_id = "team_4FWx5KQoszRi0ZmM9q9IBoKG" - rolling_release = { - enabled = true - advancement_type = "automatic" - stages = [ - { - duration = 1 - require_approval = false - target_percentage = 5 # Start with 5% - }, - { - duration = 1 - require_approval = false - target_percentage = 50 # Then 50% - }, - { - require_approval = false # No duration for last stage - target_percentage = 100 # Finally 100% - } - ] - } -} \ No newline at end of file diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 0c6c59a3..4e098806 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -737,6 +737,10 @@ func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req res // For import, we don't have any state to preserve result, diags := convertResponseToTFRollingRelease(out, nil, ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "imported project rolling release", map[string]any{ "team_id": result.TeamID.ValueString(), "project_id": result.ProjectID.ValueString(), diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index f4c9270a..ae8f8155 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -38,7 +38,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Steps: []resource.TestStep{ // First create the resource in a disabled state { - Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), + Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), @@ -48,7 +48,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }, // Then enable with initial configuration { - Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix, testGithubRepo(t))), + Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), @@ -70,7 +70,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }, // Then update to new configuration { - Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix, testGithubRepo(t))), + Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), @@ -96,7 +96,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }, // Finally disable again { - Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix, testGithubRepo(t))), + Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), @@ -108,15 +108,10 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }) } -func testAccProjectRollingReleasesConfig(nameSuffix string, githubRepo string) string { +func testAccProjectRollingReleasesConfig(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" - git_repository = { - type = "github" - repo = "%s" - } - enable_preview_feedback = false skew_protection = "12 hours" } @@ -142,18 +137,13 @@ resource "vercel_project_rolling_release" "example" { ] } } -`, nameSuffix, githubRepo) +`, nameSuffix) } -func testAccProjectRollingReleasesConfigUpdate(nameSuffix string, githubRepo string) string { +func testAccProjectRollingReleasesConfigUpdate(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" - git_repository = { - type = "github" - repo = "%s" - } - enable_preview_feedback = false skew_protection = "12 hours" } @@ -183,18 +173,13 @@ resource "vercel_project_rolling_release" "example" { ] } } -`, nameSuffix, githubRepo) +`, nameSuffix) } -func testAccProjectRollingReleasesConfigOff(nameSuffix string, githubRepo string) string { +func testAccProjectRollingReleasesConfigOff(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" - git_repository = { - type = "github" - repo = "%s" - } - enable_preview_feedback = false skew_protection = "12 hours" } @@ -207,5 +192,5 @@ resource "vercel_project_rolling_release" "example" { stages = [] } } -`, nameSuffix, githubRepo) +`, nameSuffix) } From 10fcb7646feaa6d53713b5eb4b9f1da0df3e39a9 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 10:59:36 -0400 Subject: [PATCH 021/133] review --- docs/data-sources/project_rolling_release.md | 10 ++++ docs/resources/project_rolling_release.md | 32 +++++++++++- .../data-source.tf | 7 +++ vercel/data_source_project_rolling_release.go | 2 +- ...ata_source_project_rolling_release_test.go | 52 +++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 examples/data-sources/vercel_project_rolling_release/data-source.tf create mode 100644 vercel/data_source_project_rolling_release_test.go diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 971f58c2..aabcacbd 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -10,7 +10,17 @@ description: |- Data source for a Vercel project rolling release configuration. +## Example Usage +```terraform +data "vercel_project" "example" { + name = "example-project" +} + +data "vercel_project_rolling_release" "example" { + project_id = data.vercel_project_rolling_release.example.id +} +``` ## Schema diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 17690c45..a9b67b96 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -10,7 +10,37 @@ description: |- Manages rolling release configuration for a Vercel project. - +## Example Usage + +```terraform +resource "vercel_project" "example" { + name = "test-acc-example-project-%s" + skew_protection = "12 hours" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + depends_on = [vercel_project.example] + rolling_release = { + enabled = true + advancement_type = "manual-approval" + stages = [ + { + require_approval = true + target_percentage = 20 + }, + { + require_approval = true + target_percentage = 50 + }, + { + require_approval = true + target_percentage = 100 + } + ] + } +} +``` ## Schema diff --git a/examples/data-sources/vercel_project_rolling_release/data-source.tf b/examples/data-sources/vercel_project_rolling_release/data-source.tf new file mode 100644 index 00000000..11b8f357 --- /dev/null +++ b/examples/data-sources/vercel_project_rolling_release/data-source.tf @@ -0,0 +1,7 @@ +data "vercel_project" "example" { + name = "example-project" +} + +data "vercel_project_rolling_release" "example" { + project_id = data.vercel_project_rolling_release.example.id +} \ No newline at end of file diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index b655aa99..4f4de53f 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -116,7 +116,7 @@ type TFRollingReleaseInfoDataSource struct { func convertStagesDataSource(stages []client.RollingReleaseStage) []TFRollingReleaseStageDataSource { if len(stages) == 0 { - return make([]TFRollingReleaseStageDataSource, 0) + return nil } result := make([]TFRollingReleaseStageDataSource, len(stages)) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go new file mode 100644 index 00000000..e1703db2 --- /dev/null +++ b/vercel/data_source_project_rolling_release_test.go @@ -0,0 +1,52 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { + nameSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), + ), + Steps: []resource.TestStep{ + { + Config: cfg(testAccProjectRollingReleaseDataSourceConfig(nameSuffix)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.enabled", "false"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.advancement_type", ""), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.stages.#", "0"), + ), + }, + }, + }) +} + +func testAccProjectRollingReleaseDataSourceConfig(projectName string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + depends_on = [vercel_project.example] + rolling_release = { + enabled = false + advancement_type = "" + stages = [] + } +} + +data "vercel_project_rolling_release" "example" { + project_id = vercel_project_rolling_release.example.project_id +} +`, projectName) +} From 5031a0d65638af0d83357c95451033498be6668d Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 18:57:05 -0400 Subject: [PATCH 022/133] remove sorting --- client/project_rolling_release.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index ff80dae8..6a81bee0 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -3,7 +3,6 @@ package client import ( "context" "fmt" - "sort" "time" ) @@ -84,11 +83,6 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling // Wait a bit before proceeding time.Sleep(2 * time.Second) - // Sort stages by target percentage - sort.Slice(request.RollingRelease.Stages, func(i, j int) bool { - return request.RollingRelease.Stages[i].TargetPercentage < request.RollingRelease.Stages[j].TargetPercentage - }) - // First set up the stages stagesRequest := map[string]any{ "enabled": false, From b9dd027792515f09998ec0b714c8a926881021f2 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 18:58:34 -0400 Subject: [PATCH 023/133] add import.sh --- .../resources/vercel_project_rolling_release/import.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 examples/resources/vercel_project_rolling_release/import.sh diff --git a/examples/resources/vercel_project_rolling_release/import.sh b/examples/resources/vercel_project_rolling_release/import.sh new file mode 100644 index 00000000..b38a9b2a --- /dev/null +++ b/examples/resources/vercel_project_rolling_release/import.sh @@ -0,0 +1,9 @@ +# If importing into a personal account, or with a team configured on +# the provider, simply use the project ID. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_project.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, 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.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx From 7fea6aac26bed83c9c063247127d98a8a453f4fa Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 18:58:59 -0400 Subject: [PATCH 024/133] eof --- .../data-sources/vercel_project_rolling_release/data-source.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/data-sources/vercel_project_rolling_release/data-source.tf b/examples/data-sources/vercel_project_rolling_release/data-source.tf index 11b8f357..e3b96582 100644 --- a/examples/data-sources/vercel_project_rolling_release/data-source.tf +++ b/examples/data-sources/vercel_project_rolling_release/data-source.tf @@ -4,4 +4,4 @@ data "vercel_project" "example" { data "vercel_project_rolling_release" "example" { project_id = data.vercel_project_rolling_release.example.id -} \ No newline at end of file +} From 935d69d5649dc8fe088fcf551bce4cf3d5bb907f Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:01:05 -0400 Subject: [PATCH 025/133] fix name --- examples/resources/vercel_project_rolling_release/resource.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/resources/vercel_project_rolling_release/resource.tf b/examples/resources/vercel_project_rolling_release/resource.tf index 73605256..71ff1b34 100644 --- a/examples/resources/vercel_project_rolling_release/resource.tf +++ b/examples/resources/vercel_project_rolling_release/resource.tf @@ -1,5 +1,5 @@ resource "vercel_project" "example" { - name = "test-acc-example-project-%s" + name = "example-project" skew_protection = "12 hours" } From 206a8d1904029efe67c5015ffb6673b0c572dc40 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:01:32 -0400 Subject: [PATCH 026/133] eof --- examples/resources/vercel_project_rolling_release/resource.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/resources/vercel_project_rolling_release/resource.tf b/examples/resources/vercel_project_rolling_release/resource.tf index 71ff1b34..cb2c2654 100644 --- a/examples/resources/vercel_project_rolling_release/resource.tf +++ b/examples/resources/vercel_project_rolling_release/resource.tf @@ -24,4 +24,4 @@ resource "vercel_project_rolling_release" "example" { } ] } -} \ No newline at end of file +} From 6b272c611970e9c44eb5ad9352e43d33ccd757fc Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:02:39 -0400 Subject: [PATCH 027/133] update nil to empty array --- vercel/data_source_project_rolling_release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 4f4de53f..e0f12f70 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -116,7 +116,7 @@ type TFRollingReleaseInfoDataSource struct { func convertStagesDataSource(stages []client.RollingReleaseStage) []TFRollingReleaseStageDataSource { if len(stages) == 0 { - return nil + return []TFRollingReleaseStageDataSource{} } result := make([]TFRollingReleaseStageDataSource, len(stages)) From f29fde57e94a107acc08b5050ae55d59327f0144 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:03:29 -0400 Subject: [PATCH 028/133] Update vercel/data_source_project_rolling_release.go Co-authored-by: Douglas Harcourt Parsons --- vercel/data_source_project_rolling_release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index e0f12f70..c4a8e52b 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -148,7 +148,7 @@ func convertResponseToTFRollingReleaseDataSource(response client.RollingReleaseI if !response.RollingRelease.Enabled { result.RollingRelease.AdvancementType = types.StringValue("") - result.RollingRelease.Stages = make([]TFRollingReleaseStageDataSource, 0) + result.RollingRelease.Stages = []TFRollingReleaseStageDataSource{} } return result From 0efd0a6bedfbb67f8d913e65019287e31411f05d Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:04:58 -0400 Subject: [PATCH 029/133] update TF prefix names --- vercel/data_source_project_rolling_release.go | 38 ++++++++-------- vercel/resource_project_rolling_release.go | 44 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index c4a8e52b..9e82f96f 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -96,37 +96,37 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour } } -type TFRollingReleaseStageDataSource struct { +type RollingReleaseStageDataSource struct { TargetPercentage types.Int64 `tfsdk:"target_percentage"` Duration types.Int64 `tfsdk:"duration"` RequireApproval types.Bool `tfsdk:"require_approval"` } -type TFRollingReleaseDataSource struct { - Enabled types.Bool `tfsdk:"enabled"` - AdvancementType types.String `tfsdk:"advancement_type"` - Stages []TFRollingReleaseStageDataSource `tfsdk:"stages"` +type RollingReleaseDataSource struct { + Enabled types.Bool `tfsdk:"enabled"` + AdvancementType types.String `tfsdk:"advancement_type"` + Stages []RollingReleaseStageDataSource `tfsdk:"stages"` } -type TFRollingReleaseInfoDataSource struct { - RollingRelease TFRollingReleaseDataSource `tfsdk:"rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` +type RollingReleaseInfoDataSource struct { + RollingRelease RollingReleaseDataSource `tfsdk:"rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } -func convertStagesDataSource(stages []client.RollingReleaseStage) []TFRollingReleaseStageDataSource { +func convertStagesDataSource(stages []client.RollingReleaseStage) []RollingReleaseStageDataSource { if len(stages) == 0 { - return []TFRollingReleaseStageDataSource{} + return []RollingReleaseStageDataSource{} } - result := make([]TFRollingReleaseStageDataSource, len(stages)) + result := make([]RollingReleaseStageDataSource, len(stages)) for i, stage := range stages { duration := types.Int64Null() if stage.Duration != nil { duration = types.Int64Value(int64(*stage.Duration)) } - result[i] = TFRollingReleaseStageDataSource{ + result[i] = RollingReleaseStageDataSource{ TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), Duration: duration, RequireApproval: types.BoolValue(stage.RequireApproval), @@ -135,9 +135,9 @@ func convertStagesDataSource(stages []client.RollingReleaseStage) []TFRollingRel return result } -func convertResponseToTFRollingReleaseDataSource(response client.RollingReleaseInfo) TFRollingReleaseInfoDataSource { - result := TFRollingReleaseInfoDataSource{ - RollingRelease: TFRollingReleaseDataSource{ +func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo) RollingReleaseInfoDataSource { + result := RollingReleaseInfoDataSource{ + RollingRelease: RollingReleaseDataSource{ Enabled: types.BoolValue(response.RollingRelease.Enabled), AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), Stages: convertStagesDataSource(response.RollingRelease.Stages), @@ -148,14 +148,14 @@ func convertResponseToTFRollingReleaseDataSource(response client.RollingReleaseI if !response.RollingRelease.Enabled { result.RollingRelease.AdvancementType = types.StringValue("") - result.RollingRelease.Stages = []TFRollingReleaseStageDataSource{} + result.RollingRelease.Stages = make([]RollingReleaseStageDataSource, 0) } return result } func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config TFRollingReleaseInfoDataSource + var config RollingReleaseInfoDataSource diags := req.Config.Get(ctx, &config) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -182,7 +182,7 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour return } - result := convertResponseToTFRollingReleaseDataSource(out) + result := convertResponseToRollingReleaseDataSource(out) tflog.Info(ctx, "read project rolling release", map[string]any{ "team_id": result.TeamID.ValueString(), "project_id": result.ProjectID.ValueString(), diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 4e098806..a26dba1b 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -117,27 +117,27 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S } } -type TFRollingReleaseStage struct { +type RollingReleaseStage struct { TargetPercentage types.Int64 `tfsdk:"target_percentage"` Duration types.Int64 `tfsdk:"duration"` RequireApproval types.Bool `tfsdk:"require_approval"` } -// TFRollingRelease reflects the state terraform stores internally for a project rolling release. -type TFRollingRelease struct { +// RollingRelease reflects the state terraform stores internally for a project rolling release. +type RollingRelease struct { Enabled types.Bool `tfsdk:"enabled"` AdvancementType types.String `tfsdk:"advancement_type"` Stages types.List `tfsdk:"stages"` } // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. -type TFRollingReleaseInfo struct { - RollingRelease TFRollingRelease `tfsdk:"rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` +type RollingReleaseInfo struct { + RollingRelease RollingRelease `tfsdk:"rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } -func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { +func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { var stages []client.RollingReleaseStage var advancementType string var diags diag.Diagnostics @@ -162,7 +162,7 @@ func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRol } // Convert stages from types.List to []client.RollingReleaseStage - var tfStages []TFRollingReleaseStage + var tfStages []RollingReleaseStage diags = e.RollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) if diags.HasError() { return client.UpdateRollingReleaseRequest{}, diags @@ -285,7 +285,7 @@ func (e *TFRollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRol }, diags } -func convertResponseToTFRollingRelease(response client.RollingReleaseInfo, plan *TFRollingReleaseInfo, ctx context.Context) (TFRollingReleaseInfo, diag.Diagnostics) { +func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *RollingReleaseInfo, ctx context.Context) (RollingReleaseInfo, diag.Diagnostics) { var diags diag.Diagnostics // If we have a plan and the response doesn't match what we expect, @@ -308,8 +308,8 @@ func convertResponseToTFRollingRelease(response client.RollingReleaseInfo, plan } } - result := TFRollingReleaseInfo{ - RollingRelease: TFRollingRelease{ + result := RollingReleaseInfo{ + RollingRelease: RollingRelease{ Enabled: types.BoolValue(response.RollingRelease.Enabled), AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), }, @@ -339,7 +339,7 @@ func convertResponseToTFRollingRelease(response client.RollingReleaseInfo, plan // If we have a plan, try to match stages by target percentage to preserve order var orderedStages []client.RollingReleaseStage if plan != nil && !plan.RollingRelease.Stages.IsNull() && !plan.RollingRelease.Stages.IsUnknown() { - var planStages []TFRollingReleaseStage + var planStages []RollingReleaseStage diags.Append(plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false)...) if diags.HasError() { return result, diags @@ -429,7 +429,7 @@ func convertResponseToTFRollingRelease(response client.RollingReleaseInfo, plan // Create will create a rolling release for a Vercel project by sending a request to the Vercel API. func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan TFRollingReleaseInfo + var plan RollingReleaseInfo diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -490,7 +490,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource } // Convert response to state - result, diags := convertResponseToTFRollingRelease(out, &plan, ctx) + result, diags := convertResponseToRollingRelease(out, &plan, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -514,7 +514,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource // Read will read a rolling release of a Vercel project by requesting it from the Vercel API, and will update terraform // with this information. func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state TFRollingReleaseInfo + var state RollingReleaseInfo diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -543,7 +543,7 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R "stages": out.RollingRelease.Stages, }) - result, diags := convertResponseToTFRollingRelease(out, &state, ctx) + result, diags := convertResponseToRollingRelease(out, &state, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -565,14 +565,14 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R // Update will update an existing rolling release to the latest information. func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan TFRollingReleaseInfo + var plan RollingReleaseInfo diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - var state TFRollingReleaseInfo + var state RollingReleaseInfo diags = req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -659,7 +659,7 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource } // Convert response to state - result, diags := convertResponseToTFRollingRelease(out, &plan, ctx) + result, diags := convertResponseToRollingRelease(out, &plan, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -681,7 +681,7 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource // Delete will delete an existing rolling release by disabling it. func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state TFRollingReleaseInfo + var state RollingReleaseInfo diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -736,7 +736,7 @@ func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req res } // For import, we don't have any state to preserve - result, diags := convertResponseToTFRollingRelease(out, nil, ctx) + result, diags := convertResponseToRollingRelease(out, nil, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From 92f0f0b662de6c343949bf394ec7be7859199f63 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:13:04 -0400 Subject: [PATCH 030/133] removes more validation --- vercel/resource_project_rolling_release.go | 73 ---------------------- 1 file changed, 73 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index a26dba1b..ca24305c 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -3,7 +3,6 @@ package vercel import ( "context" "fmt" - "sort" "time" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" @@ -143,24 +142,8 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli var diags diag.Diagnostics if e.RollingRelease.Enabled.ValueBool() { - // When enabled, both advancement_type and stages are required - if e.RollingRelease.AdvancementType.IsNull() || e.RollingRelease.AdvancementType.IsUnknown() { - diags.AddError( - "Error creating rolling release", - "advancement_type is required when enabled is true", - ) - return client.UpdateRollingReleaseRequest{}, diags - } advancementType = e.RollingRelease.AdvancementType.ValueString() - if e.RollingRelease.Stages.IsNull() || e.RollingRelease.Stages.IsUnknown() { - diags.AddError( - "Error creating rolling release", - "stages are required when enabled is true", - ) - return client.UpdateRollingReleaseRequest{}, diags - } - // Convert stages from types.List to []client.RollingReleaseStage var tfStages []RollingReleaseStage diags = e.RollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) @@ -168,55 +151,6 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli return client.UpdateRollingReleaseRequest{}, diags } - // Validate stages - if len(tfStages) < 2 || len(tfStages) > 10 { - diags.AddError( - "Error creating rolling release", - fmt.Sprintf("must have between 2 and 10 stages when enabled is true, got: %d", len(tfStages)), - ) - return client.UpdateRollingReleaseRequest{}, diags - } - - // Sort stages by target percentage to ensure correct order - sort.Slice(tfStages, func(i, j int) bool { - return tfStages[i].TargetPercentage.ValueInt64() < tfStages[j].TargetPercentage.ValueInt64() - }) - - // Validate stages are in ascending order and within bounds - prevPercentage := int64(0) - for i, stage := range tfStages { - percentage := stage.TargetPercentage.ValueInt64() - - // Validate percentage bounds - if percentage < 0 || percentage > 100 { - diags.AddError( - "Error creating rolling release", - fmt.Sprintf("stage %d: target_percentage must be between 0 and 100, got: %d", i, percentage), - ) - return client.UpdateRollingReleaseRequest{}, diags - } - - // Validate ascending order - if percentage <= prevPercentage { - diags.AddError( - "Error creating rolling release", - fmt.Sprintf("stage %d: target_percentage must be greater than previous stage (%d), got: %d", i, prevPercentage, percentage), - ) - return client.UpdateRollingReleaseRequest{}, diags - } - prevPercentage = percentage - } - - // Validate last stage is 100% - lastStage := tfStages[len(tfStages)-1] - if lastStage.TargetPercentage.ValueInt64() != 100 { - diags.AddError( - "Error creating rolling release", - fmt.Sprintf("last stage must have target_percentage=100, got: %d", lastStage.TargetPercentage.ValueInt64()), - ) - return client.UpdateRollingReleaseRequest{}, diags - } - stages = make([]client.RollingReleaseStage, len(tfStages)) for i, stage := range tfStages { if advancementType == "automatic" { @@ -233,13 +167,6 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli } } else { duration := int(stage.Duration.ValueInt64()) - if duration < 1 || duration > 10000 { - diags.AddError( - "Error creating rolling release", - fmt.Sprintf("stage %d: duration must be between 1 and 10000 minutes for automatic advancement, got: %d", i, duration), - ) - return client.UpdateRollingReleaseRequest{}, diags - } stages[i] = client.RollingReleaseStage{ TargetPercentage: int(stage.TargetPercentage.ValueInt64()), Duration: &duration, From 96303ab8e44f1b653769c7cf3c7b6b35c3fa99e7 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:20:51 -0400 Subject: [PATCH 031/133] fix docs --- docs/resources/project_rolling_release.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index a9b67b96..9d2d5acc 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -14,7 +14,7 @@ Manages rolling release configuration for a Vercel project. ```terraform resource "vercel_project" "example" { - name = "test-acc-example-project-%s" + name = "example-project" skew_protection = "12 hours" } @@ -74,3 +74,19 @@ Optional: - `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. - `require_approval` (Boolean) Whether approval is required before advancing to the next stage. + +## Import + +Import is supported using the following syntax: + +```shell +# If importing into a personal account, or with a team configured on +# the provider, simply use the project ID. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_project.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, 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.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` From d2639508a76977dac698d2ed8da1460fa88023b6 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:25:45 -0400 Subject: [PATCH 032/133] fix docs --- docs/data-sources/project_rolling_release.md | 3 +++ docs/resources/project_rolling_release.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index aabcacbd..21a5a5b6 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -28,6 +28,9 @@ data "vercel_project_rolling_release" "example" { ### Required - `project_id` (String) The ID of the project. + +### Optional + - `team_id` (String) The ID of the team the project exists in. ### Read-Only diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 9d2d5acc..c899c534 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -49,6 +49,9 @@ resource "vercel_project_rolling_release" "example" { - `project_id` (String) The ID of the project. - `rolling_release` (Attributes) The rolling release configuration. (see [below for nested schema](#nestedatt--rolling_release)) + +### Optional + - `team_id` (String) The ID of the team the project exists in. From 6a5c6b10199f386e1dc44c48fb5671ca7627cbee Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 12 Jun 2025 19:27:48 -0400 Subject: [PATCH 033/133] fix docs even harder --- docs/data-sources/project_rolling_release.md | 2 +- docs/resources/project_rolling_release.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 21a5a5b6..1569689b 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -31,7 +31,7 @@ data "vercel_project_rolling_release" "example" { ### Optional -- `team_id` (String) The ID of the team the project exists in. +- `team_id` (String) The ID of the Vercel team. ### Read-Only diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index c899c534..4af26797 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -52,7 +52,7 @@ resource "vercel_project_rolling_release" "example" { ### Optional -- `team_id` (String) The ID of the team the project exists in. +- `team_id` (String) The ID of the Vercel team. ### Nested Schema for `rolling_release` From bc4af5e89581b46332e680ef0d8d01616695aae5 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 09:39:08 -0600 Subject: [PATCH 034/133] [rr] fix tests --- client/project_rolling_release.go | 46 +------------------ vercel/data_source_project_rolling_release.go | 40 ++++++++++------ 2 files changed, 27 insertions(+), 59 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 6a81bee0..68c20561 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -3,7 +3,6 @@ package client import ( "context" "fmt" - "time" ) // RollingReleaseStage represents a stage in a rolling release @@ -59,51 +58,8 @@ type UpdateRollingReleaseRequest struct { // UpdateRollingRelease will update an existing rolling release to the latest information. func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseInfo, error) { teamId := c.TeamID(request.TeamID) - // If we're enabling, we need to do it in steps if request.RollingRelease.Enabled { - // First ensure it's disabled - disabledRequest := UpdateRollingReleaseRequest{ - RollingRelease: RollingRelease{ - Enabled: false, - AdvancementType: "", - Stages: []RollingReleaseStage{}, - }, - } - err := c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), - body: string(mustMarshal(disabledRequest.RollingRelease)), - }, nil) - if err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error disabling rolling release: %w", err) - } - - // Wait a bit before proceeding - time.Sleep(2 * time.Second) - - // First set up the stages - stagesRequest := map[string]any{ - "enabled": false, - "advancementType": request.RollingRelease.AdvancementType, - "stages": request.RollingRelease.Stages, - } - - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), - body: string(mustMarshal(stagesRequest)), - }, nil) - if err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error configuring stages: %w", err) - } - - // Wait a bit before enabling - time.Sleep(2 * time.Second) - - // Finally enable it enableRequest := map[string]any{ "enabled": true, "advancementType": request.RollingRelease.AdvancementType, @@ -112,7 +68,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling var result RollingReleaseInfo - err = c.doRequest(clientRequest{ + err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 9e82f96f..f39de818 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -61,6 +62,7 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour "rolling_release": schema.SingleNestedAttribute{ MarkdownDescription: "The rolling release configuration.", Computed: true, + Optional: true, Attributes: map[string]schema.Attribute{ "enabled": schema.BoolAttribute{ MarkdownDescription: "Whether rolling releases are enabled.", @@ -109,9 +111,9 @@ type RollingReleaseDataSource struct { } type RollingReleaseInfoDataSource struct { - RollingRelease RollingReleaseDataSource `tfsdk:"rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` + RollingRelease types.Object `tfsdk:"rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } func convertStagesDataSource(stages []client.RollingReleaseStage) []RollingReleaseStageDataSource { @@ -136,22 +138,32 @@ func convertStagesDataSource(stages []client.RollingReleaseStage) []RollingRelea } func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo) RollingReleaseInfoDataSource { - result := RollingReleaseInfoDataSource{ - RollingRelease: RollingReleaseDataSource{ - Enabled: types.BoolValue(response.RollingRelease.Enabled), - AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), - Stages: convertStagesDataSource(response.RollingRelease.Stages), - }, - ProjectID: types.StringValue(response.ProjectID), - TeamID: types.StringValue(response.TeamID), + rollingRelease := RollingReleaseDataSource{ + Enabled: types.BoolValue(response.RollingRelease.Enabled), + AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), + Stages: convertStagesDataSource(response.RollingRelease.Stages), } if !response.RollingRelease.Enabled { - result.RollingRelease.AdvancementType = types.StringValue("") - result.RollingRelease.Stages = make([]RollingReleaseStageDataSource, 0) + rollingRelease.AdvancementType = types.StringValue("") + rollingRelease.Stages = make([]RollingReleaseStageDataSource, 0) } - return result + rollingReleaseObj, _ := types.ObjectValueFrom(context.Background(), map[string]attr.Type{ + "enabled": types.BoolType, + "advancement_type": types.StringType, + "stages": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + "require_approval": types.BoolType, + }}}, + }, rollingRelease) + + return RollingReleaseInfoDataSource{ + RollingRelease: rollingReleaseObj, + ProjectID: types.StringValue(response.ProjectID), + TeamID: types.StringValue(response.TeamID), + } } func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { From c298f7d9445528fdbd3e7452d556f058986daf1c Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 09:41:43 -0600 Subject: [PATCH 035/133] [rr] --- docs/data-sources/project_rolling_release.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 1569689b..65bdb845 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -31,11 +31,8 @@ data "vercel_project_rolling_release" "example" { ### Optional -- `team_id` (String) The ID of the Vercel team. - -### Read-Only - - `rolling_release` (Attributes) The rolling release configuration. (see [below for nested schema](#nestedatt--rolling_release)) +- `team_id` (String) The ID of the Vercel team. ### Nested Schema for `rolling_release` From 4efcb1cc00bffec712aea014112a9195cb1d5dd2 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 10:20:48 -0600 Subject: [PATCH 036/133] [rr] --- ...ata_source_project_rolling_release_test.go | 49 +++++++++++++++++++ .../resource_project_rolling_release_test.go | 14 ++---- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index e1703db2..a55e7fb4 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -16,6 +16,24 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), ), Steps: []resource.TestStep{ + // First create the project + { + Config: cfg(testAccProjectConfig(nameSuffix)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project.example", "id"), + ), + }, + // Then create the rolling release in an enabled state + { + Config: cfg(testAccProjectRollingReleaseDataSourceConfigEnabled(nameSuffix)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.enabled", "true"), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.advancement_type", "manual-approval"), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.stages.#", "3"), + ), + }, + // Then disable it and check the data source { Config: cfg(testAccProjectRollingReleaseDataSourceConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( @@ -29,6 +47,37 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }) } +func testAccProjectRollingReleaseDataSourceConfigEnabled(projectName string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%[1]s" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + depends_on = [vercel_project.example] + rolling_release = { + enabled = true + advancement_type = "manual-approval" + stages = [ + { + require_approval = true + target_percentage = 20 + }, + { + require_approval = true + target_percentage = 50 + }, + { + require_approval = true + target_percentage = 100 + } + ] + } +} +`, projectName) +} + func testAccProjectRollingReleaseDataSourceConfig(projectName string) string { return fmt.Sprintf(` resource "vercel_project" "example" { diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index ae8f8155..d920662c 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -36,14 +36,11 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), ), Steps: []resource.TestStep{ - // First create the resource in a disabled state + // First create the project { - Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), + Config: cfg(testAccProjectConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( - testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", ""), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "0"), + resource.TestCheckResourceAttrSet("vercel_project.example", "id"), ), }, // Then enable with initial configuration @@ -94,7 +91,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }), ), }, - // Finally disable again + // Finally disable { Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( @@ -112,7 +109,6 @@ func testAccProjectRollingReleasesConfig(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" - skew_protection = "12 hours" } resource "vercel_project_rolling_release" "example" { @@ -144,7 +140,6 @@ func testAccProjectRollingReleasesConfigUpdate(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" - skew_protection = "12 hours" } resource "vercel_project_rolling_release" "example" { @@ -180,7 +175,6 @@ func testAccProjectRollingReleasesConfigOff(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" - skew_protection = "12 hours" } resource "vercel_project_rolling_release" "example" { From 6e1389c933873484c46ce08920b42329082655f2 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 10:27:55 -0600 Subject: [PATCH 037/133] Delete examples/resources/vercel_project_rolling_release/import.sh --- .../resources/vercel_project_rolling_release/import.sh | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 examples/resources/vercel_project_rolling_release/import.sh diff --git a/examples/resources/vercel_project_rolling_release/import.sh b/examples/resources/vercel_project_rolling_release/import.sh deleted file mode 100644 index b38a9b2a..00000000 --- a/examples/resources/vercel_project_rolling_release/import.sh +++ /dev/null @@ -1,9 +0,0 @@ -# If importing into a personal account, or with a team configured on -# the provider, simply use the project ID. -# - project_id can be found in the project `settings` tab in the Vercel UI. -terraform import vercel_project.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx - -# Alternatively, 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.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx From 472eb7f22c72d1eff260bcb89a998f9ab19d779e Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 10:30:25 -0600 Subject: [PATCH 038/133] [rr] --- docs/resources/project_rolling_release.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 4af26797..409c8d42 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -77,19 +77,3 @@ Optional: - `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. - `require_approval` (Boolean) Whether approval is required before advancing to the next stage. - -## Import - -Import is supported using the following syntax: - -```shell -# If importing into a personal account, or with a team configured on -# the provider, simply use the project ID. -# - project_id can be found in the project `settings` tab in the Vercel UI. -terraform import vercel_project.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx - -# Alternatively, 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.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx -``` From 9f2ae2d1785b99d446f22086341c9550c3c52bc6 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 12:13:16 -0600 Subject: [PATCH 039/133] [rr] --- ...ata_source_project_rolling_release_test.go | 68 +------------------ .../resource_project_rolling_release_test.go | 3 + 2 files changed, 6 insertions(+), 65 deletions(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index a55e7fb4..0694dc41 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -1,7 +1,6 @@ package vercel_test import ( - "fmt" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -16,26 +15,18 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), ), Steps: []resource.TestStep{ - // First create the project { - Config: cfg(testAccProjectConfig(nameSuffix)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("vercel_project.example", "id"), - ), - }, - // Then create the rolling release in an enabled state - { - Config: cfg(testAccProjectRollingReleaseDataSourceConfigEnabled(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.enabled", "true"), resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.advancement_type", "manual-approval"), - resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.stages.#", "3"), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.stages.#", "4"), ), }, // Then disable it and check the data source { - Config: cfg(testAccProjectRollingReleaseDataSourceConfig(nameSuffix)), + Config: cfg(testAccProjectConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.enabled", "false"), @@ -46,56 +37,3 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }, }) } - -func testAccProjectRollingReleaseDataSourceConfigEnabled(projectName string) string { - return fmt.Sprintf(` -resource "vercel_project" "example" { - name = "test-acc-example-project-%[1]s" -} - -resource "vercel_project_rolling_release" "example" { - project_id = vercel_project.example.id - depends_on = [vercel_project.example] - rolling_release = { - enabled = true - advancement_type = "manual-approval" - stages = [ - { - require_approval = true - target_percentage = 20 - }, - { - require_approval = true - target_percentage = 50 - }, - { - require_approval = true - target_percentage = 100 - } - ] - } -} -`, projectName) -} - -func testAccProjectRollingReleaseDataSourceConfig(projectName string) string { - return fmt.Sprintf(` -resource "vercel_project" "example" { - name = "test-acc-example-project-%[1]s" -} - -resource "vercel_project_rolling_release" "example" { - project_id = vercel_project.example.id - depends_on = [vercel_project.example] - rolling_release = { - enabled = false - advancement_type = "" - stages = [] - } -} - -data "vercel_project_rolling_release" "example" { - project_id = vercel_project_rolling_release.example.project_id -} -`, projectName) -} diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index d920662c..35abb4b8 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -47,6 +47,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { { Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "manual-approval"), @@ -69,6 +70,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { { Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "manual-approval"), @@ -95,6 +97,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { { Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", ""), From 607fe394171c015b9930664ba7803d2e2b7f16be Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 12:30:31 -0600 Subject: [PATCH 040/133] [rr] --- vercel/data_source_project_rolling_release_test.go | 3 ++- vercel/resource_project_rolling_release_test.go | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 0694dc41..f10da153 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -15,6 +15,7 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), ), Steps: []resource.TestStep{ + // First create the project and enable rolling release { Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( @@ -26,7 +27,7 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }, // Then disable it and check the data source { - Config: cfg(testAccProjectConfig(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.enabled", "false"), diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 35abb4b8..0adcde06 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -135,6 +135,9 @@ resource "vercel_project_rolling_release" "example" { } ] } + lifecycle { + depends_on = [vercel_project.example] + } } `, nameSuffix) } @@ -170,6 +173,9 @@ resource "vercel_project_rolling_release" "example" { } ] } + lifecycle { + depends_on = [vercel_project.example] + } } `, nameSuffix) } @@ -188,6 +194,9 @@ resource "vercel_project_rolling_release" "example" { advancement_type = "" stages = [] } + lifecycle { + depends_on = [vercel_project.example] + } } `, nameSuffix) } From 65607d8f888510fc1164859f8cbc487a570322f2 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 12:47:02 -0600 Subject: [PATCH 041/133] [rr] --- vercel/resource_project_rolling_release_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 0adcde06..35abb4b8 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -135,9 +135,6 @@ resource "vercel_project_rolling_release" "example" { } ] } - lifecycle { - depends_on = [vercel_project.example] - } } `, nameSuffix) } @@ -173,9 +170,6 @@ resource "vercel_project_rolling_release" "example" { } ] } - lifecycle { - depends_on = [vercel_project.example] - } } `, nameSuffix) } @@ -194,9 +188,6 @@ resource "vercel_project_rolling_release" "example" { advancement_type = "" stages = [] } - lifecycle { - depends_on = [vercel_project.example] - } } `, nameSuffix) } From 1d7d81c703d78f75595138a35e64a43d6b032cbb Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 13:03:43 -0600 Subject: [PATCH 042/133] [rr] --- ...ata_source_project_rolling_release_test.go | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index f10da153..3cb4d858 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -1,6 +1,7 @@ package vercel_test import ( + "fmt" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -27,7 +28,7 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }, // Then disable it and check the data source { - Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesConfigOffWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.enabled", "false"), @@ -38,3 +39,26 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }, }) } + +func testAccProjectRollingReleasesConfigOffWithDataSource(nameSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%s" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + depends_on = [vercel_project.example] + rolling_release = { + enabled = false + advancement_type = "" + stages = [] + } +} + +data "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + depends_on = [vercel_project_rolling_release.example] +} +`, nameSuffix) +} From 87c4c99b0fac73f1530be8211aec3a9314bfe33f Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 13:05:21 -0600 Subject: [PATCH 043/133] [rr] --- vercel/data_source_project_rolling_release_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 3cb4d858..2220e797 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -16,7 +16,14 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), ), Steps: []resource.TestStep{ - // First create the project and enable rolling release + // First create the project + { + Config: cfg(testAccProjectConfig(nameSuffix)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project.example", "id"), + ), + }, + // Then enable rolling release { Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( From b93fdf8366866c2d72e27ab8eb6e7250deae4ff4 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 13:26:36 -0600 Subject: [PATCH 044/133] [rr] --- vercel/data_source_project_rolling_release_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 2220e797..54fb6e02 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -25,12 +25,12 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }, // Then enable rolling release { - Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.enabled", "true"), resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.advancement_type", "manual-approval"), - resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.stages.#", "4"), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.stages.#", "3"), ), }, // Then disable it and check the data source From afd0809f5eaaf3cd04e0c584b6163e3dcbd9aca3 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Fri, 13 Jun 2025 21:11:49 +0100 Subject: [PATCH 045/133] Tweak and delete a load of stuff --- client/project_rolling_release.go | 14 ++++----- ...ata_source_project_rolling_release_test.go | 23 ++------------ vercel/resource_project_rolling_release.go | 30 +------------------ .../resource_project_rolling_release_test.go | 25 +++++----------- 4 files changed, 15 insertions(+), 77 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 68c20561..145e90c3 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -57,9 +57,8 @@ type UpdateRollingReleaseRequest struct { // UpdateRollingRelease will update an existing rolling release to the latest information. func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseInfo, error) { - teamId := c.TeamID(request.TeamID) + request.TeamID = c.TeamID(request.TeamID) if request.RollingRelease.Enabled { - enableRequest := map[string]any{ "enabled": true, "advancementType": request.RollingRelease.AdvancementType, @@ -67,11 +66,10 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } var result RollingReleaseInfo - err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), body: string(mustMarshal(enableRequest)), }, &result) if err != nil { @@ -79,8 +77,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } result.ProjectID = request.ProjectID - result.TeamID = teamId - + result.TeamID = request.TeamID return result, nil } else { // For disabling, just send the request as is @@ -93,11 +90,10 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } var result RollingReleaseInfo - err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, teamId), + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), body: string(mustMarshal(disabledRequest.RollingRelease)), }, &result) if err != nil { @@ -105,7 +101,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } result.ProjectID = request.ProjectID - result.TeamID = teamId + result.TeamID = request.TeamID return result, nil } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 54fb6e02..33fc588d 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -9,6 +9,7 @@ import ( ) func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { + return nameSuffix := acctest.RandString(16) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, @@ -16,24 +17,6 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), ), Steps: []resource.TestStep{ - // First create the project - { - Config: cfg(testAccProjectConfig(nameSuffix)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("vercel_project.example", "id"), - ), - }, - // Then enable rolling release - { - Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix)), - Check: resource.ComposeAggregateTestCheckFunc( - testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), - resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.enabled", "true"), - resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.advancement_type", "manual-approval"), - resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "rolling_release.stages.#", "3"), - ), - }, - // Then disable it and check the data source { Config: cfg(testAccProjectRollingReleasesConfigOffWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( @@ -55,7 +38,6 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - depends_on = [vercel_project.example] rolling_release = { enabled = false advancement_type = "" @@ -64,8 +46,7 @@ resource "vercel_project_rolling_release" "example" { } data "vercel_project_rolling_release" "example" { - project_id = vercel_project.example.id - depends_on = [vercel_project_rolling_release.example] + project_id = vercel_project_rolling_release.example.project_id } `, nameSuffix) } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index ca24305c..a27ac534 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -214,27 +214,6 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *RollingReleaseInfo, ctx context.Context) (RollingReleaseInfo, diag.Diagnostics) { var diags diag.Diagnostics - - // If we have a plan and the response doesn't match what we expect, - // this is likely a race condition and we should preserve the plan values - if plan != nil && plan.RollingRelease.Enabled.ValueBool() { - // If the plan is enabled but response shows disabled or missing fields, - // this is likely a race condition - if !response.RollingRelease.Enabled || - response.RollingRelease.AdvancementType == "" || - len(response.RollingRelease.Stages) == 0 { - tflog.Debug(ctx, "detected race condition, preserving plan values", map[string]any{ - "plan_enabled": plan.RollingRelease.Enabled.ValueBool(), - "plan_advancement_type": plan.RollingRelease.AdvancementType.ValueString(), - "plan_stages_count": len(plan.RollingRelease.Stages.Elements()), - "response_enabled": response.RollingRelease.Enabled, - "response_advancement_type": response.RollingRelease.AdvancementType, - "response_stages_count": len(response.RollingRelease.Stages), - }) - return *plan, diags - } - } - result := RollingReleaseInfo{ RollingRelease: RollingRelease{ Enabled: types.BoolValue(response.RollingRelease.Enabled), @@ -371,7 +350,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource } // Log the request for debugging - tflog.Debug(ctx, "creating rolling release", map[string]any{ + tflog.Info(ctx, "creating rolling release", map[string]any{ "enabled": request.RollingRelease.Enabled, "advancement_type": request.RollingRelease.AdvancementType, "stages": request.RollingRelease.Stages, @@ -401,7 +380,6 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } - // Wait a bit before enabling time.Sleep(2 * time.Second) } @@ -542,9 +520,6 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource ) return } - - // Wait a bit before proceeding - time.Sleep(2 * time.Second) } // If we're transitioning from disabled to enabled, first create in disabled state @@ -569,9 +544,6 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource ) return } - - // Wait a bit before enabling - time.Sleep(2 * time.Second) } out, err := r.client.UpdateRollingRelease(ctx, request) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 35abb4b8..b11d0f3d 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -36,14 +36,6 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { testAccProjectDestroy(testClient(t), "vercel_project.example", testTeam(t)), ), Steps: []resource.TestStep{ - // First create the project - { - Config: cfg(testAccProjectConfig(nameSuffix)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("vercel_project.example", "id"), - ), - }, - // Then enable with initial configuration { Config: cfg(testAccProjectRollingReleasesConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( @@ -68,7 +60,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }, // Then update to new configuration { - Config: cfg(testAccProjectRollingReleasesConfigUpdate(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesConfigUpdated(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), @@ -95,7 +87,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }, // Finally disable { - Config: cfg(testAccProjectRollingReleasesConfigOff(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesConfigDisabled(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), @@ -111,12 +103,11 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { func testAccProjectRollingReleasesConfig(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { - name = "test-acc-example-project-%s" + name = "test-acc-rolling-releases-%s" } resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - depends_on = [vercel_project.example] rolling_release = { enabled = true advancement_type = "manual-approval" @@ -139,15 +130,14 @@ resource "vercel_project_rolling_release" "example" { `, nameSuffix) } -func testAccProjectRollingReleasesConfigUpdate(nameSuffix string) string { +func testAccProjectRollingReleasesConfigUpdated(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { - name = "test-acc-example-project-%s" + name = "test-acc-rolling-releases-%s" } resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - depends_on = [vercel_project.example] rolling_release = { enabled = true advancement_type = "manual-approval" @@ -174,15 +164,14 @@ resource "vercel_project_rolling_release" "example" { `, nameSuffix) } -func testAccProjectRollingReleasesConfigOff(nameSuffix string) string { +func testAccProjectRollingReleasesConfigDisabled(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { - name = "test-acc-example-project-%s" + name = "test-acc-rolling-releases-%s" } resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - depends_on = [vercel_project.example] rolling_release = { enabled = false advancement_type = "" From 9926350254db5a8b611d76cdb121749b7cb7f371 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Fri, 13 Jun 2025 21:30:31 +0100 Subject: [PATCH 046/133] More fixes --- client/project_rolling_release.go | 6 ++++++ vercel/resource_project_rolling_release.go | 20 +++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 145e90c3..d9fd04cc 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -3,6 +3,8 @@ package client import ( "context" "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" ) // RollingReleaseStage represents a stage in a rolling release @@ -78,6 +80,10 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling result.ProjectID = request.ProjectID result.TeamID = request.TeamID + tflog.Info(ctx, "enabled rolling release", map[string]any{ + "response": result, + "request": request, + }) return result, nil } else { // For disabling, just send the request as is diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index a27ac534..943063e5 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -3,7 +3,6 @@ package vercel import ( "context" "fmt" - "time" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -101,6 +100,7 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S "duration": schema.Int64Attribute{ MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", Optional: true, + Computed: true, }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", @@ -195,7 +195,7 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli } // Log the request for debugging - tflog.Debug(context.Background(), "converting to update request", map[string]any{ + tflog.Info(context.Background(), "converting to update request", map[string]any{ "enabled": e.RollingRelease.Enabled.ValueBool(), "advancement_type": advancementType, "stages_count": len(stages), @@ -214,17 +214,21 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *RollingReleaseInfo, ctx context.Context) (RollingReleaseInfo, diag.Diagnostics) { var diags diag.Diagnostics + advancementType := types.StringNull() + if plan.RollingRelease.Enabled.ValueBool() { + advancementType = plan.RollingRelease.AdvancementType + } result := RollingReleaseInfo{ RollingRelease: RollingRelease{ - Enabled: types.BoolValue(response.RollingRelease.Enabled), - AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), + Enabled: plan.RollingRelease.Enabled, + AdvancementType: advancementType, }, ProjectID: types.StringValue(response.ProjectID), TeamID: types.StringValue(response.TeamID), } // If disabled, return empty values - if !response.RollingRelease.Enabled { + if !plan.RollingRelease.Enabled.ValueBool() { result.RollingRelease.AdvancementType = types.StringValue("") // Create an empty list instead of null emptyStages, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ @@ -293,7 +297,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R } } else { // For manual approval, duration is not used - duration = types.Int64Value(0) + duration = types.Int64Null() } elements[i] = types.ObjectValueMust( @@ -324,7 +328,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R result.RollingRelease.Stages = stages // Log the conversion result for debugging - tflog.Debug(ctx, "converted rolling release response", map[string]any{ + tflog.Info(ctx, "converted rolling release response", map[string]any{ "enabled": result.RollingRelease.Enabled.ValueBool(), "advancement_type": result.RollingRelease.AdvancementType.ValueString(), "stages_count": len(elements), @@ -379,8 +383,6 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource ) return } - - time.Sleep(2 * time.Second) } out, err := r.client.UpdateRollingRelease(ctx, request) From afbba0f321a90ed64e3f7df5811261215da5f8b6 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 14:37:06 -0600 Subject: [PATCH 047/133] Update data_source_project_rolling_release_test.go --- vercel/data_source_project_rolling_release_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 33fc588d..29c6e7e6 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -9,7 +9,6 @@ import ( ) func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { - return nameSuffix := acctest.RandString(16) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, From 6acb266930d5a583a725c12e505500848fd36b15 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 14:52:16 -0600 Subject: [PATCH 048/133] [rr] --- vercel/resource_project_rolling_release_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index b11d0f3d..f1a31d25 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -53,7 +53,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { "target_percentage": "50", }), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "true", + "require_approval": "false", "target_percentage": "100", }), ), @@ -113,15 +113,12 @@ resource "vercel_project_rolling_release" "example" { advancement_type = "manual-approval" stages = [ { - require_approval = true target_percentage = 20 }, { - require_approval = true target_percentage = 50 }, { - require_approval = true target_percentage = 100 } ] From 08370ecdbfd71b27d365c0f6bb357365f6e4df1a Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 15:05:29 -0600 Subject: [PATCH 049/133] [rr] --- vercel/resource_project_rolling_release_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index f1a31d25..81bda2d6 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -104,6 +104,7 @@ func testAccProjectRollingReleasesConfig(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-rolling-releases-%s" + skew_protection = "12 hours" } resource "vercel_project_rolling_release" "example" { From 5ec8a67f19e402a4d1386c7eeb8e4ff01e2c8f23 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 15:17:41 -0600 Subject: [PATCH 050/133] [rr] --- vercel/resource_project_rolling_release_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 81bda2d6..b1d47e2a 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -80,7 +80,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { "target_percentage": "80", }), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "true", + "require_approval": "false", "target_percentage": "100", }), ), From 1562ef526c91deedab6c32c69b29d61beef5919d Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 15:35:20 -0600 Subject: [PATCH 051/133] [rr] --- vercel/resource_project_rolling_release_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index b1d47e2a..76703618 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -153,7 +153,7 @@ resource "vercel_project_rolling_release" "example" { target_percentage = 80 }, { - require_approval = true + require_approval = false target_percentage = 100 } ] From ed0fef6f5ade361927ea0227fc334e174b8bb3a7 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 15:56:25 -0600 Subject: [PATCH 052/133] [rr] --- vercel/resource_project_rolling_release_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 76703618..7c1d3d90 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -132,6 +132,7 @@ func testAccProjectRollingReleasesConfigUpdated(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-rolling-releases-%s" + skew_protection = "12 hours" } resource "vercel_project_rolling_release" "example" { From 3e90d29c667d52367a016ba3b7e42540a1c0f239 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 16:15:36 -0600 Subject: [PATCH 053/133] Update examples/resources/vercel_project_rolling_release/resource.tf Co-authored-by: Douglas Harcourt Parsons --- examples/resources/vercel_project_rolling_release/resource.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/resources/vercel_project_rolling_release/resource.tf b/examples/resources/vercel_project_rolling_release/resource.tf index cb2c2654..e895ed1f 100644 --- a/examples/resources/vercel_project_rolling_release/resource.tf +++ b/examples/resources/vercel_project_rolling_release/resource.tf @@ -5,7 +5,6 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - depends_on = [vercel_project.example] rolling_release = { enabled = true advancement_type = "manual-approval" From d58f6e3c58e7716f1c58376baddfb11ac7f79fce Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 16:18:33 -0600 Subject: [PATCH 054/133] [rr] --- docs/resources/project_rolling_release.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 409c8d42..684722b3 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -20,7 +20,6 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - depends_on = [vercel_project.example] rolling_release = { enabled = true advancement_type = "manual-approval" From 9d249acbd87d5ce550ad943cb53afd25dfc61183 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Fri, 13 Jun 2025 16:36:43 -0600 Subject: [PATCH 055/133] Update project.go --- client/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/project.go b/client/project.go index 0d48cbc2..2433b261 100644 --- a/client/project.go +++ b/client/project.go @@ -210,7 +210,7 @@ type ProjectResponse struct { ResourceConfig *ResourceConfigResponse `json:"resourceConfig"` NodeVersion string `json:"nodeVersion"` Crons *ProjectCronsResponse `json:"crons"` - RollingRelease *RollingRelease `json:"rollingRelease,omitempty"` + RollingRelease *RollingRelease `json:"rollingRelease"` } type ProjectCronsResponse struct { From 65ba997c44bf519bab82b6733b1acd98cffc8bf6 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 09:11:36 -0600 Subject: [PATCH 056/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 943063e5..3e3a1997 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -104,7 +104,6 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", - Optional: true, Computed: true, }, }, From 305ca2b71cc4bc9909f1ac69907e634f0a2693a2 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 09:14:12 -0600 Subject: [PATCH 057/133] [rolling-release] --- docs/resources/project_rolling_release.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 684722b3..f22067aa 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -75,4 +75,7 @@ Required: Optional: - `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. + +Read-Only: + - `require_approval` (Boolean) Whether approval is required before advancing to the next stage. From edead8fd07896ad0eb5bf1b8b1b852095b147bf3 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 09:26:54 -0600 Subject: [PATCH 058/133] [rolling-release] --- vercel/resource_project_rolling_release_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 7c1d3d90..0b08404a 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -142,19 +142,15 @@ resource "vercel_project_rolling_release" "example" { advancement_type = "manual-approval" stages = [ { - require_approval = true target_percentage = 20 }, { - require_approval = true target_percentage = 50 }, { - require_approval = true target_percentage = 80 }, { - require_approval = false target_percentage = 100 } ] From 13c89646a720d70235183c6f06af94ceba6050f8 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 10:08:27 -0600 Subject: [PATCH 059/133] [rolling-release] --- docs/resources/project_rolling_release.md | 6 +- vercel/resource_project_rolling_release.go | 8 ++- .../resource_project_rolling_release_test.go | 59 +++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index f22067aa..ae31c717 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -63,6 +63,7 @@ Required: Optional: - `advancement_type` (String) The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true. +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. - `stages` (Attributes List) The stages of the rolling release. Required when enabled is true. (see [below for nested schema](#nestedatt--rolling_release--stages)) @@ -72,10 +73,7 @@ Required: - `target_percentage` (Number) The percentage of traffic to route to this stage. -Optional: - -- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. - Read-Only: +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. - `require_approval` (Boolean) Whether approval is required before advancing to the next stage. diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 3e3a1997..a04ce974 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -84,6 +84,11 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S advancementTypeValidator{}, }, }, + "duration": schema.Int64Attribute{ + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", + Optional: true, + Computed: true, + }, "stages": schema.ListNestedAttribute{ MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", Optional: true, @@ -98,8 +103,7 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", - Optional: true, + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", Computed: true, }, "require_approval": schema.BoolAttribute{ diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 0b08404a..737a8bf7 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -85,6 +85,34 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }), ), }, + // Then update to new configuration + { + Config: cfg(testAccProjectRollingReleasesConfigUpdatedAutomatic(nameSuffix)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project.example", "id"), + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.duration", "10"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "false", + "target_percentage": "20", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "false", + "target_percentage": "50", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "false", + "target_percentage": "80", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "require_approval": "false", + "target_percentage": "100", + }), + ), + }, // Finally disable { Config: cfg(testAccProjectRollingReleasesConfigDisabled(nameSuffix)), @@ -158,6 +186,37 @@ resource "vercel_project_rolling_release" "example" { } `, nameSuffix) } +func testAccProjectRollingReleasesConfigUpdatedAutomatic(nameSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-rolling-releases-%s" + skew_protection = "12 hours" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + rolling_release = { + enabled = true + advancement_type = "automatic" + duration = 10 + stages = [ + { + target_percentage = 20 + }, + { + target_percentage = 50 + }, + { + target_percentage = 80 + }, + { + target_percentage = 100 + } + ] + } +} +`, nameSuffix) +} func testAccProjectRollingReleasesConfigDisabled(nameSuffix string) string { return fmt.Sprintf(` From 802c9a3c77327f4a1c6e69e2542868ec8747b0a2 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 10:30:39 -0600 Subject: [PATCH 060/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 23 +++++++--------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index a04ce974..77125b20 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -85,7 +85,7 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. This duration will be applied to all stages except the last one.", Optional: true, Computed: true, }, @@ -129,6 +129,7 @@ type RollingReleaseStage struct { type RollingRelease struct { Enabled types.Bool `tfsdk:"enabled"` AdvancementType types.String `tfsdk:"advancement_type"` + Duration types.Int64 `tfsdk:"duration"` Stages types.List `tfsdk:"stages"` } @@ -160,21 +161,11 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli // For automatic advancement, duration is required except for last stage if i < len(tfStages)-1 { // Non-last stage needs duration - if stage.Duration.IsNull() { - // Default duration for non-last stages - duration := 60 - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - Duration: &duration, - RequireApproval: stage.RequireApproval.ValueBool(), - } - } else { - duration := int(stage.Duration.ValueInt64()) - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - Duration: &duration, - RequireApproval: stage.RequireApproval.ValueBool(), - } + duration := int(e.RollingRelease.Duration.ValueInt64()) + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + Duration: &duration, + RequireApproval: stage.RequireApproval.ValueBool(), } } else { // Last stage should not have duration From 2bd7a505e863c82f923f1c1afc4aa9fad7201b09 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 10:32:12 -0600 Subject: [PATCH 061/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 1 - vercel/resource_project_rolling_release_test.go | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 77125b20..b3a0aa92 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -129,7 +129,6 @@ type RollingReleaseStage struct { type RollingRelease struct { Enabled types.Bool `tfsdk:"enabled"` AdvancementType types.String `tfsdk:"advancement_type"` - Duration types.Int64 `tfsdk:"duration"` Stages types.List `tfsdk:"stages"` } diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 737a8bf7..cd56b5ab 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -93,7 +93,6 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.duration", "10"), resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "require_approval": "false", @@ -198,19 +197,22 @@ resource "vercel_project_rolling_release" "example" { rolling_release = { enabled = true advancement_type = "automatic" - duration = 10 stages = [ { target_percentage = 20 + duration = 10 }, { target_percentage = 50 + duration = 10 }, { target_percentage = 80 + duration = 10 }, { target_percentage = 100 + duration = 10 } ] } From f7313ee79d6cee3d4f35157ba78353d38b992353 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 10:33:49 -0600 Subject: [PATCH 062/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index b3a0aa92..019563dc 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -160,11 +160,21 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli // For automatic advancement, duration is required except for last stage if i < len(tfStages)-1 { // Non-last stage needs duration - duration := int(e.RollingRelease.Duration.ValueInt64()) - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - Duration: &duration, - RequireApproval: stage.RequireApproval.ValueBool(), + if stage.Duration.IsNull() { + // Default duration for non-last stages + duration := 60 + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + Duration: &duration, + RequireApproval: stage.RequireApproval.ValueBool(), + } + } else { + duration := int(stage.Duration.ValueInt64()) + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + Duration: &duration, + RequireApproval: stage.RequireApproval.ValueBool(), + } } } else { // Last stage should not have duration From f4a07cb1b5c3f91890ca8566c4ff27908bff21ec Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 11:20:45 -0600 Subject: [PATCH 063/133] [rolling-release] --- docs/resources/project_rolling_release.md | 1 - vercel/resource_project_rolling_release.go | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index ae31c717..73064021 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -63,7 +63,6 @@ Required: Optional: - `advancement_type` (String) The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true. -- `duration` (Number) The duration in minutes to wait before advancing to the next stage. - `stages` (Attributes List) The stages of the rolling release. Required when enabled is true. (see [below for nested schema](#nestedatt--rolling_release--stages)) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 019563dc..e7a31397 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -84,11 +84,6 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S advancementTypeValidator{}, }, }, - "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. This duration will be applied to all stages except the last one.", - Optional: true, - Computed: true, - }, "stages": schema.ListNestedAttribute{ MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", Optional: true, @@ -129,6 +124,7 @@ type RollingReleaseStage struct { type RollingRelease struct { Enabled types.Bool `tfsdk:"enabled"` AdvancementType types.String `tfsdk:"advancement_type"` + Duration types.Int64 `tfsdk:"duration"` Stages types.List `tfsdk:"stages"` } From 8d196cee121c27dc0bd69d35eedeb92ab7dfda8b Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 11:39:18 -0600 Subject: [PATCH 064/133] [rolling-release] --- docs/resources/project_rolling_release.md | 5 ++++- vercel/resource_project_rolling_release.go | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 73064021..8a25327c 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -72,7 +72,10 @@ Required: - `target_percentage` (Number) The percentage of traffic to route to this stage. -Read-Only: +Optional: - `duration` (Number) The duration in minutes to wait before advancing to the next stage. + +Read-Only: + - `require_approval` (Boolean) Whether approval is required before advancing to the next stage. diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index e7a31397..a8676768 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -100,6 +100,7 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S "duration": schema.Int64Attribute{ MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", Computed: true, + Optional: true, }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", From 81d7ca8c0a273900fbdbfb4bae1f38d277a97a02 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 11:55:32 -0600 Subject: [PATCH 065/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index a8676768..1ae85448 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -125,7 +125,6 @@ type RollingReleaseStage struct { type RollingRelease struct { Enabled types.Bool `tfsdk:"enabled"` AdvancementType types.String `tfsdk:"advancement_type"` - Duration types.Int64 `tfsdk:"duration"` Stages types.List `tfsdk:"stages"` } From 3c6533402930da523461709c299c4806166b94b9 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 12:24:34 -0600 Subject: [PATCH 066/133] [rolling-release] --- vercel/resource_project_rolling_release_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index cd56b5ab..81dd9557 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -97,14 +97,17 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "require_approval": "false", "target_percentage": "20", + "duration": "10", }), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "require_approval": "false", "target_percentage": "50", + "duration": "10", }), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "require_approval": "false", "target_percentage": "80", + "duration": "10", }), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ "require_approval": "false", @@ -200,19 +203,18 @@ resource "vercel_project_rolling_release" "example" { stages = [ { target_percentage = 20 - duration = 10 + duration = 10 }, { target_percentage = 50 - duration = 10 + duration = 10 }, { target_percentage = 80 - duration = 10 + duration = 10 }, { target_percentage = 100 - duration = 10 } ] } From 2f401da580b28449efddf29867601bc7d039b6b6 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 12:40:10 -0600 Subject: [PATCH 067/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index f39de818..751b1255 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -83,7 +83,7 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour }, "duration": schema.Int64Attribute{ MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", - Computed: true, + Optional: true, }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", From ad6201f686cc69afdbd465950c9a28747649dfcd Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 12:40:23 -0600 Subject: [PATCH 068/133] [rolling-release] --- docs/data-sources/project_rolling_release.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 65bdb845..959d02db 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -46,8 +46,11 @@ Read-Only: ### Nested Schema for `rolling_release.stages` -Read-Only: +Optional: - `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. + +Read-Only: + - `require_approval` (Boolean) Whether approval is required before advancing to the next stage. - `target_percentage` (Number) The percentage of traffic to route to this stage. From 0d24b077b848ed61d5405255d142257f357f71fb Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 12:59:50 -0600 Subject: [PATCH 069/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 80 ++++++++++++---------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 1ae85448..704c4366 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -98,9 +98,11 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", - Computed: true, + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for non-last stages when advancement_type is 'automatic'.", Optional: true, + Validators: []validator.Int64{ + int64validator.Between(0, 999), + }, }, "require_approval": schema.BoolAttribute{ MarkdownDescription: "Whether approval is required before advancing to the next stage.", @@ -180,7 +182,7 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli } } } else { - // For manual approval, omit duration field completely + // For manual approval, duration is not used stages[i] = client.RollingReleaseStage{ TargetPercentage: int(stage.TargetPercentage.ValueInt64()), RequireApproval: stage.RequireApproval.ValueBool(), @@ -247,9 +249,9 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // If we have a plan, try to match stages by target percentage to preserve order var orderedStages []client.RollingReleaseStage - if plan != nil && !plan.RollingRelease.Stages.IsNull() && !plan.RollingRelease.Stages.IsUnknown() { + if plan != nil && !plan.RollingRelease.Stages.IsNull() { var planStages []RollingReleaseStage - diags.Append(plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false)...) + diags = plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false) if diags.HasError() { return result, diags } @@ -260,46 +262,53 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stageMap[stage.TargetPercentage] = stage } - // Try to preserve the order from the plan - orderedStages = make([]client.RollingReleaseStage, 0, len(response.RollingRelease.Stages)) - for _, planStage := range planStages { - if stage, ok := stageMap[int(planStage.TargetPercentage.ValueInt64())]; ok { - orderedStages = append(orderedStages, stage) - delete(stageMap, stage.TargetPercentage) - } - } - - // Add any remaining stages that weren't in the plan - for _, stage := range response.RollingRelease.Stages { - if _, ok := stageMap[stage.TargetPercentage]; ok { - orderedStages = append(orderedStages, stage) + // Match stages by target percentage + orderedStages = make([]client.RollingReleaseStage, len(planStages)) + for i, planStage := range planStages { + targetPercentage := int(planStage.TargetPercentage.ValueInt64()) + if stage, ok := stageMap[targetPercentage]; ok { + orderedStages[i] = stage } } } else { orderedStages = response.RollingRelease.Stages } - // Convert stages from response - elements := make([]attr.Value, len(orderedStages)) + // Convert stages to Terraform types + stages := make([]attr.Value, len(orderedStages)) for i, stage := range orderedStages { var duration types.Int64 - if response.RollingRelease.AdvancementType == "automatic" { - // For automatic advancement, duration is required except for the last stage - if i < len(orderedStages)-1 { - if stage.Duration != nil { - duration = types.Int64Value(int64(*stage.Duration)) + if plan != nil && !plan.RollingRelease.Stages.IsNull() { + var planStages []RollingReleaseStage + diags = plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false) + if !diags.HasError() && i < len(planStages) { + // Use the duration from the plan if available + duration = planStages[i].Duration + } + } + + // If duration is not set in plan, handle based on advancement type + if duration.IsNull() { + if plan.RollingRelease.AdvancementType.ValueString() == "automatic" { + if i < len(orderedStages)-1 { + // For non-last stages in automatic advancement, use the duration from the API + if stage.Duration != nil { + duration = types.Int64Value(int64(*stage.Duration)) + } else { + // Default duration for non-last stages + duration = types.Int64Value(60) + } } else { - duration = types.Int64Value(60) // Default duration in minutes + // Last stage doesn't need duration + duration = types.Int64Null() } } else { - duration = types.Int64Value(0) // Last stage doesn't need duration + // For manual approval, duration is not used + duration = types.Int64Null() } - } else { - // For manual approval, duration is not used - duration = types.Int64Null() } - elements[i] = types.ObjectValueMust( + stageObj := types.ObjectValueMust( map[string]attr.Type{ "target_percentage": types.Int64Type, "duration": types.Int64Type, @@ -311,26 +320,27 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R "require_approval": types.BoolValue(stage.RequireApproval), }, ) + stages[i] = stageObj } - stages, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ + stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, "duration": types.Int64Type, "require_approval": types.BoolType, }, - }, elements) + }, stages) diags.Append(stagesDiags...) if diags.HasError() { return result, diags } - result.RollingRelease.Stages = stages + result.RollingRelease.Stages = stagesList // Log the conversion result for debugging tflog.Info(ctx, "converted rolling release response", map[string]any{ "enabled": result.RollingRelease.Enabled.ValueBool(), "advancement_type": result.RollingRelease.AdvancementType.ValueString(), - "stages_count": len(elements), + "stages_count": len(stages), }) return result, diags From 687becdd80bc7686d950716a0b916901e093dc9c Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 17 Jun 2025 13:00:05 -0600 Subject: [PATCH 070/133] [rolling-release] --- docs/resources/project_rolling_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 8a25327c..8eb1142e 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -74,7 +74,7 @@ Required: Optional: -- `duration` (Number) The duration in minutes to wait before advancing to the next stage. +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for non-last stages when advancement_type is 'automatic'. Read-Only: From 73d2393b64c0bcbad685f3ce35b70991c75f2250 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 13:41:25 -0600 Subject: [PATCH 071/133] [rr] --- vercel/resource_project_rolling_release.go | 407 ++++++++---------- .../resource_project_rolling_release_test.go | 78 +--- 2 files changed, 190 insertions(+), 295 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 704c4366..06372ace 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -68,26 +68,13 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S Description: "The ID of the Vercel team.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, }, - "rolling_release": schema.SingleNestedAttribute{ - MarkdownDescription: "The rolling release configuration.", - Required: true, + "automatic_rolling_release": schema.SingleNestedAttribute{ + MarkdownDescription: "Automatic rolling release configuration.", + Optional: true, Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - MarkdownDescription: "Whether rolling releases are enabled.", - Required: true, - }, - "advancement_type": schema.StringAttribute{ - MarkdownDescription: "The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true.", - Optional: true, - Computed: true, - Validators: []validator.String{ - advancementTypeValidator{}, - }, - }, "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", - Optional: true, - Computed: true, + MarkdownDescription: "The stages for automatic rolling release.", + Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ @@ -98,15 +85,32 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for non-last stages when advancement_type is 'automatic'.", - Optional: true, + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", + Required: true, Validators: []validator.Int64{ - int64validator.Between(0, 999), + int64validator.Between(1, 10000), }, }, - "require_approval": schema.BoolAttribute{ - MarkdownDescription: "Whether approval is required before advancing to the next stage.", - Computed: true, + }, + }, + }, + }, + }, + "manual_rolling_release": schema.SingleNestedAttribute{ + MarkdownDescription: "Manual rolling release configuration.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "stages": schema.ListNestedAttribute{ + MarkdownDescription: "The stages for manual rolling release.", + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, }, }, }, @@ -117,24 +121,29 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S } } -type RollingReleaseStage struct { +type AutomaticStage struct { TargetPercentage types.Int64 `tfsdk:"target_percentage"` Duration types.Int64 `tfsdk:"duration"` - RequireApproval types.Bool `tfsdk:"require_approval"` } -// RollingRelease reflects the state terraform stores internally for a project rolling release. -type RollingRelease struct { - Enabled types.Bool `tfsdk:"enabled"` - AdvancementType types.String `tfsdk:"advancement_type"` - Stages types.List `tfsdk:"stages"` +type ManualStage struct { + TargetPercentage types.Int64 `tfsdk:"target_percentage"` +} + +type AutomaticRollingRelease struct { + Stages types.List `tfsdk:"stages"` +} + +type ManualRollingRelease struct { + Stages types.List `tfsdk:"stages"` } // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. type RollingReleaseInfo struct { - RollingRelease RollingRelease `tfsdk:"rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` + AutomaticRollingRelease *AutomaticRollingRelease `tfsdk:"automatic_rolling_release"` + ManualRollingRelease *ManualRollingRelease `tfsdk:"manual_rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { @@ -142,69 +151,68 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli var advancementType string var diags diag.Diagnostics - if e.RollingRelease.Enabled.ValueBool() { - advancementType = e.RollingRelease.AdvancementType.ValueString() + if e.AutomaticRollingRelease != nil { + advancementType = "automatic" - // Convert stages from types.List to []client.RollingReleaseStage - var tfStages []RollingReleaseStage - diags = e.RollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) + // Convert automatic stages + var tfStages []AutomaticStage + diags = e.AutomaticRollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) if diags.HasError() { return client.UpdateRollingReleaseRequest{}, diags } + // Add all stages from config stages = make([]client.RollingReleaseStage, len(tfStages)) for i, stage := range tfStages { - if advancementType == "automatic" { - // For automatic advancement, duration is required except for last stage - if i < len(tfStages)-1 { - // Non-last stage needs duration - if stage.Duration.IsNull() { - // Default duration for non-last stages - duration := 60 - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - Duration: &duration, - RequireApproval: stage.RequireApproval.ValueBool(), - } - } else { - duration := int(stage.Duration.ValueInt64()) - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - Duration: &duration, - RequireApproval: stage.RequireApproval.ValueBool(), - } - } - } else { - // Last stage should not have duration - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - RequireApproval: stage.RequireApproval.ValueBool(), - } - } - } else { - // For manual approval, duration is not used - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - RequireApproval: stage.RequireApproval.ValueBool(), - } + duration := int(stage.Duration.ValueInt64()) + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + Duration: &duration, + RequireApproval: false, } } - } else { - // When disabled, don't send any stages or advancement type to the API - stages = []client.RollingReleaseStage{} - advancementType = "" + + // Add terminal stage (100%) without duration + stages = append(stages, client.RollingReleaseStage{ + TargetPercentage: 100, + RequireApproval: false, + }) + + } else if e.ManualRollingRelease != nil { + advancementType = "manual-approval" + + // Convert manual stages + var tfStages []ManualStage + diags = e.ManualRollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) + if diags.HasError() { + return client.UpdateRollingReleaseRequest{}, diags + } + + // Add all stages from config + stages = make([]client.RollingReleaseStage, len(tfStages)) + for i, stage := range tfStages { + stages[i] = client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + RequireApproval: true, + } + } + + // Add terminal stage (100%) without approval + stages = append(stages, client.RollingReleaseStage{ + TargetPercentage: 100, + RequireApproval: false, + }) } // Log the request for debugging tflog.Info(context.Background(), "converting to update request", map[string]any{ - "enabled": e.RollingRelease.Enabled.ValueBool(), "advancement_type": advancementType, "stages_count": len(stages), }) return client.UpdateRollingReleaseRequest{ RollingRelease: client.RollingRelease{ - Enabled: e.RollingRelease.Enabled.ValueBool(), + Enabled: true, AdvancementType: advancementType, Stages: stages, }, @@ -215,132 +223,118 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *RollingReleaseInfo, ctx context.Context) (RollingReleaseInfo, diag.Diagnostics) { var diags diag.Diagnostics - advancementType := types.StringNull() - if plan.RollingRelease.Enabled.ValueBool() { - advancementType = plan.RollingRelease.AdvancementType - } + result := RollingReleaseInfo{ - RollingRelease: RollingRelease{ - Enabled: plan.RollingRelease.Enabled, - AdvancementType: advancementType, - }, ProjectID: types.StringValue(response.ProjectID), TeamID: types.StringValue(response.TeamID), } // If disabled, return empty values - if !plan.RollingRelease.Enabled.ValueBool() { - result.RollingRelease.AdvancementType = types.StringValue("") - // Create an empty list instead of null - emptyStages, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ + if !response.RollingRelease.Enabled { + return result, diags + } + + // Determine which type of rolling release to use based on API response + if response.RollingRelease.AdvancementType == "automatic" { + // Convert API stages to automatic stages (excluding terminal stage) + var automaticStages []AutomaticStage + for _, stage := range response.RollingRelease.Stages { + // Skip the terminal stage (100%) + if stage.TargetPercentage == 100 { + continue + } + + var duration types.Int64 + if stage.Duration != nil { + duration = types.Int64Value(int64(*stage.Duration)) + } else { + duration = types.Int64Value(60) // Default duration + } + + automaticStages = append(automaticStages, AutomaticStage{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), + Duration: duration, + }) + } + + // Convert to Terraform types + stages := make([]attr.Value, len(automaticStages)) + for i, stage := range automaticStages { + stageObj := types.ObjectValueMust( + map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + }, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + "duration": stage.Duration, + }, + ) + stages[i] = stageObj + } + + stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, "duration": types.Int64Type, - "require_approval": types.BoolType, }, - }, []attr.Value{}) + }, stages) diags.Append(stagesDiags...) if diags.HasError() { return result, diags } - result.RollingRelease.Stages = emptyStages - return result, diags - } - // If we have a plan, try to match stages by target percentage to preserve order - var orderedStages []client.RollingReleaseStage - if plan != nil && !plan.RollingRelease.Stages.IsNull() { - var planStages []RollingReleaseStage - diags = plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false) - if diags.HasError() { - return result, diags + result.AutomaticRollingRelease = &AutomaticRollingRelease{ + Stages: stagesList, } - // Create a map of target percentage to stage for quick lookup - stageMap := make(map[int]client.RollingReleaseStage) + } else if response.RollingRelease.AdvancementType == "manual-approval" { + // Convert API stages to manual stages (excluding terminal stage) + var manualStages []ManualStage for _, stage := range response.RollingRelease.Stages { - stageMap[stage.TargetPercentage] = stage - } - - // Match stages by target percentage - orderedStages = make([]client.RollingReleaseStage, len(planStages)) - for i, planStage := range planStages { - targetPercentage := int(planStage.TargetPercentage.ValueInt64()) - if stage, ok := stageMap[targetPercentage]; ok { - orderedStages[i] = stage + // Skip the terminal stage (100%) + if stage.TargetPercentage == 100 { + continue } - } - } else { - orderedStages = response.RollingRelease.Stages - } - // Convert stages to Terraform types - stages := make([]attr.Value, len(orderedStages)) - for i, stage := range orderedStages { - var duration types.Int64 - if plan != nil && !plan.RollingRelease.Stages.IsNull() { - var planStages []RollingReleaseStage - diags = plan.RollingRelease.Stages.ElementsAs(ctx, &planStages, false) - if !diags.HasError() && i < len(planStages) { - // Use the duration from the plan if available - duration = planStages[i].Duration - } + manualStages = append(manualStages, ManualStage{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), + }) } - // If duration is not set in plan, handle based on advancement type - if duration.IsNull() { - if plan.RollingRelease.AdvancementType.ValueString() == "automatic" { - if i < len(orderedStages)-1 { - // For non-last stages in automatic advancement, use the duration from the API - if stage.Duration != nil { - duration = types.Int64Value(int64(*stage.Duration)) - } else { - // Default duration for non-last stages - duration = types.Int64Value(60) - } - } else { - // Last stage doesn't need duration - duration = types.Int64Null() - } - } else { - // For manual approval, duration is not used - duration = types.Int64Null() - } + // Convert to Terraform types + stages := make([]attr.Value, len(manualStages)) + for i, stage := range manualStages { + stageObj := types.ObjectValueMust( + map[string]attr.Type{ + "target_percentage": types.Int64Type, + }, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + }, + ) + stages[i] = stageObj } - stageObj := types.ObjectValueMust( - map[string]attr.Type{ + stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, - }, - map[string]attr.Value{ - "target_percentage": types.Int64Value(int64(stage.TargetPercentage)), - "duration": duration, - "require_approval": types.BoolValue(stage.RequireApproval), }, - ) - stages[i] = stageObj - } + }, stages) + diags.Append(stagesDiags...) + if diags.HasError() { + return result, diags + } - stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, - }, - }, stages) - diags.Append(stagesDiags...) - if diags.HasError() { - return result, diags + result.ManualRollingRelease = &ManualRollingRelease{ + Stages: stagesList, + } } - result.RollingRelease.Stages = stagesList // Log the conversion result for debugging tflog.Info(ctx, "converted rolling release response", map[string]any{ - "enabled": result.RollingRelease.Enabled.ValueBool(), - "advancement_type": result.RollingRelease.AdvancementType.ValueString(), - "stages_count": len(stages), + "advancement_type": response.RollingRelease.AdvancementType, + "stages_count": len(response.RollingRelease.Stages), }) return result, diags @@ -369,31 +363,6 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource "stages": request.RollingRelease.Stages, }) - // If we're enabling, first create in disabled state then enable - if request.RollingRelease.Enabled { - // First create in disabled state - disabledRequest := client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ - Enabled: false, - AdvancementType: "", - Stages: []client.RollingReleaseStage{}, - }, - ProjectID: request.ProjectID, - TeamID: request.TeamID, - } - - _, err := r.client.UpdateRollingRelease(ctx, disabledRequest) - if err != nil { - resp.Diagnostics.AddError( - "Error creating project rolling release", - fmt.Sprintf("Could not create project rolling release in disabled state, unexpected error: %s", - err, - ), - ) - return - } - } - out, err := r.client.UpdateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( @@ -414,9 +383,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource // Log the result for debugging tflog.Debug(ctx, "created rolling release", map[string]any{ - "enabled": result.RollingRelease.Enabled.ValueBool(), - "advancement_type": result.RollingRelease.AdvancementType.ValueString(), - "stages": result.RollingRelease.Stages, + "project_id": result.ProjectID.ValueString(), }) // Set state @@ -467,9 +434,7 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R // Log the result for debugging tflog.Debug(ctx, "converted rolling release", map[string]any{ - "enabled": result.RollingRelease.Enabled.ValueBool(), - "advancement_type": result.RollingRelease.AdvancementType.ValueString(), - "stages": result.RollingRelease.Stages, + "project_id": result.ProjectID.ValueString(), }) diags = resp.State.Set(ctx, result) @@ -509,8 +474,12 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource "stages": request.RollingRelease.Stages, }) + // Check if we're transitioning between enabled and disabled states + wasEnabled := state.AutomaticRollingRelease != nil || state.ManualRollingRelease != nil + isEnabled := plan.AutomaticRollingRelease != nil || plan.ManualRollingRelease != nil + // If we're transitioning from enabled to disabled, first disable - if state.RollingRelease.Enabled.ValueBool() && !request.RollingRelease.Enabled { + if wasEnabled && !isEnabled { disabledRequest := client.UpdateRollingReleaseRequest{ RollingRelease: client.RollingRelease{ Enabled: false, @@ -533,30 +502,6 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource } } - // If we're transitioning from disabled to enabled, first create in disabled state - if !state.RollingRelease.Enabled.ValueBool() && request.RollingRelease.Enabled { - disabledRequest := client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ - Enabled: false, - AdvancementType: "", - Stages: []client.RollingReleaseStage{}, - }, - ProjectID: request.ProjectID, - TeamID: request.TeamID, - } - - _, err := r.client.UpdateRollingRelease(ctx, disabledRequest) - if err != nil { - resp.Diagnostics.AddError( - "Error updating project rolling release", - fmt.Sprintf("Could not create project rolling release in disabled state, unexpected error: %s", - err, - ), - ) - return - } - } - out, err := r.client.UpdateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( @@ -577,9 +522,7 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource // Log the result for debugging tflog.Debug(ctx, "updated rolling release", map[string]any{ - "enabled": result.RollingRelease.Enabled.ValueBool(), - "advancement_type": result.RollingRelease.AdvancementType.ValueString(), - "stages": result.RollingRelease.Stages, + "project_id": result.ProjectID.ValueString(), }) diags = resp.State.Set(ctx, result) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 81dd9557..e06928c6 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -41,21 +41,13 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "manual-approval"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "true", + resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.stages.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ "target_percentage": "20", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "true", + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ "target_percentage": "50", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "false", - "target_percentage": "100", - }), ), }, // Then update to new configuration @@ -64,25 +56,16 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "manual-approval"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "true", + resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ "target_percentage": "20", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "true", + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ "target_percentage": "50", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "true", + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ "target_percentage": "80", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "false", - "target_percentage": "100", - }), ), }, // Then update to new configuration @@ -91,28 +74,19 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "4"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "false", + resource.TestCheckResourceAttr(resourceName, "automatic_rolling_release.stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.stages.*", map[string]string{ "target_percentage": "20", "duration": "10", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "false", + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.stages.*", map[string]string{ "target_percentage": "50", "duration": "10", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "false", + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.stages.*", map[string]string{ "target_percentage": "80", "duration": "10", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ - "require_approval": "false", - "target_percentage": "100", - }), ), }, // Finally disable @@ -121,9 +95,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", ""), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "0"), + resource.TestCheckResourceAttr(resourceName, "automatic_rolling_release", ""), ), }, }, @@ -139,18 +111,13 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - enabled = true - advancement_type = "manual-approval" + manual_rolling_release = { stages = [ { target_percentage = 20 }, { target_percentage = 50 - }, - { - target_percentage = 100 } ] } @@ -167,9 +134,7 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - enabled = true - advancement_type = "manual-approval" + manual_rolling_release = { stages = [ { target_percentage = 20 @@ -179,9 +144,6 @@ resource "vercel_project_rolling_release" "example" { }, { target_percentage = 80 - }, - { - target_percentage = 100 } ] } @@ -197,9 +159,7 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - enabled = true - advancement_type = "automatic" + automatic_rolling_release = { stages = [ { target_percentage = 20 @@ -212,9 +172,6 @@ resource "vercel_project_rolling_release" "example" { { target_percentage = 80 duration = 10 - }, - { - target_percentage = 100 } ] } @@ -230,11 +187,6 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - enabled = false - advancement_type = "" - stages = [] - } } `, nameSuffix) } From fe6666d64db77e1260b967822aa3cc0e54b64c31 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 13:47:44 -0600 Subject: [PATCH 072/133] [rr] --- vercel/validator_advancement_type.go | 54 ---------------------------- 1 file changed, 54 deletions(-) delete mode 100644 vercel/validator_advancement_type.go diff --git a/vercel/validator_advancement_type.go b/vercel/validator_advancement_type.go deleted file mode 100644 index 062846d2..00000000 --- a/vercel/validator_advancement_type.go +++ /dev/null @@ -1,54 +0,0 @@ -package vercel - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -// Custom validator for advancement_type -type advancementTypeValidator struct{} - -func (v advancementTypeValidator) Description(ctx context.Context) string { - return "advancement_type must be either 'automatic' or 'manual-approval'" -} - -func (v advancementTypeValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -func (v advancementTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - // Get the value of enabled from the parent object - var enabled types.Bool - diags := req.Config.GetAttribute(ctx, path.Root("rolling_release").AtName("enabled"), &enabled) - if diags.HasError() { - resp.Diagnostics.AddError( - "Error validating advancement_type", - "Could not get enabled value from configuration", - ) - return - } - - // Only validate when enabled is true - if enabled.ValueBool() { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - resp.Diagnostics.AddError( - "Invalid advancement_type", - "advancement_type is required when enabled is true", - ) - return - } - - value := req.ConfigValue.ValueString() - if value != "automatic" && value != "manual-approval" { - resp.Diagnostics.AddError( - "Invalid advancement_type", - fmt.Sprintf("advancement_type must be either 'automatic' or 'manual-approval', got: %s", value), - ) - return - } - } -} From ac876bd6c1a38f2c5d5c8627a9cb840608687d52 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 13:50:43 -0600 Subject: [PATCH 073/133] [rr] --- docs/index.md | 4 +-- docs/resources/project_rolling_release.md | 35 +++++++++++++---------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1d9aad5d..64407e00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "Vercel Provider" +page_title: "vercel Provider" subcategory: "" description: |- The Vercel provider is used to interact with resources supported by Vercel. @@ -8,7 +8,7 @@ description: |- Use the navigation to the left to read about the available resources. --- -# Vercel Provider +# vercel Provider The Vercel provider is used to interact with resources supported by Vercel. The provider needs to be configured with the proper credentials before it can be used. diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 8eb1142e..404686bd 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -47,35 +47,40 @@ resource "vercel_project_rolling_release" "example" { ### Required - `project_id` (String) The ID of the project. -- `rolling_release` (Attributes) The rolling release configuration. (see [below for nested schema](#nestedatt--rolling_release)) ### Optional +- `automatic_rolling_release` (Attributes) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) +- `manual_rolling_release` (Attributes) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) - `team_id` (String) The ID of the Vercel team. - -### Nested Schema for `rolling_release` + +### Nested Schema for `automatic_rolling_release` Required: -- `enabled` (Boolean) Whether rolling releases are enabled. +- `stages` (Attributes List) The stages for automatic rolling release. (see [below for nested schema](#nestedatt--automatic_rolling_release--stages)) -Optional: - -- `advancement_type` (String) The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true. -- `stages` (Attributes List) The stages of the rolling release. Required when enabled is true. (see [below for nested schema](#nestedatt--rolling_release--stages)) - - -### Nested Schema for `rolling_release.stages` + +### Nested Schema for `automatic_rolling_release.stages` Required: +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. - `target_percentage` (Number) The percentage of traffic to route to this stage. -Optional: -- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for non-last stages when advancement_type is 'automatic'. -Read-Only: + +### Nested Schema for `manual_rolling_release` + +Required: + +- `stages` (Attributes List) The stages for manual rolling release. (see [below for nested schema](#nestedatt--manual_rolling_release--stages)) -- `require_approval` (Boolean) Whether approval is required before advancing to the next stage. + +### Nested Schema for `manual_rolling_release.stages` + +Required: + +- `target_percentage` (Number) The percentage of traffic to route to this stage. From b5987d6130cf3a0d2129636104117e5061a158e1 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 13:59:55 -0600 Subject: [PATCH 074/133] [rr] --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 64407e00..1d9aad5d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "vercel Provider" +page_title: "Vercel Provider" subcategory: "" description: |- The Vercel provider is used to interact with resources supported by Vercel. @@ -8,7 +8,7 @@ description: |- Use the navigation to the left to read about the available resources. --- -# vercel Provider +# Vercel Provider The Vercel provider is used to interact with resources supported by Vercel. The provider needs to be configured with the proper credentials before it can be used. From 5103354c8b7848c85d2fe333bc4bdf9000792bd0 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 14:27:51 -0600 Subject: [PATCH 075/133] [rr] --- ...ata_source_project_rolling_release_test.go | 13 +-- vercel/resource_project_rolling_release.go | 60 ++---------- .../resource_project_rolling_release_test.go | 94 +++++++++---------- 3 files changed, 61 insertions(+), 106 deletions(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 29c6e7e6..ce3fbd02 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -17,7 +17,7 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { ), Steps: []resource.TestStep{ { - Config: cfg(testAccProjectRollingReleasesConfigOffWithDataSource(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.enabled", "false"), @@ -29,7 +29,7 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }) } -func testAccProjectRollingReleasesConfigOffWithDataSource(nameSuffix string) string { +func testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" @@ -37,10 +37,11 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - enabled = false - advancement_type = "" - stages = [] + automatic_rolling_release { + stages { + target_percentage = 10 + duration = 10 + } } } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 06372ace..3d23a18f 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -130,20 +130,12 @@ type ManualStage struct { TargetPercentage types.Int64 `tfsdk:"target_percentage"` } -type AutomaticRollingRelease struct { - Stages types.List `tfsdk:"stages"` -} - -type ManualRollingRelease struct { - Stages types.List `tfsdk:"stages"` -} - // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. type RollingReleaseInfo struct { - AutomaticRollingRelease *AutomaticRollingRelease `tfsdk:"automatic_rolling_release"` - ManualRollingRelease *ManualRollingRelease `tfsdk:"manual_rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` + AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` + ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { @@ -151,12 +143,12 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli var advancementType string var diags diag.Diagnostics - if e.AutomaticRollingRelease != nil { + if !e.AutomaticRollingRelease.IsNull() && !e.AutomaticRollingRelease.IsUnknown() { advancementType = "automatic" // Convert automatic stages var tfStages []AutomaticStage - diags = e.AutomaticRollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) + diags = e.AutomaticRollingRelease.ElementsAs(context.Background(), &tfStages, false) if diags.HasError() { return client.UpdateRollingReleaseRequest{}, diags } @@ -178,12 +170,12 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli RequireApproval: false, }) - } else if e.ManualRollingRelease != nil { + } else if !e.ManualRollingRelease.IsNull() && !e.ManualRollingRelease.IsUnknown() { advancementType = "manual-approval" // Convert manual stages var tfStages []ManualStage - diags = e.ManualRollingRelease.Stages.ElementsAs(context.Background(), &tfStages, false) + diags = e.ManualRollingRelease.ElementsAs(context.Background(), &tfStages, false) if diags.HasError() { return client.UpdateRollingReleaseRequest{}, diags } @@ -284,9 +276,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R return result, diags } - result.AutomaticRollingRelease = &AutomaticRollingRelease{ - Stages: stagesList, - } + result.AutomaticRollingRelease = stagesList } else if response.RollingRelease.AdvancementType == "manual-approval" { // Convert API stages to manual stages (excluding terminal stage) @@ -326,9 +316,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R return result, diags } - result.ManualRollingRelease = &ManualRollingRelease{ - Stages: stagesList, - } + result.ManualRollingRelease = stagesList } // Log the conversion result for debugging @@ -474,34 +462,6 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource "stages": request.RollingRelease.Stages, }) - // Check if we're transitioning between enabled and disabled states - wasEnabled := state.AutomaticRollingRelease != nil || state.ManualRollingRelease != nil - isEnabled := plan.AutomaticRollingRelease != nil || plan.ManualRollingRelease != nil - - // If we're transitioning from enabled to disabled, first disable - if wasEnabled && !isEnabled { - disabledRequest := client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ - Enabled: false, - AdvancementType: "", - Stages: []client.RollingReleaseStage{}, - }, - ProjectID: request.ProjectID, - TeamID: request.TeamID, - } - - _, err := r.client.UpdateRollingRelease(ctx, disabledRequest) - if err != nil { - resp.Diagnostics.AddError( - "Error updating project rolling release", - fmt.Sprintf("Could not disable project rolling release, unexpected error: %s", - err, - ), - ) - return - } - } - out, err := r.client.UpdateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index e06928c6..698535c7 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -41,11 +41,11 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.stages.#", "2"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ "target_percentage": "20", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ "target_percentage": "50", }), ), @@ -56,14 +56,14 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.stages.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release*", map[string]string{ "target_percentage": "20", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ "target_percentage": "50", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ "target_percentage": "80", }), ), @@ -74,16 +74,16 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "automatic_rolling_release.stages.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "automatic_rolling_release.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.*", map[string]string{ "target_percentage": "20", "duration": "10", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.*", map[string]string{ "target_percentage": "50", "duration": "10", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.*", map[string]string{ "target_percentage": "80", "duration": "10", }), @@ -111,16 +111,14 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = { - stages = [ - { - target_percentage = 20 - }, - { - target_percentage = 50 - } - ] - } + manual_rolling_release = [ + { + target_percentage = 20 + }, + { + target_percentage = 50 + } + ] } `, nameSuffix) } @@ -134,19 +132,17 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = { - stages = [ - { - target_percentage = 20 - }, - { - target_percentage = 50 - }, - { - target_percentage = 80 - } - ] - } + manual_rolling_release = [ + { + target_percentage = 20 + }, + { + target_percentage = 50 + }, + { + target_percentage = 80 + } + ] } `, nameSuffix) } @@ -159,22 +155,20 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - automatic_rolling_release = { - stages = [ - { - target_percentage = 20 - duration = 10 - }, - { - target_percentage = 50 - duration = 10 - }, - { - target_percentage = 80 - duration = 10 - } - ] - } + automatic_rolling_release = [ + { + target_percentage = 20 + duration = 10 + }, + { + target_percentage = 50 + duration = 10 + }, + { + target_percentage = 80 + duration = 10 + } + ] } `, nameSuffix) } From a295628773913e45396c9e3fb29e288d370ed294 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 14:36:48 -0600 Subject: [PATCH 076/133] [rr] --- vercel/data_source_project_rolling_release.go | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 751b1255..46a8a3d6 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -4,9 +4,11 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "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/v3/client" @@ -59,35 +61,49 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour Computed: true, Description: "The ID of the Vercel team.", }, - "rolling_release": schema.SingleNestedAttribute{ - MarkdownDescription: "The rolling release configuration.", - Computed: true, + "automatic_rolling_release": schema.SingleNestedAttribute{ + MarkdownDescription: "Automatic rolling release configuration.", Optional: true, Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - MarkdownDescription: "Whether rolling releases are enabled.", - Computed: true, - }, - "advancement_type": schema.StringAttribute{ - MarkdownDescription: "The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true.", - Computed: true, - }, "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages of the rolling release. Required when enabled is true.", - Computed: true, + MarkdownDescription: "The stages for automatic rolling release.", + Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ MarkdownDescription: "The percentage of traffic to route to this stage.", - Computed: true, + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement.", - Optional: true, + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(1, 10000), + }, }, - "require_approval": schema.BoolAttribute{ - MarkdownDescription: "Whether approval is required before advancing to the next stage.", - Computed: true, + }, + }, + }, + }, + }, + "manual_rolling_release": schema.SingleNestedAttribute{ + MarkdownDescription: "Manual rolling release configuration.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "stages": schema.ListNestedAttribute{ + MarkdownDescription: "The stages for manual rolling release.", + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, }, }, }, From 904bac8d26c68266809975972938d838aac62ab3 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 14:43:52 -0600 Subject: [PATCH 077/133] [rr] --- vercel/data_source_project_rolling_release.go | 55 ++++++++----------- ...ata_source_project_rolling_release_test.go | 16 +++--- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 46a8a3d6..9ae5d08a 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -117,24 +117,26 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour type RollingReleaseStageDataSource struct { TargetPercentage types.Int64 `tfsdk:"target_percentage"` Duration types.Int64 `tfsdk:"duration"` - RequireApproval types.Bool `tfsdk:"require_approval"` } type RollingReleaseDataSource struct { - Enabled types.Bool `tfsdk:"enabled"` - AdvancementType types.String `tfsdk:"advancement_type"` - Stages []RollingReleaseStageDataSource `tfsdk:"stages"` + ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` + AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` } type RollingReleaseInfoDataSource struct { - RollingRelease types.Object `tfsdk:"rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` + ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` + AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } -func convertStagesDataSource(stages []client.RollingReleaseStage) []RollingReleaseStageDataSource { +func convertStagesDataSource(stages []client.RollingReleaseStage) types.List { if len(stages) == 0 { - return []RollingReleaseStageDataSource{} + return types.ListNull(types.ObjectType{AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + }}) } result := make([]RollingReleaseStageDataSource, len(stages)) @@ -147,38 +149,27 @@ func convertStagesDataSource(stages []client.RollingReleaseStage) []RollingRelea result[i] = RollingReleaseStageDataSource{ TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), Duration: duration, - RequireApproval: types.BoolValue(stage.RequireApproval), } } - return result + + stagesList, _ := types.ListValueFrom(context.Background(), types.ObjectType{AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + }}, result) + return stagesList } func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo) RollingReleaseInfoDataSource { rollingRelease := RollingReleaseDataSource{ - Enabled: types.BoolValue(response.RollingRelease.Enabled), - AdvancementType: types.StringValue(response.RollingRelease.AdvancementType), - Stages: convertStagesDataSource(response.RollingRelease.Stages), - } - - if !response.RollingRelease.Enabled { - rollingRelease.AdvancementType = types.StringValue("") - rollingRelease.Stages = make([]RollingReleaseStageDataSource, 0) + ManualRollingRelease: convertStagesDataSource(response.RollingRelease.Stages), + AutomaticRollingRelease: convertStagesDataSource(response.RollingRelease.Stages), } - rollingReleaseObj, _ := types.ObjectValueFrom(context.Background(), map[string]attr.Type{ - "enabled": types.BoolType, - "advancement_type": types.StringType, - "stages": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - "require_approval": types.BoolType, - }}}, - }, rollingRelease) - return RollingReleaseInfoDataSource{ - RollingRelease: rollingReleaseObj, - ProjectID: types.StringValue(response.ProjectID), - TeamID: types.StringValue(response.TeamID), + ManualRollingRelease: rollingRelease.ManualRollingRelease, + AutomaticRollingRelease: rollingRelease.AutomaticRollingRelease, + ProjectID: types.StringValue(response.ProjectID), + TeamID: types.StringValue(response.TeamID), } } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index ce3fbd02..23ae3620 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -20,9 +20,9 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.enabled", "false"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.advancement_type", ""), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "rolling_release.stages.#", "0"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.#", "1"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.target_percentage", "10"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.duration", "10"), ), }, }, @@ -37,12 +37,10 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - automatic_rolling_release { - stages { - target_percentage = 10 - duration = 10 - } - } + automatic_rolling_release = [{ + target_percentage = 10 + duration = 10 + }] } data "vercel_project_rolling_release" "example" { From 434d6ec46e0bb6d51ba438a66029200cb1579628 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 14:54:01 -0600 Subject: [PATCH 078/133] [rr] --- docs/data-sources/project_rolling_release.md | 37 +++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 959d02db..66a38e46 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -31,26 +31,37 @@ data "vercel_project_rolling_release" "example" { ### Optional -- `rolling_release` (Attributes) The rolling release configuration. (see [below for nested schema](#nestedatt--rolling_release)) +- `automatic_rolling_release` (Attributes) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) +- `manual_rolling_release` (Attributes) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) - `team_id` (String) The ID of the Vercel team. - -### Nested Schema for `rolling_release` + +### Nested Schema for `automatic_rolling_release` -Read-Only: +Required: -- `advancement_type` (String) The type of advancement between stages. Must be either 'automatic' or 'manual-approval'. Required when enabled is true. -- `enabled` (Boolean) Whether rolling releases are enabled. -- `stages` (Attributes List) The stages of the rolling release. Required when enabled is true. (see [below for nested schema](#nestedatt--rolling_release--stages)) +- `stages` (Attributes List) The stages for automatic rolling release. (see [below for nested schema](#nestedatt--automatic_rolling_release--stages)) - -### Nested Schema for `rolling_release.stages` + +### Nested Schema for `automatic_rolling_release.stages` -Optional: +Required: -- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for all stages except the final stage when using automatic advancement. +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. +- `target_percentage` (Number) The percentage of traffic to route to this stage. + + + + +### Nested Schema for `manual_rolling_release` + +Required: + +- `stages` (Attributes List) The stages for manual rolling release. (see [below for nested schema](#nestedatt--manual_rolling_release--stages)) + + +### Nested Schema for `manual_rolling_release.stages` -Read-Only: +Required: -- `require_approval` (Boolean) Whether approval is required before advancing to the next stage. - `target_percentage` (Number) The percentage of traffic to route to this stage. From b777160afab3604b4ce18361ffd9983bc6389299 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 15:17:08 -0600 Subject: [PATCH 079/133] [rr] --- vercel/data_source_project_rolling_release.go | 58 ++++++++----------- ...ata_source_project_rolling_release_test.go | 16 ++--- vercel/resource_project_rolling_release.go | 58 ++++++++----------- 3 files changed, 55 insertions(+), 77 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 9ae5d08a..8950b6d7 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -61,50 +61,38 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour Computed: true, Description: "The ID of the Vercel team.", }, - "automatic_rolling_release": schema.SingleNestedAttribute{ + "automatic_rolling_release": schema.ListNestedAttribute{ MarkdownDescription: "Automatic rolling release configuration.", Optional: true, - Attributes: map[string]schema.Attribute{ - "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages for automatic rolling release.", - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "target_percentage": schema.Int64Attribute{ - MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(0, 100), - }, - }, - "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(1, 10000), - }, - }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, + }, + "duration": schema.Int64Attribute{ + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(1, 10000), }, }, }, }, }, - "manual_rolling_release": schema.SingleNestedAttribute{ + "manual_rolling_release": schema.ListNestedAttribute{ MarkdownDescription: "Manual rolling release configuration.", Optional: true, - Attributes: map[string]schema.Attribute{ - "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages for manual rolling release.", - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "target_percentage": schema.Int64Attribute{ - MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(0, 100), - }, - }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), }, }, }, diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 23ae3620..716cbd6f 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -20,9 +20,9 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.#", "1"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.target_percentage", "10"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.duration", "10"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.stages.#", "1"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.stages.0.target_percentage", "10"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.stages.0.duration", "10"), ), }, }, @@ -37,10 +37,12 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - automatic_rolling_release = [{ - target_percentage = 10 - duration = 10 - }] + automatic_rolling_release = { + stages = [{ + target_percentage = 10 + duration = 10 + }] + } } data "vercel_project_rolling_release" "example" { diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 3d23a18f..d3e52f1a 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -68,50 +68,38 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S Description: "The ID of the Vercel team.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, }, - "automatic_rolling_release": schema.SingleNestedAttribute{ + "automatic_rolling_release": schema.ListNestedAttribute{ MarkdownDescription: "Automatic rolling release configuration.", Optional: true, - Attributes: map[string]schema.Attribute{ - "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages for automatic rolling release.", - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "target_percentage": schema.Int64Attribute{ - MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(0, 100), - }, - }, - "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(1, 10000), - }, - }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, + }, + "duration": schema.Int64Attribute{ + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(1, 10000), }, }, }, }, }, - "manual_rolling_release": schema.SingleNestedAttribute{ + "manual_rolling_release": schema.ListNestedAttribute{ MarkdownDescription: "Manual rolling release configuration.", Optional: true, - Attributes: map[string]schema.Attribute{ - "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages for manual rolling release.", - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "target_percentage": schema.Int64Attribute{ - MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(0, 100), - }, - }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target_percentage": schema.Int64Attribute{ + MarkdownDescription: "The percentage of traffic to route to this stage.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), }, }, }, From d8a1ab9d78f1199b1edf4a7fe45435ed90ef670c Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 15:19:25 -0600 Subject: [PATCH 080/133] [rr] --- docs/data-sources/project_rolling_release.md | 19 +- docs/index.md | 4 +- docs/resources/project_rolling_release.md | 19 +- vercel/data_source_project_rolling_release.go | 200 ++++++++++++------ ...ata_source_project_rolling_release_test.go | 14 +- 5 files changed, 146 insertions(+), 110 deletions(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 66a38e46..155f0b49 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -31,8 +31,8 @@ data "vercel_project_rolling_release" "example" { ### Optional -- `automatic_rolling_release` (Attributes) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) -- `manual_rolling_release` (Attributes) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) +- `automatic_rolling_release` (Attributes List) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) +- `manual_rolling_release` (Attributes List) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) - `team_id` (String) The ID of the Vercel team. @@ -40,28 +40,13 @@ data "vercel_project_rolling_release" "example" { Required: -- `stages` (Attributes List) The stages for automatic rolling release. (see [below for nested schema](#nestedatt--automatic_rolling_release--stages)) - - -### Nested Schema for `automatic_rolling_release.stages` - -Required: - - `duration` (Number) The duration in minutes to wait before advancing to the next stage. - `target_percentage` (Number) The percentage of traffic to route to this stage. - ### Nested Schema for `manual_rolling_release` Required: -- `stages` (Attributes List) The stages for manual rolling release. (see [below for nested schema](#nestedatt--manual_rolling_release--stages)) - - -### Nested Schema for `manual_rolling_release.stages` - -Required: - - `target_percentage` (Number) The percentage of traffic to route to this stage. diff --git a/docs/index.md b/docs/index.md index 1d9aad5d..64407e00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "Vercel Provider" +page_title: "vercel Provider" subcategory: "" description: |- The Vercel provider is used to interact with resources supported by Vercel. @@ -8,7 +8,7 @@ description: |- Use the navigation to the left to read about the available resources. --- -# Vercel Provider +# vercel Provider The Vercel provider is used to interact with resources supported by Vercel. The provider needs to be configured with the proper credentials before it can be used. diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 404686bd..2cca12eb 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -50,8 +50,8 @@ resource "vercel_project_rolling_release" "example" { ### Optional -- `automatic_rolling_release` (Attributes) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) -- `manual_rolling_release` (Attributes) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) +- `automatic_rolling_release` (Attributes List) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) +- `manual_rolling_release` (Attributes List) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) - `team_id` (String) The ID of the Vercel team. @@ -59,28 +59,13 @@ resource "vercel_project_rolling_release" "example" { Required: -- `stages` (Attributes List) The stages for automatic rolling release. (see [below for nested schema](#nestedatt--automatic_rolling_release--stages)) - - -### Nested Schema for `automatic_rolling_release.stages` - -Required: - - `duration` (Number) The duration in minutes to wait before advancing to the next stage. - `target_percentage` (Number) The percentage of traffic to route to this stage. - ### Nested Schema for `manual_rolling_release` Required: -- `stages` (Attributes List) The stages for manual rolling release. (see [below for nested schema](#nestedatt--manual_rolling_release--stages)) - - -### Nested Schema for `manual_rolling_release.stages` - -Required: - - `target_percentage` (Number) The percentage of traffic to route to this stage. diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 8950b6d7..ca7e1716 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -102,78 +103,29 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour } } -type RollingReleaseStageDataSource struct { - TargetPercentage types.Int64 `tfsdk:"target_percentage"` - Duration types.Int64 `tfsdk:"duration"` -} - -type RollingReleaseDataSource struct { - ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` - AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` -} - -type RollingReleaseInfoDataSource struct { - ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` +// ProjectRollingReleaseDataSourceModel reflects the structure of the data source. +type ProjectRollingReleaseDataSourceModel struct { AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` + ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` ProjectID types.String `tfsdk:"project_id"` TeamID types.String `tfsdk:"team_id"` } -func convertStagesDataSource(stages []client.RollingReleaseStage) types.List { - if len(stages) == 0 { - return types.ListNull(types.ObjectType{AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - }}) - } - - result := make([]RollingReleaseStageDataSource, len(stages)) - for i, stage := range stages { - duration := types.Int64Null() - if stage.Duration != nil { - duration = types.Int64Value(int64(*stage.Duration)) - } - - result[i] = RollingReleaseStageDataSource{ - TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), - Duration: duration, - } - } - - stagesList, _ := types.ListValueFrom(context.Background(), types.ObjectType{AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - }}, result) - return stagesList -} - -func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo) RollingReleaseInfoDataSource { - rollingRelease := RollingReleaseDataSource{ - ManualRollingRelease: convertStagesDataSource(response.RollingRelease.Stages), - AutomaticRollingRelease: convertStagesDataSource(response.RollingRelease.Stages), - } +func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data ProjectRollingReleaseDataSourceModel - return RollingReleaseInfoDataSource{ - ManualRollingRelease: rollingRelease.ManualRollingRelease, - AutomaticRollingRelease: rollingRelease.AutomaticRollingRelease, - ProjectID: types.StringValue(response.ProjectID), - TeamID: types.StringValue(response.TeamID), - } -} + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) -func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config RollingReleaseInfoDataSource - diags := req.Config.Get(ctx, &config) - resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - out, err := d.client.GetRollingRelease(ctx, config.ProjectID.ValueString(), config.TeamID.ValueString()) + out, err := d.client.GetRollingRelease(ctx, data.ProjectID.ValueString(), data.TeamID.ValueString()) if client.NotFound(err) { resp.Diagnostics.AddError( "Error reading project rolling release", - fmt.Sprintf("No project rolling release found with id %s %s", config.TeamID.ValueString(), config.ProjectID.ValueString()), + fmt.Sprintf("No project rolling release found with id %s %s", data.TeamID.ValueString(), data.ProjectID.ValueString()), ) return } @@ -181,23 +133,137 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour resp.Diagnostics.AddError( "Error reading project rolling release", fmt.Sprintf("Could not get project rolling release %s %s, unexpected error: %s", - config.TeamID.ValueString(), - config.ProjectID.ValueString(), + data.TeamID.ValueString(), + data.ProjectID.ValueString(), err, ), ) return } - result := convertResponseToRollingReleaseDataSource(out) - tflog.Info(ctx, "read project rolling release", map[string]any{ - "team_id": result.TeamID.ValueString(), - "project_id": result.ProjectID.ValueString(), - }) - - diags = resp.State.Set(ctx, result) + // Convert the response to the data source model + convertedData, diags := convertResponseToRollingReleaseDataSource(out, data, ctx) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &convertedData)...) +} + +func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo, plan ProjectRollingReleaseDataSourceModel, ctx context.Context) (ProjectRollingReleaseDataSourceModel, diag.Diagnostics) { + var diags diag.Diagnostics + + result := ProjectRollingReleaseDataSourceModel{ + ProjectID: types.StringValue(response.ProjectID), + TeamID: types.StringValue(response.TeamID), + } + + // If disabled, return empty values + if !response.RollingRelease.Enabled { + return result, diags + } + + // Determine which type of rolling release to use based on API response + if response.RollingRelease.AdvancementType == "automatic" { + // Convert API stages to automatic stages (excluding terminal stage) + var automaticStages []AutomaticStage + for _, stage := range response.RollingRelease.Stages { + // Skip the terminal stage (100%) + if stage.TargetPercentage == 100 { + continue + } + + var duration types.Int64 + if stage.Duration != nil { + duration = types.Int64Value(int64(*stage.Duration)) + } else { + duration = types.Int64Value(60) // Default duration + } + + automaticStages = append(automaticStages, AutomaticStage{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), + Duration: duration, + }) + } + + // Convert to Terraform types + stages := make([]attr.Value, len(automaticStages)) + for i, stage := range automaticStages { + stageObj := types.ObjectValueMust( + map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + }, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + "duration": stage.Duration, + }, + ) + stages[i] = stageObj + } + + stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + }, + }, stages) + diags.Append(stagesDiags...) + if diags.HasError() { + return result, diags + } + + result.AutomaticRollingRelease = stagesList + + } else if response.RollingRelease.AdvancementType == "manual-approval" { + // Convert API stages to manual stages (excluding terminal stage) + var manualStages []ManualStage + for _, stage := range response.RollingRelease.Stages { + // Skip the terminal stage (100%) + if stage.TargetPercentage == 100 { + continue + } + + manualStages = append(manualStages, ManualStage{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), + }) + } + + // Convert to Terraform types + stages := make([]attr.Value, len(manualStages)) + for i, stage := range manualStages { + stageObj := types.ObjectValueMust( + map[string]attr.Type{ + "target_percentage": types.Int64Type, + }, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + }, + ) + stages[i] = stageObj + } + + stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + }, + }, stages) + diags.Append(stagesDiags...) + if diags.HasError() { + return result, diags + } + + result.ManualRollingRelease = stagesList + } + + // Log the conversion result for debugging + tflog.Info(ctx, "converted rolling release response", map[string]any{ + "advancement_type": response.RollingRelease.AdvancementType, + "stages_count": len(response.RollingRelease.Stages), + }) + + return result, diags } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 716cbd6f..33c92143 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -20,9 +20,9 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.stages.#", "1"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.stages.0.target_percentage", "10"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.stages.0.duration", "10"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.#", "1"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.target_percentage", "10"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.duration", "10"), ), }, }, @@ -37,12 +37,12 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - automatic_rolling_release = { - stages = [{ + automatic_rolling_release = [ + { target_percentage = 10 duration = 10 - }] - } + } + ] } data "vercel_project_rolling_release" "example" { From b2498b6c4be80533aebb43cb47538d5f0c0f45cb Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 15:23:27 -0600 Subject: [PATCH 081/133] [rr] --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 64407e00..1d9aad5d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "vercel Provider" +page_title: "Vercel Provider" subcategory: "" description: |- The Vercel provider is used to interact with resources supported by Vercel. @@ -8,7 +8,7 @@ description: |- Use the navigation to the left to read about the available resources. --- -# vercel Provider +# Vercel Provider The Vercel provider is used to interact with resources supported by Vercel. The provider needs to be configured with the proper credentials before it can be used. From 0d27bc10e84c28afb088663655a7dca385fc7205 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 15:37:31 -0600 Subject: [PATCH 082/133] [rr] --- docs/index.md | 4 +-- vercel/data_source_project_rolling_release.go | 22 +++--------- vercel/resource_project_rolling_release.go | 36 +++++++++---------- 3 files changed, 24 insertions(+), 38 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1d9aad5d..64407e00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "Vercel Provider" +page_title: "vercel Provider" subcategory: "" description: |- The Vercel provider is used to interact with resources supported by Vercel. @@ -8,7 +8,7 @@ description: |- Use the navigation to the left to read about the available resources. --- -# Vercel Provider +# vercel Provider The Vercel provider is used to interact with resources supported by Vercel. The provider needs to be configured with the proper credentials before it can be used. diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index ca7e1716..c5e8caa9 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -193,10 +193,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages := make([]attr.Value, len(automaticStages)) for i, stage := range automaticStages { stageObj := types.ObjectValueMust( - map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - }, + automaticRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, "duration": stage.Duration, @@ -205,12 +202,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - }, - }, stages) + stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) diags.Append(stagesDiags...) if diags.HasError() { return result, diags @@ -236,9 +228,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages := make([]attr.Value, len(manualStages)) for i, stage := range manualStages { stageObj := types.ObjectValueMust( - map[string]attr.Type{ - "target_percentage": types.Int64Type, - }, + manualRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, }, @@ -246,11 +236,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - }, - }, stages) + stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) diags.Append(stagesDiags...) if diags.HasError() { return result, diags diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index d3e52f1a..42aaa9d8 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -118,6 +118,20 @@ type ManualStage struct { TargetPercentage types.Int64 `tfsdk:"target_percentage"` } +// Define the element types for the lists +var automaticRollingReleaseElementType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + }, +} + +var manualRollingReleaseElementType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + }, +} + // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. type RollingReleaseInfo struct { AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` @@ -241,10 +255,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages := make([]attr.Value, len(automaticStages)) for i, stage := range automaticStages { stageObj := types.ObjectValueMust( - map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - }, + automaticRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, "duration": stage.Duration, @@ -253,12 +264,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - }, - }, stages) + stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) diags.Append(stagesDiags...) if diags.HasError() { return result, diags @@ -284,9 +290,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages := make([]attr.Value, len(manualStages)) for i, stage := range manualStages { stageObj := types.ObjectValueMust( - map[string]attr.Type{ - "target_percentage": types.Int64Type, - }, + manualRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, }, @@ -294,11 +298,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - }, - }, stages) + stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) diags.Append(stagesDiags...) if diags.HasError() { return result, diags From 44025ef7d465ed247451eb69e919ab387ae97d55 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 16:09:36 -0600 Subject: [PATCH 083/133] [rr] --- vercel/data_source_project_rolling_release.go | 8 ++++++++ vercel/resource_project_rolling_release.go | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index c5e8caa9..cbbd89a0 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -161,6 +161,14 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf TeamID: types.StringValue(response.TeamID), } + // Initialize empty lists for null/unknown values + if plan.AutomaticRollingRelease.IsNull() || plan.AutomaticRollingRelease.IsUnknown() { + result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + } + if plan.ManualRollingRelease.IsNull() || plan.ManualRollingRelease.IsUnknown() { + result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + } + // If disabled, return empty values if !response.RollingRelease.Enabled { return result, diags diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 42aaa9d8..1e4a5dbd 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -223,6 +223,14 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R TeamID: types.StringValue(response.TeamID), } + // Initialize empty lists for null/unknown values + if plan.AutomaticRollingRelease.IsNull() || plan.AutomaticRollingRelease.IsUnknown() { + result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + } + if plan.ManualRollingRelease.IsNull() || plan.ManualRollingRelease.IsUnknown() { + result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + } + // If disabled, return empty values if !response.RollingRelease.Enabled { return result, diags From 3d0f090099e28353fbcacaeeef46f2e4ce81b19d Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 17:09:17 -0600 Subject: [PATCH 084/133] [rr] --- vercel/resource_project_rolling_release.go | 120 +++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 1e4a5dbd..9737cf0b 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -333,6 +333,66 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } + // Ensure plan values have proper element types + if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { + // Convert to proper type + var automaticStages []AutomaticStage + diags = plan.AutomaticRollingRelease.ElementsAs(ctx, &automaticStages, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Recreate the list with proper element type + stages := make([]attr.Value, len(automaticStages)) + for i, stage := range automaticStages { + stageObj := types.ObjectValueMust( + automaticRollingReleaseElementType.AttrTypes, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + "duration": stage.Duration, + }, + ) + stages[i] = stageObj + } + + stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) + resp.Diagnostics.Append(stagesDiags...) + if resp.Diagnostics.HasError() { + return + } + plan.AutomaticRollingRelease = stagesList + } + + if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { + // Convert to proper type + var manualStages []ManualStage + diags = plan.ManualRollingRelease.ElementsAs(ctx, &manualStages, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Recreate the list with proper element type + stages := make([]attr.Value, len(manualStages)) + for i, stage := range manualStages { + stageObj := types.ObjectValueMust( + manualRollingReleaseElementType.AttrTypes, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + }, + ) + stages[i] = stageObj + } + + stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) + resp.Diagnostics.Append(stagesDiags...) + if resp.Diagnostics.HasError() { + return + } + plan.ManualRollingRelease = stagesList + } + // Convert plan to client request request, diags := plan.toUpdateRollingReleaseRequest() resp.Diagnostics.Append(diags...) @@ -444,6 +504,66 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource return } + // Ensure plan values have proper element types + if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { + // Convert to proper type + var automaticStages []AutomaticStage + diags = plan.AutomaticRollingRelease.ElementsAs(ctx, &automaticStages, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Recreate the list with proper element type + stages := make([]attr.Value, len(automaticStages)) + for i, stage := range automaticStages { + stageObj := types.ObjectValueMust( + automaticRollingReleaseElementType.AttrTypes, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + "duration": stage.Duration, + }, + ) + stages[i] = stageObj + } + + stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) + resp.Diagnostics.Append(stagesDiags...) + if resp.Diagnostics.HasError() { + return + } + plan.AutomaticRollingRelease = stagesList + } + + if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { + // Convert to proper type + var manualStages []ManualStage + diags = plan.ManualRollingRelease.ElementsAs(ctx, &manualStages, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Recreate the list with proper element type + stages := make([]attr.Value, len(manualStages)) + for i, stage := range manualStages { + stageObj := types.ObjectValueMust( + manualRollingReleaseElementType.AttrTypes, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + }, + ) + stages[i] = stageObj + } + + stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) + resp.Diagnostics.Append(stagesDiags...) + if resp.Diagnostics.HasError() { + return + } + plan.ManualRollingRelease = stagesList + } + // Convert plan to client request request, diags := plan.toUpdateRollingReleaseRequest() resp.Diagnostics.Append(diags...) From 3a6b47c862838fc36ebf6bf5f77b2d85a72c04e9 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 17:28:38 -0600 Subject: [PATCH 085/133] [rr] --- vercel/data_source_project_rolling_release.go | 14 +------ vercel/resource_project_rolling_release.go | 38 +++---------------- 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index cbbd89a0..416f6bac 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -210,12 +210,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) - diags.Append(stagesDiags...) - if diags.HasError() { - return result, diags - } - + stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) result.AutomaticRollingRelease = stagesList } else if response.RollingRelease.AdvancementType == "manual-approval" { @@ -244,12 +239,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) - diags.Append(stagesDiags...) - if diags.HasError() { - return result, diags - } - + stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) result.ManualRollingRelease = stagesList } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 9737cf0b..76fe5b38 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -272,12 +272,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) - diags.Append(stagesDiags...) - if diags.HasError() { - return result, diags - } - + stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) result.AutomaticRollingRelease = stagesList } else if response.RollingRelease.AdvancementType == "manual-approval" { @@ -306,12 +301,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) - diags.Append(stagesDiags...) - if diags.HasError() { - return result, diags - } - + stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) result.ManualRollingRelease = stagesList } @@ -356,11 +346,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) - resp.Diagnostics.Append(stagesDiags...) - if resp.Diagnostics.HasError() { - return - } + stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) plan.AutomaticRollingRelease = stagesList } @@ -385,11 +371,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) - resp.Diagnostics.Append(stagesDiags...) - if resp.Diagnostics.HasError() { - return - } + stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) plan.ManualRollingRelease = stagesList } @@ -527,11 +509,7 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, automaticRollingReleaseElementType, stages) - resp.Diagnostics.Append(stagesDiags...) - if resp.Diagnostics.HasError() { - return - } + stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) plan.AutomaticRollingRelease = stagesList } @@ -556,11 +534,7 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource stages[i] = stageObj } - stagesList, stagesDiags := types.ListValueFrom(ctx, manualRollingReleaseElementType, stages) - resp.Diagnostics.Append(stagesDiags...) - if resp.Diagnostics.HasError() { - return - } + stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) plan.ManualRollingRelease = stagesList } From c6cca0542ca19d1f1b602b8a91f07580695bf572 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 24 Jun 2025 17:50:59 -0600 Subject: [PATCH 086/133] [rr] --- vercel/resource_project_rolling_release.go | 168 +++++++-------------- 1 file changed, 52 insertions(+), 116 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 76fe5b38..231d021d 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -148,16 +148,37 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli if !e.AutomaticRollingRelease.IsNull() && !e.AutomaticRollingRelease.IsUnknown() { advancementType = "automatic" - // Convert automatic stages - var tfStages []AutomaticStage - diags = e.AutomaticRollingRelease.ElementsAs(context.Background(), &tfStages, false) + // Convert automatic stages using a more robust approach + var automaticStages []AutomaticStage + diags = e.AutomaticRollingRelease.ElementsAs(context.Background(), &automaticStages, false) if diags.HasError() { - return client.UpdateRollingReleaseRequest{}, diags + // If ElementsAs fails, try to extract values manually + automaticStages = []AutomaticStage{} + // Use ElementsAs with a different approach + var rawElements []attr.Value + diags = e.AutomaticRollingRelease.ElementsAs(context.Background(), &rawElements, false) + if !diags.HasError() { + for _, elem := range rawElements { + if elem.IsNull() || elem.IsUnknown() { + continue + } + + // Try to extract the object values + if obj, ok := elem.(types.Object); ok { + targetPercentage := obj.Attributes()["target_percentage"].(types.Int64) + duration := obj.Attributes()["duration"].(types.Int64) + automaticStages = append(automaticStages, AutomaticStage{ + TargetPercentage: targetPercentage, + Duration: duration, + }) + } + } + } } // Add all stages from config - stages = make([]client.RollingReleaseStage, len(tfStages)) - for i, stage := range tfStages { + stages = make([]client.RollingReleaseStage, len(automaticStages)) + for i, stage := range automaticStages { duration := int(stage.Duration.ValueInt64()) stages[i] = client.RollingReleaseStage{ TargetPercentage: int(stage.TargetPercentage.ValueInt64()), @@ -175,16 +196,35 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli } else if !e.ManualRollingRelease.IsNull() && !e.ManualRollingRelease.IsUnknown() { advancementType = "manual-approval" - // Convert manual stages - var tfStages []ManualStage - diags = e.ManualRollingRelease.ElementsAs(context.Background(), &tfStages, false) + // Convert manual stages using a more robust approach + var manualStages []ManualStage + diags = e.ManualRollingRelease.ElementsAs(context.Background(), &manualStages, false) if diags.HasError() { - return client.UpdateRollingReleaseRequest{}, diags + // If ElementsAs fails, try to extract values manually + manualStages = []ManualStage{} + // Use ElementsAs with a different approach + var rawElements []attr.Value + diags = e.ManualRollingRelease.ElementsAs(context.Background(), &rawElements, false) + if !diags.HasError() { + for _, elem := range rawElements { + if elem.IsNull() || elem.IsUnknown() { + continue + } + + // Try to extract the object values + if obj, ok := elem.(types.Object); ok { + targetPercentage := obj.Attributes()["target_percentage"].(types.Int64) + manualStages = append(manualStages, ManualStage{ + TargetPercentage: targetPercentage, + }) + } + } + } } // Add all stages from config - stages = make([]client.RollingReleaseStage, len(tfStages)) - for i, stage := range tfStages { + stages = make([]client.RollingReleaseStage, len(manualStages)) + for i, stage := range manualStages { stages[i] = client.RollingReleaseStage{ TargetPercentage: int(stage.TargetPercentage.ValueInt64()), RequireApproval: true, @@ -323,58 +363,6 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } - // Ensure plan values have proper element types - if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { - // Convert to proper type - var automaticStages []AutomaticStage - diags = plan.AutomaticRollingRelease.ElementsAs(ctx, &automaticStages, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Recreate the list with proper element type - stages := make([]attr.Value, len(automaticStages)) - for i, stage := range automaticStages { - stageObj := types.ObjectValueMust( - automaticRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - "duration": stage.Duration, - }, - ) - stages[i] = stageObj - } - - stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) - plan.AutomaticRollingRelease = stagesList - } - - if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { - // Convert to proper type - var manualStages []ManualStage - diags = plan.ManualRollingRelease.ElementsAs(ctx, &manualStages, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Recreate the list with proper element type - stages := make([]attr.Value, len(manualStages)) - for i, stage := range manualStages { - stageObj := types.ObjectValueMust( - manualRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - }, - ) - stages[i] = stageObj - } - - stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) - plan.ManualRollingRelease = stagesList - } - // Convert plan to client request request, diags := plan.toUpdateRollingReleaseRequest() resp.Diagnostics.Append(diags...) @@ -486,58 +474,6 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource return } - // Ensure plan values have proper element types - if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { - // Convert to proper type - var automaticStages []AutomaticStage - diags = plan.AutomaticRollingRelease.ElementsAs(ctx, &automaticStages, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Recreate the list with proper element type - stages := make([]attr.Value, len(automaticStages)) - for i, stage := range automaticStages { - stageObj := types.ObjectValueMust( - automaticRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - "duration": stage.Duration, - }, - ) - stages[i] = stageObj - } - - stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) - plan.AutomaticRollingRelease = stagesList - } - - if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { - // Convert to proper type - var manualStages []ManualStage - diags = plan.ManualRollingRelease.ElementsAs(ctx, &manualStages, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Recreate the list with proper element type - stages := make([]attr.Value, len(manualStages)) - for i, stage := range manualStages { - stageObj := types.ObjectValueMust( - manualRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - }, - ) - stages[i] = stageObj - } - - stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) - plan.ManualRollingRelease = stagesList - } - // Convert plan to client request request, diags := plan.toUpdateRollingReleaseRequest() resp.Diagnostics.Append(diags...) From 8c0865ead10ee9bcabff49cfc0e6cbad3105ce0b Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Mon, 30 Jun 2025 09:53:29 -0600 Subject: [PATCH 087/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 12 ++++---- vercel/resource_project_rolling_release.go | 29 +++++++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 416f6bac..fcafb888 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -161,6 +161,13 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf TeamID: types.StringValue(response.TeamID), } + // If disabled or advancementType is empty, return empty values + if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { + result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + return result, diags + } + // Initialize empty lists for null/unknown values if plan.AutomaticRollingRelease.IsNull() || plan.AutomaticRollingRelease.IsUnknown() { result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) @@ -169,11 +176,6 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) } - // If disabled, return empty values - if !response.RollingRelease.Enabled { - return result, diags - } - // Determine which type of rolling release to use based on API response if response.RollingRelease.AdvancementType == "automatic" { // Convert API stages to automatic stages (excluding terminal stage) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 231d021d..9d2d6f75 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -263,6 +263,30 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R TeamID: types.StringValue(response.TeamID), } + // If the API response shows disabled but we have stages, and the plan has configuration, + // use the plan configuration instead of treating it as disabled + if !response.RollingRelease.Enabled && len(response.RollingRelease.Stages) > 0 { + // Check if we have a plan with configuration + if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { + // Use the plan's automatic rolling release configuration + result.AutomaticRollingRelease = plan.AutomaticRollingRelease + result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + return result, diags + } else if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { + // Use the plan's manual rolling release configuration + result.ManualRollingRelease = plan.ManualRollingRelease + result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + return result, diags + } + } + + // If disabled or advancementType is empty, return empty values + if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { + result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + return result, diags + } + // Initialize empty lists for null/unknown values if plan.AutomaticRollingRelease.IsNull() || plan.AutomaticRollingRelease.IsUnknown() { result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) @@ -271,11 +295,6 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) } - // If disabled, return empty values - if !response.RollingRelease.Enabled { - return result, diags - } - // Determine which type of rolling release to use based on API response if response.RollingRelease.AdvancementType == "automatic" { // Convert API stages to automatic stages (excluding terminal stage) From b499223f272a1c8d2804d5c39ab524b291bfe392 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Mon, 30 Jun 2025 10:33:01 -0600 Subject: [PATCH 088/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 2 +- vercel/resource_project_rolling_release_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index fcafb888..1ca6d056 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -162,7 +162,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf } // If disabled or advancementType is empty, return empty values - if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { + if response.RollingRelease.AdvancementType == "" { result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) return result, diags diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 698535c7..c3e551ba 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -57,7 +57,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ "target_percentage": "20", }), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ From 7abbbb680181b462474522a5321341aa2335fc41 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Mon, 30 Jun 2025 10:33:43 -0600 Subject: [PATCH 089/133] [rolling-release] --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 64407e00..1d9aad5d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "vercel Provider" +page_title: "Vercel Provider" subcategory: "" description: |- The Vercel provider is used to interact with resources supported by Vercel. @@ -8,7 +8,7 @@ description: |- Use the navigation to the left to read about the available resources. --- -# vercel Provider +# Vercel Provider The Vercel provider is used to interact with resources supported by Vercel. The provider needs to be configured with the proper credentials before it can be used. From 9da99ff720080f74bafc7514632218b2ab5cacf4 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Mon, 30 Jun 2025 10:47:34 -0600 Subject: [PATCH 090/133] [rolling-release] --- vercel/data_source_project_rolling_release_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 33c92143..c6ede138 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -21,8 +21,6 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.#", "1"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.target_percentage", "10"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.0.duration", "10"), ), }, }, From 88cccf0a142436ac5bfe9267deb8d69d2c8709ca Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Mon, 30 Jun 2025 14:56:55 -0600 Subject: [PATCH 091/133] [rolling-release] --- vercel/resource_project_rolling_release_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index c3e551ba..10645881 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -179,8 +179,5 @@ resource "vercel_project" "example" { name = "test-acc-rolling-releases-%s" } -resource "vercel_project_rolling_release" "example" { - project_id = vercel_project.example.id -} `, nameSuffix) } From 313be6eb2d58210d745f1ae88a8a0e009aa41274 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Mon, 30 Jun 2025 15:22:01 -0600 Subject: [PATCH 092/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 10 +++------- ...ata_source_project_rolling_release_test.go | 2 +- vercel/resource_project_rolling_release.go | 13 ++++++++++++ .../resource_project_rolling_release_test.go | 20 +------------------ 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 1ca6d056..62858859 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -168,13 +168,9 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf return result, diags } - // Initialize empty lists for null/unknown values - if plan.AutomaticRollingRelease.IsNull() || plan.AutomaticRollingRelease.IsUnknown() { - result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) - } - if plan.ManualRollingRelease.IsNull() || plan.ManualRollingRelease.IsUnknown() { - result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) - } + // Initialize empty lists for both types + result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) // Determine which type of rolling release to use based on API response if response.RollingRelease.AdvancementType == "automatic" { diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index c6ede138..1e539ce7 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -44,7 +44,7 @@ resource "vercel_project_rolling_release" "example" { } data "vercel_project_rolling_release" "example" { - project_id = vercel_project_rolling_release.example.project_id + project_id = vercel_project.example.id } `, nameSuffix) } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 9d2d6f75..91416ad0 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -244,6 +244,19 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli "stages_count": len(stages), }) + // If no configuration is provided, disable the rolling release + if advancementType == "" { + return client.UpdateRollingReleaseRequest{ + RollingRelease: client.RollingRelease{ + Enabled: false, + AdvancementType: "", + Stages: []client.RollingReleaseStage{}, + }, + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + }, diags + } + return client.UpdateRollingReleaseRequest{ RollingRelease: client.RollingRelease{ Enabled: true, diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 10645881..62495559 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -88,16 +88,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { "duration": "10", }), ), - }, - // Finally disable - { - Config: cfg(testAccProjectRollingReleasesConfigDisabled(nameSuffix)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("vercel_project.example", "id"), - testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "automatic_rolling_release", ""), - ), - }, + } }, }) } @@ -172,12 +163,3 @@ resource "vercel_project_rolling_release" "example" { } `, nameSuffix) } - -func testAccProjectRollingReleasesConfigDisabled(nameSuffix string) string { - return fmt.Sprintf(` -resource "vercel_project" "example" { - name = "test-acc-rolling-releases-%s" -} - -`, nameSuffix) -} From 0b491959c9e2195ff7fb0621b938a6a1172f02c4 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Mon, 30 Jun 2025 15:22:53 -0600 Subject: [PATCH 093/133] [rolling-release] --- vercel/resource_project_rolling_release_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 62495559..802f0073 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -88,7 +88,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { "duration": "10", }), ), - } + }, }, }) } From 368259f00584fcdfc30edb61fd1551d364f52be4 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 09:18:59 -0600 Subject: [PATCH 094/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 62858859..61a58417 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -156,24 +156,49 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo, plan ProjectRollingReleaseDataSourceModel, ctx context.Context) (ProjectRollingReleaseDataSourceModel, diag.Diagnostics) { var diags diag.Diagnostics + // Log the raw response for debugging + tflog.Info(ctx, "raw rolling release response", map[string]any{ + "project_id": response.ProjectID, + "team_id": response.TeamID, + "enabled": response.RollingRelease.Enabled, + "advancement_type": response.RollingRelease.AdvancementType, + "stages_count": len(response.RollingRelease.Stages), + "stages": response.RollingRelease.Stages, + }) + result := ProjectRollingReleaseDataSourceModel{ ProjectID: types.StringValue(response.ProjectID), TeamID: types.StringValue(response.TeamID), } - - // If disabled or advancementType is empty, return empty values - if response.RollingRelease.AdvancementType == "" { - result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) - result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) - return result, diags - } - // Initialize empty lists for both types result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + // If disabled and no stages, return empty values + if !response.RollingRelease.Enabled && len(response.RollingRelease.Stages) == 0 { + return result, diags + } + // Determine which type of rolling release to use based on API response - if response.RollingRelease.AdvancementType == "automatic" { + // If advancementType is empty but stages exist, determine type from stage properties + advancementType := response.RollingRelease.AdvancementType + if advancementType == "" && len(response.RollingRelease.Stages) > 0 { + // Check if stages have duration (automatic) or not (manual) + hasDuration := false + for _, stage := range response.RollingRelease.Stages { + if stage.Duration != nil { + hasDuration = true + break + } + } + if hasDuration { + advancementType = "automatic" + } else { + advancementType = "manual-approval" + } + } + + if advancementType == "automatic" { // Convert API stages to automatic stages (excluding terminal stage) var automaticStages []AutomaticStage for _, stage := range response.RollingRelease.Stages { @@ -211,7 +236,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) result.AutomaticRollingRelease = stagesList - } else if response.RollingRelease.AdvancementType == "manual-approval" { + } else if advancementType == "manual-approval" { // Convert API stages to manual stages (excluding terminal stage) var manualStages []ManualStage for _, stage := range response.RollingRelease.Stages { @@ -243,8 +268,12 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf // Log the conversion result for debugging tflog.Info(ctx, "converted rolling release response", map[string]any{ - "advancement_type": response.RollingRelease.AdvancementType, - "stages_count": len(response.RollingRelease.Stages), + "original_advancement_type": response.RollingRelease.AdvancementType, + "determined_advancement_type": advancementType, + "stages_count": len(response.RollingRelease.Stages), + "enabled": response.RollingRelease.Enabled, + "automatic_rolling_release_is_null": result.AutomaticRollingRelease.IsNull(), + "manual_rolling_release_is_null": result.ManualRollingRelease.IsNull(), }) return result, diags From 1eedbe172323907542c6a37178f496b45415226d Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 09:54:22 -0600 Subject: [PATCH 095/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 2 +- vercel/data_source_project_rolling_release_test.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 61a58417..cd0a3470 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -175,7 +175,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) // If disabled and no stages, return empty values - if !response.RollingRelease.Enabled && len(response.RollingRelease.Stages) == 0 { + if len(response.RollingRelease.Stages) == 0 { return result, diags } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 1e539ce7..809a6556 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -20,7 +20,7 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "automatic_rolling_release.#", "1"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "manual_rolling_release.#", "1"), ), }, }, @@ -35,10 +35,9 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - automatic_rolling_release = [ + manual_rolling_release = [ { target_percentage = 10 - duration = 10 } ] } From 01c4a7cd0dd20b75aa77b8bfbde6cf9dc2ecce14 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 10:48:04 -0600 Subject: [PATCH 096/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index cd0a3470..047c17d4 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -171,10 +171,10 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf TeamID: types.StringValue(response.TeamID), } // Initialize empty lists for both types - result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) - result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + result.AutomaticRollingRelease = types.ListValueMust(automaticRollingReleaseElementType, []attr.Value{}) + result.ManualRollingRelease = types.ListValueMust(manualRollingReleaseElementType, []attr.Value{}) - // If disabled and no stages, return empty values + // If no stages, return empty lists if len(response.RollingRelease.Stages) == 0 { return result, diags } @@ -274,6 +274,8 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf "enabled": response.RollingRelease.Enabled, "automatic_rolling_release_is_null": result.AutomaticRollingRelease.IsNull(), "manual_rolling_release_is_null": result.ManualRollingRelease.IsNull(), + "automatic_rolling_release_unknown": result.AutomaticRollingRelease.IsUnknown(), + "manual_rolling_release_unknown": result.ManualRollingRelease.IsUnknown(), }) return result, diags From b4abf2d20270048a3f40452e72552e610b08e736 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 11:06:01 -0600 Subject: [PATCH 097/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 26 ++++++++++++++++++- ...ata_source_project_rolling_release_test.go | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 047c17d4..94332e39 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -239,9 +239,23 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf } else if advancementType == "manual-approval" { // Convert API stages to manual stages (excluding terminal stage) var manualStages []ManualStage - for _, stage := range response.RollingRelease.Stages { + tflog.Info(ctx, "processing manual stages", map[string]any{ + "total_stages": len(response.RollingRelease.Stages), + }) + + for i, stage := range response.RollingRelease.Stages { + tflog.Info(ctx, "processing stage", map[string]any{ + "stage_index": i, + "target_percentage": stage.TargetPercentage, + "require_approval": stage.RequireApproval, + "duration": stage.Duration, + }) + // Skip the terminal stage (100%) if stage.TargetPercentage == 100 { + tflog.Info(ctx, "skipping terminal stage", map[string]any{ + "stage_index": i, + }) continue } @@ -250,6 +264,10 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf }) } + tflog.Info(ctx, "manual stages after filtering", map[string]any{ + "manual_stages_count": len(manualStages), + }) + // Convert to Terraform types stages := make([]attr.Value, len(manualStages)) for i, stage := range manualStages { @@ -264,6 +282,12 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) result.ManualRollingRelease = stagesList + + tflog.Info(ctx, "final manual rolling release result", map[string]any{ + "stages_count": len(stages), + "is_null": result.ManualRollingRelease.IsNull(), + "is_unknown": result.ManualRollingRelease.IsUnknown(), + }) } // Log the conversion result for debugging diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 809a6556..4ffd67f3 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -20,7 +20,7 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "manual_rolling_release.#", "1"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "manual_rolling_release.#", "2"), ), }, }, From dc4e07c455f4816e1822a96020ef69b5764aac08 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 11:17:58 -0600 Subject: [PATCH 098/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 94332e39..9f00803a 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -182,6 +182,12 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf // Determine which type of rolling release to use based on API response // If advancementType is empty but stages exist, determine type from stage properties advancementType := response.RollingRelease.AdvancementType + tflog.Info(ctx, "determining advancement type", map[string]any{ + "original_advancement_type": advancementType, + "stages_count": len(response.RollingRelease.Stages), + "enabled": response.RollingRelease.Enabled, + }) + if advancementType == "" && len(response.RollingRelease.Stages) > 0 { // Check if stages have duration (automatic) or not (manual) hasDuration := false @@ -196,6 +202,10 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf } else { advancementType = "manual-approval" } + tflog.Info(ctx, "determined advancement type", map[string]any{ + "determined_advancement_type": advancementType, + "has_duration": hasDuration, + }) } if advancementType == "automatic" { From 671cfa3a0a2cc3307f4c6d0602cc7a520717d836 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 11:46:13 -0600 Subject: [PATCH 099/133] [rolling-release] --- .../vercel_project_rolling_release/data-source.tf | 2 +- vercel/data_source_project_rolling_release_test.go | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/data-sources/vercel_project_rolling_release/data-source.tf b/examples/data-sources/vercel_project_rolling_release/data-source.tf index e3b96582..ba7bf55a 100644 --- a/examples/data-sources/vercel_project_rolling_release/data-source.tf +++ b/examples/data-sources/vercel_project_rolling_release/data-source.tf @@ -3,5 +3,5 @@ data "vercel_project" "example" { } data "vercel_project_rolling_release" "example" { - project_id = data.vercel_project_rolling_release.example.id + project_id = data.vercel_project.example.id } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 4ffd67f3..b73caf2e 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { @@ -20,6 +21,14 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), + func(s *terraform.State) error { + rs, ok := s.RootModule().Resources["data.vercel_project_rolling_release.example"] + if !ok { + return fmt.Errorf("data source not found") + } + t.Logf("Data source state: %+v", rs.Primary.Attributes) + return nil + }, resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "manual_rolling_release.#", "2"), ), }, @@ -37,7 +46,10 @@ resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id manual_rolling_release = [ { - target_percentage = 10 + target_percentage = 20 + }, + { + target_percentage = 50 } ] } From 58c287743c67c8bdad9ea9bf40a27ec155a718d5 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 12:08:27 -0600 Subject: [PATCH 100/133] [rolling-release] --- vercel/data_source_project_rolling_release.go | 23 +++++--- ...ata_source_project_rolling_release_test.go | 32 ++++++++++- vercel/resource_project_rolling_release.go | 54 +++++-------------- vercel/rolling_release_types.go | 30 +++++++++++ 4 files changed, 89 insertions(+), 50 deletions(-) create mode 100644 vercel/rolling_release_types.go diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 9f00803a..2c24321a 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -142,7 +142,7 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour } // Convert the response to the data source model - convertedData, diags := convertResponseToRollingReleaseDataSource(out, data, ctx) + convertedData, diags := convertResponseToRollingReleaseDataSource(out, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -153,7 +153,7 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour resp.Diagnostics.Append(resp.State.Set(ctx, &convertedData)...) } -func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo, plan ProjectRollingReleaseDataSourceModel, ctx context.Context) (ProjectRollingReleaseDataSourceModel, diag.Diagnostics) { +func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo, ctx context.Context) (ProjectRollingReleaseDataSourceModel, diag.Diagnostics) { var diags diag.Diagnostics // Log the raw response for debugging @@ -171,8 +171,8 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf TeamID: types.StringValue(response.TeamID), } // Initialize empty lists for both types - result.AutomaticRollingRelease = types.ListValueMust(automaticRollingReleaseElementType, []attr.Value{}) - result.ManualRollingRelease = types.ListValueMust(manualRollingReleaseElementType, []attr.Value{}) + result.AutomaticRollingRelease = types.ListValueMust(AutomaticRollingReleaseElementType, []attr.Value{}) + result.ManualRollingRelease = types.ListValueMust(ManualRollingReleaseElementType, []attr.Value{}) // If no stages, return empty lists if len(response.RollingRelease.Stages) == 0 { @@ -234,7 +234,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages := make([]attr.Value, len(automaticStages)) for i, stage := range automaticStages { stageObj := types.ObjectValueMust( - automaticRollingReleaseElementType.AttrTypes, + AutomaticRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, "duration": stage.Duration, @@ -243,7 +243,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages[i] = stageObj } - stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) + stagesList := types.ListValueMust(AutomaticRollingReleaseElementType, stages) result.AutomaticRollingRelease = stagesList } else if advancementType == "manual-approval" { @@ -282,7 +282,7 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages := make([]attr.Value, len(manualStages)) for i, stage := range manualStages { stageObj := types.ObjectValueMust( - manualRollingReleaseElementType.AttrTypes, + ManualRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, }, @@ -290,13 +290,20 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf stages[i] = stageObj } - stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) + stagesList := types.ListValueMust(ManualRollingReleaseElementType, stages) + tflog.Info(ctx, "created manual stages list", map[string]any{ + "stages_count": len(stages), + "stages_list": stagesList, + "is_null": stagesList.IsNull(), + "is_unknown": stagesList.IsUnknown(), + }) result.ManualRollingRelease = stagesList tflog.Info(ctx, "final manual rolling release result", map[string]any{ "stages_count": len(stages), "is_null": result.ManualRollingRelease.IsNull(), "is_unknown": result.ManualRollingRelease.IsUnknown(), + "stages_list": stagesList, }) } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index b73caf2e..7870800d 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -18,9 +18,15 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { ), Steps: []resource.TestStep{ { - Config: cfg(testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix)), + Config: cfg(testAccProjectRollingReleasesDataSourceConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "manual_rolling_release.#", "2"), + ), + }, + { + Config: cfg(testAccProjectRollingReleasesDataSourceConfigWithDataSource(nameSuffix)), + Check: resource.ComposeAggregateTestCheckFunc( func(s *terraform.State) error { rs, ok := s.RootModule().Resources["data.vercel_project_rolling_release.example"] if !ok { @@ -36,10 +42,32 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { }) } -func testAccProjectRollingReleasesConfigOnWithDataSource(nameSuffix string) string { +func testAccProjectRollingReleasesDataSourceConfig(nameSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "example" { + name = "test-acc-example-project-%s" + skew_protection = "12 hours" +} + +resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + manual_rolling_release = [ + { + target_percentage = 20 + }, + { + target_percentage = 50 + } + ] +} +`, nameSuffix) +} + +func testAccProjectRollingReleasesDataSourceConfigWithDataSource(nameSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-example-project-%s" + skew_protection = "12 hours" } resource "vercel_project_rolling_release" "example" { diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 91416ad0..954a1749 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -9,8 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "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" @@ -56,17 +54,16 @@ func (r *projectRollingReleaseResource) Configure(ctx context.Context, req resou // Schema returns the schema information for a project rolling release resource. func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "Manages rolling release configuration for a Vercel project.", + MarkdownDescription: "Resource for a Vercel project rolling release configuration.", Attributes: map[string]schema.Attribute{ "project_id": schema.StringAttribute{ MarkdownDescription: "The ID of the project.", Required: true, }, "team_id": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "The ID of the Vercel team.", - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + Optional: true, + Computed: true, + Description: "The ID of the Vercel team.", }, "automatic_rolling_release": schema.ListNestedAttribute{ MarkdownDescription: "Automatic rolling release configuration.", @@ -109,29 +106,6 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S } } -type AutomaticStage struct { - TargetPercentage types.Int64 `tfsdk:"target_percentage"` - Duration types.Int64 `tfsdk:"duration"` -} - -type ManualStage struct { - TargetPercentage types.Int64 `tfsdk:"target_percentage"` -} - -// Define the element types for the lists -var automaticRollingReleaseElementType = types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - "duration": types.Int64Type, - }, -} - -var manualRollingReleaseElementType = types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - }, -} - // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. type RollingReleaseInfo struct { AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` @@ -283,29 +257,29 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { // Use the plan's automatic rolling release configuration result.AutomaticRollingRelease = plan.AutomaticRollingRelease - result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + result.ManualRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) return result, diags } else if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { // Use the plan's manual rolling release configuration result.ManualRollingRelease = plan.ManualRollingRelease - result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + result.AutomaticRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) return result, diags } } // If disabled or advancementType is empty, return empty values if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { - result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) - result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + result.AutomaticRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) + result.ManualRollingRelease = types.ListNull(ManualRollingReleaseElementType) return result, diags } // Initialize empty lists for null/unknown values if plan.AutomaticRollingRelease.IsNull() || plan.AutomaticRollingRelease.IsUnknown() { - result.AutomaticRollingRelease = types.ListNull(automaticRollingReleaseElementType) + result.AutomaticRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) } if plan.ManualRollingRelease.IsNull() || plan.ManualRollingRelease.IsUnknown() { - result.ManualRollingRelease = types.ListNull(manualRollingReleaseElementType) + result.ManualRollingRelease = types.ListNull(ManualRollingReleaseElementType) } // Determine which type of rolling release to use based on API response @@ -335,7 +309,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages := make([]attr.Value, len(automaticStages)) for i, stage := range automaticStages { stageObj := types.ObjectValueMust( - automaticRollingReleaseElementType.AttrTypes, + AutomaticRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, "duration": stage.Duration, @@ -344,7 +318,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages[i] = stageObj } - stagesList := types.ListValueMust(automaticRollingReleaseElementType, stages) + stagesList := types.ListValueMust(AutomaticRollingReleaseElementType, stages) result.AutomaticRollingRelease = stagesList } else if response.RollingRelease.AdvancementType == "manual-approval" { @@ -365,7 +339,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages := make([]attr.Value, len(manualStages)) for i, stage := range manualStages { stageObj := types.ObjectValueMust( - manualRollingReleaseElementType.AttrTypes, + ManualRollingReleaseElementType.AttrTypes, map[string]attr.Value{ "target_percentage": stage.TargetPercentage, }, @@ -373,7 +347,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R stages[i] = stageObj } - stagesList := types.ListValueMust(manualRollingReleaseElementType, stages) + stagesList := types.ListValueMust(ManualRollingReleaseElementType, stages) result.ManualRollingRelease = stagesList } diff --git a/vercel/rolling_release_types.go b/vercel/rolling_release_types.go new file mode 100644 index 00000000..1cb279f8 --- /dev/null +++ b/vercel/rolling_release_types.go @@ -0,0 +1,30 @@ +package vercel + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Define the element types for the lists +var AutomaticRollingReleaseElementType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + "duration": types.Int64Type, + }, +} + +var ManualRollingReleaseElementType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "target_percentage": types.Int64Type, + }, +} + +// Define the stage types +type AutomaticStage struct { + TargetPercentage types.Int64 `tfsdk:"target_percentage"` + Duration types.Int64 `tfsdk:"duration"` +} + +type ManualStage struct { + TargetPercentage types.Int64 `tfsdk:"target_percentage"` +} From e67363b6e1fb1b17085a364615b37360018b6923 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 14:02:28 -0600 Subject: [PATCH 101/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 954a1749..762fe8c5 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -257,7 +257,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { // Use the plan's automatic rolling release configuration result.AutomaticRollingRelease = plan.AutomaticRollingRelease - result.ManualRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) + result.ManualRollingRelease = types.ListNull(ManualRollingReleaseElementType) return result, diags } else if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { // Use the plan's manual rolling release configuration From 132f2b91bfcab9a4700cfd78861927bc458798de Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 14:18:33 -0600 Subject: [PATCH 102/133] [rolling-release] --- docs/data-sources/project_rolling_release.md | 2 +- docs/resources/project_rolling_release.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 155f0b49..58e50437 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -18,7 +18,7 @@ data "vercel_project" "example" { } data "vercel_project_rolling_release" "example" { - project_id = data.vercel_project_rolling_release.example.id + project_id = data.vercel_project.example.id } ``` diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 2cca12eb..33521fc3 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -3,12 +3,12 @@ page_title: "vercel_project_rolling_release Resource - terraform-provider-vercel" subcategory: "" description: |- - Manages rolling release configuration for a Vercel project. + Resource for a Vercel project rolling release configuration. --- # vercel_project_rolling_release (Resource) -Manages rolling release configuration for a Vercel project. +Resource for a Vercel project rolling release configuration. ## Example Usage From 2449bbce37d20bd59fff932c90cdf713be8c5718 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Tue, 1 Jul 2025 14:23:11 -0600 Subject: [PATCH 103/133] [rolling-release] --- docs/resources/project_rolling_release.md | 26 ++++++------------- .../resource.tf | 26 ++++++------------- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 33521fc3..3e023063 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -20,24 +20,14 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - enabled = true - advancement_type = "manual-approval" - stages = [ - { - require_approval = true - target_percentage = 20 - }, - { - require_approval = true - target_percentage = 50 - }, - { - require_approval = true - target_percentage = 100 - } - ] - } + manual_rolling_release = [ + { + target_percentage = 20 + }, + { + target_percentage = 50 + } + ] } ``` diff --git a/examples/resources/vercel_project_rolling_release/resource.tf b/examples/resources/vercel_project_rolling_release/resource.tf index e895ed1f..9bfe98cd 100644 --- a/examples/resources/vercel_project_rolling_release/resource.tf +++ b/examples/resources/vercel_project_rolling_release/resource.tf @@ -5,22 +5,12 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - enabled = true - advancement_type = "manual-approval" - stages = [ - { - require_approval = true - target_percentage = 20 - }, - { - require_approval = true - target_percentage = 50 - }, - { - require_approval = true - target_percentage = 100 - } - ] - } + manual_rolling_release = [ + { + target_percentage = 20 + }, + { + target_percentage = 50 + } + ] } From b8e8428b6ace60722b22ff6fe05d1660d1f9b7d2 Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Wed, 2 Jul 2025 08:09:19 -0600 Subject: [PATCH 104/133] [rolling-release] --- docs/data-sources/project_rolling_release.md | 19 +- docs/resources/project_rolling_release.md | 20 +- .../resource.tf | 3 +- vercel/data_source_project_rolling_release.go | 249 +++++---------- ...ata_source_project_rolling_release_test.go | 21 +- vercel/resource_project_rolling_release.go | 290 ++++++------------ .../resource_project_rolling_release_test.go | 34 +- vercel/rolling_release_types.go | 18 +- 8 files changed, 230 insertions(+), 424 deletions(-) diff --git a/docs/data-sources/project_rolling_release.md b/docs/data-sources/project_rolling_release.md index 58e50437..fca98b14 100644 --- a/docs/data-sources/project_rolling_release.md +++ b/docs/data-sources/project_rolling_release.md @@ -31,22 +31,17 @@ data "vercel_project_rolling_release" "example" { ### Optional -- `automatic_rolling_release` (Attributes List) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) -- `manual_rolling_release` (Attributes List) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) - `team_id` (String) The ID of the Vercel team. - -### Nested Schema for `automatic_rolling_release` +### Read-Only -Required: +- `advancement_type` (String) The type of advancement for the rolling release. Either 'automatic' or 'manual-approval'. +- `stages` (Attributes List) The stages for the rolling release configuration. (see [below for nested schema](#nestedatt--stages)) -- `duration` (Number) The duration in minutes to wait before advancing to the next stage. -- `target_percentage` (Number) The percentage of traffic to route to this stage. - - - -### Nested Schema for `manual_rolling_release` + +### Nested Schema for `stages` -Required: +Read-Only: +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Present for automatic advancement type. - `target_percentage` (Number) The percentage of traffic to route to this stage. diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 3e023063..33e7ef17 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -20,7 +20,8 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = [ + advancement_type = "manual-approval" + stages = [ { target_percentage = 20 }, @@ -36,26 +37,21 @@ resource "vercel_project_rolling_release" "example" { ### Required +- `advancement_type` (String) The type of advancement for the rolling release. Must be either 'automatic' or 'manual-approval'. - `project_id` (String) The ID of the project. +- `stages` (Attributes List) The stages for the rolling release configuration. (see [below for nested schema](#nestedatt--stages)) ### Optional -- `automatic_rolling_release` (Attributes List) Automatic rolling release configuration. (see [below for nested schema](#nestedatt--automatic_rolling_release)) -- `manual_rolling_release` (Attributes List) Manual rolling release configuration. (see [below for nested schema](#nestedatt--manual_rolling_release)) - `team_id` (String) The ID of the Vercel team. - -### Nested Schema for `automatic_rolling_release` + +### Nested Schema for `stages` Required: -- `duration` (Number) The duration in minutes to wait before advancing to the next stage. - `target_percentage` (Number) The percentage of traffic to route to this stage. +Optional: - -### Nested Schema for `manual_rolling_release` - -Required: - -- `target_percentage` (Number) The percentage of traffic to route to this stage. +- `duration` (Number) The duration in minutes to wait before advancing to the next stage. Required for automatic advancement type. diff --git a/examples/resources/vercel_project_rolling_release/resource.tf b/examples/resources/vercel_project_rolling_release/resource.tf index 9bfe98cd..e20e74cb 100644 --- a/examples/resources/vercel_project_rolling_release/resource.tf +++ b/examples/resources/vercel_project_rolling_release/resource.tf @@ -5,7 +5,8 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = [ + advancement_type = "manual-approval" + stages = [ { target_percentage = 20 }, diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 2c24321a..79c95f4a 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -3,13 +3,12 @@ package vercel import ( "context" "fmt" + "time" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" - "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/v3/client" @@ -62,39 +61,22 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour Computed: true, Description: "The ID of the Vercel team.", }, - "automatic_rolling_release": schema.ListNestedAttribute{ - MarkdownDescription: "Automatic rolling release configuration.", - Optional: true, + "advancement_type": schema.StringAttribute{ + MarkdownDescription: "The type of advancement for the rolling release. Either 'automatic' or 'manual-approval'.", + Computed: true, + }, + "stages": schema.ListNestedAttribute{ + MarkdownDescription: "The stages for the rolling release configuration.", + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(0, 100), - }, + Computed: true, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(1, 10000), - }, - }, - }, - }, - }, - "manual_rolling_release": schema.ListNestedAttribute{ - MarkdownDescription: "Manual rolling release configuration.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "target_percentage": schema.Int64Attribute{ - MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(0, 100), - }, + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Present for automatic advancement type.", + Computed: true, }, }, }, @@ -105,23 +87,34 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour // ProjectRollingReleaseDataSourceModel reflects the structure of the data source. type ProjectRollingReleaseDataSourceModel struct { - AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` - ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` + AdvancementType types.String `tfsdk:"advancement_type"` + Stages types.List `tfsdk:"stages"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data ProjectRollingReleaseDataSourceModel - // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { return } - out, err := d.client.GetRollingRelease(ctx, data.ProjectID.ValueString(), data.TeamID.ValueString()) + var out client.RollingReleaseInfo + var err error + var convertedData ProjectRollingReleaseDataSourceModel + var diags diag.Diagnostics + + // Retry up to 10 times (10s) for eventual consistency + for i := 0; i < 10; i++ { + out, err = d.client.GetRollingRelease(ctx, data.ProjectID.ValueString(), data.TeamID.ValueString()) + if err == nil && out.RollingRelease.Enabled && len(out.RollingRelease.Stages) > 0 { + break // found config + } + time.Sleep(1 * time.Second) + } + if client.NotFound(err) { resp.Diagnostics.AddError( "Error reading project rolling release", @@ -141,55 +134,45 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour return } - // Convert the response to the data source model - convertedData, diags := convertResponseToRollingReleaseDataSource(out, ctx) + convertedData, diags = convertResponseToRollingReleaseDataSourceWithConfig(out, data, ctx) resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { return } - // Save data into Terraform state + tflog.Info(ctx, "converted data before setting state", map[string]any{ + "project_id": convertedData.ProjectID.ValueString(), + "team_id": convertedData.TeamID.ValueString(), + "advancement_type": convertedData.AdvancementType.ValueString(), + "stages_is_null": convertedData.Stages.IsNull(), + "stages_length": len(convertedData.Stages.Elements()), + }) + resp.Diagnostics.Append(resp.State.Set(ctx, &convertedData)...) } -func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInfo, ctx context.Context) (ProjectRollingReleaseDataSourceModel, diag.Diagnostics) { +// Like convertResponseToRollingReleaseDataSource, but if API is empty/disabled, use config if present +func convertResponseToRollingReleaseDataSourceWithConfig(response client.RollingReleaseInfo, config ProjectRollingReleaseDataSourceModel, ctx context.Context) (ProjectRollingReleaseDataSourceModel, diag.Diagnostics) { var diags diag.Diagnostics - // Log the raw response for debugging - tflog.Info(ctx, "raw rolling release response", map[string]any{ - "project_id": response.ProjectID, - "team_id": response.TeamID, - "enabled": response.RollingRelease.Enabled, - "advancement_type": response.RollingRelease.AdvancementType, - "stages_count": len(response.RollingRelease.Stages), - "stages": response.RollingRelease.Stages, - }) - result := ProjectRollingReleaseDataSourceModel{ ProjectID: types.StringValue(response.ProjectID), TeamID: types.StringValue(response.TeamID), } - // Initialize empty lists for both types - result.AutomaticRollingRelease = types.ListValueMust(AutomaticRollingReleaseElementType, []attr.Value{}) - result.ManualRollingRelease = types.ListValueMust(ManualRollingReleaseElementType, []attr.Value{}) - // If no stages, return empty lists + // Always initialize advancement_type and stages, even if null + result.AdvancementType = types.StringValue("") + result.Stages = types.ListValueMust(RollingReleaseStageElementType, []attr.Value{}) + + // If API has no stages, return empty values if len(response.RollingRelease.Stages) == 0 { + tflog.Info(ctx, "API has no stages, returning empty values") return result, diags } - // Determine which type of rolling release to use based on API response - // If advancementType is empty but stages exist, determine type from stage properties + // Infer advancement_type if not set advancementType := response.RollingRelease.AdvancementType - tflog.Info(ctx, "determining advancement type", map[string]any{ - "original_advancement_type": advancementType, - "stages_count": len(response.RollingRelease.Stages), - "enabled": response.RollingRelease.Enabled, - }) - - if advancementType == "" && len(response.RollingRelease.Stages) > 0 { - // Check if stages have duration (automatic) or not (manual) + if advancementType == "" { hasDuration := false for _, stage := range response.RollingRelease.Stages { if stage.Duration != nil { @@ -207,117 +190,45 @@ func convertResponseToRollingReleaseDataSource(response client.RollingReleaseInf "has_duration": hasDuration, }) } + result.AdvancementType = types.StringValue(advancementType) - if advancementType == "automatic" { - // Convert API stages to automatic stages (excluding terminal stage) - var automaticStages []AutomaticStage - for _, stage := range response.RollingRelease.Stages { - // Skip the terminal stage (100%) - if stage.TargetPercentage == 100 { - continue - } - - var duration types.Int64 - if stage.Duration != nil { - duration = types.Int64Value(int64(*stage.Duration)) - } else { - duration = types.Int64Value(60) // Default duration - } - - automaticStages = append(automaticStages, AutomaticStage{ - TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), - Duration: duration, - }) + // Map stages (excluding terminal stage) + var rollingReleaseStages []RollingReleaseStage + for _, stage := range response.RollingRelease.Stages { + if stage.TargetPercentage == 100 { + continue } - - // Convert to Terraform types - stages := make([]attr.Value, len(automaticStages)) - for i, stage := range automaticStages { - stageObj := types.ObjectValueMust( - AutomaticRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - "duration": stage.Duration, - }, - ) - stages[i] = stageObj + rollingReleaseStage := RollingReleaseStage{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), } - - stagesList := types.ListValueMust(AutomaticRollingReleaseElementType, stages) - result.AutomaticRollingRelease = stagesList - - } else if advancementType == "manual-approval" { - // Convert API stages to manual stages (excluding terminal stage) - var manualStages []ManualStage - tflog.Info(ctx, "processing manual stages", map[string]any{ - "total_stages": len(response.RollingRelease.Stages), - }) - - for i, stage := range response.RollingRelease.Stages { - tflog.Info(ctx, "processing stage", map[string]any{ - "stage_index": i, + if stage.Duration != nil { + rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) + } + rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) + } + tflog.Info(ctx, "converted stages", map[string]any{ + "rolling_release_stages_count": len(rollingReleaseStages), + }) + stages := make([]attr.Value, len(rollingReleaseStages)) + for i, stage := range rollingReleaseStages { + stageObj := types.ObjectValueMust( + RollingReleaseStageElementType.AttrTypes, + map[string]attr.Value{ "target_percentage": stage.TargetPercentage, - "require_approval": stage.RequireApproval, "duration": stage.Duration, - }) - - // Skip the terminal stage (100%) - if stage.TargetPercentage == 100 { - tflog.Info(ctx, "skipping terminal stage", map[string]any{ - "stage_index": i, - }) - continue - } - - manualStages = append(manualStages, ManualStage{ - TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), - }) - } - - tflog.Info(ctx, "manual stages after filtering", map[string]any{ - "manual_stages_count": len(manualStages), - }) - - // Convert to Terraform types - stages := make([]attr.Value, len(manualStages)) - for i, stage := range manualStages { - stageObj := types.ObjectValueMust( - ManualRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - }, - ) - stages[i] = stageObj - } - - stagesList := types.ListValueMust(ManualRollingReleaseElementType, stages) - tflog.Info(ctx, "created manual stages list", map[string]any{ - "stages_count": len(stages), - "stages_list": stagesList, - "is_null": stagesList.IsNull(), - "is_unknown": stagesList.IsUnknown(), - }) - result.ManualRollingRelease = stagesList - - tflog.Info(ctx, "final manual rolling release result", map[string]any{ - "stages_count": len(stages), - "is_null": result.ManualRollingRelease.IsNull(), - "is_unknown": result.ManualRollingRelease.IsUnknown(), - "stages_list": stagesList, - }) + }, + ) + stages[i] = stageObj } - - // Log the conversion result for debugging + stagesList := types.ListValueMust(RollingReleaseStageElementType, stages) + result.Stages = stagesList tflog.Info(ctx, "converted rolling release response", map[string]any{ - "original_advancement_type": response.RollingRelease.AdvancementType, - "determined_advancement_type": advancementType, - "stages_count": len(response.RollingRelease.Stages), - "enabled": response.RollingRelease.Enabled, - "automatic_rolling_release_is_null": result.AutomaticRollingRelease.IsNull(), - "manual_rolling_release_is_null": result.ManualRollingRelease.IsNull(), - "automatic_rolling_release_unknown": result.AutomaticRollingRelease.IsUnknown(), - "manual_rolling_release_unknown": result.ManualRollingRelease.IsUnknown(), + "advancement_type": advancementType, + "stages_count": len(response.RollingRelease.Stages), + "enabled": response.RollingRelease.Enabled, + "result_advancement_type": result.AdvancementType.ValueString(), + "result_stages_is_null": result.Stages.IsNull(), + "result_stages_length": len(result.Stages.Elements()), }) - return result, diags } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 7870800d..843c3761 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -6,7 +6,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { @@ -21,21 +20,15 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Config: cfg(testAccProjectRollingReleasesDataSourceConfig(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), - resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "manual_rolling_release.#", "2"), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "advancement_type", "manual-approval"), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "stages.#", "2"), ), }, { Config: cfg(testAccProjectRollingReleasesDataSourceConfigWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( - func(s *terraform.State) error { - rs, ok := s.RootModule().Resources["data.vercel_project_rolling_release.example"] - if !ok { - return fmt.Errorf("data source not found") - } - t.Logf("Data source state: %+v", rs.Primary.Attributes) - return nil - }, - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "manual_rolling_release.#", "2"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "advancement_type", "manual-approval"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "stages.#", "2"), ), }, }, @@ -51,7 +44,8 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = [ + advancement_type = "manual-approval" + stages = [ { target_percentage = 20 }, @@ -72,7 +66,8 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = [ + advancement_type = "manual-approval" + stages = [ { target_percentage = 20 }, diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 762fe8c5..ee940bdf 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -65,9 +66,16 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S Computed: true, Description: "The ID of the Vercel team.", }, - "automatic_rolling_release": schema.ListNestedAttribute{ - MarkdownDescription: "Automatic rolling release configuration.", - Optional: true, + "advancement_type": schema.StringAttribute{ + MarkdownDescription: "The type of advancement for the rolling release. Must be either 'automatic' or 'manual-approval'.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("automatic", "manual-approval"), + }, + }, + "stages": schema.ListNestedAttribute{ + MarkdownDescription: "The stages for the rolling release configuration.", + Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ @@ -78,8 +86,8 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage.", - Required: true, + MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Required for automatic advancement type.", + Optional: true, Validators: []validator.Int64{ int64validator.Between(1, 10000), }, @@ -87,131 +95,81 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, }, - "manual_rolling_release": schema.ListNestedAttribute{ - MarkdownDescription: "Manual rolling release configuration.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "target_percentage": schema.Int64Attribute{ - MarkdownDescription: "The percentage of traffic to route to this stage.", - Required: true, - Validators: []validator.Int64{ - int64validator.Between(0, 100), - }, - }, - }, - }, - }, }, } } // ProjectRollingRelease reflects the state terraform stores internally for a project rolling release. type RollingReleaseInfo struct { - AutomaticRollingRelease types.List `tfsdk:"automatic_rolling_release"` - ManualRollingRelease types.List `tfsdk:"manual_rolling_release"` - ProjectID types.String `tfsdk:"project_id"` - TeamID types.String `tfsdk:"team_id"` + AdvancementType types.String `tfsdk:"advancement_type"` + Stages types.List `tfsdk:"stages"` + ProjectID types.String `tfsdk:"project_id"` + TeamID types.String `tfsdk:"team_id"` } func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { var stages []client.RollingReleaseStage - var advancementType string var diags diag.Diagnostics - if !e.AutomaticRollingRelease.IsNull() && !e.AutomaticRollingRelease.IsUnknown() { - advancementType = "automatic" - - // Convert automatic stages using a more robust approach - var automaticStages []AutomaticStage - diags = e.AutomaticRollingRelease.ElementsAs(context.Background(), &automaticStages, false) - if diags.HasError() { - // If ElementsAs fails, try to extract values manually - automaticStages = []AutomaticStage{} - // Use ElementsAs with a different approach - var rawElements []attr.Value - diags = e.AutomaticRollingRelease.ElementsAs(context.Background(), &rawElements, false) - if !diags.HasError() { - for _, elem := range rawElements { - if elem.IsNull() || elem.IsUnknown() { - continue - } - - // Try to extract the object values - if obj, ok := elem.(types.Object); ok { - targetPercentage := obj.Attributes()["target_percentage"].(types.Int64) - duration := obj.Attributes()["duration"].(types.Int64) - automaticStages = append(automaticStages, AutomaticStage{ - TargetPercentage: targetPercentage, - Duration: duration, - }) - } + advancementType := e.AdvancementType.ValueString() + + // Convert stages using a more robust approach + var rollingReleaseStages []RollingReleaseStage + diags = e.Stages.ElementsAs(context.Background(), &rollingReleaseStages, false) + if diags.HasError() { + // If ElementsAs fails, try to extract values manually + rollingReleaseStages = []RollingReleaseStage{} + // Use ElementsAs with a different approach + var rawElements []attr.Value + diags = e.Stages.ElementsAs(context.Background(), &rawElements, false) + if !diags.HasError() { + for _, elem := range rawElements { + if elem.IsNull() || elem.IsUnknown() { + continue } - } - } - // Add all stages from config - stages = make([]client.RollingReleaseStage, len(automaticStages)) - for i, stage := range automaticStages { - duration := int(stage.Duration.ValueInt64()) - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - Duration: &duration, - RequireApproval: false, - } - } + // Try to extract the object values + if obj, ok := elem.(types.Object); ok { + targetPercentage := obj.Attributes()["target_percentage"].(types.Int64) + duration := obj.Attributes()["duration"] - // Add terminal stage (100%) without duration - stages = append(stages, client.RollingReleaseStage{ - TargetPercentage: 100, - RequireApproval: false, - }) - - } else if !e.ManualRollingRelease.IsNull() && !e.ManualRollingRelease.IsUnknown() { - advancementType = "manual-approval" - - // Convert manual stages using a more robust approach - var manualStages []ManualStage - diags = e.ManualRollingRelease.ElementsAs(context.Background(), &manualStages, false) - if diags.HasError() { - // If ElementsAs fails, try to extract values manually - manualStages = []ManualStage{} - // Use ElementsAs with a different approach - var rawElements []attr.Value - diags = e.ManualRollingRelease.ElementsAs(context.Background(), &rawElements, false) - if !diags.HasError() { - for _, elem := range rawElements { - if elem.IsNull() || elem.IsUnknown() { - continue + stage := RollingReleaseStage{ + TargetPercentage: targetPercentage, } - // Try to extract the object values - if obj, ok := elem.(types.Object); ok { - targetPercentage := obj.Attributes()["target_percentage"].(types.Int64) - manualStages = append(manualStages, ManualStage{ - TargetPercentage: targetPercentage, - }) + if !duration.IsNull() && !duration.IsUnknown() { + stage.Duration = duration.(types.Int64) } + + rollingReleaseStages = append(rollingReleaseStages, stage) } } } + } - // Add all stages from config - stages = make([]client.RollingReleaseStage, len(manualStages)) - for i, stage := range manualStages { - stages[i] = client.RollingReleaseStage{ - TargetPercentage: int(stage.TargetPercentage.ValueInt64()), - RequireApproval: true, - } + // Add all stages from config + stages = make([]client.RollingReleaseStage, len(rollingReleaseStages)) + for i, stage := range rollingReleaseStages { + clientStage := client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + RequireApproval: advancementType == "manual-approval", } - // Add terminal stage (100%) without approval - stages = append(stages, client.RollingReleaseStage{ - TargetPercentage: 100, - RequireApproval: false, - }) + // Add duration for automatic advancement type + if advancementType == "automatic" && !stage.Duration.IsNull() && !stage.Duration.IsUnknown() { + duration := int(stage.Duration.ValueInt64()) + clientStage.Duration = &duration + } + + stages[i] = clientStage } + // Add terminal stage (100%) without approval + stages = append(stages, client.RollingReleaseStage{ + TargetPercentage: 100, + RequireApproval: false, + }) + // Log the request for debugging tflog.Info(context.Background(), "converting to update request", map[string]any{ "advancement_type": advancementType, @@ -250,107 +208,61 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R TeamID: types.StringValue(response.TeamID), } - // If the API response shows disabled but we have stages, and the plan has configuration, - // use the plan configuration instead of treating it as disabled - if !response.RollingRelease.Enabled && len(response.RollingRelease.Stages) > 0 { - // Check if we have a plan with configuration - if !plan.AutomaticRollingRelease.IsNull() && !plan.AutomaticRollingRelease.IsUnknown() { - // Use the plan's automatic rolling release configuration - result.AutomaticRollingRelease = plan.AutomaticRollingRelease - result.ManualRollingRelease = types.ListNull(ManualRollingReleaseElementType) - return result, diags - } else if !plan.ManualRollingRelease.IsNull() && !plan.ManualRollingRelease.IsUnknown() { - // Use the plan's manual rolling release configuration - result.ManualRollingRelease = plan.ManualRollingRelease - result.AutomaticRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) - return result, diags - } + // If the API response shows disabled or advancementType is empty, but the plan has configuration, use the plan's values + if (!response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "") && plan != nil && + !plan.AdvancementType.IsNull() && plan.AdvancementType.ValueString() != "" && + !plan.Stages.IsNull() && len(plan.Stages.Elements()) > 0 { + result.AdvancementType = plan.AdvancementType + result.Stages = plan.Stages + return result, diags } - // If disabled or advancementType is empty, return empty values + // If disabled or advancementType is empty and no plan, return empty values if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { - result.AutomaticRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) - result.ManualRollingRelease = types.ListNull(ManualRollingReleaseElementType) + result.AdvancementType = types.StringNull() + result.Stages = types.ListNull(RollingReleaseStageElementType) return result, diags } - // Initialize empty lists for null/unknown values - if plan.AutomaticRollingRelease.IsNull() || plan.AutomaticRollingRelease.IsUnknown() { - result.AutomaticRollingRelease = types.ListNull(AutomaticRollingReleaseElementType) - } - if plan.ManualRollingRelease.IsNull() || plan.ManualRollingRelease.IsUnknown() { - result.ManualRollingRelease = types.ListNull(ManualRollingReleaseElementType) - } - - // Determine which type of rolling release to use based on API response - if response.RollingRelease.AdvancementType == "automatic" { - // Convert API stages to automatic stages (excluding terminal stage) - var automaticStages []AutomaticStage - for _, stage := range response.RollingRelease.Stages { - // Skip the terminal stage (100%) - if stage.TargetPercentage == 100 { - continue - } + // Set the advancement type + result.AdvancementType = types.StringValue(response.RollingRelease.AdvancementType) - var duration types.Int64 - if stage.Duration != nil { - duration = types.Int64Value(int64(*stage.Duration)) - } else { - duration = types.Int64Value(60) // Default duration - } - - automaticStages = append(automaticStages, AutomaticStage{ - TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), - Duration: duration, - }) + // Convert API stages to stages (excluding terminal stage) + var rollingReleaseStages []RollingReleaseStage + for _, stage := range response.RollingRelease.Stages { + // Skip the terminal stage (100%) + if stage.TargetPercentage == 100 { + continue } - // Convert to Terraform types - stages := make([]attr.Value, len(automaticStages)) - for i, stage := range automaticStages { - stageObj := types.ObjectValueMust( - AutomaticRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - "duration": stage.Duration, - }, - ) - stages[i] = stageObj + rollingReleaseStage := RollingReleaseStage{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), } - stagesList := types.ListValueMust(AutomaticRollingReleaseElementType, stages) - result.AutomaticRollingRelease = stagesList - - } else if response.RollingRelease.AdvancementType == "manual-approval" { - // Convert API stages to manual stages (excluding terminal stage) - var manualStages []ManualStage - for _, stage := range response.RollingRelease.Stages { - // Skip the terminal stage (100%) - if stage.TargetPercentage == 100 { - continue - } - - manualStages = append(manualStages, ManualStage{ - TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), - }) + // Add duration if it exists (for automatic advancement type) + if stage.Duration != nil { + rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) } - // Convert to Terraform types - stages := make([]attr.Value, len(manualStages)) - for i, stage := range manualStages { - stageObj := types.ObjectValueMust( - ManualRollingReleaseElementType.AttrTypes, - map[string]attr.Value{ - "target_percentage": stage.TargetPercentage, - }, - ) - stages[i] = stageObj - } + rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) + } - stagesList := types.ListValueMust(ManualRollingReleaseElementType, stages) - result.ManualRollingRelease = stagesList + // Convert to Terraform types + stages := make([]attr.Value, len(rollingReleaseStages)) + for i, stage := range rollingReleaseStages { + stageObj := types.ObjectValueMust( + RollingReleaseStageElementType.AttrTypes, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + "duration": stage.Duration, + }, + ) + stages[i] = stageObj } + stagesList := types.ListValueMust(RollingReleaseStageElementType, stages) + result.Stages = stagesList + // Log the conversion result for debugging tflog.Info(ctx, "converted rolling release response", map[string]any{ "advancement_type": response.RollingRelease.AdvancementType, diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 802f0073..a8b432a0 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -41,11 +41,12 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.#", "2"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "advancement_type", "manual-approval"), + resource.TestCheckResourceAttr(resourceName, "stages.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "20", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "50", }), ), @@ -56,14 +57,15 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "manual_rolling_release.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "advancement_type", "manual-approval"), + resource.TestCheckResourceAttr(resourceName, "stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "20", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "50", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "manual_rolling_release.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "80", }), ), @@ -74,16 +76,17 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "automatic_rolling_release.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "20", "duration": "10", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "50", "duration": "10", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "automatic_rolling_release.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "80", "duration": "10", }), @@ -102,7 +105,8 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = [ + advancement_type = "manual-approval" + stages = [ { target_percentage = 20 }, @@ -123,7 +127,8 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - manual_rolling_release = [ + advancement_type = "manual-approval" + stages = [ { target_percentage = 20 }, @@ -146,7 +151,8 @@ resource "vercel_project" "example" { resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - automatic_rolling_release = [ + advancement_type = "automatic" + stages = [ { target_percentage = 20 duration = 10 diff --git a/vercel/rolling_release_types.go b/vercel/rolling_release_types.go index 1cb279f8..cfd81934 100644 --- a/vercel/rolling_release_types.go +++ b/vercel/rolling_release_types.go @@ -5,26 +5,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -// Define the element types for the lists -var AutomaticRollingReleaseElementType = types.ObjectType{ +// Define the element type for stages +var RollingReleaseStageElementType = types.ObjectType{ AttrTypes: map[string]attr.Type{ "target_percentage": types.Int64Type, "duration": types.Int64Type, }, } -var ManualRollingReleaseElementType = types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "target_percentage": types.Int64Type, - }, -} - -// Define the stage types -type AutomaticStage struct { +// Define the stage type +type RollingReleaseStage struct { TargetPercentage types.Int64 `tfsdk:"target_percentage"` Duration types.Int64 `tfsdk:"duration"` } - -type ManualStage struct { - TargetPercentage types.Int64 `tfsdk:"target_percentage"` -} From b02ea8b3cbeb0de5f709501b96105ee1848baafb Mon Sep 17 00:00:00 2001 From: Brooke Mosby Date: Thu, 3 Jul 2025 12:31:14 -0600 Subject: [PATCH 105/133] [rolling-release] --- vercel/resource_project_rolling_release.go | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index ee940bdf..7aa2e131 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -52,6 +53,56 @@ func (r *projectRollingReleaseResource) Configure(ctx context.Context, req resou r.client = client } +// durationValidator validates that duration is only present when advancement_type is "automatic" +type durationValidator struct{} + +func (v durationValidator) Description(ctx context.Context) string { + return "duration can only be set when advancement_type is 'automatic'" +} + +func (v durationValidator) MarkdownDescription(ctx context.Context) string { + return "`duration` can only be set when `advancement_type` is `automatic`" +} + +func (v durationValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + // Get the parent advancement_type value + parentPath := req.Path.ParentPath() + advancementTypePath := parentPath.AtName("advancement_type") + + var advancementType types.String + diags := req.Config.GetAttribute(ctx, advancementTypePath, &advancementType) + if diags.HasError() { + return + } + + // Check if duration is set + durationAttr, exists := req.ConfigValue.Attributes()["duration"] + if !exists { + return + } + + duration := durationAttr.(types.Int64) + if duration.IsNull() || duration.IsUnknown() { + if advancementType.ValueString() == "manual-approval" { + return + } + resp.Diagnostics.AddAttributeError( + req.Path.AtName("duration"), + "Invalid duration configuration", + "duration can only be set when advancement_type is 'automatic'", + ) + } + + // If duration is set but advancement_type is not "automatic", add an error + if advancementType.ValueString() != "automatic" { + resp.Diagnostics.AddAttributeError( + req.Path.AtName("duration"), + "Invalid duration configuration", + "duration must be set when advancement_type is 'automatic'", + ) + } +} + // Schema returns the schema information for a project rolling release resource. func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ @@ -76,6 +127,10 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S "stages": schema.ListNestedAttribute{ MarkdownDescription: "The stages for the rolling release configuration.", Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(10), + }, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ @@ -93,6 +148,9 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, }, + Validators: []validator.Object{ + durationValidator{}, + }, }, }, }, From db79e937babd1400f06d81eed891d7997955b86d Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:14:29 -0400 Subject: [PATCH 106/133] removes rolling release item from project response --- client/project.go | 1 - 1 file changed, 1 deletion(-) diff --git a/client/project.go b/client/project.go index 7e2e5378..f650093e 100644 --- a/client/project.go +++ b/client/project.go @@ -210,7 +210,6 @@ type ProjectResponse struct { ResourceConfig *ResourceConfigResponse `json:"resourceConfig"` NodeVersion string `json:"nodeVersion"` Crons *ProjectCronsResponse `json:"crons"` - RollingRelease *RollingRelease `json:"rollingRelease"` } type ProjectCronsResponse struct { From e8d36076edccd29fde6c476cef9a28e717a81be2 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:16:25 -0400 Subject: [PATCH 107/133] remove enabled check --- client/project_rolling_release.go | 73 ++++++++++--------------------- 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index d9fd04cc..5d6148fe 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -60,57 +60,30 @@ type UpdateRollingReleaseRequest struct { // UpdateRollingRelease will update an existing rolling release to the latest information. func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseInfo, error) { request.TeamID = c.TeamID(request.TeamID) - if request.RollingRelease.Enabled { - enableRequest := map[string]any{ - "enabled": true, - "advancementType": request.RollingRelease.AdvancementType, - "stages": request.RollingRelease.Stages, - } - - var result RollingReleaseInfo - err := c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), - body: string(mustMarshal(enableRequest)), - }, &result) - if err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %w", err) - } - - result.ProjectID = request.ProjectID - result.TeamID = request.TeamID - tflog.Info(ctx, "enabled rolling release", map[string]any{ - "response": result, - "request": request, - }) - return result, nil - } else { - // For disabling, just send the request as is - disabledRequest := UpdateRollingReleaseRequest{ - RollingRelease: RollingRelease{ - Enabled: false, - AdvancementType: "", - Stages: []RollingReleaseStage{}, - }, - } - - var result RollingReleaseInfo - err := c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), - body: string(mustMarshal(disabledRequest.RollingRelease)), - }, &result) - if err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error disabling rolling release: %w", err) - } - - result.ProjectID = request.ProjectID - result.TeamID = request.TeamID - - return result, nil + enableRequest := map[string]any{ + "enabled": true, + "advancementType": request.RollingRelease.AdvancementType, + "stages": request.RollingRelease.Stages, } + + var result RollingReleaseInfo + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + body: string(mustMarshal(enableRequest)), + }, &result) + if err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %w", err) + } + + result.ProjectID = request.ProjectID + result.TeamID = request.TeamID + tflog.Info(ctx, "enabled rolling release", map[string]any{ + "response": result, + "request": request, + }) + return result, nil } // DeleteRollingRelease will delete the rolling release for a given project. From 4d6d9b238041905f927c40084cd81d993703659e Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:17:30 -0400 Subject: [PATCH 108/133] use Description rather than MarkdownDescription --- vercel/data_source_project_rolling_release.go | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 79c95f4a..4b1ae404 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -53,8 +53,8 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour MarkdownDescription: "Data source for a Vercel project rolling release configuration.", Attributes: map[string]schema.Attribute{ "project_id": schema.StringAttribute{ - MarkdownDescription: "The ID of the project.", - Required: true, + Description: "The ID of the project.", + Required: true, }, "team_id": schema.StringAttribute{ Optional: true, @@ -62,21 +62,21 @@ func (d *projectRollingReleaseDataSource) Schema(ctx context.Context, _ datasour Description: "The ID of the Vercel team.", }, "advancement_type": schema.StringAttribute{ - MarkdownDescription: "The type of advancement for the rolling release. Either 'automatic' or 'manual-approval'.", - Computed: true, + Description: "The type of advancement for the rolling release. Either 'automatic' or 'manual-approval'.", + Computed: true, }, "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages for the rolling release configuration.", - Computed: true, + Description: "The stages for the rolling release configuration.", + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ - MarkdownDescription: "The percentage of traffic to route to this stage.", - Computed: true, + Description: "The percentage of traffic to route to this stage.", + Computed: true, }, "duration": schema.Int64Attribute{ - MarkdownDescription: "The duration in minutes to wait before advancing to the next stage. Present for automatic advancement type.", - Computed: true, + Description: "The duration in minutes to wait before advancing to the next stage. Present for automatic advancement type.", + Computed: true, }, }, }, From a0692ca65d2965ecf005d0c4cb668a83b32cb40a Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:19:29 -0400 Subject: [PATCH 109/133] remove loop sleep --- vercel/data_source_project_rolling_release.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 4b1ae404..43748cac 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -3,7 +3,6 @@ package vercel import ( "context" "fmt" - "time" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -106,14 +105,7 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour var convertedData ProjectRollingReleaseDataSourceModel var diags diag.Diagnostics - // Retry up to 10 times (10s) for eventual consistency - for i := 0; i < 10; i++ { - out, err = d.client.GetRollingRelease(ctx, data.ProjectID.ValueString(), data.TeamID.ValueString()) - if err == nil && out.RollingRelease.Enabled && len(out.RollingRelease.Stages) > 0 { - break // found config - } - time.Sleep(1 * time.Second) - } + out, err = d.client.GetRollingRelease(ctx, data.ProjectID.ValueString(), data.TeamID.ValueString()) if client.NotFound(err) { resp.Diagnostics.AddError( From e78835bff501aa01e239b61384c047c42577ca7e Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:20:17 -0400 Subject: [PATCH 110/133] remove added check --- vercel/data_source_project_rolling_release.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 43748cac..076e3e8c 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -107,13 +107,6 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour out, err = d.client.GetRollingRelease(ctx, data.ProjectID.ValueString(), data.TeamID.ValueString()) - if client.NotFound(err) { - resp.Diagnostics.AddError( - "Error reading project rolling release", - fmt.Sprintf("No project rolling release found with id %s %s", data.TeamID.ValueString(), data.ProjectID.ValueString()), - ) - return - } if err != nil { resp.Diagnostics.AddError( "Error reading project rolling release", From a8847abd5eb0a6a42ff17dd9036c8dfb904d999c Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:21:06 -0400 Subject: [PATCH 111/133] removes comment --- vercel/data_source_project_rolling_release.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 076e3e8c..0eab493f 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -136,7 +136,6 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour resp.Diagnostics.Append(resp.State.Set(ctx, &convertedData)...) } -// Like convertResponseToRollingReleaseDataSource, but if API is empty/disabled, use config if present func convertResponseToRollingReleaseDataSourceWithConfig(response client.RollingReleaseInfo, config ProjectRollingReleaseDataSourceModel, ctx context.Context) (ProjectRollingReleaseDataSourceModel, diag.Diagnostics) { var diags diag.Diagnostics From 75dbc74ce117047be8dce4e3193a5183e3c63c72 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:33:16 -0400 Subject: [PATCH 112/133] remove diag handling --- vercel/resource_project_rolling_release.go | 30 ---------------------- 1 file changed, 30 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 7aa2e131..aabeec40 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -174,36 +174,6 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli // Convert stages using a more robust approach var rollingReleaseStages []RollingReleaseStage diags = e.Stages.ElementsAs(context.Background(), &rollingReleaseStages, false) - if diags.HasError() { - // If ElementsAs fails, try to extract values manually - rollingReleaseStages = []RollingReleaseStage{} - // Use ElementsAs with a different approach - var rawElements []attr.Value - diags = e.Stages.ElementsAs(context.Background(), &rawElements, false) - if !diags.HasError() { - for _, elem := range rawElements { - if elem.IsNull() || elem.IsUnknown() { - continue - } - - // Try to extract the object values - if obj, ok := elem.(types.Object); ok { - targetPercentage := obj.Attributes()["target_percentage"].(types.Int64) - duration := obj.Attributes()["duration"] - - stage := RollingReleaseStage{ - TargetPercentage: targetPercentage, - } - - if !duration.IsNull() && !duration.IsUnknown() { - stage.Duration = duration.(types.Int64) - } - - rollingReleaseStages = append(rollingReleaseStages, stage) - } - } - } - } // Add all stages from config stages = make([]client.RollingReleaseStage, len(rollingReleaseStages)) From 8e1d5215e3dcb80ad43f41be71e17f2bf702df54 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:43:49 -0400 Subject: [PATCH 113/133] actually use delete --- vercel/resource_project_rolling_release.go | 26 +--------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index aabeec40..2143ce41 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -204,19 +204,6 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli "stages_count": len(stages), }) - // If no configuration is provided, disable the rolling release - if advancementType == "" { - return client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ - Enabled: false, - AdvancementType: "", - Stages: []client.RollingReleaseStage{}, - }, - ProjectID: e.ProjectID.ValueString(), - TeamID: e.TeamID.ValueString(), - }, diags - } - return client.UpdateRollingReleaseRequest{ RollingRelease: client.RollingRelease{ Enabled: true, @@ -473,18 +460,7 @@ func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource return } - // Disable rolling release - request := client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ - Enabled: false, - AdvancementType: "", - Stages: []client.RollingReleaseStage{}, - }, - ProjectID: state.ProjectID.ValueString(), - TeamID: state.TeamID.ValueString(), - } - - _, err := r.client.UpdateRollingRelease(ctx, request) + err := r.client.DeleteRollingRelease(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) if err != nil { resp.Diagnostics.AddError( "Error deleting project rolling release", From 54b50ac5fc670a33f0bef9f4d7b052c7a78bfe69 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:45:04 -0400 Subject: [PATCH 114/133] removes extra debug info --- vercel/data_source_project_rolling_release.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index 0eab493f..f4a80b45 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -125,14 +125,6 @@ func (d *projectRollingReleaseDataSource) Read(ctx context.Context, req datasour return } - tflog.Info(ctx, "converted data before setting state", map[string]any{ - "project_id": convertedData.ProjectID.ValueString(), - "team_id": convertedData.TeamID.ValueString(), - "advancement_type": convertedData.AdvancementType.ValueString(), - "stages_is_null": convertedData.Stages.IsNull(), - "stages_length": len(convertedData.Stages.Elements()), - }) - resp.Diagnostics.Append(resp.State.Set(ctx, &convertedData)...) } From c9ae422d1b883afa29abf7bfb031bc1b3370c7eb Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:50:30 -0400 Subject: [PATCH 115/133] remove duplication of check --- vercel/resource_project_rolling_release.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 2143ce41..f9058a5c 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -223,17 +223,18 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R TeamID: types.StringValue(response.TeamID), } - // If the API response shows disabled or advancementType is empty, but the plan has configuration, use the plan's values - if (!response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "") && plan != nil && - !plan.AdvancementType.IsNull() && plan.AdvancementType.ValueString() != "" && - !plan.Stages.IsNull() && len(plan.Stages.Elements()) > 0 { - result.AdvancementType = plan.AdvancementType - result.Stages = plan.Stages - return result, diags - } - // If disabled or advancementType is empty and no plan, return empty values if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { + // If the API response shows disabled or advancementType is empty, but the plan has configuration, use the plan's values + if plan != nil && + !plan.AdvancementType.IsNull() && plan.AdvancementType.ValueString() != "" && + !plan.Stages.IsNull() && len(plan.Stages.Elements()) > 0 { + + result.AdvancementType = plan.AdvancementType + result.Stages = plan.Stages + return result, diags + } + result.AdvancementType = types.StringNull() result.Stages = types.ListNull(RollingReleaseStageElementType) return result, diags From e12ba0861e558a40aed4a1c89212a454563440cf Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 21 Jul 2025 16:51:21 -0400 Subject: [PATCH 116/133] remove debugging line --- vercel/resource_project_rolling_release.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index f9058a5c..d0b102e9 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -304,13 +304,6 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } - // Log the request for debugging - tflog.Info(ctx, "creating rolling release", map[string]any{ - "enabled": request.RollingRelease.Enabled, - "advancement_type": request.RollingRelease.AdvancementType, - "stages": request.RollingRelease.Stages, - }) - out, err := r.client.UpdateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( From 035aadef3de3b74b11ad9158dc278f836c3b36d3 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:47:51 -0600 Subject: [PATCH 117/133] remove un-used state --- vercel/resource_project_rolling_release.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index d0b102e9..8e7f6d60 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -394,13 +394,6 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource return } - var state RollingReleaseInfo - diags = req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - // Convert plan to client request request, diags := plan.toUpdateRollingReleaseRequest() resp.Diagnostics.Append(diags...) From ed11f39d62c63d768fdb82fafd4caaaf8c882303 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:05:04 -0600 Subject: [PATCH 118/133] separate create from update --- client/project_rolling_release.go | 39 +++++++++++++++- vercel/resource_project_rolling_release.go | 54 +++++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index 5d6148fe..6bf41879 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -49,6 +49,43 @@ func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string return d, nil } +// CreateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to +// create a new rolling release. +type CreateRollingReleaseRequest struct { + RollingRelease RollingRelease `json:"rollingRelease"` + ProjectID string `json:"projectId"` + TeamID string `json:"teamId"` +} + +// CreateRollingRelease will create a new rolling release for a given project. +func (c *Client) CreateRollingRelease(ctx context.Context, request CreateRollingReleaseRequest) (RollingReleaseInfo, error) { + request.TeamID = c.TeamID(request.TeamID) + enableRequest := map[string]any{ + "enabled": true, + "advancementType": request.RollingRelease.AdvancementType, + "stages": request.RollingRelease.Stages, + } + + var result RollingReleaseInfo + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, request.ProjectID, request.TeamID), + body: string(mustMarshal(enableRequest)), + }, &result) + if err != nil { + return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %w", err) + } + + result.ProjectID = request.ProjectID + result.TeamID = request.TeamID + tflog.Info(ctx, "created rolling release", map[string]any{ + "response": result, + "request": request, + }) + return result, nil +} + // UpdateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to // update a rolling release. type UpdateRollingReleaseRequest struct { @@ -79,7 +116,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling result.ProjectID = request.ProjectID result.TeamID = request.TeamID - tflog.Info(ctx, "enabled rolling release", map[string]any{ + tflog.Info(ctx, "updated rolling release", map[string]any{ "response": result, "request": request, }) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 8e7f6d60..369ce047 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -165,6 +165,56 @@ type RollingReleaseInfo struct { TeamID types.String `tfsdk:"team_id"` } +func (e *RollingReleaseInfo) toCreateRollingReleaseRequest() (client.CreateRollingReleaseRequest, diag.Diagnostics) { + var stages []client.RollingReleaseStage + var diags diag.Diagnostics + + advancementType := e.AdvancementType.ValueString() + + // Convert stages using a more robust approach + var rollingReleaseStages []RollingReleaseStage + diags = e.Stages.ElementsAs(context.Background(), &rollingReleaseStages, false) + + // Add all stages from config + stages = make([]client.RollingReleaseStage, len(rollingReleaseStages)) + for i, stage := range rollingReleaseStages { + clientStage := client.RollingReleaseStage{ + TargetPercentage: int(stage.TargetPercentage.ValueInt64()), + RequireApproval: advancementType == "manual-approval", + } + + // Add duration for automatic advancement type + if advancementType == "automatic" && !stage.Duration.IsNull() && !stage.Duration.IsUnknown() { + duration := int(stage.Duration.ValueInt64()) + clientStage.Duration = &duration + } + + stages[i] = clientStage + } + + // Add terminal stage (100%) without approval + stages = append(stages, client.RollingReleaseStage{ + TargetPercentage: 100, + RequireApproval: false, + }) + + // Log the request for debugging + tflog.Info(context.Background(), "converting to update request", map[string]any{ + "advancement_type": advancementType, + "stages_count": len(stages), + }) + + return client.CreateRollingReleaseRequest{ + RollingRelease: client.RollingRelease{ + Enabled: true, + AdvancementType: advancementType, + Stages: stages, + }, + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + }, diags +} + func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRollingReleaseRequest, diag.Diagnostics) { var stages []client.RollingReleaseStage var diags diag.Diagnostics @@ -298,13 +348,13 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource } // Convert plan to client request - request, diags := plan.toUpdateRollingReleaseRequest() + request, diags := plan.toCreateRollingReleaseRequest() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - out, err := r.client.UpdateRollingRelease(ctx, request) + out, err := r.client.CreateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( "Error creating project rolling release", From 29603cd5aa8f7d8bcaae88a747f2a6e66c3d06a0 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:15:31 -0600 Subject: [PATCH 119/133] separate create from update --- .../resource_project_rolling_release_test.go | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index a8b432a0..52b2b071 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -11,6 +11,17 @@ import ( "github.com/vercel/terraform-provider-vercel/v3/client" ) +func getRollingReleaseImportId(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) + } + + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.Attributes["project_id"]), nil + } +} + func testAccProjectRollingReleaseExists(testClient *client.Client, n, teamID string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -51,6 +62,14 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }), ), }, + // Now, import the existing resource + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getRollingReleaseImportId(resourceName), + ImportStateVerifyIdentifierAttribute: "project_id", + }, // Then update to new configuration { Config: cfg(testAccProjectRollingReleasesConfigUpdated(nameSuffix)), @@ -92,6 +111,49 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { }), ), }, + { + Config: cfg(fmt.Sprintf(` + resource "vercel_project" "example" { + name = "test-acc-rolling-releases-auto-duration-%s" + } + resource "vercel_project_rolling_release" "example" { + project_id = vercel_project.example.id + rolling_release = { + advancement_type = "automatic" + stages = [ + { + target_percentage = 30 + // Duration is omitted here for the first stage + }, + { + target_percentage = 70 + duration = 30 // Explicit duration for a middle stage + } + ] + } + } + `, nameSuffix)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), + resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "target_percentage": "30", + "duration": "60", // Asserting the default value + "require_approval": "false", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "target_percentage": "70", + "duration": "30", // Asserting the explicit value + "require_approval": "false", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + "target_percentage": "100", + // Duration for the last stage is expected to be null or not present + "require_approval": "false", + }), + ), + }, }, }) } From 696f00525d1aa3f5fe805f2a8983cccc9608ac30 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:29:58 -0600 Subject: [PATCH 120/133] separate create from update --- .../resource_project_rolling_release_test.go | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 52b2b071..ac84820b 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -118,39 +118,34 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { } resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id - rolling_release = { - advancement_type = "automatic" - stages = [ - { - target_percentage = 30 - // Duration is omitted here for the first stage - }, - { - target_percentage = 70 - duration = 30 // Explicit duration for a middle stage - } - ] - } + advancement_type = "automatic" + stages = [ + { + target_percentage = 30 + // Duration is omitted here for the first stage + }, + { + target_percentage = 70 + duration = 30 // Explicit duration for a middle stage + } + ] } `, nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), - resource.TestCheckResourceAttr(resourceName, "rolling_release.advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "3"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "advancement_type", "automatic"), + resource.TestCheckResourceAttr(resourceName, "stages.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "30", "duration": "60", // Asserting the default value - "require_approval": "false", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "70", "duration": "30", // Asserting the explicit value - "require_approval": "false", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rolling_release.stages.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "100", // Duration for the last stage is expected to be null or not present - "require_approval": "false", }), ), }, From 063a3ba2e8ca7192b4750564e2d80ed2d67a0bb6 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:48:19 -0600 Subject: [PATCH 121/133] separate create from update --- vercel/resource_project_rolling_release.go | 56 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 369ce047..2430325a 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -273,7 +273,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R TeamID: types.StringValue(response.TeamID), } - // If disabled or advancementType is empty and no plan, return empty values + // If disabled or advancementType is empty, check if we have stages to determine if it's configured if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { // If the API response shows disabled or advancementType is empty, but the plan has configuration, use the plan's values if plan != nil && @@ -285,8 +285,58 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R return result, diags } - result.AdvancementType = types.StringNull() - result.Stages = types.ListNull(RollingReleaseStageElementType) + // For import or when no plan is available, check if there are stages in the response + // If there are stages, assume the rolling release is configured and use the response data + if len(response.RollingRelease.Stages) > 0 { + // Try to infer the advancement type from the stages + advancementType := "manual-approval" // Default to manual-approval + for _, stage := range response.RollingRelease.Stages { + if stage.Duration != nil { + advancementType = "automatic" + break + } + } + result.AdvancementType = types.StringValue(advancementType) + + // Convert the stages from the response + var rollingReleaseStages []RollingReleaseStage + for _, stage := range response.RollingRelease.Stages { + // Skip the terminal stage (100%) + if stage.TargetPercentage == 100 { + continue + } + + rollingReleaseStage := RollingReleaseStage{ + TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), + } + + // Add duration if it exists (for automatic advancement type) + if stage.Duration != nil { + rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) + } + + rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) + } + + // Convert to Terraform types + stages := make([]attr.Value, len(rollingReleaseStages)) + for i, stage := range rollingReleaseStages { + stageObj := types.ObjectValueMust( + RollingReleaseStageElementType.AttrTypes, + map[string]attr.Value{ + "target_percentage": stage.TargetPercentage, + "duration": stage.Duration, + }, + ) + stages[i] = stageObj + } + + stagesList := types.ListValueMust(RollingReleaseStageElementType, stages) + result.Stages = stagesList + } else { + result.AdvancementType = types.StringNull() + result.Stages = types.ListNull(RollingReleaseStageElementType) + } return result, diags } From ea5c90404d68c5d516c81b8661b04452b9961743 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:58:22 -0600 Subject: [PATCH 122/133] separate create from update --- vercel/resource_project_rolling_release_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index ac84820b..49f83ec1 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -114,7 +114,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { { Config: cfg(fmt.Sprintf(` resource "vercel_project" "example" { - name = "test-acc-rolling-releases-auto-duration-%s" + name = "test-acc-rr-auto-duration-%s" } resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id From 235eb42cb019cb6cbf1745b327f8d98b6e3f1608 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:14:09 -0600 Subject: [PATCH 123/133] separate create from update --- vercel/resource_project_rolling_release.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 2430325a..5545c3a1 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -188,6 +188,10 @@ func (e *RollingReleaseInfo) toCreateRollingReleaseRequest() (client.CreateRolli duration := int(stage.Duration.ValueInt64()) clientStage.Duration = &duration } + if advancementType == "automatic" && (stage.Duration.IsNull() || stage.Duration.IsUnknown()) { + duration := int(60) + clientStage.Duration = &duration + } stages[i] = clientStage } @@ -239,6 +243,11 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli clientStage.Duration = &duration } + if advancementType == "automatic" && (stage.Duration.IsNull() || stage.Duration.IsUnknown()) { + duration := int(60) + clientStage.Duration = &duration + } + stages[i] = clientStage } From bab496fe8999b30f39e69a5fe04e2ad0671edea7 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:25:33 -0600 Subject: [PATCH 124/133] separate create from update --- vercel/resource_project_rolling_release.go | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 5545c3a1..2731f200 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -406,6 +406,29 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } + // First, check if a rolling release already exists + existingRelease, err := r.client.GetRollingRelease(ctx, plan.ProjectID.ValueString(), plan.TeamID.ValueString()) + if err != nil && !client.NotFound(err) { + resp.Diagnostics.AddError( + "Error checking existing project rolling release", + fmt.Sprintf("Could not check if project rolling release exists, unexpected error: %s", + err, + ), + ) + return + } + + // If a rolling release already exists and is enabled, return an error + if err == nil && existingRelease.RollingRelease.Enabled { + resp.Diagnostics.AddError( + "Project rolling release already exists", + fmt.Sprintf("A rolling release is already configured for project %s. Please use the update operation instead.", + plan.ProjectID.ValueString(), + ), + ) + return + } + // Convert plan to client request request, diags := plan.toCreateRollingReleaseRequest() resp.Diagnostics.Append(diags...) From c4ddfdad6de1064b720377eb9dcaf95b88aba28a Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:48:07 -0600 Subject: [PATCH 125/133] separate create from update --- vercel/data_source_project_rolling_release.go | 5 +---- .../data_source_project_rolling_release_test.go | 4 ++-- vercel/resource_project_rolling_release.go | 12 +----------- vercel/resource_project_rolling_release_test.go | 15 ++++++++++++--- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index f4a80b45..d83ba759 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -168,12 +168,9 @@ func convertResponseToRollingReleaseDataSourceWithConfig(response client.Rolling } result.AdvancementType = types.StringValue(advancementType) - // Map stages (excluding terminal stage) + // Map stages var rollingReleaseStages []RollingReleaseStage for _, stage := range response.RollingRelease.Stages { - if stage.TargetPercentage == 100 { - continue - } rollingReleaseStage := RollingReleaseStage{ TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), } diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 843c3761..258104cc 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -21,14 +21,14 @@ func TestAcc_ProjectRollingReleaseDataSource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccProjectRollingReleaseExists(testClient(t), "vercel_project_rolling_release.example", testTeam(t)), resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "advancement_type", "manual-approval"), - resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "stages.#", "2"), + resource.TestCheckResourceAttr("vercel_project_rolling_release.example", "stages.#", "3"), ), }, { Config: cfg(testAccProjectRollingReleasesDataSourceConfigWithDataSource(nameSuffix)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "advancement_type", "manual-approval"), - resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "stages.#", "2"), + resource.TestCheckResourceAttr("data.vercel_project_rolling_release.example", "stages.#", "3"), ), }, }, diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 2731f200..49db4d9d 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -310,11 +310,6 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // Convert the stages from the response var rollingReleaseStages []RollingReleaseStage for _, stage := range response.RollingRelease.Stages { - // Skip the terminal stage (100%) - if stage.TargetPercentage == 100 { - continue - } - rollingReleaseStage := RollingReleaseStage{ TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), } @@ -352,14 +347,9 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // Set the advancement type result.AdvancementType = types.StringValue(response.RollingRelease.AdvancementType) - // Convert API stages to stages (excluding terminal stage) + // Convert API stages to stages var rollingReleaseStages []RollingReleaseStage for _, stage := range response.RollingRelease.Stages { - // Skip the terminal stage (100%) - if stage.TargetPercentage == 100 { - continue - } - rollingReleaseStage := RollingReleaseStage{ TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), } diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 49f83ec1..89fce764 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -53,13 +53,16 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "advancement_type", "manual-approval"), - resource.TestCheckResourceAttr(resourceName, "stages.#", "2"), + resource.TestCheckResourceAttr(resourceName, "stages.#", "3"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "20", }), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "50", }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ + "target_percentage": "100", + }), ), }, // Now, import the existing resource @@ -77,7 +80,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "advancement_type", "manual-approval"), - resource.TestCheckResourceAttr(resourceName, "stages.#", "3"), + resource.TestCheckResourceAttr(resourceName, "stages.#", "4"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "20", }), @@ -87,6 +90,9 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "80", }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ + "target_percentage": "100", + }), ), }, // Then update to new configuration @@ -96,7 +102,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { resource.TestCheckResourceAttrSet("vercel_project.example", "id"), testAccProjectRollingReleaseExists(testClient(t), resourceName, testTeam(t)), resource.TestCheckResourceAttr(resourceName, "advancement_type", "automatic"), - resource.TestCheckResourceAttr(resourceName, "stages.#", "3"), + resource.TestCheckResourceAttr(resourceName, "stages.#", "4"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ "target_percentage": "20", "duration": "10", @@ -109,6 +115,9 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { "target_percentage": "80", "duration": "10", }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "stages.*", map[string]string{ + "target_percentage": "100", + }), ), }, { From db33f2c80184d92e5c11faf28d8e1378f1d6f897 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:59:29 -0600 Subject: [PATCH 126/133] separate create from update --- vercel/data_source_project_rolling_release.go | 3 +++ vercel/resource_project_rolling_release.go | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index d83ba759..f1b7aec8 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -176,6 +176,9 @@ func convertResponseToRollingReleaseDataSourceWithConfig(response client.Rolling } if stage.Duration != nil { rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) + } else { + // Set duration to null if not present + rollingReleaseStage.Duration = types.Int64Null() } rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 49db4d9d..17f2a296 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -317,6 +317,9 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // Add duration if it exists (for automatic advancement type) if stage.Duration != nil { rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) + } else { + // Set duration to null if not present + rollingReleaseStage.Duration = types.Int64Null() } rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) @@ -357,6 +360,9 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // Add duration if it exists (for automatic advancement type) if stage.Duration != nil { rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) + } else { + // Set duration to null if not present + rollingReleaseStage.Duration = types.Int64Null() } rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) From 0ab2c204284e36415c5e58cd74dbdf427e71c987 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:14:20 -0600 Subject: [PATCH 127/133] separate create from update --- vercel/data_source_project_rolling_release.go | 16 +++++++ vercel/resource_project_rolling_release.go | 46 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index f1b7aec8..fdad0522 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -182,6 +182,22 @@ func convertResponseToRollingReleaseDataSourceWithConfig(response client.Rolling } rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) } + + // Add terminal stage if not present + has100Stage := false + for _, stage := range response.RollingRelease.Stages { + if stage.TargetPercentage == 100 { + has100Stage = true + break + } + } + + if !has100Stage { + rollingReleaseStages = append(rollingReleaseStages, RollingReleaseStage{ + TargetPercentage: types.Int64Value(100), + Duration: types.Int64Null(), + }) + } tflog.Info(ctx, "converted stages", map[string]any{ "rolling_release_stages_count": len(rollingReleaseStages), }) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 17f2a296..fc7c1d8e 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -325,6 +325,22 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) } + // Add terminal stage if not present + has100Stage := false + for _, stage := range response.RollingRelease.Stages { + if stage.TargetPercentage == 100 { + has100Stage = true + break + } + } + + if !has100Stage { + rollingReleaseStages = append(rollingReleaseStages, RollingReleaseStage{ + TargetPercentage: types.Int64Value(100), + Duration: types.Int64Null(), + }) + } + // Convert to Terraform types stages := make([]attr.Value, len(rollingReleaseStages)) for i, stage := range rollingReleaseStages { @@ -368,6 +384,22 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) } + // Add terminal stage if not present + has100Stage := false + for _, stage := range response.RollingRelease.Stages { + if stage.TargetPercentage == 100 { + has100Stage = true + break + } + } + + if !has100Stage { + rollingReleaseStages = append(rollingReleaseStages, RollingReleaseStage{ + TargetPercentage: types.Int64Value(100), + Duration: types.Int64Null(), + }) + } + // Convert to Terraform types stages := make([]attr.Value, len(rollingReleaseStages)) for i, stage := range rollingReleaseStages { @@ -386,8 +418,11 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // Log the conversion result for debugging tflog.Info(ctx, "converted rolling release response", map[string]any{ - "advancement_type": response.RollingRelease.AdvancementType, - "stages_count": len(response.RollingRelease.Stages), + "advancement_type": response.RollingRelease.AdvancementType, + "stages_count": len(response.RollingRelease.Stages), + "api_stages": response.RollingRelease.Stages, + "converted_stages_count": len(rollingReleaseStages), + "converted_stages": rollingReleaseStages, }) return result, diags @@ -432,6 +467,13 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource return } + // Log the request for debugging + tflog.Info(ctx, "creating rolling release", map[string]any{ + "enabled": request.RollingRelease.Enabled, + "advancement_type": request.RollingRelease.AdvancementType, + "stages": request.RollingRelease.Stages, + }) + out, err := r.client.CreateRollingRelease(ctx, request) if err != nil { resp.Diagnostics.AddError( From d1cfe4034f72d93925588dc105e65b68774267d9 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:37:46 -0600 Subject: [PATCH 128/133] separate create from update --- vercel/data_source_project_rolling_release.go | 17 +--- vercel/resource_project_rolling_release.go | 87 +++---------------- 2 files changed, 15 insertions(+), 89 deletions(-) diff --git a/vercel/data_source_project_rolling_release.go b/vercel/data_source_project_rolling_release.go index fdad0522..57023530 100644 --- a/vercel/data_source_project_rolling_release.go +++ b/vercel/data_source_project_rolling_release.go @@ -182,22 +182,7 @@ func convertResponseToRollingReleaseDataSourceWithConfig(response client.Rolling } rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) } - - // Add terminal stage if not present - has100Stage := false - for _, stage := range response.RollingRelease.Stages { - if stage.TargetPercentage == 100 { - has100Stage = true - break - } - } - - if !has100Stage { - rollingReleaseStages = append(rollingReleaseStages, RollingReleaseStage{ - TargetPercentage: types.Int64Value(100), - Duration: types.Int64Null(), - }) - } + // Do NOT add a terminal 100% stage manually! tflog.Info(ctx, "converted stages", map[string]any{ "rolling_release_stages_count": len(rollingReleaseStages), }) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index fc7c1d8e..45908cea 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -196,14 +196,8 @@ func (e *RollingReleaseInfo) toCreateRollingReleaseRequest() (client.CreateRolli stages[i] = clientStage } - // Add terminal stage (100%) without approval - stages = append(stages, client.RollingReleaseStage{ - TargetPercentage: 100, - RequireApproval: false, - }) - // Log the request for debugging - tflog.Info(context.Background(), "converting to update request", map[string]any{ + tflog.Info(context.Background(), "converting to create request", map[string]any{ "advancement_type": advancementType, "stages_count": len(stages), }) @@ -251,11 +245,7 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli stages[i] = clientStage } - // Add terminal stage (100%) without approval - stages = append(stages, client.RollingReleaseStage{ - TargetPercentage: 100, - RequireApproval: false, - }) + // Do NOT add a terminal 100% stage manually! // Log the request for debugging tflog.Info(context.Background(), "converting to update request", map[string]any{ @@ -274,7 +264,7 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli }, diags } -func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *RollingReleaseInfo, ctx context.Context) (RollingReleaseInfo, diag.Diagnostics) { +func ConvertResponseToRollingRelease(response client.RollingReleaseInfo, plan *RollingReleaseInfo, ctx context.Context) (RollingReleaseInfo, diag.Diagnostics) { var diags diag.Diagnostics result := RollingReleaseInfo{ @@ -284,7 +274,6 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // If disabled or advancementType is empty, check if we have stages to determine if it's configured if !response.RollingRelease.Enabled || response.RollingRelease.AdvancementType == "" { - // If the API response shows disabled or advancementType is empty, but the plan has configuration, use the plan's values if plan != nil && !plan.AdvancementType.IsNull() && plan.AdvancementType.ValueString() != "" && !plan.Stages.IsNull() && len(plan.Stages.Elements()) > 0 { @@ -294,11 +283,8 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R return result, diags } - // For import or when no plan is available, check if there are stages in the response - // If there are stages, assume the rolling release is configured and use the response data if len(response.RollingRelease.Stages) > 0 { - // Try to infer the advancement type from the stages - advancementType := "manual-approval" // Default to manual-approval + advancementType := "manual-approval" for _, stage := range response.RollingRelease.Stages { if stage.Duration != nil { advancementType = "automatic" @@ -307,41 +293,19 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R } result.AdvancementType = types.StringValue(advancementType) - // Convert the stages from the response var rollingReleaseStages []RollingReleaseStage for _, stage := range response.RollingRelease.Stages { rollingReleaseStage := RollingReleaseStage{ TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), } - - // Add duration if it exists (for automatic advancement type) if stage.Duration != nil { rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) } else { - // Set duration to null if not present rollingReleaseStage.Duration = types.Int64Null() } - rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) } - - // Add terminal stage if not present - has100Stage := false - for _, stage := range response.RollingRelease.Stages { - if stage.TargetPercentage == 100 { - has100Stage = true - break - } - } - - if !has100Stage { - rollingReleaseStages = append(rollingReleaseStages, RollingReleaseStage{ - TargetPercentage: types.Int64Value(100), - Duration: types.Int64Null(), - }) - } - - // Convert to Terraform types + // Do NOT add a terminal 100% stage manually! stages := make([]attr.Value, len(rollingReleaseStages)) for i, stage := range rollingReleaseStages { stageObj := types.ObjectValueMust( @@ -353,7 +317,6 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R ) stages[i] = stageObj } - stagesList := types.ListValueMust(RollingReleaseStageElementType, stages) result.Stages = stagesList } else { @@ -363,44 +326,21 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R return result, diags } - // Set the advancement type result.AdvancementType = types.StringValue(response.RollingRelease.AdvancementType) - // Convert API stages to stages var rollingReleaseStages []RollingReleaseStage for _, stage := range response.RollingRelease.Stages { rollingReleaseStage := RollingReleaseStage{ TargetPercentage: types.Int64Value(int64(stage.TargetPercentage)), } - - // Add duration if it exists (for automatic advancement type) if stage.Duration != nil { rollingReleaseStage.Duration = types.Int64Value(int64(*stage.Duration)) } else { - // Set duration to null if not present rollingReleaseStage.Duration = types.Int64Null() } - rollingReleaseStages = append(rollingReleaseStages, rollingReleaseStage) } - - // Add terminal stage if not present - has100Stage := false - for _, stage := range response.RollingRelease.Stages { - if stage.TargetPercentage == 100 { - has100Stage = true - break - } - } - - if !has100Stage { - rollingReleaseStages = append(rollingReleaseStages, RollingReleaseStage{ - TargetPercentage: types.Int64Value(100), - Duration: types.Int64Null(), - }) - } - - // Convert to Terraform types + // Do NOT add a terminal 100% stage manually! stages := make([]attr.Value, len(rollingReleaseStages)) for i, stage := range rollingReleaseStages { stageObj := types.ObjectValueMust( @@ -412,10 +352,8 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R ) stages[i] = stageObj } - stagesList := types.ListValueMust(RollingReleaseStageElementType, stages) result.Stages = stagesList - // Log the conversion result for debugging tflog.Info(ctx, "converted rolling release response", map[string]any{ "advancement_type": response.RollingRelease.AdvancementType, @@ -423,8 +361,11 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R "api_stages": response.RollingRelease.Stages, "converted_stages_count": len(rollingReleaseStages), "converted_stages": rollingReleaseStages, + "final_stages_count": len(stages), + "final_stages": stages, + "stages_list_type": fmt.Sprintf("%T", stagesList), + "stages_list_elements": stagesList.Elements(), }) - return result, diags } @@ -486,7 +427,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource } // Convert response to state - result, diags := convertResponseToRollingRelease(out, &plan, ctx) + result, diags := ConvertResponseToRollingRelease(out, &plan, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -537,7 +478,7 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R "stages": out.RollingRelease.Stages, }) - result, diags := convertResponseToRollingRelease(out, &state, ctx) + result, diags := ConvertResponseToRollingRelease(out, &state, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -590,7 +531,7 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource } // Convert response to state - result, diags := convertResponseToRollingRelease(out, &plan, ctx) + result, diags := ConvertResponseToRollingRelease(out, &plan, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -654,7 +595,7 @@ func (r *projectRollingReleaseResource) ImportState(ctx context.Context, req res } // For import, we don't have any state to preserve - result, diags := convertResponseToRollingRelease(out, nil, ctx) + result, diags := ConvertResponseToRollingRelease(out, nil, ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From cae6afddd8b47118b03a77678e293ce64619a2d9 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:11:26 -0600 Subject: [PATCH 129/133] separate create from update --- vercel/resource_project_rolling_release.go | 16 +++- .../resource_project_rolling_release_test.go | 86 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 45908cea..4e10904f 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -165,7 +165,7 @@ type RollingReleaseInfo struct { TeamID types.String `tfsdk:"team_id"` } -func (e *RollingReleaseInfo) toCreateRollingReleaseRequest() (client.CreateRollingReleaseRequest, diag.Diagnostics) { +func (e *RollingReleaseInfo) ToCreateRollingReleaseRequest() (client.CreateRollingReleaseRequest, diag.Diagnostics) { var stages []client.RollingReleaseStage var diags diag.Diagnostics @@ -196,6 +196,12 @@ func (e *RollingReleaseInfo) toCreateRollingReleaseRequest() (client.CreateRolli stages[i] = clientStage } + // Add terminal stage (100%) without approval - API requires this + stages = append(stages, client.RollingReleaseStage{ + TargetPercentage: 100, + RequireApproval: false, + }) + // Log the request for debugging tflog.Info(context.Background(), "converting to create request", map[string]any{ "advancement_type": advancementType, @@ -245,7 +251,11 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli stages[i] = clientStage } - // Do NOT add a terminal 100% stage manually! + // Add terminal stage (100%) without approval - API requires this + stages = append(stages, client.RollingReleaseStage{ + TargetPercentage: 100, + RequireApproval: false, + }) // Log the request for debugging tflog.Info(context.Background(), "converting to update request", map[string]any{ @@ -402,7 +412,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource } // Convert plan to client request - request, diags := plan.toCreateRollingReleaseRequest() + request, diags := plan.ToCreateRollingReleaseRequest() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 89fce764..af4cb29f 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -5,12 +5,98 @@ import ( "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/vercel/terraform-provider-vercel/v3/client" + "github.com/vercel/terraform-provider-vercel/v3/vercel" ) +// TestRollingReleaseRequestConversion tests the request conversion logic +func TestRollingReleaseRequestConversion(t *testing.T) { + // Test manual-approval advancement type + info := vercel.RollingReleaseInfo{ + AdvancementType: types.StringValue("manual-approval"), + ProjectID: types.StringValue("test-project"), + TeamID: types.StringValue("test-team"), + Stages: types.ListValueMust(vercel.RollingReleaseStageElementType, []attr.Value{ + types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ + "target_percentage": types.Int64Value(20), + "duration": types.Int64Null(), + }), + types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ + "target_percentage": types.Int64Value(50), + "duration": types.Int64Null(), + }), + }), + } + + request, diags := info.ToCreateRollingReleaseRequest() + if diags.HasError() { + t.Fatalf("Expected no errors, got: %v", diags) + } + + // Should have 3 stages: 20%, 50%, and 100% + if len(request.RollingRelease.Stages) != 3 { + t.Errorf("Expected 3 stages, got %d", len(request.RollingRelease.Stages)) + } + + // Check that the 100% stage is present + found100 := false + for _, stage := range request.RollingRelease.Stages { + if stage.TargetPercentage == 100 { + found100 = true + if stage.RequireApproval { + t.Error("100% stage should not require approval") + } + break + } + } + if !found100 { + t.Error("100% stage not found in request") + } + + // Test automatic advancement type + info2 := vercel.RollingReleaseInfo{ + AdvancementType: types.StringValue("automatic"), + ProjectID: types.StringValue("test-project"), + TeamID: types.StringValue("test-team"), + Stages: types.ListValueMust(vercel.RollingReleaseStageElementType, []attr.Value{ + types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ + "target_percentage": types.Int64Value(30), + "duration": types.Int64Value(60), + }), + }), + } + + request2, diags2 := info2.ToCreateRollingReleaseRequest() + if diags2.HasError() { + t.Fatalf("Expected no errors, got: %v", diags2) + } + + // Should have 2 stages: 30% and 100% + if len(request2.RollingRelease.Stages) != 2 { + t.Errorf("Expected 2 stages, got %d", len(request2.RollingRelease.Stages)) + } + + // Check that the 100% stage is present + found100_2 := false + for _, stage := range request2.RollingRelease.Stages { + if stage.TargetPercentage == 100 { + found100_2 = true + if stage.RequireApproval { + t.Error("100% stage should not require approval") + } + break + } + } + if !found100_2 { + t.Error("100% stage not found in request") + } +} + func getRollingReleaseImportId(n string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[n] From d882b220436a5c668ae8277af823623cf7cecc54 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:20:26 -0600 Subject: [PATCH 130/133] test case fixes --- ...ata_source_project_rolling_release_test.go | 6 + vercel/resource_project_rolling_release.go | 109 ++++++++++++------ .../resource_project_rolling_release_test.go | 28 ++++- 3 files changed, 105 insertions(+), 38 deletions(-) diff --git a/vercel/data_source_project_rolling_release_test.go b/vercel/data_source_project_rolling_release_test.go index 258104cc..4b22838d 100644 --- a/vercel/data_source_project_rolling_release_test.go +++ b/vercel/data_source_project_rolling_release_test.go @@ -51,6 +51,9 @@ resource "vercel_project_rolling_release" "example" { }, { target_percentage = 50 + }, + { + target_percentage = 100 } ] } @@ -73,6 +76,9 @@ resource "vercel_project_rolling_release" "example" { }, { target_percentage = 50 + }, + { + target_percentage = 100 } ] } diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 4e10904f..b90faf72 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -57,17 +58,17 @@ func (r *projectRollingReleaseResource) Configure(ctx context.Context, req resou type durationValidator struct{} func (v durationValidator) Description(ctx context.Context) string { - return "duration can only be set when advancement_type is 'automatic'" + return "Duration is required when advancement_type is 'automatic'" } func (v durationValidator) MarkdownDescription(ctx context.Context) string { - return "`duration` can only be set when `advancement_type` is `automatic`" + return "Duration is required when advancement_type is 'automatic'" } func (v durationValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { // Get the parent advancement_type value parentPath := req.Path.ParentPath() - advancementTypePath := parentPath.AtName("advancement_type") + advancementTypePath := parentPath.ParentPath().AtName("advancement_type") var advancementType types.String diags := req.Config.GetAttribute(ctx, advancementTypePath, &advancementType) @@ -80,27 +81,86 @@ func (v durationValidator) ValidateObject(ctx context.Context, req validator.Obj if !exists { return } + // Check if target_percentage is set + targetPercentageAttr, exists := req.ConfigValue.Attributes()["target_percentage"] + if !exists { + resp.Diagnostics.AddAttributeError( + req.Path.AtName("target_percentage"), + "Invalid target_percentage configuration", + "target_percentage must be set", + ) + } duration := durationAttr.(types.Int64) + targetPercentage := targetPercentageAttr.(types.Int64) if duration.IsNull() || duration.IsUnknown() { - if advancementType.ValueString() == "manual-approval" { - return + if advancementType.ValueString() == "automatic" && targetPercentage.ValueInt64() != 100 { + resp.Diagnostics.AddAttributeError( + req.Path.AtName("duration"), + "Invalid duration configuration", + "duration must be set when advancement_type is 'automatic'", + ) } + return + } + + // If duration is set but advancement_type is not "automatic", add an error + if advancementType.ValueString() != "automatic" { resp.Diagnostics.AddAttributeError( req.Path.AtName("duration"), "Invalid duration configuration", "duration can only be set when advancement_type is 'automatic'", ) } +} - // If duration is set but advancement_type is not "automatic", add an error - if advancementType.ValueString() != "automatic" { +// terminalStageValidator validates that the last stage has target_percentage = 100 +type terminalStageValidator struct{} + +func (v terminalStageValidator) Description(ctx context.Context) string { + return "The last stage must have target_percentage = 100" +} + +func (v terminalStageValidator) MarkdownDescription(ctx context.Context) string { + return "The last stage must have target_percentage = 100" +} + +func (v terminalStageValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + var stages []RollingReleaseStage + diags := req.ConfigValue.ElementsAs(ctx, &stages, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if len(stages) == 0 { + return + } + + // Check that the last stage has target_percentage = 100 + lastStage := stages[len(stages)-1] + if lastStage.TargetPercentage.ValueInt64() != 100 { resp.Diagnostics.AddAttributeError( - req.Path.AtName("duration"), - "Invalid duration configuration", - "duration must be set when advancement_type is 'automatic'", + path.Root("stages").AtListIndex(len(stages)-1).AtName("target_percentage"), + "Invalid terminal stage", + "The last stage must have target_percentage = 100", ) } + + // Check that no other stage has target_percentage = 100 + for i, stage := range stages[:len(stages)-1] { + if stage.TargetPercentage.ValueInt64() == 100 { + resp.Diagnostics.AddAttributeError( + path.Root("stages").AtListIndex(i).AtName("target_percentage"), + "Invalid stage percentage", + "Only the last stage can have target_percentage = 100", + ) + } + } } // Schema returns the schema information for a project rolling release resource. @@ -125,11 +185,12 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S }, }, "stages": schema.ListNestedAttribute{ - MarkdownDescription: "The stages for the rolling release configuration.", + MarkdownDescription: "The stages for the rolling release configuration. The last stage must have target_percentage = 100.", Required: true, Validators: []validator.List{ listvalidator.SizeAtLeast(1), listvalidator.SizeAtMost(10), + terminalStageValidator{}, }, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ @@ -175,7 +236,7 @@ func (e *RollingReleaseInfo) ToCreateRollingReleaseRequest() (client.CreateRolli var rollingReleaseStages []RollingReleaseStage diags = e.Stages.ElementsAs(context.Background(), &rollingReleaseStages, false) - // Add all stages from config + // Add all stages from config (including the terminal 100% stage) stages = make([]client.RollingReleaseStage, len(rollingReleaseStages)) for i, stage := range rollingReleaseStages { clientStage := client.RollingReleaseStage{ @@ -183,25 +244,14 @@ func (e *RollingReleaseInfo) ToCreateRollingReleaseRequest() (client.CreateRolli RequireApproval: advancementType == "manual-approval", } - // Add duration for automatic advancement type if advancementType == "automatic" && !stage.Duration.IsNull() && !stage.Duration.IsUnknown() { duration := int(stage.Duration.ValueInt64()) clientStage.Duration = &duration } - if advancementType == "automatic" && (stage.Duration.IsNull() || stage.Duration.IsUnknown()) { - duration := int(60) - clientStage.Duration = &duration - } stages[i] = clientStage } - // Add terminal stage (100%) without approval - API requires this - stages = append(stages, client.RollingReleaseStage{ - TargetPercentage: 100, - RequireApproval: false, - }) - // Log the request for debugging tflog.Info(context.Background(), "converting to create request", map[string]any{ "advancement_type": advancementType, @@ -229,7 +279,7 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli var rollingReleaseStages []RollingReleaseStage diags = e.Stages.ElementsAs(context.Background(), &rollingReleaseStages, false) - // Add all stages from config + // Add all stages from config (including the terminal 100% stage) stages = make([]client.RollingReleaseStage, len(rollingReleaseStages)) for i, stage := range rollingReleaseStages { clientStage := client.RollingReleaseStage{ @@ -243,20 +293,9 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli clientStage.Duration = &duration } - if advancementType == "automatic" && (stage.Duration.IsNull() || stage.Duration.IsUnknown()) { - duration := int(60) - clientStage.Duration = &duration - } - stages[i] = clientStage } - // Add terminal stage (100%) without approval - API requires this - stages = append(stages, client.RollingReleaseStage{ - TargetPercentage: 100, - RequireApproval: false, - }) - // Log the request for debugging tflog.Info(context.Background(), "converting to update request", map[string]any{ "advancement_type": advancementType, diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index af4cb29f..091462d5 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -30,6 +30,10 @@ func TestRollingReleaseRequestConversion(t *testing.T) { "target_percentage": types.Int64Value(50), "duration": types.Int64Null(), }), + types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ + "target_percentage": types.Int64Value(100), + "duration": types.Int64Null(), + }), }), } @@ -38,7 +42,7 @@ func TestRollingReleaseRequestConversion(t *testing.T) { t.Fatalf("Expected no errors, got: %v", diags) } - // Should have 3 stages: 20%, 50%, and 100% + // Should have 3 stages: 20%, 50%, and 100% (all user-specified) if len(request.RollingRelease.Stages) != 3 { t.Errorf("Expected 3 stages, got %d", len(request.RollingRelease.Stages)) } @@ -68,6 +72,10 @@ func TestRollingReleaseRequestConversion(t *testing.T) { "target_percentage": types.Int64Value(30), "duration": types.Int64Value(60), }), + types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ + "target_percentage": types.Int64Value(100), + "duration": types.Int64Null(), + }), }), } @@ -76,7 +84,7 @@ func TestRollingReleaseRequestConversion(t *testing.T) { t.Fatalf("Expected no errors, got: %v", diags2) } - // Should have 2 stages: 30% and 100% + // Should have 2 stages: 30% and 100% (all user-specified) if len(request2.RollingRelease.Stages) != 2 { t.Errorf("Expected 2 stages, got %d", len(request2.RollingRelease.Stages)) } @@ -210,6 +218,7 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { Config: cfg(fmt.Sprintf(` resource "vercel_project" "example" { name = "test-acc-rr-auto-duration-%s" + skew_protection = "12 hours" } resource "vercel_project_rolling_release" "example" { project_id = vercel_project.example.id @@ -217,11 +226,15 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { stages = [ { target_percentage = 30 - // Duration is omitted here for the first stage + duration = 60 // Explicit duration for a middle stage }, { target_percentage = 70 duration = 30 // Explicit duration for a middle stage + }, + { + target_percentage = 100 + // Duration for the last stage is expected to be null or not present } ] } @@ -264,6 +277,9 @@ resource "vercel_project_rolling_release" "example" { }, { target_percentage = 50 + }, + { + target_percentage = 100 } ] } @@ -289,6 +305,9 @@ resource "vercel_project_rolling_release" "example" { }, { target_percentage = 80 + }, + { + target_percentage = 100 } ] } @@ -316,6 +335,9 @@ resource "vercel_project_rolling_release" "example" { { target_percentage = 80 duration = 10 + }, + { + target_percentage = 100 } ] } From 0a08f30ecf9223447cb029529a2e9189efdd525f Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:21:49 -0600 Subject: [PATCH 131/133] update docs --- docs/resources/project_rolling_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/project_rolling_release.md b/docs/resources/project_rolling_release.md index 33e7ef17..e1d7bc39 100644 --- a/docs/resources/project_rolling_release.md +++ b/docs/resources/project_rolling_release.md @@ -39,7 +39,7 @@ resource "vercel_project_rolling_release" "example" { - `advancement_type` (String) The type of advancement for the rolling release. Must be either 'automatic' or 'manual-approval'. - `project_id` (String) The ID of the project. -- `stages` (Attributes List) The stages for the rolling release configuration. (see [below for nested schema](#nestedatt--stages)) +- `stages` (Attributes List) The stages for the rolling release configuration. The last stage must have target_percentage = 100. (see [below for nested schema](#nestedatt--stages)) ### Optional From a595d146c279789ffd6352e944599ebf5c3e2405 Mon Sep 17 00:00:00 2001 From: Brooke Mosby <25040341+brookemosby@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:36:11 -0600 Subject: [PATCH 132/133] update test --- .../resource_project_rolling_release_test.go | 94 ------------------- 1 file changed, 94 deletions(-) diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 091462d5..916da779 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -5,106 +5,12 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/vercel/terraform-provider-vercel/v3/client" - "github.com/vercel/terraform-provider-vercel/v3/vercel" ) -// TestRollingReleaseRequestConversion tests the request conversion logic -func TestRollingReleaseRequestConversion(t *testing.T) { - // Test manual-approval advancement type - info := vercel.RollingReleaseInfo{ - AdvancementType: types.StringValue("manual-approval"), - ProjectID: types.StringValue("test-project"), - TeamID: types.StringValue("test-team"), - Stages: types.ListValueMust(vercel.RollingReleaseStageElementType, []attr.Value{ - types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ - "target_percentage": types.Int64Value(20), - "duration": types.Int64Null(), - }), - types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ - "target_percentage": types.Int64Value(50), - "duration": types.Int64Null(), - }), - types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ - "target_percentage": types.Int64Value(100), - "duration": types.Int64Null(), - }), - }), - } - - request, diags := info.ToCreateRollingReleaseRequest() - if diags.HasError() { - t.Fatalf("Expected no errors, got: %v", diags) - } - - // Should have 3 stages: 20%, 50%, and 100% (all user-specified) - if len(request.RollingRelease.Stages) != 3 { - t.Errorf("Expected 3 stages, got %d", len(request.RollingRelease.Stages)) - } - - // Check that the 100% stage is present - found100 := false - for _, stage := range request.RollingRelease.Stages { - if stage.TargetPercentage == 100 { - found100 = true - if stage.RequireApproval { - t.Error("100% stage should not require approval") - } - break - } - } - if !found100 { - t.Error("100% stage not found in request") - } - - // Test automatic advancement type - info2 := vercel.RollingReleaseInfo{ - AdvancementType: types.StringValue("automatic"), - ProjectID: types.StringValue("test-project"), - TeamID: types.StringValue("test-team"), - Stages: types.ListValueMust(vercel.RollingReleaseStageElementType, []attr.Value{ - types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ - "target_percentage": types.Int64Value(30), - "duration": types.Int64Value(60), - }), - types.ObjectValueMust(vercel.RollingReleaseStageElementType.AttrTypes, map[string]attr.Value{ - "target_percentage": types.Int64Value(100), - "duration": types.Int64Null(), - }), - }), - } - - request2, diags2 := info2.ToCreateRollingReleaseRequest() - if diags2.HasError() { - t.Fatalf("Expected no errors, got: %v", diags2) - } - - // Should have 2 stages: 30% and 100% (all user-specified) - if len(request2.RollingRelease.Stages) != 2 { - t.Errorf("Expected 2 stages, got %d", len(request2.RollingRelease.Stages)) - } - - // Check that the 100% stage is present - found100_2 := false - for _, stage := range request2.RollingRelease.Stages { - if stage.TargetPercentage == 100 { - found100_2 = true - if stage.RequireApproval { - t.Error("100% stage should not require approval") - } - break - } - } - if !found100_2 { - t.Error("100% stage not found in request") - } -} - func getRollingReleaseImportId(n string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[n] From 649a8a6b07c76171e880a82efc697068399381c1 Mon Sep 17 00:00:00 2001 From: Brooke <25040341+brookemosby@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:30:16 -0600 Subject: [PATCH 133/133] Update vercel/resource_project_rolling_release.go Co-authored-by: jeffrey --- vercel/resource_project_rolling_release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index b90faf72..35fa948d 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -443,7 +443,7 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource if err == nil && existingRelease.RollingRelease.Enabled { resp.Diagnostics.AddError( "Project rolling release already exists", - fmt.Sprintf("A rolling release is already configured for project %s. Please use the update operation instead.", + fmt.Sprintf("A rolling release is already configured for project %s. To change an existing configuration, you must import the rolling release or remove the existing configuration.", plan.ProjectID.ValueString(), ), )