diff --git a/client/project_rolling_release.go b/client/project_rolling_release.go index d9fd04cc..c18e4264 100644 --- a/client/project_rolling_release.go +++ b/client/project_rolling_release.go @@ -14,25 +14,25 @@ type RollingReleaseStage struct { RequireApproval bool `json:"requireApproval,omitempty"` // Only in response for manual-approval type } -// RollingRelease represents the rolling release configuration -type RollingRelease struct { +// RollingReleaseInfo represents the rolling release configuration +type RollingReleaseInfo 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 } -type RollingReleaseInfo struct { - RollingRelease RollingRelease `json:"rollingRelease"` - ProjectID string `json:"projectId"` - TeamID string `json:"teamId"` +type RollingReleaseResponse struct { + RollingRelease RollingReleaseInfo `json:"rollingRelease"` + ProjectID string `json:"-"` // Not in API response + TeamID string `json:"-"` // Not in API response } // GetRollingRelease returns the rolling release for a given project. -func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (RollingReleaseInfo, error) { +func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string) (RollingReleaseResponse, error) { teamId := c.TeamID(teamID) url := fmt.Sprintf("%s/v1/projects/%s/rolling-release/config?teamId=%s", c.baseURL, projectID, teamId) - var d RollingReleaseInfo + var d RollingReleaseResponse err := c.doRequest(clientRequest{ ctx: ctx, method: "GET", @@ -40,7 +40,7 @@ func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string }, &d) if err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error getting rolling-release: %w", err) + return RollingReleaseResponse{}, fmt.Errorf("error getting rolling-release: %w", err) } d.ProjectID = projectID @@ -49,16 +49,15 @@ func (c *Client) GetRollingRelease(ctx context.Context, projectID, teamID string return d, nil } -// UpdateRollingReleaseRequest defines the information that needs to be passed to Vercel in order to -// update a rolling release. +// 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"` - ProjectID string `json:"projectId"` - TeamID string `json:"teamId"` + RollingRelease RollingReleaseInfo `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) { +func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRollingReleaseRequest) (RollingReleaseResponse, error) { request.TeamID = c.TeamID(request.TeamID) if request.RollingRelease.Enabled { enableRequest := map[string]any{ @@ -67,7 +66,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling "stages": request.RollingRelease.Stages, } - var result RollingReleaseInfo + var result RollingReleaseResponse err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", @@ -75,7 +74,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling body: string(mustMarshal(enableRequest)), }, &result) if err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error enabling rolling release: %w", err) + return RollingReleaseResponse{}, fmt.Errorf("error enabling rolling release: %w", err) } result.ProjectID = request.ProjectID @@ -88,14 +87,14 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling } else { // For disabling, just send the request as is disabledRequest := UpdateRollingReleaseRequest{ - RollingRelease: RollingRelease{ + RollingRelease: RollingReleaseInfo{ Enabled: false, AdvancementType: "", Stages: []RollingReleaseStage{}, }, } - var result RollingReleaseInfo + var result RollingReleaseResponse err := c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", @@ -103,7 +102,7 @@ func (c *Client) UpdateRollingRelease(ctx context.Context, request UpdateRolling body: string(mustMarshal(disabledRequest.RollingRelease)), }, &result) if err != nil { - return RollingReleaseInfo{}, fmt.Errorf("error disabling rolling release: %w", err) + return RollingReleaseResponse{}, fmt.Errorf("error disabling rolling release: %w", err) } result.ProjectID = request.ProjectID diff --git a/vercel/resource_project_rolling_release.go b/vercel/resource_project_rolling_release.go index 704c4366..6419f3c1 100644 --- a/vercel/resource_project_rolling_release.go +++ b/vercel/resource_project_rolling_release.go @@ -72,22 +72,16 @@ func (r *projectRollingReleaseResource) Schema(ctx context.Context, _ resource.S 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, + MarkdownDescription: "The type of advancement between stages. Must be either 'automatic' or 'manual-approval'.", + Required: 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 of the rolling release.", + Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target_percentage": schema.Int64Attribute{ @@ -125,7 +119,6 @@ type RollingReleaseStage 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"` } @@ -142,7 +135,6 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli var advancementType string var diags diag.Diagnostics - if e.RollingRelease.Enabled.ValueBool() { advancementType = e.RollingRelease.AdvancementType.ValueString() // Convert stages from types.List to []client.RollingReleaseStage @@ -189,22 +181,16 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli } } } - } else { - // When disabled, don't send any stages or advancement type to the API - stages = []client.RollingReleaseStage{} - advancementType = "" } // 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(), + RollingRelease: client.RollingReleaseInfo{ AdvancementType: advancementType, Stages: stages, }, @@ -213,15 +199,11 @@ func (e *RollingReleaseInfo) toUpdateRollingReleaseRequest() (client.UpdateRolli }, diags } -func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *RollingReleaseInfo, ctx context.Context) (RollingReleaseInfo, diag.Diagnostics) { +func convertResponseToRollingRelease(response client.RollingReleaseResponse, 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), @@ -229,7 +211,6 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R } // 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{ @@ -245,7 +226,6 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R } 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 @@ -289,7 +269,7 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // If duration is not set in plan, handle based on advancement type if duration.IsNull() { - if plan.RollingRelease.AdvancementType.ValueString() == "automatic" { + if plan != nil && 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 { @@ -338,7 +318,6 @@ func convertResponseToRollingRelease(response client.RollingReleaseInfo, plan *R // 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), }) @@ -364,17 +343,13 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource // 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, }) - // 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, + RollingRelease: client.RollingReleaseInfo{ AdvancementType: "", Stages: []client.RollingReleaseStage{}, }, @@ -392,7 +367,6 @@ func (r *projectRollingReleaseResource) Create(ctx context.Context, req resource ) return } - } out, err := r.client.UpdateRollingRelease(ctx, request) if err != nil { @@ -414,7 +388,6 @@ 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, }) @@ -454,7 +427,6 @@ func (r *projectRollingReleaseResource) Read(ctx context.Context, req resource.R // 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, }) @@ -467,7 +439,6 @@ 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, }) @@ -504,7 +475,6 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource // 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, }) @@ -512,7 +482,7 @@ func (r *projectRollingReleaseResource) Update(ctx context.Context, req resource // If we're transitioning from enabled to disabled, first disable if state.RollingRelease.Enabled.ValueBool() && !request.RollingRelease.Enabled { disabledRequest := client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ + RollingRelease: client.RollingReleaseInfo{ Enabled: false, AdvancementType: "", Stages: []client.RollingReleaseStage{}, @@ -536,7 +506,7 @@ 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{ + RollingRelease: client.RollingReleaseInfo{ Enabled: false, AdvancementType: "", Stages: []client.RollingReleaseStage{}, @@ -577,7 +547,6 @@ 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, }) @@ -600,7 +569,7 @@ func (r *projectRollingReleaseResource) Delete(ctx context.Context, req resource // Disable rolling release request := client.UpdateRollingReleaseRequest{ - RollingRelease: client.RollingRelease{ + RollingRelease: client.RollingReleaseInfo{ Enabled: false, AdvancementType: "", Stages: []client.RollingReleaseStage{}, diff --git a/vercel/resource_project_rolling_release_test.go b/vercel/resource_project_rolling_release_test.go index 81dd9557..8d87acc4 100644 --- a/vercel/resource_project_rolling_release_test.go +++ b/vercel/resource_project_rolling_release_test.go @@ -27,6 +27,17 @@ func testAccProjectRollingReleaseExists(testClient *client.Client, n, teamID str } } +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 TestAcc_ProjectRollingRelease(t *testing.T) { resourceName := "vercel_project_rolling_release.example" nameSuffix := acctest.RandString(16) @@ -58,6 +69,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)), @@ -126,6 +145,56 @@ func TestAcc_ProjectRollingRelease(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "rolling_release.stages.#", "0"), ), }, + { + 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 = { + enabled = true + 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 + }, + { + target_percentage = 100 + // Duration is omitted for the last stage (as it should be) + } + ] + } +} +`, nameSuffix)), + 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{ + "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", + }), + ), + }, }, }) }