diff --git a/internal/provider/data_source_oauth_client.go b/internal/provider/data_source_oauth_client.go index fc24d1fd5..ff92d9a49 100644 --- a/internal/provider/data_source_oauth_client.go +++ b/internal/provider/data_source_oauth_client.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 ( @@ -14,150 +9,250 @@ import ( "time" "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + // Compile-time proof of interface implementation. + _ datasource.DataSource = &dataSourceTFEOAuthClient{} + _ datasource.DataSourceWithConfigure = &dataSourceTFEOAuthClient{} ) -func dataSourceTFEOAuthClient() *schema.Resource { - return &schema.Resource{ - Read: dataSourceTFEOAuthClientRead, - Schema: map[string]*schema.Schema{ - "oauth_client_id": { - Type: schema.TypeString, - Optional: true, - AtLeastOneOf: []string{"oauth_client_id", "name", "service_provider"}, +func NewOAuthClientDataSource() datasource.DataSource { + return &dataSourceTFEOAuthClient{} +} + +type dataSourceTFEOAuthClient struct { + config ConfiguredClient +} + +type modelDataSourceTFEOAuthClient struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Organization types.String `tfsdk:"organization"` + OAuthClientID types.String `tfsdk:"oauth_client_id"` + ServiceProvider types.String `tfsdk:"service_provider"` + APIURL types.String `tfsdk:"api_url"` + CallbackURL types.String `tfsdk:"callback_url"` + CreatedAt types.String `tfsdk:"created_at"` + HTTPURL types.String `tfsdk:"http_url"` + OAuthTokenID types.String `tfsdk:"oauth_token_id"` + ServiceProviderDisplayName types.String `tfsdk:"service_provider_display_name"` + OrganizationScoped types.Bool `tfsdk:"organization_scoped"` + ProjectIDs types.Set `tfsdk:"project_ids"` +} + +func modelDataSourceFromTFEOAuthClient(ctx context.Context, c *tfe.OAuthClient) (*modelDataSourceTFEOAuthClient, diag.Diagnostics) { + var diags diag.Diagnostics + m := modelDataSourceTFEOAuthClient{ + ID: types.StringValue(c.ID), + Name: types.StringPointerValue(c.Name), + Organization: types.StringValue(c.Organization.Name), + OAuthClientID: types.StringValue(c.ID), + ServiceProvider: types.StringValue(string(c.ServiceProvider)), + APIURL: types.StringValue(c.APIURL), + CallbackURL: types.StringValue(c.CallbackURL), + CreatedAt: types.StringValue(c.CreatedAt.Format(time.RFC3339)), + OrganizationScoped: types.BoolPointerValue(c.OrganizationScoped), + HTTPURL: types.StringValue(c.HTTPURL), + ServiceProviderDisplayName: types.StringValue(c.ServiceProviderName), + } + + // Set project IDs + projectIDs := make([]string, len(c.Projects)) + for i, project := range c.Projects { + projectIDs[i] = project.ID + } + + projectIDSet, diags := types.SetValueFrom(ctx, types.StringType, projectIDs) + if diags.HasError() { + return nil, diags + } + m.ProjectIDs = projectIDSet + + // Set OAuth token ID + switch len(c.OAuthTokens) { + case 0: + m.OAuthTokenID = types.StringValue("") + case 1: + m.OAuthTokenID = types.StringValue(c.OAuthTokens[0].ID) + default: + diags.AddError("Error parsing API result", fmt.Sprintf("unexpected number of OAuth tokens: %d", len(c.OAuthTokens))) + } + + return &m, diags +} + +// Configure implements datasource.ResourceWithConfigure +func (d *dataSourceTFEOAuthClient) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.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), + ) + } + d.config = client +} + +// Metadata implements datasource.Resource +func (d *dataSourceTFEOAuthClient) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_oauth_client" +} + +// Schema implements datasource.Resource +func (d *dataSourceTFEOAuthClient) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Service-generated identifier for the variable", }, - "organization": { - Type: schema.TypeString, - Optional: true, + + "organization": schema.StringAttribute{ + Description: "Name of the organization", + Computed: true, + Optional: true, }, - "name": { - Type: schema.TypeString, - Optional: true, - RequiredWith: []string{"organization"}, + + "name": schema.StringAttribute{ + Description: "Display name for the OAuth Client", + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf( + path.MatchRoot("oauth_client_id"), + path.MatchRoot("service_provider"), + ), + }, }, - "service_provider": { - Type: schema.TypeString, - Optional: true, - RequiredWith: []string{"organization"}, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice( - []string{ - string(tfe.ServiceProviderAzureDevOpsServer), - string(tfe.ServiceProviderAzureDevOpsServices), - string(tfe.ServiceProviderBitbucket), - string(tfe.ServiceProviderBitbucketDataCenter), - string(tfe.ServiceProviderBitbucketServer), - string(tfe.ServiceProviderBitbucketServerLegacy), + + "oauth_client_id": schema.StringAttribute{ + Description: "OAuth Token ID for the OAuth Client", + Optional: true, + }, + + "service_provider": schema.StringAttribute{ + Description: "The VCS provider being connected with", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf( string(tfe.ServiceProviderGithub), string(tfe.ServiceProviderGithubEE), string(tfe.ServiceProviderGitlab), string(tfe.ServiceProviderGitlabCE), string(tfe.ServiceProviderGitlabEE), - }, - false, - )), + string(tfe.ServiceProviderBitbucket), + string(tfe.ServiceProviderBitbucketServer), + string(tfe.ServiceProviderBitbucketServerLegacy), + string(tfe.ServiceProviderBitbucketDataCenter), + string(tfe.ServiceProviderAzureDevOpsServer), + string(tfe.ServiceProviderAzureDevOpsServices), + ), + }, }, - "service_provider_display_name": { - Type: schema.TypeString, - Computed: true, + + "api_url": schema.StringAttribute{ + Description: "The base URL of the VCS provider's API", + Computed: true, }, - "api_url": { - Type: schema.TypeString, - Computed: true, + + "callback_url": schema.StringAttribute{ + Description: "The base URL of the VCS provider's API", + Computed: true, }, - "callback_url": { - Type: schema.TypeString, - Computed: true, + + "created_at": schema.StringAttribute{ + Description: "The base URL of the VCS provider's API", + Computed: true, }, - "created_at": { - Type: schema.TypeString, - Computed: true, + + "http_url": schema.StringAttribute{ + Description: "The homepage of the VCS provider", + Computed: true, }, - "http_url": { - Type: schema.TypeString, - Computed: true, + + "oauth_token_id": schema.StringAttribute{ + Description: "OAuth Token ID for the OAuth Client", + Computed: true, }, - "oauth_token_id": { - Type: schema.TypeString, - Computed: true, + + "service_provider_display_name": schema.StringAttribute{ + Description: "The display name of the VCS provider", + Computed: true, }, - "organization_scoped": { - Type: schema.TypeBool, - Computed: true, + + "organization_scoped": schema.BoolAttribute{ + Description: "Whether or not the oauth client is scoped to all projects and workspaces in the organization", + Computed: true, }, - "project_ids": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - Computed: true, + + "project_ids": schema.SetAttribute{ + Description: "The IDs of the projects that the OAuth client is associated with", + Computed: true, + ElementType: types.StringType, }, }, } } -func dataSourceTFEOAuthClientRead(d *schema.ResourceData, meta interface{}) error { - ctx := context.TODO() - config := meta.(ConfiguredClient) +// Read implements datasource.Resource +func (d *dataSourceTFEOAuthClient) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // Load the config into the model + var config modelDataSourceTFEOAuthClient + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // Get the organization name from the data source or provider config + var organization string + resp.Diagnostics.Append(d.config.dataOrDefaultOrganization(ctx, req.Config, &organization)...) + if resp.Diagnostics.HasError() { + return + } + + id := config.OAuthClientID.ValueString() + name := config.Name.ValueString() + serviceProvider := tfe.ServiceProviderType(config.ServiceProvider.ValueString()) - var oc *tfe.OAuthClient var err error + var oc *tfe.OAuthClient + tflog.Debug(ctx, fmt.Sprintf("Read OAuth client: %s", id)) - switch v, ok := d.GetOk("oauth_client_id"); { - case ok: - oc, err = config.Client.OAuthClients.Read(ctx, v.(string)) + if !config.OAuthClientID.IsNull() { + // Read the OAuth client using its ID + oc, err = d.config.Client.OAuthClients.Read(ctx, id) if err != nil { - return fmt.Errorf("Error retrieving OAuth client: %w", err) - } - default: - // search by name or service provider within a specific organization instead - organization, err := config.schemaOrDefaultOrganization(d) - if err != nil { - return err - } - - var name string - var serviceProvider tfe.ServiceProviderType - vName, ok := d.GetOk("name") - if ok { - name = vName.(string) + resp.Diagnostics.AddError("Error reading OAuth client", err.Error()) + return } - vServiceProvider, ok := d.GetOk("service_provider") - if ok { - serviceProvider = tfe.ServiceProviderType(vServiceProvider.(string)) - } - - oc, err = fetchOAuthClientByNameOrServiceProvider(ctx, config.Client, organization, name, serviceProvider) + } else { + // Read the OAuth client using its name or service provider + oc, err = fetchOAuthClientByNameOrServiceProvider(ctx, d.config.Client, organization, name, serviceProvider) if err != nil { - return err + resp.Diagnostics.AddError("Error reading OAuth client", err.Error()) + return } } - d.SetId(oc.ID) - d.Set("oauth_client_id", oc.ID) - d.Set("api_url", oc.APIURL) - d.Set("callback_url", oc.CallbackURL) - d.Set("created_at", oc.CreatedAt.Format(time.RFC3339)) - d.Set("http_url", oc.HTTPURL) - if oc.Name != nil { - d.Set("name", *oc.Name) - } - d.Set("service_provider", oc.ServiceProvider) - d.Set("service_provider_display_name", oc.ServiceProviderName) - d.Set("organization_scoped", oc.OrganizationScoped) - - switch len(oc.OAuthTokens) { - case 0: - d.Set("oauth_token_id", "") - case 1: - d.Set("oauth_token_id", oc.OAuthTokens[0].ID) - default: - return fmt.Errorf("unexpected number of OAuth tokens: %d", len(oc.OAuthTokens)) - } - - var projectIDs []interface{} - for _, project := range oc.Projects { - projectIDs = append(projectIDs, project.ID) + // Load the result into the model + result, diags := modelDataSourceFromTFEOAuthClient(ctx, oc) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return } - d.Set("project_ids", projectIDs) - return nil + // Update state + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) } diff --git a/internal/provider/data_source_oauth_client_test.go b/internal/provider/data_source_oauth_client_test.go index ed8152883..baf526f07 100644 --- a/internal/provider/data_source_oauth_client_test.go +++ b/internal/provider/data_source_oauth_client_test.go @@ -46,7 +46,7 @@ func TestAccTFEOAuthClientDataSource_findByID(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccTFEOAuthClientDataSourcePreCheck(t) }, - ProtoV5ProviderFactories: testAccMuxedProviders, + ProtoV5ProviderFactories: muxedProvidersWithDefaultOrganization("foobar"), Steps: []resource.TestStep{ { Config: testAccTFEOAuthClientDataSourceConfig_findByID(rInt), @@ -137,35 +137,7 @@ func TestAccTFEOAuthClientDataSource_missingParameters(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccTFEOAuthClientDataSourceConfig_missingParameters(rInt), - ExpectError: regexp.MustCompile("one of `name,oauth_client_id,service_provider` must"), - }, - }, - }) -} - -func TestAccTFEOAuthClientDataSource_missingOrgWithName(t *testing.T) { - rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccTFEOAuthClientDataSourcePreCheck(t) }, - ProtoV5ProviderFactories: testAccMuxedProviders, - Steps: []resource.TestStep{ - { - Config: testAccTFEOAuthClientDataSourceConfig_missingOrgWithName(rInt), - ExpectError: regexp.MustCompile("all of `name,organization` must"), - }, - }, - }) -} - -func TestAccTFEOAuthClientDataSource_missingOrgWithServiceProvider(t *testing.T) { - rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccTFEOAuthClientDataSourcePreCheck(t) }, - ProtoV5ProviderFactories: testAccMuxedProviders, - Steps: []resource.TestStep{ - { - Config: testAccTFEOAuthClientDataSourceConfig_missingOrgWithServiceProvider(rInt), - ExpectError: regexp.MustCompile("all of `organization,service_provider` must be"), + ExpectError: regexp.MustCompile("Invalid Attribute Combination"), }, }, }) @@ -332,46 +304,6 @@ data "tfe_oauth_client" "client" { `, rInt, envGithubToken, rInt) } -func testAccTFEOAuthClientDataSourceConfig_missingOrgWithName(rInt int) string { - return fmt.Sprintf(` -resource "tfe_organization" "foobar" { - name = "tst-terraform-%d" - email = "admin@company.com" -} -resource "tfe_oauth_client" "test" { - organization = tfe_organization.foobar.name - api_url = "https://api.github.com" - http_url = "https://github.com" - oauth_token = "%s" - service_provider = "github" -} -data "tfe_oauth_client" "client" { - name = "github" - depends_on = [tfe_oauth_client.test] -} -`, rInt, envGithubToken) -} - -func testAccTFEOAuthClientDataSourceConfig_missingOrgWithServiceProvider(rInt int) string { - return fmt.Sprintf(` -resource "tfe_organization" "foobar" { - name = "tst-terraform-%d" - email = "admin@company.com" -} -resource "tfe_oauth_client" "test" { - organization = tfe_organization.foobar.name - api_url = "https://api.github.com" - http_url = "https://github.com" - oauth_token = "%s" - service_provider = "github" -} -data "tfe_oauth_client" "client" { - service_provider = "github" - depends_on = [tfe_oauth_client.test] -} -`, rInt, envGithubToken) -} - func testAccTFEOAuthClientDataSourceConfig_sameName(rInt int) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 781e80591..1f716076c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -84,7 +84,6 @@ func Provider() *schema.Provider { "tfe_organization": dataSourceTFEOrganization(), "tfe_agent_pool": dataSourceTFEAgentPool(), "tfe_ip_ranges": dataSourceTFEIPRanges(), - "tfe_oauth_client": dataSourceTFEOAuthClient(), "tfe_organization_membership": dataSourceTFEOrganizationMembership(), "tfe_organization_tags": dataSourceTFEOrganizationTags(), "tfe_slug": dataSourceTFESlug(), @@ -110,7 +109,6 @@ func Provider() *schema.Provider { "tfe_agent_pool": resourceTFEAgentPool(), "tfe_agent_pool_allowed_workspaces": resourceTFEAgentPoolAllowedWorkspaces(), "tfe_agent_token": resourceTFEAgentToken(), - "tfe_oauth_client": resourceTFEOAuthClient(), "tfe_opa_version": resourceTFEOPAVersion(), "tfe_organization": resourceTFEOrganization(), "tfe_organization_membership": resourceTFEOrganizationMembership(), diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index 9d34a5107..b54449057 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -131,6 +131,7 @@ func (p *frameworkProvider) Configure(ctx context.Context, req provider.Configur func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ NewNoCodeModuleDataSource, + NewOAuthClientDataSource, NewOrganizationRunTaskDataSource, NewOrganizationRunTaskGlobalSettingsDataSource, NewOutputsDataSource, @@ -149,6 +150,7 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res return []func() resource.Resource{ NewAuditTrailTokenResource, NewDataRetentionPolicyResource, + NewOAuthClient, NewOrganizationDefaultSettings, NewOrganizationRunTaskGlobalSettingsResource, NewOrganizationRunTaskResource, diff --git a/internal/provider/resource_tfe_oauth_client.go b/internal/provider/resource_tfe_oauth_client.go index 59b0de3e2..d62a10a37 100644 --- a/internal/provider/resource_tfe_oauth_client.go +++ b/internal/provider/resource_tfe_oauth_client.go @@ -1,257 +1,470 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// NOTE: This is a legacy resource and should be migrated to the Plugin -// Framework if substantial modifications are planned. See -// docs/new-resources.md if planning to use this code as boilerplate for -// a new resource. - package provider import ( + "context" + "errors" "fmt" - "log" - 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/go-tfe" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + // Compile-time proof of interface implementation. + _ resource.Resource = &resourceTFEOAuthClient{} + _ resource.ResourceWithConfigure = &resourceTFEOAuthClient{} + _ resource.ResourceWithValidateConfig = &resourceTFEOAuthClient{} ) -func resourceTFEOAuthClient() *schema.Resource { - return &schema.Resource{ - Create: resourceTFEOAuthClientCreate, - Read: resourceTFEOAuthClientRead, - Delete: resourceTFEOAuthClientDelete, - Update: resourceTFEOAuthClientUpdate, +func NewOAuthClient() resource.Resource { + return &resourceTFEOAuthClient{} +} + +type resourceTFEOAuthClient struct { + config ConfiguredClient +} + +type modelTFEOAuthClient struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Organization types.String `tfsdk:"organization"` + APIURL types.String `tfsdk:"api_url"` + HTTPURL types.String `tfsdk:"http_url"` + Key types.String `tfsdk:"key"` + OAuthToken types.String `tfsdk:"oauth_token"` + PrivateKey types.String `tfsdk:"private_key"` + Secret types.String `tfsdk:"secret"` + RSAPublicKey types.String `tfsdk:"rsa_public_key"` + ServiceProvider types.String `tfsdk:"service_provider"` + OAuthTokenID types.String `tfsdk:"oauth_token_id"` + AgentPoolID types.String `tfsdk:"agent_pool_id"` + OrganizationScoped types.Bool `tfsdk:"organization_scoped"` +} + +func modelFromTFEOAuthClient(c *tfe.OAuthClient, lastValues map[string]types.String) (*modelTFEOAuthClient, diag.Diagnostics) { + var diags diag.Diagnostics + m := modelTFEOAuthClient{ + ID: types.StringValue(c.ID), + Name: types.StringPointerValue(c.Name), + Organization: types.StringValue(c.Organization.Name), + APIURL: types.StringValue(c.APIURL), + HTTPURL: types.StringValue(c.HTTPURL), + ServiceProvider: types.StringValue(string(c.ServiceProvider)), + OrganizationScoped: types.BoolPointerValue(c.OrganizationScoped), + } + + if oauthToken, ok := lastValues["oauth_token"]; ok { + m.OAuthToken = oauthToken + } + + if privateKey, ok := lastValues["private_key"]; ok { + m.PrivateKey = privateKey + } + + if key, ok := lastValues["key"]; ok { + m.Key = key + } + + if secret, ok := lastValues["secret"]; ok { + m.Secret = secret + } + + if c.AgentPool != nil { + m.AgentPoolID = types.StringValue(c.AgentPool.ID) + } + + if len(c.RSAPublicKey) > 0 { + m.RSAPublicKey = types.StringValue(c.RSAPublicKey) + } + + if len(c.RSAPublicKey) > 0 { + m.RSAPublicKey = types.StringValue(c.RSAPublicKey) + } + + switch len(c.OAuthTokens) { + case 0: + m.OAuthTokenID = types.StringValue("") + case 1: + m.OAuthTokenID = types.StringValue(c.OAuthTokens[0].ID) + default: + diags.AddError("Error parsing API result", fmt.Sprintf("unexpected number of OAuth tokens: %d", len(c.OAuthTokens))) + } + + return &m, diags +} + +// Configure implements resource.ResourceWithConfigure +func (r *resourceTFEOAuthClient) 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 *resourceTFEOAuthClient) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_oauth_client" +} - CustomizeDiff: customizeDiffIfProviderDefaultOrganizationChanged, +// Schema implements resource.Resource +func (r *resourceTFEOAuthClient) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Service-generated identifier for the variable", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, + "name": schema.StringAttribute{ + Description: "Display name for the OAuth Client", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "organization": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, + "organization": schema.StringAttribute{ + Description: "Name of the organization", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "api_url": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "api_url": schema.StringAttribute{ + Description: "The base URL of the VCS provider's API", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "http_url": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "http_url": schema.StringAttribute{ + Description: "The homepage of the VCS provider", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "key": { - Type: schema.TypeString, - ForceNew: true, - Sensitive: true, - Optional: true, + "key": schema.StringAttribute{ + Description: "The OAuth Client key can refer to a Consumer Key, Application Key, or another type of client key for the VCS provider", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "oauth_token": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - ForceNew: true, + "oauth_token": schema.StringAttribute{ + Description: "The OAuth token string for the VCS provider", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "private_key": { - Type: schema.TypeString, - ForceNew: true, - Sensitive: true, - Optional: true, + "private_key": schema.StringAttribute{ + Description: "The text of the private key associated with a Azure DevOps Server account", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "secret": { - Type: schema.TypeString, - ForceNew: true, - Sensitive: true, - Optional: true, + "secret": schema.StringAttribute{ + Description: "The text of the SSH private key associated with a Bitbucket Data Center Application Link", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "rsa_public_key": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - // this field is only for BitBucket Data Center, and requires these other - RequiredWith: []string{"secret", "key"}, + "rsa_public_key": schema.StringAttribute{ + Description: "The text of the SSH public key associated with a Bitbucket Data Center Application Link", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "service_provider": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice( - []string{ - string(tfe.ServiceProviderAzureDevOpsServer), - string(tfe.ServiceProviderAzureDevOpsServices), - string(tfe.ServiceProviderBitbucket), - string(tfe.ServiceProviderBitbucketServer), - string(tfe.ServiceProviderBitbucketDataCenter), + "service_provider": schema.StringAttribute{ + Description: "The VCS provider being connected with", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf( string(tfe.ServiceProviderGithub), string(tfe.ServiceProviderGithubEE), string(tfe.ServiceProviderGitlab), string(tfe.ServiceProviderGitlabCE), string(tfe.ServiceProviderGitlabEE), - }, - false, - ), + string(tfe.ServiceProviderBitbucket), + string(tfe.ServiceProviderBitbucketServer), + string(tfe.ServiceProviderBitbucketServerLegacy), + string(tfe.ServiceProviderBitbucketDataCenter), + string(tfe.ServiceProviderAzureDevOpsServer), + string(tfe.ServiceProviderAzureDevOpsServices), + ), + }, }, - "oauth_token_id": { - Type: schema.TypeString, - Computed: true, + + "oauth_token_id": schema.StringAttribute{ + Description: "OAuth Token ID for the OAuth Client", + Computed: true, }, - "agent_pool_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, + + "agent_pool_id": schema.StringAttribute{ + Description: "An existing agent pool ID within the organization that has Private VCS support enabled", + Optional: true, + Computed: true, }, - "organization_scoped": { - Type: schema.TypeBool, - Optional: true, - Default: true, + + "organization_scoped": schema.BoolAttribute{ + Description: "Whether or not the oauth client is scoped to all projects and workspaces in the organization", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), }, }, } } -func resourceTFEOAuthClientCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) - - // Get the organization and provider. - organization, err := config.schemaOrDefaultOrganization(d) - if err != nil { - return err +// Create implements resource.Resource +func (r *resourceTFEOAuthClient) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Load the plan into the model + var plan modelTFEOAuthClient + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - name := d.Get("name").(string) - privateKey := d.Get("private_key").(string) - rsaPublicKey := d.Get("rsa_public_key").(string) - key := d.Get("key").(string) - secret := d.Get("secret").(string) - serviceProvider := tfe.ServiceProviderType(d.Get("service_provider").(string)) - if serviceProvider == tfe.ServiceProviderAzureDevOpsServer && privateKey == "" { - return fmt.Errorf("private_key is required for service_provider %s", serviceProvider) + var organization string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &organization)...) + if resp.Diagnostics.HasError() { + return } // Create a new options struct. // The tfe.OAuthClientCreateOptions has omitempty for these values, so if it // is empty, then it will be ignored in the create request options := tfe.OAuthClientCreateOptions{ - Name: tfe.String(name), - APIURL: tfe.String(d.Get("api_url").(string)), - HTTPURL: tfe.String(d.Get("http_url").(string)), - OAuthToken: tfe.String(d.Get("oauth_token").(string)), - Key: tfe.String(key), - ServiceProvider: tfe.ServiceProvider(serviceProvider), - OrganizationScoped: tfe.Bool(d.Get("organization_scoped").(bool)), + Name: plan.Name.ValueStringPointer(), + APIURL: plan.APIURL.ValueStringPointer(), + HTTPURL: plan.HTTPURL.ValueStringPointer(), + OAuthToken: plan.OAuthToken.ValueStringPointer(), + Key: plan.Key.ValueStringPointer(), + ServiceProvider: tfe.ServiceProvider(tfe.ServiceProviderType(plan.ServiceProvider.ValueString())), + OrganizationScoped: plan.OrganizationScoped.ValueBoolPointer(), } - if serviceProvider == tfe.ServiceProviderAzureDevOpsServer { - options.PrivateKey = tfe.String(privateKey) + serviceProviderType := tfe.ServiceProviderType(plan.ServiceProvider.ValueString()) + + if serviceProviderType == tfe.ServiceProviderAzureDevOpsServer { + options.PrivateKey = plan.PrivateKey.ValueStringPointer() } - if serviceProvider == tfe.ServiceProviderBitbucketServer || serviceProvider == tfe.ServiceProviderBitbucketDataCenter { - options.RSAPublicKey = tfe.String(rsaPublicKey) - options.Secret = tfe.String(secret) + + if serviceProviderType == tfe.ServiceProviderBitbucketServer || serviceProviderType == tfe.ServiceProviderBitbucketDataCenter { + options.RSAPublicKey = plan.RSAPublicKey.ValueStringPointer() + options.Secret = plan.Secret.ValueStringPointer() } - if serviceProvider == tfe.ServiceProviderBitbucket { - options.Secret = tfe.String(secret) + + if serviceProviderType == tfe.ServiceProviderBitbucket { + options.Secret = plan.Secret.ValueStringPointer() } - if v, ok := d.GetOk("agent_pool_id"); ok && v.(string) != "" { - options.AgentPool = &tfe.AgentPool{ID: *tfe.String(v.(string))} + + if !plan.AgentPoolID.IsNull() { + options.AgentPool = &tfe.AgentPool{ID: plan.AgentPoolID.ValueString()} } - log.Printf("[DEBUG] Create an OAuth client for organization: %s", organization) - oc, err := config.Client.OAuthClients.Create(ctx, organization, options) + tflog.Debug(ctx, fmt.Sprintf("Create an OAuth client for organization: %s", organization)) + oc, err := r.config.Client.OAuthClients.Create(ctx, organization, options) if err != nil { - return fmt.Errorf( - "Error creating OAuth client for organization %s: %w", organization, err) + resp.Diagnostics.AddError("Error creating OAuth client", err.Error()) + return } - d.SetId(oc.ID) + lastValues := map[string]types.String{ + "oauth_token": plan.OAuthToken, + "rsa_public_key": plan.RSAPublicKey, + "key": plan.Key, + "secret": plan.Secret, + } - if len(oc.OAuthTokens) > 0 { - d.Set("oauth_token_id", oc.OAuthTokens[0].ID) - } else { - d.Set("oauth_token_id", "") + // Load the result into the model + result, diags := modelFromTFEOAuthClient(oc, lastValues) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - return resourceTFEOAuthClientRead(d, meta) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) } -func resourceTFEOAuthClientRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) +// Read implements resource.Resource +func (r *resourceTFEOAuthClient) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Load the state into the model + var state modelTFEOAuthClient + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() - log.Printf("[DEBUG] Read configuration of OAuth client: %s", d.Id()) - oc, err := config.Client.OAuthClients.Read(ctx, d.Id()) + // Read the OAuth client + tflog.Debug(ctx, fmt.Sprintf("Read OAuth client: %s", id)) + oc, err := r.config.Client.OAuthClients.Read(ctx, id) if err != nil { - if err == tfe.ErrResourceNotFound { - log.Printf("[DEBUG] OAuth client %s no longer exists", d.Id()) - d.SetId("") - return nil + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("OAuth client %s no longer exists", id)) + resp.State.RemoveResource(ctx) + return } - return err + resp.Diagnostics.AddError("Error reading OAuth client", err.Error()) + return } - // Update the config. - d.Set("organization", oc.Organization.Name) - d.Set("api_url", oc.APIURL) - d.Set("http_url", oc.HTTPURL) - d.Set("service_provider", string(oc.ServiceProvider)) - d.Set("organization_scoped", oc.OrganizationScoped) + lastValues := map[string]types.String{ + "oauth_token": state.OAuthToken, + "rsa_public_key": state.RSAPublicKey, + "key": state.Key, + "secret": state.Secret, + } - switch len(oc.OAuthTokens) { - case 0: - d.Set("oauth_token_id", "") - case 1: - d.Set("oauth_token_id", oc.OAuthTokens[0].ID) - default: - return fmt.Errorf("unexpected number of OAuth tokens: %d", len(oc.OAuthTokens)) + // Load the result into the model + result, diags := modelFromTFEOAuthClient(oc, lastValues) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - return nil + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) } -func resourceTFEOAuthClientDelete(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) +// Update implements resource.Resource +func (r *resourceTFEOAuthClient) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Load the plan and state into the models + var plan, state modelTFEOAuthClient + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var organization string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &organization)...) + if resp.Diagnostics.HasError() { + return + } + + // Create a new options struct. + options := tfe.OAuthClientUpdateOptions{ + OrganizationScoped: plan.OrganizationScoped.ValueBoolPointer(), + OAuthToken: plan.OAuthToken.ValueStringPointer(), + } + + id := state.ID.ValueString() - log.Printf("[DEBUG] Delete OAuth client: %s", d.Id()) - err := config.Client.OAuthClients.Delete(ctx, d.Id()) + tflog.Debug(ctx, fmt.Sprintf("Update OAuth client: %s", id)) + oc, err := r.config.Client.OAuthClients.Update(ctx, id, options) if err != nil { - if err == tfe.ErrResourceNotFound { - return nil + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("OAuth client %s no longer exists", id)) + resp.State.RemoveResource(ctx) + return } - return fmt.Errorf("Error deleting OAuth client %s: %w", d.Id(), err) + + resp.Diagnostics.AddError("Error updating OAuth client", err.Error()) + return } - return nil -} + lastValues := map[string]types.String{ + "oauth_token": plan.OAuthToken, + "rsa_public_key": plan.RSAPublicKey, + "key": plan.Key, + "secret": plan.Secret, + } -func resourceTFEOAuthClientUpdate(d *schema.ResourceData, meta interface{}) error { - config := meta.(ConfiguredClient) + // Load the result into the model + result, diags := modelFromTFEOAuthClient(oc, lastValues) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } - // Create a new options struct. - options := tfe.OAuthClientUpdateOptions{ - OrganizationScoped: tfe.Bool(d.Get("organization_scoped").(bool)), - OAuthToken: tfe.String(d.Get("oauth_token").(string)), + // Update state + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +// Delete implements resource.Resource +func (r *resourceTFEOAuthClient) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Load the state into the model + var state modelTFEOAuthClient + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - log.Printf("[DEBUG] Update OAuth client %s", d.Id()) - _, err := config.Client.OAuthClients.Update(ctx, d.Id(), options) + id := state.ID.ValueString() + + tflog.Debug(ctx, fmt.Sprintf("Delete OAuth client: %s", id)) + err := r.config.Client.OAuthClients.Delete(ctx, id) if err != nil { - return fmt.Errorf("Error updating OAuth client %s: %w", d.Id(), err) + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("OAuth client %s no longer exists", id)) + // The resource will implicitly be removed from state on return + return + } + + resp.Diagnostics.AddError("Error deleting OAuth client", err.Error()) + return } - return resourceTFEOAuthClientRead(d, meta) + resp.State.RemoveResource(ctx) +} + +func (r *resourceTFEOAuthClient) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + // Load the config into the model + var config modelTFEOAuthClient + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + serviceProviderType := tfe.ServiceProviderType(config.ServiceProvider.ValueString()) + if serviceProviderType == tfe.ServiceProviderAzureDevOpsServer && + config.PrivateKey.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("private_key"), + "Invalid configuration", + fmt.Sprintf("private_key is required for service_provider %s", serviceProviderType)) + return + } }