diff --git a/go.mod b/go.mod index 69a406c8c..555015a1d 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-slug v0.16.4 - github.com/hashicorp/go-tfe v1.75.0 + github.com/hashicorp/go-tfe v1.76.0 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.23.0 // indirect @@ -30,7 +30,7 @@ require ( golang.org/x/oauth2 v0.25.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/time v0.10.0 // indirect google.golang.org/protobuf v1.36.4 // indirect ) @@ -42,7 +42,7 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-uuid v1.0.3 - github.com/hashicorp/jsonapi v1.3.2 + github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.22.0 // indirect github.com/hashicorp/terraform-json v0.24.0 // indirect diff --git a/go.sum b/go.sum index fe16febfa..bee37c832 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/hashicorp/go-slug v0.16.4 h1:kI0mOUVjbBsyocwO29pZIQzzkBnfQNdU4eqlUpNd github.com/hashicorp/go-slug v0.16.4/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= github.com/hashicorp/go-tfe v1.75.0 h1:7dixINN0rOmNo4Vpe/6aVCbF0j4bQpSVzw64CSLBTe8= github.com/hashicorp/go-tfe v1.75.0/go.mod h1:IyJtCSk6TxidVAWcZeEgFzc/6fDDjinf21wyaBBc2mg= +github.com/hashicorp/go-tfe v1.76.0 h1:wI13qPREEMKkHISn4Sg4V4YDx/T7FHyRWfSnbq70vIo= +github.com/hashicorp/go-tfe v1.76.0/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -87,6 +89,8 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= +github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e h1:xwy/1T0cxHWaLx2MM0g4BlaQc1BXn/9835mPrBqwSPU= +github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64= @@ -230,6 +234,8 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index cd0e1c15c..762f697f3 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -115,7 +115,6 @@ func Provider() *schema.Provider { "tfe_oauth_client": resourceTFEOAuthClient(), "tfe_opa_version": resourceTFEOPAVersion(), "tfe_organization": resourceTFEOrganization(), - "tfe_organization_default_settings": resourceTFEOrganizationDefaultSettings(), "tfe_organization_membership": resourceTFEOrganizationMembership(), "tfe_organization_module_sharing": resourceTFEOrganizationModuleSharing(), "tfe_organization_token": resourceTFEOrganizationToken(), diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index 1b2663869..d00f9bc79 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -132,6 +132,7 @@ func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewAuditTrailTokenResource, + NewOrganizationDefaultSettings, NewOrganizationRunTaskGlobalSettingsResource, NewOrganizationRunTaskResource, NewRegistryGPGKeyResource, diff --git a/internal/provider/resource_tfe_organization_default_settings.go b/internal/provider/resource_tfe_organization_default_settings.go index 4462d5007..22e182493 100644 --- a/internal/provider/resource_tfe_organization_default_settings.go +++ b/internal/provider/resource_tfe_organization_default_settings.go @@ -1,11 +1,6 @@ // 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 ( @@ -15,159 +10,320 @@ import ( "log" 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/jsonapi" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" ) -func resourceTFEOrganizationDefaultSettings() *schema.Resource { - return &schema.Resource{ - Create: resourceTFEOrganizationDefaultSettingsCreate, - Read: resourceTFEOrganizationDefaultSettingsRead, - Delete: resourceTFEOrganizationDefaultSettingsDelete, - Importer: &schema.ResourceImporter{ - StateContext: resourceTFEOrganizationDefaultSettingsImporter, - }, +const ( + RemoteExecutionMode = "remote" + LocalExecutionMode = "local" + AgentExecutionMode = "agent" - CustomizeDiff: customizeDiffIfProviderDefaultOrganizationChanged, + DefaultExecutionMode = RemoteExecutionMode +) - Schema: map[string]*schema.Schema{ - "organization": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, +var ( + _ resource.Resource = (*resourceTFEOrganizationDefaultSettings)(nil) + _ resource.ResourceWithConfigure = (*resourceTFEOrganizationDefaultSettings)(nil) + _ resource.ResourceWithImportState = (*resourceTFEOrganizationDefaultSettings)(nil) + _ resource.ResourceWithModifyPlan = (*resourceTFEOrganizationDefaultSettings)(nil) + + ValidExecutionModes = []string{ + AgentExecutionMode, + LocalExecutionMode, + RemoteExecutionMode, + } +) + +func NewOrganizationDefaultSettings() resource.Resource { + return &resourceTFEOrganizationDefaultSettings{} +} + +type resourceTFEOrganizationDefaultSettings struct { + config ConfiguredClient +} + +type modelTFEOrganizationDefaultSettings struct { + Organization types.String `tfsdk:"organization"` + DefaultExecutionMode types.String `tfsdk:"default_execution_mode"` + DefaultAgentPoolID types.String `tfsdk:"default_agent_pool_id"` + DefaultProjectID types.String `tfsdk:"default_project_id"` +} + +func modelFromTFEOrganization(v *tfe.Organization) modelTFEOrganizationDefaultSettings { + model := modelTFEOrganizationDefaultSettings{ + Organization: types.StringValue(v.Name), + DefaultExecutionMode: types.StringValue(v.DefaultExecutionMode), + } + + if v.DefaultAgentPool != nil { + model.DefaultAgentPoolID = types.StringValue(v.DefaultAgentPool.ID) + } + + if v.DefaultProject != nil { + model.DefaultProjectID = types.StringValue(v.DefaultProject.ID) + } + + return model +} + +// Configure implements resource.ResourceWithConfigure +func (r *resourceTFEOrganizationDefaultSettings) 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 +} + +// Metadata implements resource.Resource +func (r *resourceTFEOrganizationDefaultSettings) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_organization_default_settings" +} + +// Schema implements resource.Resource +func (r *resourceTFEOrganizationDefaultSettings) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "organization": schema.StringAttribute{ + Description: "The name of the organization.", + Optional: true, + Computed: true, }, - "default_execution_mode": { - Type: schema.TypeString, + "default_execution_mode": schema.StringAttribute{ Required: true, - ValidateFunc: validation.StringInSlice( - []string{ - "agent", - "local", - "remote", - }, - false, - ), - ForceNew: true, + Validators: []validator.String{ + stringvalidator.OneOf(ValidExecutionModes...), + }, }, - "default_agent_pool_id": { - Type: schema.TypeString, + "default_agent_pool_id": schema.StringAttribute{ Optional: true, - ForceNew: true, + }, + + "default_project_id": schema.StringAttribute{ + Optional: true, + Computed: true, }, }, } } -func resourceTFEOrganizationDefaultSettingsCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) +// Create implements resource.Resource +func (r *resourceTFEOrganizationDefaultSettings) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan data + var data modelTFEOrganizationDefaultSettings + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } - // Get the organization name. - organization, err := config.schemaOrDefaultOrganization(d) - if err != nil { - return fmt.Errorf("error getting organization name: %w", err) + // Get org name or default + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + // Create options struct + options := tfe.OrganizationUpdateOptions{} + + if !data.DefaultExecutionMode.IsNull() { + options.DefaultExecutionMode = data.DefaultExecutionMode.ValueStringPointer() } - // If the "default_agent_pool_id" was provided, get the agent pool - var agentPool *tfe.AgentPool - if v, ok := d.GetOk("default_agent_pool_id"); ok && v.(string) != "" { - agentPool = &tfe.AgentPool{ - ID: v.(string), + if !data.DefaultAgentPoolID.IsNull() { + // Get the referenced agent pool + agentPool, err := r.config.Client.AgentPools.Read(ctx, data.DefaultAgentPoolID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to read agent pool", err.Error()) + return } + + options.DefaultAgentPool = agentPool } - defaultExecutionMode := "" - if v, ok := d.GetOk("default_execution_mode"); ok { - defaultExecutionMode = v.(string) - } else { - return fmt.Errorf("default_execution_mode was missing from tfstate, please create an issue to report this error") + if !data.DefaultProjectID.IsNull() && !data.DefaultProjectID.IsUnknown() { + project, err := r.config.Client.Projects.Read(ctx, data.DefaultProjectID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to read project", err.Error()) + return + } + + options.DefaultProject = jsonapi.NewNullableRelationshipWithValue(project) } - // set organization default execution mode - _, err = config.Client.Organizations.Update(context.Background(), organization, tfe.OrganizationUpdateOptions{ - DefaultExecutionMode: tfe.String(defaultExecutionMode), - DefaultAgentPool: agentPool, - }) + o, err := r.config.Client.Organizations.Update(ctx, orgName, options) if err != nil { - return fmt.Errorf("error setting default execution mode of organization %s: %w", d.Id(), err) + resp.Diagnostics.AddError("Unable to update organization default settings", err.Error()) + return } - d.SetId(organization) + result := modelFromTFEOrganization(o) - return resourceTFEOrganizationDefaultSettingsRead(d, meta) + // Write the data back to the resource + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) } -func resourceTFEOrganizationDefaultSettingsRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) +// Update implements resource.Resource +func (r *resourceTFEOrganizationDefaultSettings) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Read Terraform config data + // Read Terraform plan data + var plan modelTFEOrganizationDefaultSettings + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform state data + var state modelTFEOrganizationDefaultSettings + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } - log.Printf("[DEBUG] Read the organization: %s", d.Id()) - organization, err := config.Client.Organizations.Read(ctx, d.Id()) - if err != nil { - if errors.Is(err, tfe.ErrResourceNotFound) { - log.Printf("[DEBUG] organization %s no longer exists", d.Id()) - d.SetId("") - return nil + // Get org name or default + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + // Create options struct + options := tfe.OrganizationUpdateOptions{} + + if !plan.DefaultExecutionMode.IsNull() { + options.DefaultExecutionMode = plan.DefaultExecutionMode.ValueStringPointer() + } + + if !plan.DefaultAgentPoolID.IsNull() { + options.DefaultAgentPool = &tfe.AgentPool{ + ID: plan.DefaultAgentPoolID.ValueString(), } - return fmt.Errorf("error reading organization %s: %w", d.Id(), err) } - defaultExecutionMode := "" - if v, ok := d.GetOk("default_execution_mode"); ok { - defaultExecutionMode = v.(string) - } else { - return fmt.Errorf("default_execution_mode was missing from tfstate, please create an issue to report this error") + // Check if an explicit null is being set for default project id + // This has the effect of removing the relationship + if plan.DefaultProjectID.IsNull() && !plan.DefaultProjectID.IsUnknown() { + options.DefaultProject = jsonapi.NewNullNullableRelationship[*tfe.Project]() + } else if !plan.DefaultProjectID.IsNull() && !plan.DefaultProjectID.IsUnknown() { + project, err := r.config.Client.Projects.Read(ctx, plan.DefaultProjectID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to read project", err.Error()) + return + } + options.DefaultProject = jsonapi.NewNullableRelationshipWithValue(project) } - if organization.DefaultExecutionMode != defaultExecutionMode { - // set id to empty string so that the provider knows it needs to set the default execution mode again - d.SetId("") + + o, err := r.config.Client.Organizations.Update(ctx, orgName, options) + if err != nil { + resp.Diagnostics.AddError("Unable to update organization default settings", err.Error()) + return } - return nil + result := modelFromTFEOrganization(o) + + // Write the data back to the resource + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) } -func resourceTFEOrganizationDefaultSettingsDelete(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) +// Read implements resource.Resource +func (r *resourceTFEOrganizationDefaultSettings) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform state data + var data modelTFEOrganizationDefaultSettings + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } - // Get the organization name. - organization, err := config.schemaOrDefaultOrganization(d) - if err != nil { - return fmt.Errorf("error getting organization name: %w", err) + // Get org name or default + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.State, &orgName)...) + if resp.Diagnostics.HasError() { + return } - log.Printf("[DEBUG] Reseting default execution mode of organization: %s", organization) - // reset organization default execution mode - _, err = config.Client.Organizations.Update(context.Background(), organization, tfe.OrganizationUpdateOptions{ - DefaultExecutionMode: tfe.String("remote"), - DefaultAgentPool: nil, - }) + // Get organization + o, err := r.config.Client.Organizations.Read(ctx, orgName) if err != nil { - return fmt.Errorf("error updating organization default execution mode: %w", err) + if errors.Is(err, tfe.ErrResourceNotFound) { + resp.Diagnostics.AddError("Organization not found", err.Error()) + return + } + + resp.Diagnostics.AddError("Unable to read organization", err.Error()) + return } - return nil + result := modelFromTFEOrganization(o) + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) } -func resourceTFEOrganizationDefaultSettingsImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - config := meta.(ConfiguredClient) +// Delete implements resource.Resource +func (r *resourceTFEOrganizationDefaultSettings) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Read Terraform state data + var data modelTFEOrganizationDefaultSettings + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Get org name or default + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.State, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + // Create options struct with system defaults + options := tfe.OrganizationUpdateOptions{ + DefaultExecutionMode: tfe.String(DefaultExecutionMode), + DefaultAgentPool: nil, + } - log.Printf("[DEBUG] Read the organization: %s", d.Id()) - organization, err := config.Client.Organizations.Read(ctx, d.Id()) + // Reset organization settings + log.Printf("[DEBUG] Reseting default execution mode of organization: %s", orgName) + o, err := r.config.Client.Organizations.Update(ctx, orgName, options) if err != nil { - if errors.Is(err, tfe.ErrResourceNotFound) { - log.Printf("[DEBUG] organization %s no longer exists", d.Id()) - d.SetId("") - } - return nil, fmt.Errorf("error reading organization %s: %w", d.Id(), err) + resp.Diagnostics.AddError("Unable to update organization default settings", err.Error()) + return } - // Set the organization field. - d.Set("organization", d.Id()) - d.Set("default_execution_mode", organization.DefaultExecutionMode) - if organization.DefaultAgentPool != nil { - d.Set("default_agent_pool_id", organization.DefaultAgentPool.ID) + result := modelFromTFEOrganization(o) + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +// Implement ModifyPlan to force updates when explicit null has been set for +// default project id +func (r *resourceTFEOrganizationDefaultSettings) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + // Read config data + var config modelTFEOrganizationDefaultSettings + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // Check if an explicit null is being set for default project id + // This has the effect of removing the relationship + if config.DefaultProjectID.IsNull() && !config.DefaultProjectID.IsUnknown() { + resp.Plan.SetAttribute(ctx, path.Root("default_project_id"), types.StringNull()) } +} - return []*schema.ResourceData{d}, nil +// ImportState implements resource.ResourceWithImportState +func (r *resourceTFEOrganizationDefaultSettings) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization"), req.ID)...) } diff --git a/internal/provider/resource_tfe_organization_default_settings_test.go b/internal/provider/resource_tfe_organization_default_settings_test.go index 6d7ab14ce..b9c37144d 100644 --- a/internal/provider/resource_tfe_organization_default_settings_test.go +++ b/internal/provider/resource_tfe_organization_default_settings_test.go @@ -21,9 +21,9 @@ func TestAccTFEOrganizationDefaultSettings_remote(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckTFEOrganizationDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { Config: testAccTFEOrganizationDefaultSettings_remote(rInt), @@ -42,9 +42,9 @@ func TestAccTFEOrganizationDefaultSettings_local(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckTFEOrganizationDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { Config: testAccTFEOrganizationDefaultSettings_local(rInt), @@ -63,9 +63,9 @@ func TestAccTFEOrganizationDefaultSettings_agent(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckTFEOrganizationDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { Config: testAccTFEOrganizationDefaultSettings_agent(rInt), @@ -85,9 +85,9 @@ func TestAccTFEOrganizationDefaultSettings_update(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckTFEOrganizationDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { Config: testAccTFEOrganizationDefaultSettings_remote(rInt), @@ -130,18 +130,47 @@ func TestAccTFEOrganizationDefaultSettings_import(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckTFEOrganizationDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationDestroy, Steps: []resource.TestStep{ { Config: testAccTFEOrganizationDefaultSettings_remote(rInt), }, { - ResourceName: "tfe_organization_default_settings.foobar", - ImportState: true, - ImportStateVerify: true, + ResourceName: "tfe_organization_default_settings.foobar", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "organization", + ImportStateId: fmt.Sprintf("tst-terraform-%d", rInt), + }, + }, + }) +} + +func TestAccTFEOrganizationDefaultSettings_defaultProject(t *testing.T) { + org := &tfe.Organization{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEOrganizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEOrganizationDefaultSettings_defaultProject(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEOrganizationExists("tfe_organization.foobar", org), + testAccCheckTFEOrganizationDefaultProjectExists(org), + ), + }, + { + Config: testAccTFEOrganizationDefaultSettings_removeDefaultProject(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEOrganizationExists("tfe_organization.foobar", org), + testAccCheckTFEOrganizationDefaultProjectDoesNotExist(org), + ), }, }, }) @@ -167,6 +196,26 @@ func testAccCheckTFEOrganizationDefaultAgentPoolIDExists(org *tfe.Organization) } } +func testAccCheckTFEOrganizationDefaultProjectExists(org *tfe.Organization) resource.TestCheckFunc { + return func(s *terraform.State) error { + if org.DefaultProject == nil { + return errors.New("default project was not set") + } + + return nil + } +} + +func testAccCheckTFEOrganizationDefaultProjectDoesNotExist(org *tfe.Organization) resource.TestCheckFunc { + return func(s *terraform.State) error { + if org.DefaultProject != nil { + return errors.New("default project was set, but expected nil") + } + + return nil + } +} + func testAccTFEOrganizationDefaultSettings_remote(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { @@ -187,6 +236,11 @@ resource "tfe_organization" "foobar" { email = "admin@company.com" } +resource "tfe_agent_pool" "foobar" { + name = "agent-pool-test" + organization = tfe_organization.foobar.name +} + resource "tfe_organization_default_settings" "foobar" { organization = tfe_organization.foobar.name default_execution_mode = "local" @@ -211,3 +265,41 @@ resource "tfe_organization_default_settings" "foobar" { default_agent_pool_id = tfe_agent_pool.foobar.id }`, rInt) } + +func testAccTFEOrganizationDefaultSettings_defaultProject(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_project" "foobar" { + name = "project-test" + organization = tfe_organization.foobar.name +} + +resource "tfe_organization_default_settings" "foobar" { + organization = tfe_organization.foobar.name + default_execution_mode = "remote" + default_project_id = tfe_project.foobar.id +}`, rInt) +} + +func testAccTFEOrganizationDefaultSettings_removeDefaultProject(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_project" "foobar" { + name = "project-test" + organization = tfe_organization.foobar.name +} + +resource "tfe_organization_default_settings" "foobar" { + organization = tfe_organization.foobar.name + default_execution_mode = "remote" + default_project_id = null +}`, rInt) +} diff --git a/website/docs/r/organization_default_settings.html.markdown b/website/docs/r/organization_default_settings.html.markdown index 04bdee476..e473e5ca8 100644 --- a/website/docs/r/organization_default_settings.html.markdown +++ b/website/docs/r/organization_default_settings.html.markdown @@ -1,6 +1,6 @@ --- layout: "tfe" -page_title: "Terraform Enterprise: tfe_organization_default_settings +page_title: "Terraform Enterprise: tfe_organization_default_settings" description: |- Sets the workspace defaults for an organization --- @@ -24,10 +24,16 @@ resource "tfe_agent_pool" "my_agents" { organization = tfe_organization.test.name } +resource "tfe_project" "my_project" { + name = "my-project" + 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_project_id = tfe_project.my_project.id } resource "tfe_workspace" "my_workspace" { @@ -46,12 +52,13 @@ 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`. * `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. +* `default_project_id` - (Optional) The ID of a project to assign as the default project for the organization. * `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. ## Import -Organization default execution mode can be imported; use `` as the import ID. For example: +Organization default execution mode and default project can be imported; use `` as the import ID. For example: ```shell terraform import tfe_organization_default_settings.test my-org-name