Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 212 additions & 117 deletions internal/provider/data_source_oauth_client.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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)...)
}
Loading