From 172981e06a25d18acefb38671a143dad860e2202 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 27 Mar 2024 11:35:59 +0800 Subject: [PATCH 1/3] Migrate tfe_workspace_run_task to plugin model This commit migrates the tfe_workspace_run_task resource to the newer plugin model. It uses a schema v0 as there is no difference in schema. Later changes will migrate the other data source objects. --- internal/provider/provider.go | 1 - internal/provider/provider_next.go | 5 +- .../resource_tfe_workspace_run_task.go | 334 +++++++++++------- 3 files changed, 214 insertions(+), 126 deletions(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 550d1c7c1..aee1c4ae4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -143,7 +143,6 @@ func Provider() *schema.Provider { "tfe_team_token": resourceTFETeamToken(), "tfe_terraform_version": resourceTFETerraformVersion(), "tfe_workspace": resourceTFEWorkspace(), - "tfe_workspace_run_task": resourceTFEWorkspaceRunTask(), "tfe_variable_set": resourceTFEVariableSet(), "tfe_workspace_policy_set": resourceTFEWorkspacePolicySet(), "tfe_workspace_policy_set_exclusion": resourceTFEWorkspacePolicySetExclusion(), diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index f558b420d..9cca9d601 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -134,11 +134,12 @@ func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ + NewOrganizationRunTaskResource, NewRegistryGPGKeyResource, NewRegistryProviderResource, NewResourceVariable, - NewSAMLSettingsResource, NewResourceWorkspaceSettings, - NewOrganizationRunTaskResource, + NewSAMLSettingsResource, + NewWorkspaceRunTaskResource, } } diff --git a/internal/provider/resource_tfe_workspace_run_task.go b/internal/provider/resource_tfe_workspace_run_task.go index 227a9a848..58c39a651 100644 --- a/internal/provider/resource_tfe_workspace_run_task.go +++ b/internal/provider/resource_tfe_workspace_run_task.go @@ -1,22 +1,25 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// NOTE: This is a legacy resource and should be migrated to the Plugin -// Framework if substantial modifications are planned. See -// docs/new-resources.md if planning to use this code as boilerplate for -// a new resource. - package provider import ( "context" + "errors" "fmt" - "log" "strings" tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" ) func workspaceRunTaskEnforcementLevels() []string { @@ -51,186 +54,271 @@ func sentenceList(items []string, prefix string, suffix string, conjunction stri return b.String() } -func resourceTFEWorkspaceRunTask() *schema.Resource { - return &schema.Resource{ - Create: resourceTFEWorkspaceRunTaskCreate, - Read: resourceTFEWorkspaceRunTaskRead, - Delete: resourceTFEWorkspaceRunTaskDelete, - Update: resourceTFEWorkspaceRunTaskUpdate, - Importer: &schema.ResourceImporter{ - StateContext: resourceTFEWorkspaceRunTaskImporter, - }, +type resourceWorkspaceRunTask struct { + config ConfiguredClient +} + +var _ resource.Resource = &resourceWorkspaceRunTask{} +var _ resource.ResourceWithConfigure = &resourceWorkspaceRunTask{} +var _ resource.ResourceWithImportState = &resourceWorkspaceRunTask{} + +func NewWorkspaceRunTaskResource() resource.Resource { + return &resourceWorkspaceRunTask{} +} + +type modelTFEWorkspaceRunTaskV0 struct { + ID types.String `tfsdk:"id"` + WorkspaceID types.String `tfsdk:"workspace_id"` + TaskID types.String `tfsdk:"task_id"` + EnforcementLevel types.String `tfsdk:"enforcement_level"` + Stage types.String `tfsdk:"stage"` +} + +func modelFromTFEWorkspaceRunTask(v *tfe.WorkspaceRunTask) modelTFEWorkspaceRunTaskV0 { + return modelTFEWorkspaceRunTaskV0{ + ID: types.StringValue(v.ID), + WorkspaceID: types.StringValue(v.Workspace.ID), + TaskID: types.StringValue(v.RunTask.ID), + EnforcementLevel: types.StringValue(string(v.EnforcementLevel)), + Stage: types.StringValue(string(v.Stage)), + } +} + +func (r *resourceWorkspaceRunTask) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_workspace_run_task" +} + +// Configure implements resource.ResourceWithConfigure +func (r *resourceWorkspaceRunTask) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } - Schema: map[string]*schema.Schema{ - "workspace_id": { + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected resource Configure type", + fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData), + ) + } + r.config = client +} + +func (r *resourceWorkspaceRunTask) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 0, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Service-generated identifier for the workspace task", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "workspace_id": schema.StringAttribute{ Description: "The id of the workspace to associate the Run task to.", - Type: schema.TypeString, - ForceNew: true, Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - - "task_id": { + "task_id": schema.StringAttribute{ Description: "The id of the Run task to associate to the Workspace.", - - Type: schema.TypeString, - ForceNew: true, - Required: true, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - - "enforcement_level": { + "enforcement_level": schema.StringAttribute{ Description: fmt.Sprintf("The enforcement level of the task. Valid values are %s.", sentenceList( workspaceRunTaskEnforcementLevels(), "`", "`", "and", )), - Type: schema.TypeString, Required: true, - ValidateFunc: validation.StringInSlice( - workspaceRunTaskEnforcementLevels(), - false, - ), + Validators: []validator.String{ + stringvalidator.OneOf(workspaceRunTaskEnforcementLevels()...), + }, }, - - "stage": { + "stage": schema.StringAttribute{ Description: fmt.Sprintf("The stage to run the task in. Valid values are %s.", sentenceList( workspaceRunTaskStages(), "`", "`", "and", )), - Type: schema.TypeString, Optional: true, - Default: tfe.PostPlan, - ValidateFunc: validation.StringInSlice( - workspaceRunTaskStages(), - false, - ), + Computed: true, + Default: stringdefault.StaticString(string(tfe.PostPlan)), + Validators: []validator.String{ + stringvalidator.OneOf(workspaceRunTaskStages()...), + }, }, }, } } -func resourceTFEWorkspaceRunTaskCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) +func (r *resourceWorkspaceRunTask) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state modelTFEWorkspaceRunTaskV0 + + // Read Terraform current state into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } - workspaceID := d.Get("workspace_id").(string) - taskID := d.Get("task_id").(string) + wstaskID := state.ID.ValueString() + workspaceID := state.WorkspaceID.ValueString() - task, err := config.Client.RunTasks.Read(ctx, taskID) + tflog.Debug(ctx, "Reading workspace run task") + wstask, err := r.config.Client.WorkspaceRunTasks.Read(ctx, workspaceID, wstaskID) if err != nil { - return fmt.Errorf( - "Error retrieving task %s: %w", taskID, err) + resp.Diagnostics.AddError("Error reading Workspace Run Task", "Could not read Workspace Run Task, unexpected error: "+err.Error()) + return } - ws, err := config.Client.Workspaces.ReadByID(ctx, workspaceID) + result := modelFromTFEWorkspaceRunTask(wstask) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +func (r *resourceWorkspaceRunTask) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan modelTFEWorkspaceRunTaskV0 + + // Read Terraform planned changes into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + taskID := plan.TaskID.ValueString() + task, err := r.config.Client.RunTasks.Read(ctx, taskID) if err != nil { - return fmt.Errorf( - "Error retrieving workspace %s: %w", workspaceID, err) + resp.Diagnostics.AddError("Error retrieving task", "Could not read Organization Run Task "+taskID+", unexpected error: "+err.Error()) + return + } + + workspaceID := plan.WorkspaceID.ValueString() + if _, err := r.config.Client.Workspaces.ReadByID(ctx, workspaceID); err != nil { + resp.Diagnostics.AddError("Error retrieving workspace", "Could not read Workspace "+workspaceID+", unexpected error: "+err.Error()) + return } - stage := tfe.Stage(d.Get("stage").(string)) + + stage := tfe.Stage(plan.Stage.ValueString()) + level := tfe.TaskEnforcementLevel(plan.EnforcementLevel.ValueString()) options := tfe.WorkspaceRunTaskCreateOptions{ RunTask: task, - EnforcementLevel: tfe.TaskEnforcementLevel(d.Get("enforcement_level").(string)), + EnforcementLevel: level, Stage: &stage, } - log.Printf("[DEBUG] Create task %s in workspace %s", task.ID, ws.ID) - wstask, err := config.Client.WorkspaceRunTasks.Create(ctx, ws.ID, options) + tflog.Debug(ctx, fmt.Sprintf("Create task %s in workspace: %s", taskID, workspaceID)) + wstask, err := r.config.Client.WorkspaceRunTasks.Create(ctx, workspaceID, options) if err != nil { - return fmt.Errorf("Error creating task %s in workspace %s: %w", task.ID, ws.ID, err) + resp.Diagnostics.AddError("Unable to create workspace task", err.Error()) + return } - d.SetId(wstask.ID) + result := modelFromTFEWorkspaceRunTask(wstask) - return resourceTFEWorkspaceRunTaskRead(d, meta) + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) } -func resourceTFEWorkspaceRunTaskDelete(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) - - // Get the workspace - workspaceID := d.Get("workspace_id").(string) - - log.Printf("[DEBUG] Delete task %s in workspace %s", d.Id(), workspaceID) - err := config.Client.WorkspaceRunTasks.Delete(ctx, workspaceID, d.Id()) - if err != nil && !isErrResourceNotFound(err) { - return fmt.Errorf("Error deleting task %s in workspace %s: %w", d.Id(), workspaceID, err) +func (r *resourceWorkspaceRunTask) stringPointerToStagePointer(val *string) *tfe.Stage { + if val == nil { + return nil } - - return nil + newVal := tfe.Stage(*val) + return &newVal } -func resourceTFEWorkspaceRunTaskUpdate(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) - - // Get the workspace - workspaceID := d.Get("workspace_id").(string) - - // Setup the options struct - options := tfe.WorkspaceRunTaskUpdateOptions{} - if d.HasChange("enforcement_level") { - options.EnforcementLevel = tfe.TaskEnforcementLevel(d.Get("enforcement_level").(string)) - } - if d.HasChange("stage") { - stage := tfe.Stage(d.Get("stage").(string)) - options.Stage = &stage - } +func (r *resourceWorkspaceRunTask) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFEWorkspaceRunTaskV0 - log.Printf("[DEBUG] Update configuration of task %s in workspace %s", d.Id(), workspaceID) - _, err := config.Client.WorkspaceRunTasks.Update(ctx, workspaceID, d.Id(), options) - if err != nil { - return fmt.Errorf("Error updating task %s in workspace %s: %w", d.Id(), workspaceID, err) + // Read Terraform planned changes into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - return nil -} + level := tfe.TaskEnforcementLevel(plan.EnforcementLevel.ValueString()) + stage := r.stringPointerToStagePointer(plan.Stage.ValueStringPointer()) -func resourceTFEWorkspaceRunTaskRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) + options := tfe.WorkspaceRunTaskUpdateOptions{ + EnforcementLevel: level, + Stage: stage, + } - // Get the workspace - workspaceID := d.Get("workspace_id").(string) + wstaskID := plan.ID.ValueString() + workspaceID := plan.WorkspaceID.ValueString() - wstask, err := config.Client.WorkspaceRunTasks.Read(ctx, workspaceID, d.Id()) + tflog.Debug(ctx, fmt.Sprintf("Update task %s in workspace %s", wstaskID, workspaceID)) + wstask, err := r.config.Client.WorkspaceRunTasks.Update(ctx, workspaceID, wstaskID, options) if err != nil { - if isErrResourceNotFound(err) { - log.Printf("[DEBUG] Workspace Task %s does not exist in workspace %s", d.Id(), workspaceID) - d.SetId("") - return nil - } - return fmt.Errorf("Error reading configuration of task %s in workspace %s: %w", d.Id(), workspaceID, err) + resp.Diagnostics.AddError("Unable to update workspace task", err.Error()) + return } - // Update the config. - d.Set("workspace_id", wstask.Workspace.ID) - d.Set("task_id", wstask.RunTask.ID) - d.Set("enforcement_level", string(wstask.EnforcementLevel)) - d.Set("stage", string(wstask.Stage)) + result := modelFromTFEWorkspaceRunTask(wstask) - return nil + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) } -func resourceTFEWorkspaceRunTaskImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - config := meta.(ConfiguredClient) +func (r *resourceWorkspaceRunTask) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFEWorkspaceRunTaskV0 + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + wstaskID := state.ID.ValueString() + workspaceID := state.WorkspaceID.ValueString() - s := strings.Split(d.Id(), "/") - if len(s) != 3 { - return nil, fmt.Errorf( - "invalid task input format: %s (expected //)", - d.Id(), + tflog.Debug(ctx, fmt.Sprintf("Delete task %s in workspace %s", wstaskID, workspaceID)) + err := r.config.Client.WorkspaceRunTasks.Delete(ctx, workspaceID, wstaskID) + // Ignore 404s for delete + if err != nil && !errors.Is(err, tfe.ErrResourceNotFound) { + resp.Diagnostics.AddError( + "Error deleting workspace run task", + fmt.Sprintf("Couldn't delete task %s in workspace %s: %s", wstaskID, workspaceID, err.Error()), ) } + // Resource is implicitly deleted from resp.State if diagnostics have no errors. +} - wstask, err := fetchWorkspaceRunTask(s[2], s[1], s[0], config.Client) - if err != nil { - return nil, err +func (r *resourceWorkspaceRunTask) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + s := strings.SplitN(req.ID, "/", 3) + if len(s) != 3 { + resp.Diagnostics.AddError( + "Error importing workspace run task", + fmt.Sprintf("Invalid task input format: %s (expected //)", req.ID), + ) + return } - d.Set("workspace_id", wstask.Workspace.ID) - d.Set("task_id", wstask.RunTask.ID) - d.SetId(wstask.ID) + taskName := s[2] + workspaceName := s[1] + orgName := s[0] - return []*schema.ResourceData{d}, nil + if wstask, err := fetchWorkspaceRunTask(taskName, workspaceName, orgName, r.config.Client); err != nil { + resp.Diagnostics.AddError( + "Error importing workspace run task", + err.Error(), + ) + } else if wstask == nil { + resp.Diagnostics.AddError( + "Error importing workspace run task", + "Workspace task does not exist or has no details", + ) + } else { + result := modelFromTFEWorkspaceRunTask(wstask) + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) + } } From 0121b6cc54d050f140a5575290f8759cf31672c9 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 27 Mar 2024 11:36:34 +0800 Subject: [PATCH 2/3] Add acceptance tests for tfe_workspace_run_tasks Previously there were no tests to assert that the validated attributes actually worked. --- .../resource_tfe_workspace_run_task_test.go | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/provider/resource_tfe_workspace_run_task_test.go b/internal/provider/resource_tfe_workspace_run_task_test.go index e8431e4d2..2456b3d5b 100644 --- a/internal/provider/resource_tfe_workspace_run_task_test.go +++ b/internal/provider/resource_tfe_workspace_run_task_test.go @@ -5,6 +5,7 @@ package provider import ( "fmt" + "regexp" "testing" tfe "github.com/hashicorp/go-tfe" @@ -12,6 +13,23 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) +func TestAccTFEWorkspaceRunTask_validateSchemaAttributes(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspaceRunTask_attributes("bad_level", string(tfe.PostPlan)), + ExpectError: regexp.MustCompile(`enforcement_level value must be one of:`), + }, + { + Config: testAccTFEWorkspaceRunTask_attributes(string(tfe.Advisory), "bad_stage"), + ExpectError: regexp.MustCompile(`stage value must be one of:`), + }, + }, + }) +} + func TestAccTFEWorkspaceRunTask_create(t *testing.T) { skipUnlessRunTasksDefined(t) @@ -134,6 +152,17 @@ func testAccCheckTFEWorkspaceRunTaskDestroy(s *terraform.State) error { return nil } +func testAccTFEWorkspaceRunTask_attributes(enforcementLevel, stage string) string { + return fmt.Sprintf(` +resource "tfe_workspace_run_task" "foobar" { + workspace_id = "ws-abc123" + task_id = "task-abc123" + enforcement_level = "%s" + stage = "%s" +} +`, enforcementLevel, stage) +} + func testAccTFEWorkspaceRunTask_basic(orgName, runTaskURL string) string { return fmt.Sprintf(` locals { From 85c510dbda5f0263dd1e7209986c10beb97c64b7 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 27 Mar 2024 11:42:31 +0800 Subject: [PATCH 3/3] Remove unused function --- internal/provider/provider.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index aee1c4ae4..6713a06f8 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -191,9 +191,3 @@ var descriptions = map[string]string{ "organization": "The organization to apply to a resource if one is not defined on\n" + "the resource itself", } - -// A commonly used helper method to check if the error -// returned was tfe.ErrResourceNotFound -func isErrResourceNotFound(err error) bool { - return errors.Is(err, tfe.ErrResourceNotFound) -}