From 9543098574ac3ea22a0dc7ea3f6982319ee2cd57 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 30 Nov 2023 12:44:42 -0700 Subject: [PATCH 1/8] Adds tfe_workspace_settings resource This resource is added for two reasons: to break the circular dependency between tfe_workspace agent_pool_id and tfe_agent_pool_allowed_workspaces, as well as create a resource symmetry between tfe_organization_default_execution_mode (will be renamed to tfe_organization_default_settings in a followup commit) --- internal/provider/provider_next.go | 11 + .../resource_tfe_workspace_settings.go | 410 ++++++++++++++++++ .../resource_tfe_workspace_settings_test.go | 241 ++++++++++ internal/provider/testing.go | 22 +- 4 files changed, 675 insertions(+), 9 deletions(-) create mode 100644 internal/provider/resource_tfe_workspace_settings.go create mode 100644 internal/provider/resource_tfe_workspace_settings_test.go diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index a8b964b4d..014bb0c4e 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -5,7 +5,9 @@ package provider import ( "context" + "fmt" "os" + "regexp" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -23,6 +25,14 @@ type frameworkProvider struct{} // Compile-time interface check var _ provider.Provider = &frameworkProvider{} +// Can be used to construct ID regexp patterns +var base58Alphabet = "[1-9A-HJ-NP-Za-km-z]" + +// IDPattern constructs a regexp pattern for Terraform Cloud with the given prefix +func IDPattern(prefix string) *regexp.Regexp { + return regexp.MustCompile(fmt.Sprintf("^%s-%s{16}$", prefix, base58Alphabet)) +} + // FrameworkProviderConfig is a helper type for extracting the provider // configuration from the provider block. type FrameworkProviderConfig struct { @@ -121,5 +131,6 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res return []func() resource.Resource{ NewResourceVariable, NewSAMLSettingsResource, + NewResourceWorkspaceSettings, } } diff --git a/internal/provider/resource_tfe_workspace_settings.go b/internal/provider/resource_tfe_workspace_settings.go new file mode 100644 index 000000000..118a19d29 --- /dev/null +++ b/internal/provider/resource_tfe_workspace_settings.go @@ -0,0 +1,410 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "strings" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// tfe_workspace_settings resource +var _ resource.Resource = &workspaceSettings{} + +// overwritesElementType is the object type definition for the +// overwrites field schema. +var overwritesElementType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "execution_mode": types.BoolType, + "agent_pool": types.BoolType, + }, +} + +type workspaceSettings struct { + config ConfiguredClient +} + +type modelWorkspaceSettings struct { + ID types.String `tfsdk:"id"` + WorkspaceID types.String `tfsdk:"workspace_id"` + ExecutionMode types.String `tfsdk:"execution_mode"` + AgentPoolID types.String `tfsdk:"agent_pool_id"` + Overwrites types.List `tfsdk:"overwrites"` +} + +type modelOverwrites struct { + ExecutionMode types.Bool `tfsdk:"execution_mode"` + AgentPool types.Bool `tfsdk:"agent_pool"` +} + +// errWorkspaceNoLongerExists is returned when reading the workspace settings but +// the workspace no longer exists. +var errWorkspaceNoLongerExists = errors.New("workspace no longer exists") + +// validateAgentExecutionMode is a PlanModifier that validates that the combination +// of "execution_mode" and "agent_pool_id" is compatible. +type validateAgentExecutionMode struct{} + +// revertOverwritesIfExecutionModeUnset is a PlanModifier for "overwrites" that +// sets the values to false if execution_mode is unset. This tells the server to +// compute execution_mode and agent_pool_id if defaults are set. This +// modifier must be used in conjunction with unknownIfExecutionModeUnset plan +// modifier on the execution_mode and agent_pool_id fields. +type revertOverwritesIfExecutionModeUnset struct{} + +// unknownIfExecutionModeUnset sets the planned value to (known after apply) if +// execution_mode is unset, avoiding an inconsistent state after the apply. This +// allows the server to compute the new value based on the default. It should be +// applied to both execution_mode and agent_pool_id in conjunction with +// revertOverwritesIfExecutionModeUnset. +type unknownIfExecutionModeUnset struct{} + +var _ planmodifier.String = (*validateAgentExecutionMode)(nil) +var _ planmodifier.List = (*revertOverwritesIfExecutionModeUnset)(nil) +var _ planmodifier.String = (*unknownIfExecutionModeUnset)(nil) + +func (m validateAgentExecutionMode) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Check if the resource is being created. + if req.State.Raw.IsNull() { + return + } + + configured := modelWorkspaceSettings{} + resp.Diagnostics.Append(req.Config.Get(ctx, &configured)...) + + if configured.ExecutionMode.ValueString() == "agent" && configured.AgentPoolID.IsNull() { + resp.Diagnostics.AddError("Invalid agent_pool_id", "If execution mode is \"agent\", \"agent_pool_id\" is required") + } + + if configured.ExecutionMode.ValueString() != "agent" && !configured.AgentPoolID.IsNull() { + resp.Diagnostics.AddError("Invalid agent_pool_id", "If execution mode is not \"agent\", \"agent_pool_id\" must not be set") + } +} + +func (m validateAgentExecutionMode) Description(_ context.Context) string { + return "Validates that configuration values for \"agent_pool_id\" and \"execution_mode\" are compatible" +} + +func (m validateAgentExecutionMode) MarkdownDescription(_ context.Context) string { + return "Validates that configuration values for \"agent_pool_id\" and \"execution_mode\" are compatible" +} + +func (m revertOverwritesIfExecutionModeUnset) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + // Check if the resource is being created. + if req.State.Raw.IsNull() { + return + } + + // Determine if configured execution_mode is being unset + state := modelWorkspaceSettings{} + configured := modelWorkspaceSettings{} + + resp.Diagnostics.Append(req.Config.Get(ctx, &configured)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + overwritesState := make([]modelOverwrites, 1) + state.Overwrites.ElementsAs(ctx, &overwritesState, true) + + if configured.ExecutionMode.IsNull() && overwritesState[0].ExecutionMode.ValueBool() { + overwritesState[0].AgentPool = types.BoolValue(false) + overwritesState[0].ExecutionMode = types.BoolValue(false) + + newList, diags := types.ListValueFrom(ctx, overwritesElementType, overwritesState) + resp.Diagnostics.Append(diags...) + + resp.PlanValue = newList + } +} + +func (m revertOverwritesIfExecutionModeUnset) Description(_ context.Context) string { + return "Reverts to computed defaults if settings are unset" +} + +func (m revertOverwritesIfExecutionModeUnset) MarkdownDescription(_ context.Context) string { + return "Reverts to computed defaults if settings are unset" +} + +func (m unknownIfExecutionModeUnset) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Check if the resource is being created. + if req.State.Raw.IsNull() { + return + } + + // Determine if configured execution_mode is being unset + state := modelWorkspaceSettings{} + configured := modelWorkspaceSettings{} + + resp.Diagnostics.Append(req.Config.Get(ctx, &configured)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + overwritesState := make([]modelOverwrites, 1) + state.Overwrites.ElementsAs(ctx, &overwritesState, true) + + if configured.ExecutionMode.IsNull() && overwritesState[0].ExecutionMode.ValueBool() { + resp.PlanValue = types.StringUnknown() + } +} + +func (m unknownIfExecutionModeUnset) Description(_ context.Context) string { + return "Resets execution_mode to \"remote\" if it is unset" +} + +func (m unknownIfExecutionModeUnset) MarkdownDescription(_ context.Context) string { + return "Resets execution_mode to \"remote\" if it is unset" +} + +func (r *workspaceSettings) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Additional Workspace settings that override organization defaults", + DeprecationMessage: "", + Version: 1, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Service-generated identifier for the variable", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + + "workspace_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches( + IDPattern("ws"), + "must be a valid workspace ID (ws-)", + ), + }, + }, + + "execution_mode": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + unknownIfExecutionModeUnset{}, + }, + Validators: []validator.String{ + stringvalidator.OneOf("agent", "local", "remote"), + }, + }, + + "agent_pool_id": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + unknownIfExecutionModeUnset{}, + validateAgentExecutionMode{}, + }, + Validators: []validator.String{ + stringvalidator.RegexMatches( + IDPattern("apool"), + "must be a valid workspace ID (apool-)", + ), + }, + }, + + // ListAttribute was required here because we are still using plugin protocol v5. + // Once compatibility is broken for v1, and we convert all + // providers to protocol v6, this can become a single nested object. + "overwrites": schema.ListAttribute{ + Computed: true, + ElementType: overwritesElementType, + PlanModifiers: []planmodifier.List{ + revertOverwritesIfExecutionModeUnset{}, + }, + }, + }, + } +} + +// workspaceSettingsModelFromTFEWorkspace builds a resource model from the TFE model +func workspaceSettingsModelFromTFEWorkspace(ws *tfe.Workspace) *modelWorkspaceSettings { + result := modelWorkspaceSettings{ + ID: types.StringValue(ws.ID), + WorkspaceID: types.StringValue(ws.ID), + ExecutionMode: types.StringValue(ws.ExecutionMode), + } + + if ws.AgentPool != nil && ws.ExecutionMode == "agent" { + result.AgentPoolID = types.StringValue(ws.AgentPool.ID) + } + + settingsModel := modelOverwrites{ + ExecutionMode: types.BoolValue(false), + AgentPool: types.BoolValue(false), + } + + if ws.SettingOverwrites != nil { + settingsModel = modelOverwrites{ + ExecutionMode: types.BoolValue(*ws.SettingOverwrites.ExecutionMode), + AgentPool: types.BoolValue(*ws.SettingOverwrites.AgentPool), + } + } + + listOverwrites, diags := types.ListValueFrom(ctx, overwritesElementType, []modelOverwrites{settingsModel}) + if diags.HasError() { + panic("Could not build list value from slice of models. This should not be possible unless the model breaks reflection rules.") + } + + result.Overwrites = listOverwrites + + return &result +} + +func (r *workspaceSettings) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data modelWorkspaceSettings + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + model, err := r.readSettings(ctx, data.WorkspaceID.ValueString()) + if errors.Is(err, errWorkspaceNoLongerExists) { + resp.State.RemoveResource(ctx) + return + } else if err != nil { + resp.Diagnostics.AddError("Error reading workspace", err.Error()) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} + +func (r *workspaceSettings) readSettings(ctx context.Context, workspaceID string) (*modelWorkspaceSettings, error) { + ws, err := r.config.Client.Workspaces.ReadByID(ctx, workspaceID) + if err != nil { + // If it's gone: that's not an error, but we are done. + if errors.Is(err, tfe.ErrResourceNotFound) { + log.Printf("[DEBUG] Workspace %s no longer exists", workspaceID) + return nil, errWorkspaceNoLongerExists + } + return nil, fmt.Errorf("couldn't read workspace %s: %s", workspaceID, err.Error()) + } + + return workspaceSettingsModelFromTFEWorkspace(ws), nil +} + +func (r *workspaceSettings) updateSettings(ctx context.Context, data *modelWorkspaceSettings, state *tfsdk.State) error { + workspaceID := data.WorkspaceID.ValueString() + + updateOptions := tfe.WorkspaceUpdateOptions{ + SettingOverwrites: &tfe.WorkspaceSettingOverwritesOptions{ + ExecutionMode: tfe.Bool(false), + AgentPool: tfe.Bool(false), + }, + } + + if executionMode := data.ExecutionMode.ValueString(); executionMode != "" { + updateOptions.ExecutionMode = tfe.String(executionMode) + updateOptions.SettingOverwrites.ExecutionMode = tfe.Bool(true) + updateOptions.SettingOverwrites.AgentPool = tfe.Bool(true) + + agentPoolID := data.AgentPoolID.ValueString() // may be empty + updateOptions.AgentPoolID = tfe.String(agentPoolID) + } + + ws, err := r.config.Client.Workspaces.UpdateByID(ctx, workspaceID, updateOptions) + if err != nil { + return fmt.Errorf("couldn't update workspace %s: %w", workspaceID, err) + } + + model, err := r.readSettings(ctx, ws.ID) + if err != nil { + return fmt.Errorf("couldn't read workspace %s after update: %w", workspaceID, err) + } + state.Set(ctx, model) + return nil +} + +func (r *workspaceSettings) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data modelWorkspaceSettings + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if err := r.updateSettings(ctx, &data, &resp.State); err != nil { + resp.Diagnostics.AddError("Error updating workspace", err.Error()) + } +} + +func (r *workspaceSettings) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data modelWorkspaceSettings + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if err := r.updateSettings(ctx, &data, &resp.State); err != nil { + resp.Diagnostics.AddError("Error updating workspace", err.Error()) + } +} + +func (r *workspaceSettings) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data modelWorkspaceSettings + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + noneModel := modelWorkspaceSettings{ + ID: data.ID, + WorkspaceID: data.ID, + } + + if err := r.updateSettings(ctx, &noneModel, &resp.State); err == nil { + resp.State.RemoveResource(ctx) + } +} + +func (r *workspaceSettings) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "tfe_workspace_settings" +} + +func (r *workspaceSettings) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Early exit if provider is unconfigured (i.e. we're only validating config or something) + if req.ProviderData == nil { + return + } + + 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 *workspaceSettings) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + s := strings.Split(req.ID, "/") + if len(s) >= 3 { + resp.Diagnostics.AddError("Error importing workspace settings", fmt.Sprintf( + "invalid workspace input format: %s (expected / or )", + req.ID, + )) + } else if len(s) == 2 { + workspaceID, err := fetchWorkspaceExternalID(s[0]+"/"+s[1], r.config.Client) + if err != nil { + resp.Diagnostics.AddError("Error importing workspace settings", fmt.Sprintf( + "error retrieving workspace with name %s from organization %s: %s", s[1], s[0], err.Error(), + )) + } + + req.ID = workspaceID + } + + resp.State.SetAttribute(ctx, path.Root("workspace_id"), req.ID) + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func NewResourceWorkspaceSettings() resource.Resource { + return &workspaceSettings{} +} diff --git a/internal/provider/resource_tfe_workspace_settings_test.go b/internal/provider/resource_tfe_workspace_settings_test.go new file mode 100644 index 000000000..db644c6cc --- /dev/null +++ b/internal/provider/resource_tfe_workspace_settings_test.go @@ -0,0 +1,241 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "errors" + "fmt" + "testing" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccTFEWorkspaceSettings(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createBusinessOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + ws := createTempWorkspace(t, tfeClient, org.Name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEWorkspaceSettingsDestroy, + Steps: []resource.TestStep{ + // Start with local execution + { + Config: testAccTFEWorkspaceSettings_basic(ws.ID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "tfe_workspace_settings.foobar", "id"), + resource.TestCheckResourceAttrSet( + "tfe_workspace_settings.foobar", "workspace_id"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "execution_mode", "local"), + resource.TestCheckNoResourceAttr( + "tfe_workspace_settings.foobar", "agent_pool_id"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.0.execution_mode", "true"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.0.agent_pool", "true"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.#", "1"), + ), + }, + // Change to agent pool + { + Config: testAccTFEWorkspaceSettings_updateExecutionMode(org.Name, ws.ID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "tfe_workspace_settings.foobar", "id"), + resource.TestCheckResourceAttrSet( + "tfe_workspace_settings.foobar", "workspace_id"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "execution_mode", "agent"), + resource.TestCheckResourceAttrSet( + "tfe_workspace_settings.foobar", "agent_pool_id"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.0.execution_mode", "true"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.0.agent_pool", "true"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.#", "1"), + ), + }, + // Unset execution mode + { + Config: testAccTFEWorkspaceSettings_unsetExecutionMode(org.Name, ws.ID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "tfe_workspace_settings.foobar", "id"), + resource.TestCheckResourceAttrSet( + "tfe_workspace_settings.foobar", "workspace_id"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "execution_mode", "remote"), + resource.TestCheckNoResourceAttr( + "tfe_workspace_settings.foobar", "agent_pool_id"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.0.execution_mode", "false"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.0.agent_pool", "false"), + resource.TestCheckResourceAttr( + "tfe_workspace_settings.foobar", "overwrites.#", "1"), + ), + }, + }, + }) +} + +func TestAccTFEWorkspaceSettingsImport(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createBusinessOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + ws := createTempWorkspace(t, tfeClient, org.Name) + + _, err = tfeClient.Workspaces.UpdateByID(ctx, ws.ID, tfe.WorkspaceUpdateOptions{ + ExecutionMode: tfe.String("local"), + }) + if err != nil { + t.Fatal(err) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEWorkspaceSettingsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspaceSettings_basic(ws.ID), + }, + { + ResourceName: "tfe_workspace_settings.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccTFEWorkspaceSettingsImport_ByName(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createBusinessOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + ws := createTempWorkspace(t, tfeClient, org.Name) + + _, err = tfeClient.Workspaces.UpdateByID(ctx, ws.ID, tfe.WorkspaceUpdateOptions{ + ExecutionMode: tfe.String("local"), + }) + if err != nil { + t.Fatal(err) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspaceSettings_basic(ws.ID), + }, + { + ResourceName: "tfe_workspace_settings.foobar", + ImportState: true, + ImportStateId: fmt.Sprintf("%s/%s", org.Name, ws.Name), + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckTFEWorkspaceSettingsDestroy(s *terraform.State) error { + return testAccCheckTFEWorkspaceSettingsDestroyProvider(testAccProvider)(s) +} + +func testAccCheckTFEWorkspaceSettingsDestroyProvider(p *schema.Provider) func(s *terraform.State) error { + return func(s *terraform.State) error { + tfeClient, err := getClientUsingEnv() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_workspace_settings" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + ws, err := tfeClient.Workspaces.ReadByID(ctx, rs.Primary.ID) + if err != nil { + return fmt.Errorf("Workspace %s does not exist", rs.Primary.ID) + } + + if ws.ExecutionMode != "remote" { + return fmt.Errorf("expected execution mode to be remote after destroy, but was %s", ws.ExecutionMode) + } + + if ws.AgentPool != nil { + return errors.New("expected agent pool to be nil after destroy, but wasn't") + } + } + + return nil + } +} + +func testAccTFEWorkspaceSettings_basic(workspaceID string) string { + return fmt.Sprintf(` +resource "tfe_workspace_settings" "foobar" { + workspace_id = "%s" + execution_mode = "local" +} +`, workspaceID) +} + +func testAccTFEWorkspaceSettings_updateExecutionMode(orgName, workspaceID string) string { + return fmt.Sprintf(` +resource "tfe_agent_pool" "mypool" { + name = "test-pool-default" + organization = "%s" +} + +resource "tfe_workspace_settings" "foobar" { + workspace_id = "%s" + execution_mode = "agent" + agent_pool_id = tfe_agent_pool.mypool.id +} +`, orgName, workspaceID) +} + +func testAccTFEWorkspaceSettings_unsetExecutionMode(orgName, workspaceID string) string { + return fmt.Sprintf(` +resource "tfe_agent_pool" "mypool" { + name = "test-pool-default" + organization = "%s" +} + +resource "tfe_workspace_settings" "foobar" { + workspace_id = "%s" +} +`, orgName, workspaceID) +} diff --git a/internal/provider/testing.go b/internal/provider/testing.go index ae4510774..0302cb495 100644 --- a/internal/provider/testing.go +++ b/internal/provider/testing.go @@ -164,22 +164,26 @@ func createOrganization(t *testing.T, client *tfe.Client, options tfe.Organizati } } -func createAgentPool(t *testing.T, client *tfe.Client, org *tfe.Organization) (*tfe.AgentPool, func()) { +func createTempWorkspace(t *testing.T, client *tfe.Client, orgName string) *tfe.Workspace { + t.Helper() + ctx := context.Background() - pool, err := client.AgentPools.Create(ctx, org.Name, tfe.AgentPoolCreateOptions{ - Name: tfe.String(randomString(t)), + ws, err := client.Workspaces.Create(ctx, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String(fmt.Sprintf("tst-workspace-%s", randomString(t))), }) if err != nil { t.Fatal(err) } - return pool, func() { - if err := client.AgentPools.Delete(ctx, pool.ID); err != nil { - t.Logf("Error destroying agent pool! WARNING: Dangling resources "+ - "may exist! The full error is shown below.\n\n"+ - "Agent pool ID: %s\nError: %s", pool.ID, err) + t.Cleanup(func() { + if err := client.Workspaces.DeleteByID(ctx, ws.ID); err != nil { + t.Errorf("Error destroying workspace! WARNING: Dangling resources\n"+ + "may exist! The full error is show below:\n\n"+ + "Workspace:%s\nError: %s", ws.ID, err) } - } + }) + + return ws } func createOrganizationMembership(t *testing.T, client *tfe.Client, orgName string, options tfe.OrganizationMembershipCreateOptions) *tfe.OrganizationMembership { From dd50c7c0c3cc4584190ec7c26fdc7223a8d3e94f Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 30 Nov 2023 16:23:11 -0700 Subject: [PATCH 2/8] Remove setting_overwrites from tfe_workspace --- internal/provider/resource_tfe_workspace.go | 203 +----------------- .../provider/resource_tfe_workspace_test.go | 105 --------- internal/provider/testing.go | 19 -- 3 files changed, 11 insertions(+), 316 deletions(-) diff --git a/internal/provider/resource_tfe_workspace.go b/internal/provider/resource_tfe_workspace.go index bf9cb6528..ea9df5acb 100644 --- a/internal/provider/resource_tfe_workspace.go +++ b/internal/provider/resource_tfe_workspace.go @@ -41,16 +41,6 @@ func resourceTFEWorkspace() *schema.Resource { }, CustomizeDiff: func(c context.Context, d *schema.ResourceDiff, meta interface{}) error { - // NOTE: execution mode and agent_pool_id must be set to default first before calling - // the validation functions - if err := setComputedDefaults(c, d); err != nil { - return err - } - - if err := overwriteDefaultExecutionMode(c, d); err != nil { - return err - } - if err := validateAgentExecution(c, d); err != nil { return err } @@ -128,24 +118,6 @@ func resourceTFEWorkspace() *schema.Resource { ), }, - "setting_overwrites": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "execution_mode": { - Type: schema.TypeBool, - Required: true, - }, - - "agent_pool": { - Type: schema.TypeBool, - Required: true, - }, - }, - }, - }, - "file_triggers_enabled": { Type: schema.TypeBool, Optional: true, @@ -519,18 +491,6 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error { d.Set("organization", workspace.Organization.Name) d.Set("resource_count", workspace.ResourceCount) - var settingOverwrites []interface{} - if workspace.SettingOverwrites != nil { - settingOverwrites = append(settingOverwrites, map[string]interface{}{ - "execution_mode": workspace.SettingOverwrites.ExecutionMode, - "agent_pool": workspace.SettingOverwrites.AgentPool, - }) - } - err = d.Set("setting_overwrites", settingOverwrites) - if err != nil { - return err - } - if workspace.Links["self-html"] != nil { baseAPI := config.Client.BaseURL() htmlURL := url.URL{ @@ -600,18 +560,15 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error config := meta.(ConfiguredClient) id := d.Id() - workspaceControlsAgentPool := isSettingOverwritten("agent_pool", d) - workspaceControlsExecutionMode := isSettingOverwritten("execution_mode", d) - if d.HasChange("name") || d.HasChange("auto_apply") || d.HasChange("auto_apply_run_trigger") || d.HasChange("queue_all_runs") || d.HasChange("terraform_version") || d.HasChange("working_directory") || d.HasChange("vcs_repo") || d.HasChange("file_triggers_enabled") || d.HasChange("trigger_prefixes") || d.HasChange("trigger_patterns") || d.HasChange("allow_destroy_plan") || d.HasChange("speculative_enabled") || - d.HasChange("operations") || (d.HasChange("execution_mode") && workspaceControlsExecutionMode) || - d.HasChange("description") || (d.HasChange("agent_pool_id") && workspaceControlsAgentPool) || + d.HasChange("operations") || d.HasChange("execution_mode") || + d.HasChange("description") || d.HasChange("agent_pool_id") || d.HasChange("global_remote_state") || d.HasChange("structured_run_output_enabled") || - d.HasChange("assessments_enabled") || d.HasChange("project_id") || d.HasChange("setting_overwrites") { + d.HasChange("assessments_enabled") || d.HasChange("project_id") { // Create a new options struct. options := tfe.WorkspaceUpdateOptions{ Name: tfe.String(d.Get("name").(string)), @@ -639,7 +596,7 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error } } - if (d.HasChange("agent_pool_id") && workspaceControlsAgentPool) || d.HasChange("setting_overwrites") { + if d.HasChange("agent_pool_id") { // Need the raw configuration value of the agent_pool_id because when the workspace's execution mode is set // to default, we can't know for certain what the default value of the agent pool will be. This means we can // only set the agent_pool_id as "NewComputed", meaning that the value returned by the ResourceData will be @@ -650,6 +607,7 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error // be sufficient if !agentPoolID.IsNull() { options.AgentPoolID = tfe.String(agentPoolID.AsString()) + // set setting overwrites options.SettingOverwrites = &tfe.WorkspaceSettingOverwritesOptions{ AgentPool: tfe.Bool(true), @@ -657,33 +615,13 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error } } - if (d.HasChange("execution_mode") && workspaceControlsExecutionMode) || d.HasChange("setting_overwrites") { - executionMode := d.GetRawConfig().GetAttr("execution_mode") - - // if the TFE instance knows about setting-overwrites - if _, ok := d.GetOk("setting_overwrites"); ok { - if options.SettingOverwrites == nil { - // initialize setting-overwrites if it has not been initialized already - options.SettingOverwrites = &tfe.WorkspaceSettingOverwritesOptions{} - } - - // if execution mode is currently unset... - operations := d.GetRawConfig().GetAttr("operations") - if executionMode.IsNull() && operations.IsNull() { - // set execution mode to default (inherit from the parent organization/project) - options.SettingOverwrites.ExecutionMode = tfe.Bool(false) - } + if d.HasChange("execution_mode") { + if v, ok := d.GetOk("execution_mode"); ok { + options.ExecutionMode = tfe.String(v.(string)) - // if execution has been set... - if !executionMode.IsNull() { - // set the execution mode to be "overwritten" - options.SettingOverwrites.ExecutionMode = tfe.Bool(true) - options.ExecutionMode = tfe.String(executionMode.AsString()) - } - } else { - // since the TFE instance doesn't know about setting-overwrites, set the execution mode as normal - if v, ok := d.GetOk("execution_mode"); ok { - options.ExecutionMode = tfe.String(v.(string)) + // set setting overwrites + options.SettingOverwrites = &tfe.WorkspaceSettingOverwritesOptions{ + ExecutionMode: tfe.Bool(true), } } } @@ -942,105 +880,6 @@ func resourceTFEWorkspaceDelete(d *schema.ResourceData, meta interface{}) error return nil } -// since execution_mode and agent_pool_id are marked as Optional: true, and -// Computed: true, unsetting the execution_mode/agent_pool_id in the config -// after it's been set to a valid value is not detected by ResourceDiff so -// we need to read the value from RawConfig instead -func setComputedDefaults(_ context.Context, d *schema.ResourceDiff) error { - configMap := d.GetRawConfig().AsValueMap() - operations, operationsReadOk := configMap["operations"] - executionMode, executionModeReadOk := configMap["execution_mode"] - executionModeState := d.Get("execution_mode") - agentPoolID, agentPoolIDReadOk := configMap["agent_pool_id"] - agentPoolIDState := d.Get("agent_pool_id") - - // forcefully setting the defaults is only necessary when an existing workspace is being updated - isRecordPersisted := d.Id() != "" - if isRecordPersisted != true { - return nil - } - - if !operationsReadOk || !executionModeReadOk || !agentPoolIDReadOk { - return nil - } - - // find out if the current TFE version supports setting-overwrites - currentTfeSupportsSettingOverwrites := false - if v, ok := d.GetOkExists("setting_overwrites"); ok { - settingOverwrites := v.([]interface{}) - currentTfeSupportsSettingOverwrites = len(settingOverwrites) != 0 - } - executionModeWasUnset := executionModeState != "remote" && operations.IsNull() && executionMode.IsNull() - agentPoolWasUnset := (agentPoolID.IsNull() || !agentPoolIDReadOk) && agentPoolIDState != "" - - // if current version of TFE does not support setting-overwrites, update the computed values if either of - // them have been set to a nil value - if !currentTfeSupportsSettingOverwrites { - if executionModeWasUnset { - err := d.SetNew("execution_mode", "remote") - if err != nil { - return fmt.Errorf("failed to set execution_mode: %w", err) - } - } - - if agentPoolWasUnset { - err := d.SetNew("agent_pool_id", nil) - if err != nil { - return fmt.Errorf("failed to clear agent_pool_id: %w", err) - } - } - return nil - } - - return nil -} - -func overwriteDefaultExecutionMode(_ context.Context, d *schema.ResourceDiff) error { - configMap := d.GetRawConfig().AsValueMap() - executionMode, executionModeReadOk := configMap["execution_mode"] - operations, operationsReadOk := configMap["operations"] - - // if the execution mode was previously overwritten, but being set to default in the current config, make sure that - // the setting overwrites will be set to false and the execution_mode and agent_pool_id are set to computed as we - // are not able to tell what the default execution mode is until after we update the workspace - if executionMode.IsNull() && operations.IsNull() { - if v, ok := d.GetOk("setting_overwrites"); ok { - settingOverwrites := v.([]interface{})[0].(map[string]interface{}) - if settingOverwrites["execution_mode"] == true { - newSettingOverwrites := map[string]interface{}{ - "execution_mode": false, - "agent_pool": false, - } - d.SetNew("setting_overwrites", []interface{}{newSettingOverwrites}) - d.SetNewComputed("execution_mode") - d.SetNewComputed("agent_pool_id") - } - return nil - } - } - - if (executionMode.IsNull() || !executionModeReadOk) && (operations.IsNull() || !operationsReadOk) { - return nil - } - - // if the default execution mode and the execution_mode in the config matches, nothing will happen - // unless we inform TFE that the new execution_mode is meant to overwrite the current execution mode - if v, ok := d.GetOk("setting_overwrites"); ok { - settingOverwrites := v.([]interface{})[0].(map[string]interface{}) - if settingOverwrites["execution_mode"] == false { - agentPoolID, agentPoolReadOk := configMap["agent_pool_id"] - - newSettingOverwrites := map[string]interface{}{ - "execution_mode": true, - "agent_pool": agentPoolID.IsKnown() && agentPoolReadOk, - } - d.SetNew("setting_overwrites", []interface{}{newSettingOverwrites}) - } - } - - return nil -} - // An agent pool can only be specified when execution_mode is set to "agent". You currently cannot specify a // schema validation based on a different argument's value, so we do so here at plan time instead. func validateAgentExecution(_ context.Context, d *schema.ResourceDiff) error { @@ -1157,23 +996,3 @@ func errWorkspaceResourceCountCheck(workspaceID string, resourceCount int) error } return nil } - -// isSettingOverwritten checks if the value of a setting is being overwritten by the workspace or not. in other words, -// if the value of the setting is determined by the workspace, this function will return true for that setting -func isSettingOverwritten(setting string, d *schema.ResourceData) bool { - if v, ok := d.GetOk("setting_overwrites"); ok { - settingOverwrites := v.([]interface{}) - if len(settingOverwrites) != 1 { - // current TFE version does not support setting-overwrites, so all settings are set at workspace-level - return true - } - - // check the value of the setting - settingOverwritesValue := settingOverwrites[0].(map[string]interface{}) - executionModeOverwritten := settingOverwritesValue[setting] - - return executionModeOverwritten.(bool) - } - - return true -} diff --git a/internal/provider/resource_tfe_workspace_test.go b/internal/provider/resource_tfe_workspace_test.go index 3173c1894..81cfef2d1 100644 --- a/internal/provider/resource_tfe_workspace_test.go +++ b/internal/provider/resource_tfe_workspace_test.go @@ -1927,111 +1927,6 @@ func TestAccTFEWorkspace_operationsAndExecutionModeInteroperability(t *testing.T }) } -func TestAccTFEWorkspace_unsetExecutionMode(t *testing.T) { - skipIfEnterprise(t) - - tfeClient, err := getClientUsingEnv() - if err != nil { - t.Fatal(err) - } - - org, orgCleanup := createBusinessOrganization(t, tfeClient) - t.Cleanup(orgCleanup) - - workspace := &tfe.Workspace{} - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckTFEWorkspaceDestroy, - Steps: []resource.TestStep{ - { - Config: testAccTFEWorkspace_executionModeAgent(org.Name), - Check: resource.ComposeTestCheckFunc( - testAccCheckTFEWorkspaceExists( - "tfe_workspace.foobar", workspace, testAccProvider), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "operations", "true"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "execution_mode", "agent"), - resource.TestCheckResourceAttrSet( - "tfe_workspace.foobar", "agent_pool_id"), - ), - }, - { - Config: testAccTFEWorkspace_executionModeNull(org.Name), - Check: resource.ComposeTestCheckFunc( - testAccCheckTFEWorkspaceExists( - "tfe_workspace.foobar", workspace, testAccProvider), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "operations", "true"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "execution_mode", "remote"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "agent_pool_id", ""), - ), - }, - }, - }) -} - -func TestAccTFEWorkspace_unsetExecutionModeWithOrgLevelDefault(t *testing.T) { - skipIfEnterprise(t) - - tfeClient, err := getClientUsingEnv() - if err != nil { - t.Fatal(err) - } - - org, agentPool, orgCleanup := createBusinessOrganizationWithAgentDefaultExecutionMode(t, tfeClient) - t.Cleanup(orgCleanup) - - workspace := &tfe.Workspace{} - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckTFEWorkspaceDestroy, - Steps: []resource.TestStep{ - { - Config: testAccTFEWorkspace_executionModeAgent(org.Name), - Check: resource.ComposeTestCheckFunc( - testAccCheckTFEWorkspaceExists( - "tfe_workspace.foobar", workspace, testAccProvider), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "operations", "true"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "execution_mode", "agent"), - resource.TestCheckResourceAttrSet( - "tfe_workspace.foobar", "agent_pool_id"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "setting_overwrites.0.execution_mode", "true"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "setting_overwrites.0.agent_pool", "true"), - ), - }, - { - Config: testAccTFEWorkspace_executionModeNull(org.Name), - Check: resource.ComposeTestCheckFunc( - testAccCheckTFEWorkspaceExists( - "tfe_workspace.foobar", workspace, testAccProvider), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "operations", "true"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "execution_mode", "agent"), - // workspace should now be using the organization default agent pool - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "agent_pool_id", agentPool.ID), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "setting_overwrites.0.execution_mode", "false"), - resource.TestCheckResourceAttr( - "tfe_workspace.foobar", "setting_overwrites.0.agent_pool", "false"), - ), - }, - }, - }) -} - func TestAccTFEWorkspace_globalRemoteState(t *testing.T) { workspace := &tfe.Workspace{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() diff --git a/internal/provider/testing.go b/internal/provider/testing.go index 0302cb495..8c428510f 100644 --- a/internal/provider/testing.go +++ b/internal/provider/testing.go @@ -129,25 +129,6 @@ func createBusinessOrganization(t *testing.T, client *tfe.Client) (*tfe.Organiza return org, orgCleanup } -func createBusinessOrganizationWithAgentDefaultExecutionMode(t *testing.T, tfeClient *tfe.Client) (*tfe.Organization, *tfe.AgentPool, func()) { - org, orgCleanup := createBusinessOrganization(t, tfeClient) - - agentPool, _ := createAgentPool(t, tfeClient, org) - - // update organization to use default execution mode of "agent" - org, err := tfeClient.Organizations.Update(context.Background(), org.Name, tfe.OrganizationUpdateOptions{ - DefaultExecutionMode: tfe.String("agent"), - DefaultAgentPool: agentPool, - }) - if err != nil { - t.Fatal(err) - } - - return org, agentPool, func() { - orgCleanup() - } -} - func createOrganization(t *testing.T, client *tfe.Client, options tfe.OrganizationCreateOptions) (*tfe.Organization, func()) { ctx := context.Background() org, err := client.Organizations.Create(ctx, options) From 730c8560c861fa19b215130c8204d74f4e2830be Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 30 Nov 2023 16:28:30 -0700 Subject: [PATCH 3/8] Deprecate execution_mode and agent_pool_id on tfe_workspace --- internal/provider/resource_tfe_workspace.go | 4 ++- website/docs/r/workspace.html.markdown | 39 ++++++++++----------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/internal/provider/resource_tfe_workspace.go b/internal/provider/resource_tfe_workspace.go index ea9df5acb..b19513c04 100644 --- a/internal/provider/resource_tfe_workspace.go +++ b/internal/provider/resource_tfe_workspace.go @@ -83,6 +83,7 @@ func resourceTFEWorkspace() *schema.Resource { Optional: true, Computed: true, ConflictsWith: []string{"operations"}, + Deprecated: "Use resource tfe_workspace_settings to modify the workspace execution settings. This attribute will be removed in a future release of the provider.", }, "allow_destroy_plan": { @@ -108,6 +109,7 @@ func resourceTFEWorkspace() *schema.Resource { Optional: true, Computed: true, ConflictsWith: []string{"operations"}, + Deprecated: "Use resource tfe_workspace_settings to modify the workspace execution settings. This attribute will be removed in a future release of the provider.", ValidateFunc: validation.StringInSlice( []string{ "agent", @@ -146,7 +148,7 @@ func resourceTFEWorkspace() *schema.Resource { Type: schema.TypeBool, Optional: true, Computed: true, - Deprecated: "Use execution_mode instead.", + Deprecated: "Use tfe_workspace_settings to modify the workspace execution settings. This attribute will be removed in a future release of the provider.", ConflictsWith: []string{"execution_mode", "agent_pool_id"}, }, diff --git a/website/docs/r/workspace.html.markdown b/website/docs/r/workspace.html.markdown index ee78ad65e..b5e8e5358 100644 --- a/website/docs/r/workspace.html.markdown +++ b/website/docs/r/workspace.html.markdown @@ -28,7 +28,7 @@ resource "tfe_workspace" "test" { } ``` -With `execution_mode` of `agent`: +Usage with vcs_repo: ```hcl resource "tfe_organization" "test-organization" { @@ -36,25 +36,31 @@ resource "tfe_organization" "test-organization" { email = "admin@company.com" } -resource "tfe_agent_pool" "test-agent-pool" { - name = "my-agent-pool-name" - organization = tfe_organization.test-organization.name +resource "tfe_oauth_client" "test" { + organization = tfe_organization.test-organization + api_url = "https://api.github.com" + http_url = "https://github.com" + oauth_token = "oauth_token_id" + service_provider = "github" } -resource "tfe_workspace" "test" { - name = "my-workspace-name" - organization = tfe_organization.test-organization.name - agent_pool_id = tfe_agent_pool.test-agent-pool.id - execution_mode = "agent" +resource "tfe_workspace" "parent" { + name = "parent-ws" + organization = tfe_organization.test-organization + queue_all_runs = false + vcs_repo { + branch = "main" + identifier = "my-org-name/vcs-repository" + oauth_token_id = tfe_oauth_client.test.oauth_token_id + } } -``` ## Argument Reference The following arguments are supported: * `name` - (Required) Name of the workspace. -* `agent_pool_id` - (Optional) The ID of an agent pool to assign to the workspace. Requires `execution_mode` +* `agent_pool_id` - (Optional) **Deprecated** The ID of an agent pool to assign to the workspace. Requires `execution_mode` to be set to `agent`. This value _must not_ be provided if `execution_mode` is set to any other value or if `operations` is provided. * `allow_destroy_plan` - (Optional) Whether destroy plans can be queued on the workspace. @@ -62,12 +68,8 @@ The following arguments are supported: * `auto_apply` - (Optional) Whether to automatically apply changes when a Terraform plan is successful. Defaults to `false`. * `auto_apply_run_trigger` - (Optional) Whether to automatically apply changes for runs that were created by run triggers from another workspace. Defaults to `false`. * `description` - (Optional) A description for the workspace. -* `execution_mode` - (Optional) Which [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) - to use. Using Terraform Cloud, valid values are `remote`, `local` or `agent`. - Defaults your organization's default execution mode, or `remote` if no organization default is set. Using Terraform Enterprise, only `remote` and `local` - execution modes are valid. When set to `local`, the workspace will be used - for state storage only. This value _must not_ be provided if `operations` - is provided. +* `execution_mode` - (Optional) **Deprecated** Which [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) + to use. Using Terraform Cloud, valid values are `remote`, `local` or `agent`. Using Terraform Enterprise, only `remote` and `local` execution modes are valid. When set to `local`, the workspace will be used for state storage only. This value _must not_ be provided if `operations` is provided. * `file_triggers_enabled` - (Optional) Whether to filter runs based on the changed files in a VCS push. Defaults to `true`. If enabled, the working directory and trigger prefixes describe a set of paths which must contain changes for a @@ -146,9 +148,6 @@ In addition to all arguments above, the following attributes are exported: * `id` - The workspace ID. * `resource_count` - The number of resources managed by the workspace. * `html_url` - The URL to the browsable HTML overview of the workspace. -* `setting_overwrites` - Can be used to check whether a setting is currently inheriting its value from another resource. - - `execution_mode` - Set to `true` if the execution mode of the workspace is being determined by the setting on the workspace itself. It will be `false` if the execution mode is inherited from another resource (e.g. the organization's default execution mode) - - `agent_pool` - Set to `true` if the agent pool of the workspace is being determined by the setting on the workspace itself. It will be `false` if the agent pool is inherited from another resource (e.g. the organization's default agent pool) ## Import From 71766e51efcc02e5f3d01d65b898e89d0da2432d Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 30 Nov 2023 18:27:02 -0700 Subject: [PATCH 4/8] Rename tfe_organization_default_execution_mode -> tfe_organization_default_settings Even though the diff is large, there are no code changes and some minor documentation repairs. --- internal/provider/provider.go | 78 +++++++++---------- ...urce_tfe_organization_default_settings.go} | 20 ++--- ...tfe_organization_default_settings_test.go} | 57 +++++++------- ...ganization_default_settings.html.markdown} | 28 ++++--- 4 files changed, 94 insertions(+), 89 deletions(-) rename internal/provider/{resource_tfe_organization_default_execution_mode.go => resource_tfe_organization_default_settings.go} (82%) rename internal/provider/{resource_tfe_organization_default_execution_mode_test.go => resource_tfe_organization_default_settings_test.go} (68%) rename website/docs/r/{tfe_organization_default_execution_mode.html.markdown => organization_default_settings.html.markdown} (50%) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c5abaac2d..a37f68b6c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -106,45 +106,45 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ - "tfe_admin_organization_settings": resourceTFEAdminOrganizationSettings(), - "tfe_agent_pool": resourceTFEAgentPool(), - "tfe_agent_pool_allowed_workspaces": resourceTFEAgentPoolAllowedWorkspaces(), - "tfe_agent_token": resourceTFEAgentToken(), - "tfe_notification_configuration": resourceTFENotificationConfiguration(), - "tfe_oauth_client": resourceTFEOAuthClient(), - "tfe_organization": resourceTFEOrganization(), - "tfe_organization_default_execution_mode": resourceTFEOrganizationDefaultExecutionMode(), - "tfe_organization_membership": resourceTFEOrganizationMembership(), - "tfe_organization_module_sharing": resourceTFEOrganizationModuleSharing(), - "tfe_organization_run_task": resourceTFEOrganizationRunTask(), - "tfe_organization_token": resourceTFEOrganizationToken(), - "tfe_policy": resourceTFEPolicy(), - "tfe_policy_set": resourceTFEPolicySet(), - "tfe_policy_set_parameter": resourceTFEPolicySetParameter(), - "tfe_project": resourceTFEProject(), - "tfe_project_policy_set": resourceTFEProjectPolicySet(), - "tfe_project_variable_set": resourceTFEProjectVariableSet(), - "tfe_registry_module": resourceTFERegistryModule(), - "tfe_no_code_module": resourceTFENoCodeModule(), - "tfe_run_trigger": resourceTFERunTrigger(), - "tfe_sentinel_policy": resourceTFESentinelPolicy(), - "tfe_ssh_key": resourceTFESSHKey(), - "tfe_team": resourceTFETeam(), - "tfe_team_access": resourceTFETeamAccess(), - "tfe_team_organization_member": resourceTFETeamOrganizationMember(), - "tfe_team_organization_members": resourceTFETeamOrganizationMembers(), - "tfe_team_project_access": resourceTFETeamProjectAccess(), - "tfe_team_member": resourceTFETeamMember(), - "tfe_team_members": resourceTFETeamMembers(), - "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(), - "tfe_workspace_run": resourceTFEWorkspaceRun(), - "tfe_workspace_variable_set": resourceTFEWorkspaceVariableSet(), + "tfe_admin_organization_settings": resourceTFEAdminOrganizationSettings(), + "tfe_agent_pool": resourceTFEAgentPool(), + "tfe_agent_pool_allowed_workspaces": resourceTFEAgentPoolAllowedWorkspaces(), + "tfe_agent_token": resourceTFEAgentToken(), + "tfe_notification_configuration": resourceTFENotificationConfiguration(), + "tfe_oauth_client": resourceTFEOAuthClient(), + "tfe_organization": resourceTFEOrganization(), + "tfe_organization_default_settings": resourceTFEOrganizationDefaultSettings(), + "tfe_organization_membership": resourceTFEOrganizationMembership(), + "tfe_organization_module_sharing": resourceTFEOrganizationModuleSharing(), + "tfe_organization_run_task": resourceTFEOrganizationRunTask(), + "tfe_organization_token": resourceTFEOrganizationToken(), + "tfe_policy": resourceTFEPolicy(), + "tfe_policy_set": resourceTFEPolicySet(), + "tfe_policy_set_parameter": resourceTFEPolicySetParameter(), + "tfe_project": resourceTFEProject(), + "tfe_project_policy_set": resourceTFEProjectPolicySet(), + "tfe_project_variable_set": resourceTFEProjectVariableSet(), + "tfe_registry_module": resourceTFERegistryModule(), + "tfe_no_code_module": resourceTFENoCodeModule(), + "tfe_run_trigger": resourceTFERunTrigger(), + "tfe_sentinel_policy": resourceTFESentinelPolicy(), + "tfe_ssh_key": resourceTFESSHKey(), + "tfe_team": resourceTFETeam(), + "tfe_team_access": resourceTFETeamAccess(), + "tfe_team_organization_member": resourceTFETeamOrganizationMember(), + "tfe_team_organization_members": resourceTFETeamOrganizationMembers(), + "tfe_team_project_access": resourceTFETeamProjectAccess(), + "tfe_team_member": resourceTFETeamMember(), + "tfe_team_members": resourceTFETeamMembers(), + "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(), + "tfe_workspace_run": resourceTFEWorkspaceRun(), + "tfe_workspace_variable_set": resourceTFEWorkspaceVariableSet(), }, ConfigureContextFunc: configure(), } diff --git a/internal/provider/resource_tfe_organization_default_execution_mode.go b/internal/provider/resource_tfe_organization_default_settings.go similarity index 82% rename from internal/provider/resource_tfe_organization_default_execution_mode.go rename to internal/provider/resource_tfe_organization_default_settings.go index 3fb7ccb4f..9a9c6cc95 100644 --- a/internal/provider/resource_tfe_organization_default_execution_mode.go +++ b/internal/provider/resource_tfe_organization_default_settings.go @@ -11,13 +11,13 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -func resourceTFEOrganizationDefaultExecutionMode() *schema.Resource { +func resourceTFEOrganizationDefaultSettings() *schema.Resource { return &schema.Resource{ - Create: resourceTFEOrganizationDefaultExecutionModeCreate, - Read: resourceTFEOrganizationDefaultExecutionModeRead, - Delete: resourceTFEOrganizationDefaultExecutionModeDelete, + Create: resourceTFEOrganizationDefaultSettingsCreate, + Read: resourceTFEOrganizationDefaultSettingsRead, + Delete: resourceTFEOrganizationDefaultSettingsDelete, Importer: &schema.ResourceImporter{ - StateContext: resourceTFEOrganizationDefaultExecutionModeImporter, + StateContext: resourceTFEOrganizationDefaultSettingsImporter, }, CustomizeDiff: customizeDiffIfProviderDefaultOrganizationChanged, @@ -53,7 +53,7 @@ func resourceTFEOrganizationDefaultExecutionMode() *schema.Resource { } } -func resourceTFEOrganizationDefaultExecutionModeCreate(d *schema.ResourceData, meta interface{}) error { +func resourceTFEOrganizationDefaultSettingsCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(ConfiguredClient) // Get the organization name. @@ -88,10 +88,10 @@ func resourceTFEOrganizationDefaultExecutionModeCreate(d *schema.ResourceData, m d.SetId(organization) - return resourceTFEOrganizationDefaultExecutionModeRead(d, meta) + return resourceTFEOrganizationDefaultSettingsRead(d, meta) } -func resourceTFEOrganizationDefaultExecutionModeRead(d *schema.ResourceData, meta interface{}) error { +func resourceTFEOrganizationDefaultSettingsRead(d *schema.ResourceData, meta interface{}) error { config := meta.(ConfiguredClient) log.Printf("[DEBUG] Read the organization: %s", d.Id()) @@ -119,7 +119,7 @@ func resourceTFEOrganizationDefaultExecutionModeRead(d *schema.ResourceData, met return nil } -func resourceTFEOrganizationDefaultExecutionModeDelete(d *schema.ResourceData, meta interface{}) error { +func resourceTFEOrganizationDefaultSettingsDelete(d *schema.ResourceData, meta interface{}) error { config := meta.(ConfiguredClient) // Get the organization name. @@ -141,7 +141,7 @@ func resourceTFEOrganizationDefaultExecutionModeDelete(d *schema.ResourceData, m return nil } -func resourceTFEOrganizationDefaultExecutionModeImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { +func resourceTFEOrganizationDefaultSettingsImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { config := meta.(ConfiguredClient) log.Printf("[DEBUG] Read the organization: %s", d.Id()) diff --git a/internal/provider/resource_tfe_organization_default_execution_mode_test.go b/internal/provider/resource_tfe_organization_default_settings_test.go similarity index 68% rename from internal/provider/resource_tfe_organization_default_execution_mode_test.go rename to internal/provider/resource_tfe_organization_default_settings_test.go index 77cc4c11e..c7beb2891 100644 --- a/internal/provider/resource_tfe_organization_default_execution_mode_test.go +++ b/internal/provider/resource_tfe_organization_default_settings_test.go @@ -7,12 +7,13 @@ import ( "time" "errors" + tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestAccTFEOrganizationDefaultExecutionMode_remote(t *testing.T) { +func TestAccTFEOrganizationDefaultSettings_remote(t *testing.T) { org := &tfe.Organization{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -22,18 +23,18 @@ func TestAccTFEOrganizationDefaultExecutionMode_remote(t *testing.T) { CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { - Config: testAccTFEOrganizationDefaultExecutionMode_remote(rInt), + Config: testAccTFEOrganizationDefaultSettings_remote(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckTFEOrganizationExists( "tfe_organization.foobar", org), - testAccCheckTFEOrganizationDefaultExecutionMode(org, "remote"), + testAccCheckTFEOrganizationDefaultSettings(org, "remote"), ), }, }, }) } -func TestAccTFEOrganizationDefaultExecutionMode_local(t *testing.T) { +func TestAccTFEOrganizationDefaultSettings_local(t *testing.T) { org := &tfe.Organization{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -43,18 +44,18 @@ func TestAccTFEOrganizationDefaultExecutionMode_local(t *testing.T) { CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { - Config: testAccTFEOrganizationDefaultExecutionMode_local(rInt), + Config: testAccTFEOrganizationDefaultSettings_local(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckTFEOrganizationExists( "tfe_organization.foobar", org), - testAccCheckTFEOrganizationDefaultExecutionMode(org, "local"), + testAccCheckTFEOrganizationDefaultSettings(org, "local"), ), }, }, }) } -func TestAccTFEOrganizationDefaultExecutionMode_agent(t *testing.T) { +func TestAccTFEOrganizationDefaultSettings_agent(t *testing.T) { org := &tfe.Organization{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -64,11 +65,11 @@ func TestAccTFEOrganizationDefaultExecutionMode_agent(t *testing.T) { CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { - Config: testAccTFEOrganizationDefaultExecutionMode_agent(rInt), + Config: testAccTFEOrganizationDefaultSettings_agent(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckTFEOrganizationExists( "tfe_organization.foobar", org), - testAccCheckTFEOrganizationDefaultExecutionMode(org, "agent"), + testAccCheckTFEOrganizationDefaultSettings(org, "agent"), testAccCheckTFEOrganizationDefaultAgentPoolIDExists(org), ), }, @@ -76,7 +77,7 @@ func TestAccTFEOrganizationDefaultExecutionMode_agent(t *testing.T) { }) } -func TestAccTFEOrganizationDefaultExecutionMode_update(t *testing.T) { +func TestAccTFEOrganizationDefaultSettings_update(t *testing.T) { org := &tfe.Organization{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -86,43 +87,43 @@ func TestAccTFEOrganizationDefaultExecutionMode_update(t *testing.T) { CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { - Config: testAccTFEOrganizationDefaultExecutionMode_remote(rInt), + Config: testAccTFEOrganizationDefaultSettings_remote(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckTFEOrganizationExists( "tfe_organization.foobar", org), - testAccCheckTFEOrganizationDefaultExecutionMode(org, "remote"), + testAccCheckTFEOrganizationDefaultSettings(org, "remote"), ), }, { - Config: testAccTFEOrganizationDefaultExecutionMode_agent(rInt), + Config: testAccTFEOrganizationDefaultSettings_agent(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckTFEOrganizationExists( "tfe_organization.foobar", org), - testAccCheckTFEOrganizationDefaultExecutionMode(org, "agent"), + testAccCheckTFEOrganizationDefaultSettings(org, "agent"), testAccCheckTFEOrganizationDefaultAgentPoolIDExists(org), ), }, { - Config: testAccTFEOrganizationDefaultExecutionMode_local(rInt), + Config: testAccTFEOrganizationDefaultSettings_local(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckTFEOrganizationExists( "tfe_organization.foobar", org), - testAccCheckTFEOrganizationDefaultExecutionMode(org, "local"), + testAccCheckTFEOrganizationDefaultSettings(org, "local"), ), }, { - Config: testAccTFEOrganizationDefaultExecutionMode_remote(rInt), + Config: testAccTFEOrganizationDefaultSettings_remote(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckTFEOrganizationExists( "tfe_organization.foobar", org), - testAccCheckTFEOrganizationDefaultExecutionMode(org, "remote"), + testAccCheckTFEOrganizationDefaultSettings(org, "remote"), ), }, }, }) } -func TestAccTFEOrganizationDefaultExecutionMode_import(t *testing.T) { +func TestAccTFEOrganizationDefaultSettings_import(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ @@ -131,11 +132,11 @@ func TestAccTFEOrganizationDefaultExecutionMode_import(t *testing.T) { CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { - Config: testAccTFEOrganizationDefaultExecutionMode_remote(rInt), + Config: testAccTFEOrganizationDefaultSettings_remote(rInt), }, { - ResourceName: "tfe_organization_default_execution_mode.foobar", + ResourceName: "tfe_organization_default_settings.foobar", ImportState: true, ImportStateVerify: true, }, @@ -143,7 +144,7 @@ func TestAccTFEOrganizationDefaultExecutionMode_import(t *testing.T) { }) } -func testAccCheckTFEOrganizationDefaultExecutionMode(org *tfe.Organization, expectedExecutionMode string) resource.TestCheckFunc { +func testAccCheckTFEOrganizationDefaultSettings(org *tfe.Organization, expectedExecutionMode string) resource.TestCheckFunc { return func(s *terraform.State) error { if org.DefaultExecutionMode != expectedExecutionMode { return fmt.Errorf("default Execution Mode did not match, expected: %s, but was: %s", expectedExecutionMode, org.DefaultExecutionMode) @@ -163,33 +164,33 @@ func testAccCheckTFEOrganizationDefaultAgentPoolIDExists(org *tfe.Organization) } } -func testAccTFEOrganizationDefaultExecutionMode_remote(rInt int) string { +func testAccTFEOrganizationDefaultSettings_remote(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { name = "tst-terraform-%d" email = "admin@company.com" } -resource "tfe_organization_default_execution_mode" "foobar" { +resource "tfe_organization_default_settings" "foobar" { organization = tfe_organization.foobar.name default_execution_mode = "remote" }`, rInt) } -func testAccTFEOrganizationDefaultExecutionMode_local(rInt int) string { +func testAccTFEOrganizationDefaultSettings_local(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { name = "tst-terraform-%d" email = "admin@company.com" } -resource "tfe_organization_default_execution_mode" "foobar" { +resource "tfe_organization_default_settings" "foobar" { organization = tfe_organization.foobar.name default_execution_mode = "local" }`, rInt) } -func testAccTFEOrganizationDefaultExecutionMode_agent(rInt int) string { +func testAccTFEOrganizationDefaultSettings_agent(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { name = "tst-terraform-%d" @@ -201,7 +202,7 @@ resource "tfe_agent_pool" "foobar" { organization = tfe_organization.foobar.name } -resource "tfe_organization_default_execution_mode" "foobar" { +resource "tfe_organization_default_settings" "foobar" { organization = tfe_organization.foobar.name default_execution_mode = "agent" default_agent_pool_id = tfe_agent_pool.foobar.id diff --git a/website/docs/r/tfe_organization_default_execution_mode.html.markdown b/website/docs/r/organization_default_settings.html.markdown similarity index 50% rename from website/docs/r/tfe_organization_default_execution_mode.html.markdown rename to website/docs/r/organization_default_settings.html.markdown index 88448c323..b6e8754d6 100644 --- a/website/docs/r/tfe_organization_default_execution_mode.html.markdown +++ b/website/docs/r/organization_default_settings.html.markdown @@ -1,13 +1,13 @@ --- layout: "tfe" -page_title: "Terraform Enterprise: tfe_organization_default_execution_mode" +page_title: "Terraform Enterprise: tfe_organization_default_settings description: |- - Sets the default execution mode of an organization + Sets the workspace defaults for an organization --- -# tfe_organization_default_execution_mode +# tfe_organization_default_settings -Sets the default execution mode of an organization. This default execution mode will be used as the default execution mode for all workspaces in the organization. +Primarily, this is used to set the default execution mode of an organization. This setting will be used as the default for all workspaces in the organization. ## Example Usage @@ -20,14 +20,20 @@ resource "tfe_organization" "test" { } resource "tfe_agent_pool" "my_agents" { - name = "agent_smiths" + name = "agent_smiths" organization = tfe_organization.test.name } -resource "tfe_organization_default_execution_mode" "org_default" { - organization = tfe_organization.test.name +resource "tfe_organization_default_settings" "org_default" { + organization = tfe_organization.test.name default_execution_mode = "agent" - default_agent_pool_id = tfe_agent_pool.my_agents.id + default_agent_pool_id = tfe_agent_pool.my_agents.id +} + +resource "tfe_workspace" "my_workspace" { + name = "my-workspace" + # Ensures this workspace will inherit the org defaults + depends_on = [tfe_organization_default_settings.org_default] } ``` @@ -37,9 +43,7 @@ The following arguments are supported: * `default_execution_mode` - (Optional) Which [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) to use as the default for all workspaces in the organization. Valid values are `remote`, `local` or`agent`. -* `agent_pool_id` - (Optional) The ID of an agent pool to assign to the workspace. Requires `default_execution_mode` - to be set to `agent`. This value _must not_ be provided if `default_execution_mode` is set to any other value or if `operations` is - provided. +* `default_agent_pool_id` - (Optional) The ID of an agent pool to assign to the workspace. Requires `default_execution_mode` to be set to `agent`. This value _must not_ be provided if `default_execution_mode` is set to any other value. * `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. @@ -49,4 +53,4 @@ Organization default execution mode can be imported; use `` a ```shell terraform import tfe_organization_default_execution_mode.test my-org-name -``` \ No newline at end of file +``` From 04182e5abb98c81eeeb7d6e4dd0adcdbc6604139 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 1 Dec 2023 15:19:22 -0700 Subject: [PATCH 5/8] Update CHANGELOG --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f9b4f13..41b9f5209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,18 @@ -BREAKING CHANGES: -* `r/tfe_workspace`: Default value of the `execution_mode` field now uses the organization's `default_execution_mode`. If no `default_execution_mode` has been set, the default `execution_mode` will be unchanged (i.e. `remote`). +DEPRECATIONS and BREAKING CHANGES: +* `r/tfe_workspace`: `execution_mode` and `agent_pool_id` attributes have been deprecated in favor of a new resource, `tfe_workspace_settings`. Note that these fields no longer compute defaults which is consistent with using a new resource to manage these same settings. What this means in practice is that if you unset `execution_mode` or `agent_pool_id` without also creating a `tfe_workspace_settings`, the setting will no longer revert to the default "remote" mode. To migrate, relocate the `execution_mode` and `agent_pool_id` arguments to `tfe_workspace_settings`. BUG FIXES: * `r/tfe_policy`: Fix the provider ignoring updates to the `query` field, by @skeggse [1108](https://github.com/hashicorp/terraform-provider-tfe/pull/1108) * Fix the undetected change when modifying the `organization` default in the provider configuration by @brandonc [1152](https://github.com/hashicorp/terraform-provider-tfe/issue/1152) +* New resource `r/tfe_workspace_settings`: Can be used to break any circular dependency between `tfe_workspace` and `tfe_agent_pool_allowed_workspaces` by managing the `agent_pool_id` for a Workspace by @brandonc [1159](https://github.com/hashicorp/terraform-provider-tfe/pull/1159) FEATURES: * `d/tfe_registry_module`: Add `vcs_repo.tags` and `vcs_repo.branch` attributes to allow configuration of `publishing_mechanism`. Add `test_config` to support running tests on `branch`-based registry modules, by @hashimoon [1096](https://github.com/hashicorp/terraform-provider-tfe/pull/1096) -* **New Resource**: `r/tfe_organization_default_execution_mode` is a new resource to set the `default_execution_mode` and `default_agent_pool_id` for an organization, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' -* `r/tfe_workspace`: Now uses the organization's `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode`, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' +* **New Resource**: `r/tfe_organization_default_settings` is a new resource to set the `default_execution_mode` and `default_agent_pool_id` for an organization, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' +* **New Resource**: `r/tfe_workspace_settings` Uses the `tfe_organization_default_settings` `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode` by @brandonc [1159](https://github.com/hashicorp/terraform-provider-tfe/pull/1159) ENHANCEMENTS: * `d/tfe_organization`: Make `name` argument optional if configured for the provider, by @tmatilai [1133](https://github.com/hashicorp/terraform-provider-tfe/pull/1133) From 8363b465140eb02aeca56001f5bbc0b385dbd199 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 1 Dec 2023 15:47:58 -0700 Subject: [PATCH 6/8] Document tfe_workspace_settings --- website/docs/d/workspace.html.markdown | 2 +- website/docs/r/workspace.html.markdown | 10 +- website/docs/r/workspace_settings.markdown | 113 +++++++++++++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 website/docs/r/workspace_settings.markdown diff --git a/website/docs/d/workspace.html.markdown b/website/docs/d/workspace.html.markdown index 9bfe672d1..2c1740d1f 100644 --- a/website/docs/d/workspace.html.markdown +++ b/website/docs/d/workspace.html.markdown @@ -60,7 +60,7 @@ In addition to all arguments above, the following attributes are exported: * `trigger_patterns` - List of [glob patterns](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings/vcs#glob-patterns-for-automatic-run-triggering) that describe the files Terraform Cloud monitors for changes. Trigger patterns are always appended to the root directory of the repository. * `vcs_repo` - Settings for the workspace's VCS repository. * `working_directory` - A relative path that Terraform will execute within. -* `execution_mode` - Indicates the [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) of the workspace. Possible values include `remote`, `local`, or `agent`. +* `execution_mode` - **Deprecated** Indicates the [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) of the workspace. Use the `tfe_workspace_settings` resource instead. * `html_url` - The URL to the browsable HTML overview of the workspace diff --git a/website/docs/r/workspace.html.markdown b/website/docs/r/workspace.html.markdown index b5e8e5358..22c6d9da7 100644 --- a/website/docs/r/workspace.html.markdown +++ b/website/docs/r/workspace.html.markdown @@ -9,6 +9,8 @@ description: |- Provides a workspace resource. +~> **NOTE:** Setting the execution mode and agent pool affinity directly on the workspace has been deprecated in favor of using both [tfe_workspace_settings](workspace_settings) and [tfe_organization_default_settings](organization_default_settings). Use caution when unsetting `execution_mode` because the default value `"remote"` is no longer applied when the `execution_mode` is unset. + ~> **NOTE:** Using `global_remote_state` or `remote_state_consumer_ids` requires using the provider with Terraform Cloud or an instance of Terraform Enterprise at least as recent as v202104-1. ## Example Usage @@ -54,22 +56,20 @@ resource "tfe_workspace" "parent" { oauth_token_id = tfe_oauth_client.test.oauth_token_id } } +``` ## Argument Reference The following arguments are supported: * `name` - (Required) Name of the workspace. -* `agent_pool_id` - (Optional) **Deprecated** The ID of an agent pool to assign to the workspace. Requires `execution_mode` - to be set to `agent`. This value _must not_ be provided if `execution_mode` is set to any other value or if `operations` is - provided. +* `agent_pool_id` - (Optional) **Deprecated** The ID of an agent pool to assign to the workspace. Use [tfe_workspace_settings](workspace_settings) instead. * `allow_destroy_plan` - (Optional) Whether destroy plans can be queued on the workspace. * `assessments_enabled` - (Optional) Whether to regularly run health assessments such as drift detection on the workspace. Defaults to `false`. * `auto_apply` - (Optional) Whether to automatically apply changes when a Terraform plan is successful. Defaults to `false`. * `auto_apply_run_trigger` - (Optional) Whether to automatically apply changes for runs that were created by run triggers from another workspace. Defaults to `false`. * `description` - (Optional) A description for the workspace. -* `execution_mode` - (Optional) **Deprecated** Which [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) - to use. Using Terraform Cloud, valid values are `remote`, `local` or `agent`. Using Terraform Enterprise, only `remote` and `local` execution modes are valid. When set to `local`, the workspace will be used for state storage only. This value _must not_ be provided if `operations` is provided. +* `execution_mode` - (Optional) **Deprecated** Which [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) to use. Use [tfe_workspace_settings](workspace_settings) instead. * `file_triggers_enabled` - (Optional) Whether to filter runs based on the changed files in a VCS push. Defaults to `true`. If enabled, the working directory and trigger prefixes describe a set of paths which must contain changes for a diff --git a/website/docs/r/workspace_settings.markdown b/website/docs/r/workspace_settings.markdown new file mode 100644 index 000000000..880e3ef90 --- /dev/null +++ b/website/docs/r/workspace_settings.markdown @@ -0,0 +1,113 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_workspace_setting" +description: |- + Manages workspace settings. +--- + +# tfe_workspace_settings + +Manages or reads execution mode and agent pool settings for a workspace. If [tfe_organization_default_settings](organization_default_settings.html) are used, those settings may be read using a combination of the read-only `overwrites` argument and the setting itself. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_organization" "test-organization" { + name = "my-org-name" + email = "admin@company.com" +} + +resource "tfe_workspace" "test" { + name = "my-workspace-name" + organization = tfe_organization.test-organization.name +} + +resource "tfe_workspace_settings" "test-settings" { + workspace_id = tfe_workspace.test.id + execution_mode = "local" +} +``` + +With `execution_mode` of `agent`: + +```hcl +resource "tfe_organization" "test-organization" { + name = "my-org-name" + email = "admin@company.com" +} + +resource "tfe_agent_pool" "test-agent-pool" { + name = "my-agent-pool-name" + organization = tfe_organization.test-organization.name +} + +resource "tfe_agent_pool_allowed_workspaces" "test" { + agent_pool_id = tfe_agent_pool.test-agent-pool.id + allowed_workspace_ids = [tfe_workspace.test.id] +} + +resource "tfe_workspace" "test" { + name = "my-workspace-name" + organization = tfe_organization.test-organization.name +} + +resource "tfe_workspace_settings" "test-settings" { + workspace_id = tfe_workspace.test.id + agent_pool_id = tfe_agent_pool.test-agent-pool.id + execution_mode = "agent" +} +``` + +This resource may be used as a data source when no optional arguments are defined: + +```hcl +data "tfe_workspace" "test" { + name = "my-workspace-name" + organization = "my-org-name" +} + +resource "tfe_workspace_settings" "test" { + workspace_id = data.tfe_workspace.test.id +} + +output "workspace-explicit-local-execution" { + value = alltrue([ + tfe_workspace_settings.test.execution_mode == "local", + tfe_workspace_settings.test.overwrites[0]["execution_mode"] + ]) +} +``` + +## Argument Reference + +The following arguments are supported: + +* `workspace_id` - (Required) ID of the workspace. +* `agent_pool_id` - (Optional) The ID of an agent pool to assign to the workspace. Requires `execution_mode` + to be set to `agent`. This value _must not_ be provided if `execution_mode` is set to any other value. +* `execution_mode` - (Optional) Which [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) + to use. Using Terraform Cloud, valid values are `remote`, `local` or `agent`. Defaults your organization's default execution mode, or `remote` if no organization default is set. Using Terraform Enterprise, only `remote` and `local` execution modes are valid. When set to `local`, the workspace will be used for state storage only. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The workspace ID. +* `overwrites` - Can be used to check whether a setting is currently inheriting its value from another resource. + - `execution_mode` - Set to `true` if the execution mode of the workspace is being determined by the setting on the workspace itself. It will be `false` if the execution mode is inherited from another resource (e.g. the organization's default execution mode) + - `agent_pool` - Set to `true` if the agent pool of the workspace is being determined by the setting on the workspace itself. It will be `false` if the agent pool is inherited from another resource (e.g. the organization's default agent pool) + +## Import + +Workspaces can be imported; use `` or `/` as the +import ID. For example: + +```shell +terraform import tfe_workspace_settings.test ws-CH5in3chf8RJjrVd +``` + +```shell +terraform import tfe_workspace_settings.test my-org-name/my-wkspace-name +``` From bab158be6e0e092509e5a36ba19841d2172ae056 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Tue, 5 Dec 2023 15:17:15 -0700 Subject: [PATCH 7/8] Ensure support for TFE when unsetting execution_mode Because organization defaults do not apply to older versions of TFE, the client needs to set this value to the previous default, "remote" --- .../resource_tfe_workspace_settings.go | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/internal/provider/resource_tfe_workspace_settings.go b/internal/provider/resource_tfe_workspace_settings.go index 118a19d29..f8ab6653b 100644 --- a/internal/provider/resource_tfe_workspace_settings.go +++ b/internal/provider/resource_tfe_workspace_settings.go @@ -36,7 +36,8 @@ var overwritesElementType = types.ObjectType{ } type workspaceSettings struct { - config ConfiguredClient + config ConfiguredClient + supportsOverwrites bool } type modelWorkspaceSettings struct { @@ -117,6 +118,11 @@ func (m revertOverwritesIfExecutionModeUnset) PlanModifyList(ctx context.Context resp.Diagnostics.Append(req.Config.Get(ctx, &configured)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + // Check if overwrites are supported by the platform + if state.Overwrites.IsNull() { + return + } + overwritesState := make([]modelOverwrites, 1) state.Overwrites.ElementsAs(ctx, &overwritesState, true) @@ -152,20 +158,26 @@ func (m unknownIfExecutionModeUnset) PlanModifyString(ctx context.Context, req p resp.Diagnostics.Append(req.Config.Get(ctx, &configured)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - overwritesState := make([]modelOverwrites, 1) - state.Overwrites.ElementsAs(ctx, &overwritesState, true) + if !state.Overwrites.IsNull() { + // Normal operation + overwritesState := make([]modelOverwrites, 1) + state.Overwrites.ElementsAs(ctx, &overwritesState, true) - if configured.ExecutionMode.IsNull() && overwritesState[0].ExecutionMode.ValueBool() { - resp.PlanValue = types.StringUnknown() + if configured.ExecutionMode.IsNull() && overwritesState[0].ExecutionMode.ValueBool() { + resp.PlanValue = types.StringUnknown() + } + } else if req.Path.Equal(path.Root("execution_mode")) { + // TFE does not support overwrites so default the execution mode to "remote" + resp.PlanValue = types.StringValue("remote") } } func (m unknownIfExecutionModeUnset) Description(_ context.Context) string { - return "Resets execution_mode to \"remote\" if it is unset" + return "Resets execution_mode to an unknown value if it is unset" } func (m unknownIfExecutionModeUnset) MarkdownDescription(_ context.Context) string { - return "Resets execution_mode to \"remote\" if it is unset" + return "Resets execution_mode to an unknown value if it is unset" } func (r *workspaceSettings) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -237,7 +249,7 @@ func (r *workspaceSettings) Schema(ctx context.Context, req resource.SchemaReque } // workspaceSettingsModelFromTFEWorkspace builds a resource model from the TFE model -func workspaceSettingsModelFromTFEWorkspace(ws *tfe.Workspace) *modelWorkspaceSettings { +func (r *workspaceSettings) workspaceSettingsModelFromTFEWorkspace(ws *tfe.Workspace) *modelWorkspaceSettings { result := modelWorkspaceSettings{ ID: types.StringValue(ws.ID), WorkspaceID: types.StringValue(ws.ID), @@ -248,24 +260,20 @@ func workspaceSettingsModelFromTFEWorkspace(ws *tfe.Workspace) *modelWorkspaceSe result.AgentPoolID = types.StringValue(ws.AgentPool.ID) } - settingsModel := modelOverwrites{ - ExecutionMode: types.BoolValue(false), - AgentPool: types.BoolValue(false), - } - - if ws.SettingOverwrites != nil { - settingsModel = modelOverwrites{ + result.Overwrites = types.ListNull(overwritesElementType) + if r.supportsOverwrites = ws.SettingOverwrites != nil; r.supportsOverwrites { + settingsModel := modelOverwrites{ ExecutionMode: types.BoolValue(*ws.SettingOverwrites.ExecutionMode), AgentPool: types.BoolValue(*ws.SettingOverwrites.AgentPool), } - } - listOverwrites, diags := types.ListValueFrom(ctx, overwritesElementType, []modelOverwrites{settingsModel}) - if diags.HasError() { - panic("Could not build list value from slice of models. This should not be possible unless the model breaks reflection rules.") - } + listOverwrites, diags := types.ListValueFrom(ctx, overwritesElementType, []modelOverwrites{settingsModel}) + if diags.HasError() { + panic("Could not build list value from slice of models. This should not be possible unless the model breaks reflection rules.") + } - result.Overwrites = listOverwrites + result.Overwrites = listOverwrites + } return &result } @@ -296,7 +304,7 @@ func (r *workspaceSettings) readSettings(ctx context.Context, workspaceID string return nil, fmt.Errorf("couldn't read workspace %s: %s", workspaceID, err.Error()) } - return workspaceSettingsModelFromTFEWorkspace(ws), nil + return r.workspaceSettingsModelFromTFEWorkspace(ws), nil } func (r *workspaceSettings) updateSettings(ctx context.Context, data *modelWorkspaceSettings, state *tfsdk.State) error { @@ -309,13 +317,17 @@ func (r *workspaceSettings) updateSettings(ctx context.Context, data *modelWorks }, } - if executionMode := data.ExecutionMode.ValueString(); executionMode != "" { + executionMode := data.ExecutionMode.ValueString() + if executionMode != "" { updateOptions.ExecutionMode = tfe.String(executionMode) updateOptions.SettingOverwrites.ExecutionMode = tfe.Bool(true) updateOptions.SettingOverwrites.AgentPool = tfe.Bool(true) agentPoolID := data.AgentPoolID.ValueString() // may be empty updateOptions.AgentPoolID = tfe.String(agentPoolID) + } else if executionMode == "" && data.Overwrites.IsNull() { + // Not supported by TFE + updateOptions.ExecutionMode = tfe.String("remote") } ws, err := r.config.Client.Workspaces.UpdateByID(ctx, workspaceID, updateOptions) From 843759294ce47e81769b75f66b87d094b5d6e0fc Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Tue, 5 Dec 2023 15:18:05 -0700 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b9f5209..f5b710331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ BUG FIXES: FEATURES: * `d/tfe_registry_module`: Add `vcs_repo.tags` and `vcs_repo.branch` attributes to allow configuration of `publishing_mechanism`. Add `test_config` to support running tests on `branch`-based registry modules, by @hashimoon [1096](https://github.com/hashicorp/terraform-provider-tfe/pull/1096) * **New Resource**: `r/tfe_organization_default_settings` is a new resource to set the `default_execution_mode` and `default_agent_pool_id` for an organization, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' -* **New Resource**: `r/tfe_workspace_settings` Uses the `tfe_organization_default_settings` `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode` by @brandonc [1159](https://github.com/hashicorp/terraform-provider-tfe/pull/1159) +* **New Resource**: `r/tfe_workspace_settings` Uses the `tfe_organization_default_settings` `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode` by @brandonc and @laurenolivia [1159](https://github.com/hashicorp/terraform-provider-tfe/pull/1159) ENHANCEMENTS: * `d/tfe_organization`: Make `name` argument optional if configured for the provider, by @tmatilai [1133](https://github.com/hashicorp/terraform-provider-tfe/pull/1133)