diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b5498e1..4c3b21aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ ## Unreleased +FEATURES: +* **New resource**: `r/tfe_vault_oidc_configuration` for managing Vault OIDC configurations. by @helenjw [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1835) +* **New resource**: `r/tfe_aws_oidc_configuration` for managing AWS OIDC configurations. by @helenjw [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1835) +* **New resource**: `r/tfe_gcp_oidc_configuration` for managing GCP OIDC configurations. by @helenjw [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1835) +* **New resource**: `r/tfe_azure_oidc_configuration` for managing Azure OIDC configurations. by @helenjw. [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1835) +* **New resource**: `r/tfe_hyok_configuration` for managing HYOK configurations. by @helenjw [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1841) +* **New Data Source:** `d/hyok_customer_key_version` is a new data source for finding HYOK customer key versions. by @dominicretli [#1842](https://github.com/hashicorp/terraform-provider-tfe/pull/1842) +* **New Data Source:** `d/hyok_encrypted_data_key` is a new data source for finding HYOK encrypted data keys. by @dominicretli [#1842](https://github.com/hashicorp/terraform-provider-tfe/pull/1842) ## v0.70.0 diff --git a/docs/testing.md b/docs/testing.md index 23e59c3d0..c05490ce8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -49,6 +49,10 @@ these values with the environment variables specified below: 1. `RUN_TASKS_HMAC` - The optional HMAC Key that should be used for Run Task operations. The default is no key. 1. `GITHUB_APP_INSTALLATION_ID` - GitHub App installation internal id in the format `ghain-xxxxxxx`. Required for running any tests that use GitHub App VCS (workspace, policy sets, registry module). 1. `GITHUB_APP_INSTALLATION_NAME` - GitHub App installation name. Required for running tfe_github_app_installation data source test. +1. `ENABLE_HYOK` - Set `ENABLE_HYOK=1` to enable HYOK-related tests. +1. `HYOK_ORGANIZATION_NAME` - Name of an organization entitled to use HYOK. Required to run tests for HYOK resources and data sources. +1. `HYOK_ENCRYPTED_DATA_KEY_ID` - HYOK encrypted data key id. Required for running hyok_encrypted_data_key data source test. +1. `HYOK_CUSTOMER_KEY_VERSION_ID` - HYOK customer key version id. Required for running hyok_customer_key_version data source test. **Note:** In order to run integration tests for **Paid** features you will need a token `TFE_TOKEN` with HCP Terraform or Terraform Enterprise administrator privileges, otherwise the attempt to upgrade an organization's feature set will fail. diff --git a/internal/provider/data_source_hyok_customer_key_version.go b/internal/provider/data_source_hyok_customer_key_version.go new file mode 100644 index 000000000..8c45465f3 --- /dev/null +++ b/internal/provider/data_source_hyok_customer_key_version.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "time" +) + +var ( + _ datasource.DataSource = &dataSourceHYOKCustomerKeyVersion{} + _ datasource.DataSourceWithConfigure = &dataSourceHYOKCustomerKeyVersion{} +) + +func NewHYOKCustomerKeyVersionDataSource() datasource.DataSource { + return &dataSourceHYOKCustomerKeyVersion{} +} + +type dataSourceHYOKCustomerKeyVersion struct { + config ConfiguredClient +} + +type HYOKCustomerKeyVersionDataSourceModel struct { + ID types.String `tfsdk:"id"` + Status types.String `tfsdk:"status"` + Error types.String `tfsdk:"error"` + KeyVersion types.String `tfsdk:"key_version"` + CreatedAt types.String `tfsdk:"created_at"` + WorkspacesSecured types.Int64 `tfsdk:"workspaces_secured"` +} + +func (d *dataSourceHYOKCustomerKeyVersion) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source 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), + ) + + return + } + d.config = client +} + +func (d *dataSourceHYOKCustomerKeyVersion) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_hyok_customer_key_version" +} + +func (d *dataSourceHYOKCustomerKeyVersion) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "This data source can be used to retrieve a HYOK customer key version.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the HYOK customer key version.", + Required: true, + }, + "status": schema.StringAttribute{ + Description: "The status of the HYOK customer key version.", + Computed: true, + }, + "error": schema.StringAttribute{ + Description: "Any error message associated with the HYOK customer key version.", + Computed: true, + }, + "key_version": schema.StringAttribute{ + Description: "The version number of the customer key version.", + Computed: true, + }, + "workspaces_secured": schema.Int64Attribute{ + Description: "The number workspaces secured by this customer key version.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "The timestamp when the key version was created.", + Computed: true, + }, + }, + } +} + +func (d *dataSourceHYOKCustomerKeyVersion) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data HYOKCustomerKeyVersionDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Make API call to fetch the HYOK customer key version + keyVersion, err := d.config.Client.HYOKCustomerKeyVersions.Read(ctx, data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to read HYOK customer key version", err.Error()) + return + } + + // Set the computed attributes from the API response + data.Status = types.StringValue(string(keyVersion.Status)) + data.KeyVersion = types.StringValue(keyVersion.KeyVersion) + data.CreatedAt = types.StringValue(keyVersion.CreatedAt.Format(time.RFC3339)) + data.WorkspacesSecured = types.Int64Value(int64(keyVersion.WorkspacesSecured)) + data.Error = types.StringValue(keyVersion.Error) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/data_source_hyok_customer_key_version_test.go b/internal/provider/data_source_hyok_customer_key_version_test.go new file mode 100644 index 000000000..3515f9c7f --- /dev/null +++ b/internal/provider/data_source_hyok_customer_key_version_test.go @@ -0,0 +1,42 @@ +package provider + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccTFEHYOKCustomerKeyVersionDataSource_basic(t *testing.T) { + skipUnlessHYOKEnabled(t) + + hyokCustomerKeyVersionID := os.Getenv("HYOK_CUSTOMER_KEY_VERSION_ID") + if hyokCustomerKeyVersionID == "" { + t.Skip("HYOK_CUSTOMER_KEY_VERSION_ID environment variable must be set to run this test") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEHYOKCustomerKeyVersionDataSourceConfig(hyokCustomerKeyVersionID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.tfe_hyok_customer_key_version.test", "id", hyokCustomerKeyVersionID), + resource.TestCheckResourceAttrSet("data.tfe_hyok_customer_key_version.test", "status"), + resource.TestCheckResourceAttrSet("data.tfe_hyok_customer_key_version.test", "key_version"), + resource.TestCheckResourceAttrSet("data.tfe_hyok_customer_key_version.test", "created_at"), + resource.TestCheckResourceAttrSet("data.tfe_hyok_customer_key_version.test", "workspaces_secured"), + ), + }, + }, + }) +} + +func testAccTFEHYOKCustomerKeyVersionDataSourceConfig(id string) string { + return ` +data "tfe_hyok_customer_key_version" "test" { + id = "` + id + `" +} +` +} diff --git a/internal/provider/data_source_hyok_encrypted_data_key.go b/internal/provider/data_source_hyok_encrypted_data_key.go new file mode 100644 index 000000000..f3b148427 --- /dev/null +++ b/internal/provider/data_source_hyok_encrypted_data_key.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "time" +) + +var ( + _ datasource.DataSource = &dataSourceHYOKEncryptedDataKey{} + _ datasource.DataSourceWithConfigure = &dataSourceHYOKEncryptedDataKey{} +) + +func NewHYOKEncryptedDataKeyDataSource() datasource.DataSource { + return &dataSourceHYOKEncryptedDataKey{} +} + +type dataSourceHYOKEncryptedDataKey struct { + config ConfiguredClient +} + +type HYOKEncryptedDataKeyDataSourceModel struct { + ID types.String `tfsdk:"id"` + EncryptedDEK types.String `tfsdk:"encrypted_dek"` + CustomerKeyName types.String `tfsdk:"customer_key_name"` + CreatedAt types.String `tfsdk:"created_at"` +} + +func (d *dataSourceHYOKEncryptedDataKey) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source 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), + ) + + return + } + d.config = client +} + +func (d *dataSourceHYOKEncryptedDataKey) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_hyok_encrypted_data_key" +} + +func (d *dataSourceHYOKEncryptedDataKey) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "This data source can be used to retrieve a HYOK customer key version.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the HYOK encrypted data key.", + Required: true, + }, + "encrypted_dek": schema.StringAttribute{ + Description: "The encrypted data encryption key of the HYOK encrypted data key.", + Computed: true, + }, + "customer_key_name": schema.StringAttribute{ + Description: "The customer provided name of the HYOK encrypted data key.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "The timestamp when the key version was created.", + Computed: true, + }, + }, + } +} + +func (d *dataSourceHYOKEncryptedDataKey) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data HYOKEncryptedDataKeyDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Make API call to fetch the HYOK customer key version + keyVersion, err := d.config.Client.HYOKEncryptedDataKeys.Read(ctx, data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to read HYOK customer key version", err.Error()) + return + } + + // Set the computed attributes from the API response + data.EncryptedDEK = types.StringValue(keyVersion.EncryptedDEK) + data.CustomerKeyName = types.StringValue(keyVersion.CustomerKeyName) + data.CreatedAt = types.StringValue(keyVersion.CreatedAt.Format(time.RFC3339)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/data_source_hyok_encrypted_data_key_test.go b/internal/provider/data_source_hyok_encrypted_data_key_test.go new file mode 100644 index 000000000..f9f066bbf --- /dev/null +++ b/internal/provider/data_source_hyok_encrypted_data_key_test.go @@ -0,0 +1,41 @@ +package provider + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccTFEHYOKEncryptedDataKeyDataSource_basic(t *testing.T) { + skipUnlessHYOKEnabled(t) + + hyokEncryptedDataKeyID := os.Getenv("HYOK_ENCRYPTED_DATA_KEY_ID") + if hyokEncryptedDataKeyID == "" { + t.Skip("HYOK_ENCRYPTED_DATA_KEY_ID environment variable must be set to run this test") + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEHYOKEncryptedDataKeyDataSourceConfig(hyokEncryptedDataKeyID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.tfe_hyok_encrypted_data_key.test", "id", hyokEncryptedDataKeyID), + resource.TestCheckResourceAttrSet("data.tfe_hyok_encrypted_data_key.test", "encrypted_dek"), + resource.TestCheckResourceAttrSet("data.tfe_hyok_encrypted_data_key.test", "customer_key_name"), + resource.TestCheckResourceAttrSet("data.tfe_hyok_encrypted_data_key.test", "created_at"), + ), + }, + }, + }) +} + +func testAccTFEHYOKEncryptedDataKeyDataSourceConfig(id string) string { + return ` +data "tfe_hyok_encrypted_data_key" "test" { + id = "` + id + `" +} +` +} diff --git a/internal/provider/helper_test.go b/internal/provider/helper_test.go index 2efb3d438..2a122967b 100644 --- a/internal/provider/helper_test.go +++ b/internal/provider/helper_test.go @@ -18,6 +18,7 @@ import ( const RunTasksURLEnvName = "RUN_TASKS_URL" const RunTasksHMACKeyEnvName = "RUN_TASKS_HMAC" +const EnableHYOKEnvName = "ENABLE_HYOK" type testClientOptions struct { defaultOrganization string @@ -238,6 +239,14 @@ func skipUnlessBeta(t *testing.T) { } } +func skipUnlessHYOKEnabled(t *testing.T) { + skipIfEnterprise(t) + + if value, ok := os.LookupEnv(EnableHYOKEnvName); !ok || value == "" { + t.Skipf("Skipping tests for HYOK. Set '%s' to enable tests.", EnableHYOKEnvName) + } +} + // Temporarily skip a test that may be experiencing API errors. This method // purposefully errors after the set date to remind contributors to remove this check // and verify that the API errors are no longer occurring. diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index caa20b4dc..d7f963bfd 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -130,6 +130,8 @@ func (p *frameworkProvider) Configure(ctx context.Context, req provider.Configur func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ + NewHYOKCustomerKeyVersionDataSource, + NewHYOKEncryptedDataKeyDataSource, NewNoCodeModuleDataSource, NewOrganizationRunTaskDataSource, NewOrganizationRunTaskGlobalSettingsDataSource, @@ -172,6 +174,11 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res NewTerraformVersionResource, NewOPAVersionResource, NewsentinelVersionResource, + NewAWSOIDCConfigurationResource, + NewGCPOIDCConfigurationResource, + NewAzureOIDCConfigurationResource, + NewVaultOIDCConfigurationResource, + NewHYOKConfigurationResource, } } diff --git a/internal/provider/resource_tfe_aws_oidc_configuration.go b/internal/provider/resource_tfe_aws_oidc_configuration.go new file mode 100644 index 000000000..a81ea4dc4 --- /dev/null +++ b/internal/provider/resource_tfe_aws_oidc_configuration.go @@ -0,0 +1,204 @@ +// // Copyright (c) HashiCorp, Inc. +// // SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" + + tfe "github.com/hashicorp/go-tfe" + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + _ resource.ResourceWithConfigure = &resourceTFEAWSOIDCConfiguration{} + _ resource.ResourceWithImportState = &resourceTFEAWSOIDCConfiguration{} +) + +func NewAWSOIDCConfigurationResource() resource.Resource { + return &resourceTFEAWSOIDCConfiguration{} +} + +type resourceTFEAWSOIDCConfiguration struct { + config ConfiguredClient +} + +type modelTFEAWSOIDCConfiguration struct { + ID types.String `tfsdk:"id"` + RoleARN types.String `tfsdk:"role_arn"` + Organization types.String `tfsdk:"organization"` +} + +func (r *resourceTFEAWSOIDCConfiguration) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + 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 *resourceTFEAWSOIDCConfiguration) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_aws_oidc_configuration" +} + +func (r *resourceTFEAWSOIDCConfiguration) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the AWS OIDC configuration.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "role_arn": schema.StringAttribute{ + Description: "The AWS ARN of your role.", + Required: true, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization to which the TFE AWS OIDC configuration belongs.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Description: "Generates a new TFE AWS OIDC Configuration.", + } +} + +func (r *resourceTFEAWSOIDCConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceTFEAWSOIDCConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan into the model + var plan modelTFEAWSOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the organization name from resource or provider config + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.AWSOIDCConfigurationCreateOptions{ + RoleARN: plan.RoleARN.ValueString(), + } + + tflog.Debug(ctx, fmt.Sprintf("Create TFE AWS OIDC Configuration for organization %s", orgName)) + oidc, err := r.config.Client.AWSOIDCConfigurations.Create(ctx, orgName, options) + if err != nil { + resp.Diagnostics.AddError("Error creating TFE AWS OIDC Configuration", err.Error()) + return + } + result := modelFromTFEAWSOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEAWSOIDCConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform state into the model + var state modelTFEAWSOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Read AWS OIDC configuration: %s", oidcID)) + oidc, err := r.config.Client.AWSOIDCConfigurations.Read(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("AWS OIDC configuration %s no longer exists", oidcID)) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading AWS OIDC configuration %s", oidcID), + err.Error(), + ) + return + } + result := modelFromTFEAWSOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEAWSOIDCConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFEAWSOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + var state modelTFEAWSOIDCConfiguration + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.AWSOIDCConfigurationUpdateOptions{ + RoleARN: plan.RoleARN.ValueString(), + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Update TFE AWS OIDC Configuration %s", oidcID)) + oidc, err := r.config.Client.AWSOIDCConfigurations.Update(ctx, oidcID, options) + if err != nil { + resp.Diagnostics.AddError("Error updating TFE AWS OIDC Configuration", err.Error()) + return + } + + result := modelFromTFEAWSOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEAWSOIDCConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFEAWSOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Delete TFE AWS OIDC configuration: %s", oidcID)) + err := r.config.Client.AWSOIDCConfigurations.Delete(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("TFE AWS OIDC configuration %s no longer exists", oidcID)) + return + } + + resp.Diagnostics.AddError("Error deleting TFE AWS OIDC Configuration", err.Error()) + return + } +} + +func modelFromTFEAWSOIDCConfiguration(p *tfe.AWSOIDCConfiguration) modelTFEAWSOIDCConfiguration { + return modelTFEAWSOIDCConfiguration{ + ID: types.StringValue(p.ID), + RoleARN: types.StringValue(p.RoleARN), + Organization: types.StringValue(p.Organization.Name), + } +} diff --git a/internal/provider/resource_tfe_aws_oidc_configuration_test.go b/internal/provider/resource_tfe_aws_oidc_configuration_test.go new file mode 100644 index 000000000..d3f7ba5a0 --- /dev/null +++ b/internal/provider/resource_tfe_aws_oidc_configuration_test.go @@ -0,0 +1,79 @@ +package provider + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccTFEAWSOIDCConfiguration_basic(t *testing.T) { + skipUnlessHYOKEnabled(t) + + orgName := os.Getenv("HYOK_ORGANIZATION_NAME") + if orgName == "" { + t.Skip("Skipping test. Set HYOK_ORGANIZATION_NAME environment to enable test.") + } + + originalRoleARN := "arn:aws:iam::123456789012:role/terraform-provider-tfe-example-1" + newRoleARN := "arn:aws:iam::123456789012:role/terraform-provider-tfe-example-2" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEAWSOIDCConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEAWSOIDCConfigurationConfig(orgName, originalRoleARN), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_aws_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_aws_oidc_configuration.test", "role_arn", originalRoleARN), + ), + }, + // Import + { + ResourceName: "tfe_aws_oidc_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update role ARN + { + Config: testAccTFEAWSOIDCConfigurationConfig(orgName, newRoleARN), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_aws_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_aws_oidc_configuration.test", "role_arn", newRoleARN), + ), + }, + }, + }) +} + +func testAccTFEAWSOIDCConfigurationConfig(orgName string, roleARN string) string { + return fmt.Sprintf(` +resource "tfe_aws_oidc_configuration" "test" { + role_arn = "%s" + organization = "%s" +} +`, roleARN, orgName) +} + +func testAccCheckTFEAWSOIDCConfigurationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_aws_oidc_configuration" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no instance ID is set") + } + + _, err := testAccConfiguredClient.Client.AWSOIDCConfigurations.Read(ctx, rs.Primary.ID) + if err == nil { + return fmt.Errorf("TFE AWS OIDC Configuration %s still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/internal/provider/resource_tfe_azure_oidc_configuration.go b/internal/provider/resource_tfe_azure_oidc_configuration.go new file mode 100644 index 000000000..62305911c --- /dev/null +++ b/internal/provider/resource_tfe_azure_oidc_configuration.go @@ -0,0 +1,220 @@ +// // Copyright (c) HashiCorp, Inc. +// // SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" + + tfe "github.com/hashicorp/go-tfe" + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + _ resource.ResourceWithConfigure = &resourceTFEAzureOIDCConfiguration{} + _ resource.ResourceWithImportState = &resourceTFEAzureOIDCConfiguration{} +) + +func NewAzureOIDCConfigurationResource() resource.Resource { + return &resourceTFEAzureOIDCConfiguration{} +} + +type resourceTFEAzureOIDCConfiguration struct { + config ConfiguredClient +} + +type modelTFEAzureOIDCConfiguration struct { + ID types.String `tfsdk:"id"` + ClientID types.String `tfsdk:"client_id"` + SubscriptionID types.String `tfsdk:"subscription_id"` + TenantID types.String `tfsdk:"tenant_id"` + Organization types.String `tfsdk:"organization"` +} + +func (r *resourceTFEAzureOIDCConfiguration) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + 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 *resourceTFEAzureOIDCConfiguration) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_azure_oidc_configuration" +} + +func (r *resourceTFEAzureOIDCConfiguration) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the Azure OIDC configuration.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "client_id": schema.StringAttribute{ + Description: "The Client (or Application) ID of your Entra ID application.", + Required: true, + }, + "subscription_id": schema.StringAttribute{ + Description: "The ID of your Azure subscription.", + Required: true, + }, + "tenant_id": schema.StringAttribute{ + Description: "The Tenant (or Directory) ID of your Entra ID application.", + Required: true, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization to which the TFE Azure OIDC configuration belongs.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Description: "Generates a new TFE Azure OIDC Configuration.", + } +} + +func (r *resourceTFEAzureOIDCConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceTFEAzureOIDCConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan into the model + var plan modelTFEAzureOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the organization name from resource or provider config + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.AzureOIDCConfigurationCreateOptions{ + ClientID: plan.ClientID.ValueString(), + SubscriptionID: plan.SubscriptionID.ValueString(), + TenantID: plan.TenantID.ValueString(), + } + + tflog.Debug(ctx, fmt.Sprintf("Create TFE Azure OIDC Configuration for organization %s", orgName)) + oidc, err := r.config.Client.AzureOIDCConfigurations.Create(ctx, orgName, options) + if err != nil { + resp.Diagnostics.AddError("Error creating TFE Azure OIDC Configuration", err.Error()) + return + } + result := modelFromTFEAzureOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEAzureOIDCConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform state into the model + var state modelTFEAzureOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Read Azure OIDC configuration: %s", oidcID)) + oidc, err := r.config.Client.AzureOIDCConfigurations.Read(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("Azure OIDC configuration %s no longer exists", oidcID)) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading Azure OIDC configuration %s", oidcID), + err.Error(), + ) + return + } + result := modelFromTFEAzureOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEAzureOIDCConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFEAzureOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + var state modelTFEAzureOIDCConfiguration + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.AzureOIDCConfigurationUpdateOptions{ + ClientID: plan.ClientID.ValueStringPointer(), + SubscriptionID: plan.SubscriptionID.ValueStringPointer(), + TenantID: plan.TenantID.ValueStringPointer(), + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Update TFE Azure OIDC Configuration %s", oidcID)) + oidc, err := r.config.Client.AzureOIDCConfigurations.Update(ctx, oidcID, options) + if err != nil { + resp.Diagnostics.AddError("Error updating TFE Azure OIDC Configuration", err.Error()) + return + } + + result := modelFromTFEAzureOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEAzureOIDCConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFEAzureOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Delete TFE Azure OIDC configuration: %s", oidcID)) + err := r.config.Client.AzureOIDCConfigurations.Delete(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("TFE Azure OIDC configuration %s no longer exists", oidcID)) + return + } + + resp.Diagnostics.AddError("Error deleting TFE Azure OIDC Configuration", err.Error()) + return + } +} + +func modelFromTFEAzureOIDCConfiguration(p *tfe.AzureOIDCConfiguration) modelTFEAzureOIDCConfiguration { + return modelTFEAzureOIDCConfiguration{ + ID: types.StringValue(p.ID), + ClientID: types.StringValue(p.ClientID), + SubscriptionID: types.StringValue(p.SubscriptionID), + TenantID: types.StringValue(p.TenantID), + Organization: types.StringValue(p.Organization.Name), + } +} diff --git a/internal/provider/resource_tfe_azure_oidc_configuration_test.go b/internal/provider/resource_tfe_azure_oidc_configuration_test.go new file mode 100644 index 000000000..3379a0010 --- /dev/null +++ b/internal/provider/resource_tfe_azure_oidc_configuration_test.go @@ -0,0 +1,89 @@ +package provider + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccTFEAzureOIDCConfiguration_basic(t *testing.T) { + skipUnlessHYOKEnabled(t) + + orgName := os.Getenv("HYOK_ORGANIZATION_NAME") + if orgName == "" { + t.Skip("Skipping test. Set HYOK_ORGANIZATION_NAME environment to enable test.") + } + + originalClientID := "client-id-1" + updatedClientID := "client-id-2" + originalSubscriptionID := "subscription-id-1" + updatedSubscriptionID := "subscription-id-2" + originalTenantID := "tenant-id-1" + updatedTenantID := "tenant-id-2" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEAzureOIDCConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEAzureOIDCConfigurationConfig(orgName, originalClientID, originalSubscriptionID, originalTenantID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_azure_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_azure_oidc_configuration.test", "client_id", originalClientID), + resource.TestCheckResourceAttr("tfe_azure_oidc_configuration.test", "subscription_id", originalSubscriptionID), + resource.TestCheckResourceAttr("tfe_azure_oidc_configuration.test", "tenant_id", originalTenantID), + ), + }, + // Import + { + ResourceName: "tfe_azure_oidc_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + Config: testAccTFEAzureOIDCConfigurationConfig(orgName, updatedClientID, updatedSubscriptionID, updatedTenantID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_azure_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_azure_oidc_configuration.test", "client_id", updatedClientID), + resource.TestCheckResourceAttr("tfe_azure_oidc_configuration.test", "subscription_id", updatedSubscriptionID), + resource.TestCheckResourceAttr("tfe_azure_oidc_configuration.test", "tenant_id", updatedTenantID), + ), + }, + }, + }) +} + +func testAccTFEAzureOIDCConfigurationConfig(orgName string, clientID string, subscriptionID string, tenantID string) string { + return fmt.Sprintf(` +resource "tfe_azure_oidc_configuration" "test" { + client_id = "%s" + subscription_id = "%s" + tenant_id = "%s" + organization = "%s" +} +`, clientID, subscriptionID, tenantID, orgName) +} + +func testAccCheckTFEAzureOIDCConfigurationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_azure_oidc_configuration" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no instance ID is set") + } + + _, err := testAccConfiguredClient.Client.AzureOIDCConfigurations.Read(ctx, rs.Primary.ID) + if err == nil { + return fmt.Errorf("TFE Azure OIDC Configuration %s still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/internal/provider/resource_tfe_gcp_oidc_configuration.go b/internal/provider/resource_tfe_gcp_oidc_configuration.go new file mode 100644 index 000000000..6be69e86e --- /dev/null +++ b/internal/provider/resource_tfe_gcp_oidc_configuration.go @@ -0,0 +1,220 @@ +// // Copyright (c) HashiCorp, Inc. +// // SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" + + tfe "github.com/hashicorp/go-tfe" + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + _ resource.ResourceWithConfigure = &resourceTFEGCPOIDCConfiguration{} + _ resource.ResourceWithImportState = &resourceTFEGCPOIDCConfiguration{} +) + +func NewGCPOIDCConfigurationResource() resource.Resource { + return &resourceTFEGCPOIDCConfiguration{} +} + +type resourceTFEGCPOIDCConfiguration struct { + config ConfiguredClient +} + +type modelTFEGCPOIDCConfiguration struct { + ID types.String `tfsdk:"id"` + ServiceAccountEmail types.String `tfsdk:"service_account_email"` + ProjectNumber types.String `tfsdk:"project_number"` + WorkloadProviderName types.String `tfsdk:"workload_provider_name"` + Organization types.String `tfsdk:"organization"` +} + +func (r *resourceTFEGCPOIDCConfiguration) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + 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 *resourceTFEGCPOIDCConfiguration) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_gcp_oidc_configuration" +} + +func (r *resourceTFEGCPOIDCConfiguration) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the GCP OIDC configuration.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "service_account_email": schema.StringAttribute{ + Description: "The email of your GCP service account, with permissions to encrypt and decrypt using a Cloud KMS key.", + Required: true, + }, + "project_number": schema.StringAttribute{ + Description: "The GCP Project containing the workload provider and service account.", + Required: true, + }, + "workload_provider_name": schema.StringAttribute{ + Description: "The fully qualified workload provider path.", + Required: true, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization to which the TFE GCP OIDC configuration belongs.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Description: "Generates a new TFE GCP OIDC Configuration.", + } +} + +func (r *resourceTFEGCPOIDCConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceTFEGCPOIDCConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan into the model + var plan modelTFEGCPOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the organization name from resource or provider config + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.GCPOIDCConfigurationCreateOptions{ + ServiceAccountEmail: plan.ServiceAccountEmail.ValueString(), + ProjectNumber: plan.ProjectNumber.ValueString(), + WorkloadProviderName: plan.WorkloadProviderName.ValueString(), + } + + tflog.Debug(ctx, fmt.Sprintf("Create TFE GCP OIDC Configuration for organization %s", orgName)) + oidc, err := r.config.Client.GCPOIDCConfigurations.Create(ctx, orgName, options) + if err != nil { + resp.Diagnostics.AddError("Error creating TFE GCP OIDC Configuration", err.Error()) + return + } + result := modelFromTFEGCPOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEGCPOIDCConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform state into the model + var state modelTFEGCPOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Read GCP OIDC configuration: %s", oidcID)) + oidc, err := r.config.Client.GCPOIDCConfigurations.Read(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("GCP OIDC configuration %s no longer exists", oidcID)) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading GCP OIDC configuration %s", oidcID), + err.Error(), + ) + return + } + result := modelFromTFEGCPOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEGCPOIDCConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFEGCPOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + var state modelTFEGCPOIDCConfiguration + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.GCPOIDCConfigurationUpdateOptions{ + ServiceAccountEmail: plan.ServiceAccountEmail.ValueStringPointer(), + ProjectNumber: plan.ProjectNumber.ValueStringPointer(), + WorkloadProviderName: plan.WorkloadProviderName.ValueStringPointer(), + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Update TFE GCP OIDC Configuration %s", oidcID)) + oidc, err := r.config.Client.GCPOIDCConfigurations.Update(ctx, oidcID, options) + if err != nil { + resp.Diagnostics.AddError("Error updating TFE GCP OIDC Configuration", err.Error()) + return + } + + result := modelFromTFEGCPOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEGCPOIDCConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFEGCPOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Delete TFE GCP OIDC configuration: %s", oidcID)) + err := r.config.Client.GCPOIDCConfigurations.Delete(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("TFE GCP OIDC configuration %s no longer exists", oidcID)) + return + } + + resp.Diagnostics.AddError("Error deleting TFE GCP OIDC Configuration", err.Error()) + return + } +} + +func modelFromTFEGCPOIDCConfiguration(p *tfe.GCPOIDCConfiguration) modelTFEGCPOIDCConfiguration { + return modelTFEGCPOIDCConfiguration{ + ID: types.StringValue(p.ID), + ServiceAccountEmail: types.StringValue(p.ServiceAccountEmail), + WorkloadProviderName: types.StringValue(p.WorkloadProviderName), + ProjectNumber: types.StringValue(p.ProjectNumber), + Organization: types.StringValue(p.Organization.Name), + } +} diff --git a/internal/provider/resource_tfe_gcp_oidc_configuration_test.go b/internal/provider/resource_tfe_gcp_oidc_configuration_test.go new file mode 100644 index 000000000..571110e3a --- /dev/null +++ b/internal/provider/resource_tfe_gcp_oidc_configuration_test.go @@ -0,0 +1,89 @@ +package provider + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccTFEGCPOIDCConfiguration_basic(t *testing.T) { + skipUnlessHYOKEnabled(t) + + orgName := os.Getenv("HYOK_ORGANIZATION_NAME") + if orgName == "" { + t.Skip("Skipping test. Set HYOK_ORGANIZATION_NAME environment to enable test.") + } + + originalServiceAccountEmail := "service-account@example.iam.gserviceaccount.com" + updatedServiceAccountEmail := "updated-service-account@example.iam.gserviceaccount.com" + originalProjectNumber := "123456789012" + updatedProjectNumber := "999999999999" + originalWorkloadProviderName := "workload-provider-name-1" + updatedWorkloadProviderName := "workload-provider-name-2" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEGCPOIDCConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEGCPOIDCConfigurationConfig(orgName, originalServiceAccountEmail, originalProjectNumber, originalWorkloadProviderName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_gcp_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_gcp_oidc_configuration.test", "workload_provider_name", originalWorkloadProviderName), + resource.TestCheckResourceAttr("tfe_gcp_oidc_configuration.test", "project_number", originalProjectNumber), + resource.TestCheckResourceAttr("tfe_gcp_oidc_configuration.test", "service_account_email", originalServiceAccountEmail), + ), + }, + // Import + { + ResourceName: "tfe_gcp_oidc_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + Config: testAccTFEGCPOIDCConfigurationConfig(orgName, updatedServiceAccountEmail, updatedProjectNumber, updatedWorkloadProviderName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_gcp_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_gcp_oidc_configuration.test", "workload_provider_name", updatedWorkloadProviderName), + resource.TestCheckResourceAttr("tfe_gcp_oidc_configuration.test", "project_number", updatedProjectNumber), + resource.TestCheckResourceAttr("tfe_gcp_oidc_configuration.test", "service_account_email", updatedServiceAccountEmail), + ), + }, + }, + }) +} + +func testAccTFEGCPOIDCConfigurationConfig(orgName string, serviceAccountEmail string, projectNumber string, workloadProviderName string) string { + return fmt.Sprintf(` +resource "tfe_gcp_oidc_configuration" "test" { + service_account_email = "%s" + project_number = "%s" + workload_provider_name = "%s" + organization = "%s" +} +`, serviceAccountEmail, projectNumber, workloadProviderName, orgName) +} + +func testAccCheckTFEGCPOIDCConfigurationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_gcp_oidc_configuration" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no instance ID is set") + } + + _, err := testAccConfiguredClient.Client.GCPOIDCConfigurations.Read(ctx, rs.Primary.ID) + if err == nil { + return fmt.Errorf("TFE GCP OIDC Configuration %s still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/internal/provider/resource_tfe_hyok_configuration.go b/internal/provider/resource_tfe_hyok_configuration.go new file mode 100644 index 000000000..1c75fe09e --- /dev/null +++ b/internal/provider/resource_tfe_hyok_configuration.go @@ -0,0 +1,349 @@ +// // Copyright (c) HashiCorp, Inc. +// // SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + tfe "github.com/hashicorp/go-tfe" + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + _ resource.ResourceWithConfigure = &resourceTFEHYOKConfiguration{} + _ resource.ResourceWithImportState = &resourceTFEHYOKConfiguration{} +) + +func NewHYOKConfigurationResource() resource.Resource { + return &resourceTFEHYOKConfiguration{} +} + +type resourceTFEHYOKConfiguration struct { + config ConfiguredClient +} + +type modelTFEHYOKConfiguration struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + KEKID types.String `tfsdk:"kek_id"` + KMSOptions *modelTFEKMSOptions `tfsdk:"kms_options"` + OIDCConfigurationID types.String `tfsdk:"oidc_configuration_id"` + OIDCConfigurationType types.String `tfsdk:"oidc_configuration_type"` + AgentPoolID types.String `tfsdk:"agent_pool_id"` + Organization types.String `tfsdk:"organization"` +} + +func (m *modelTFEHYOKConfiguration) TFEOIDCConfigurationTypeChoice() *tfe.OIDCConfigurationTypeChoice { + var typeChoice *tfe.OIDCConfigurationTypeChoice + id := m.OIDCConfigurationID.ValueString() + + switch m.OIDCConfigurationType.ValueString() { + case OIDCConfigurationTypeAWS: + typeChoice = &tfe.OIDCConfigurationTypeChoice{AWSOIDCConfiguration: &tfe.AWSOIDCConfiguration{ID: id}} + case OIDCConfigurationTypeGCP: + typeChoice = &tfe.OIDCConfigurationTypeChoice{GCPOIDCConfiguration: &tfe.GCPOIDCConfiguration{ID: id}} + case OIDCConfigurationTypeVault: + typeChoice = &tfe.OIDCConfigurationTypeChoice{VaultOIDCConfiguration: &tfe.VaultOIDCConfiguration{ID: id}} + case OIDCConfigurationTypeAzure: + typeChoice = &tfe.OIDCConfigurationTypeChoice{AzureOIDCConfiguration: &tfe.AzureOIDCConfiguration{ID: id}} + } + + return typeChoice +} + +type modelTFEKMSOptions struct { + KeyRegion types.String `tfsdk:"key_region"` + KeyLocation types.String `tfsdk:"key_location"` + KeyRingID types.String `tfsdk:"key_ring_id"` +} + +func (m *modelTFEKMSOptions) TFEKMSOptions() *tfe.KMSOptions { + var kmsOptions *tfe.KMSOptions + if m != nil { + kmsOptions = &tfe.KMSOptions{ + KeyRegion: m.KeyRegion.ValueString(), + KeyLocation: m.KeyLocation.ValueString(), + KeyRingID: m.KeyRingID.ValueString(), + } + } + return kmsOptions +} + +// List all available OIDC configuration types. +const ( + OIDCConfigurationTypeAWS string = "aws" + OIDCConfigurationTypeGCP string = "gcp" + OIDCConfigurationTypeVault string = "vault" + OIDCConfigurationTypeAzure string = "azure" +) + +func (r *resourceTFEHYOKConfiguration) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + 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 *resourceTFEHYOKConfiguration) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_hyok_configuration" +} + +func (r *resourceTFEHYOKConfiguration) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the HYOK configuration.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "Label for the HYOK configuration to be used within HCP Terraform.", + Required: true, + }, + "kek_id": schema.StringAttribute{ + Description: "Refers to the name of your key encryption key stored in your key management service.", + Required: true, + }, + "oidc_configuration_id": schema.StringAttribute{ + Description: "The ID of the TFE OIDC configuration.", + Required: true, + }, + "oidc_configuration_type": schema.StringAttribute{ + Description: "The type of the TFE OIDC configuration.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf( + OIDCConfigurationTypeAWS, + OIDCConfigurationTypeGCP, + OIDCConfigurationTypeVault, + OIDCConfigurationTypeAzure, + ), + }, + }, + "agent_pool_id": schema.StringAttribute{ + Description: "The ID of the agent-pool to associate with the HYOK configuration.", + Required: true, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization to which the TFE HYOK configuration belongs.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "kms_options": schema.SingleNestedBlock{ + Description: "Optional object used to specify additional fields for some key management services.", + Attributes: map[string]schema.Attribute{ + "key_region": schema.StringAttribute{ + Description: "The AWS region where your key is located.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "key_location": schema.StringAttribute{ + Description: "The location in which the GCP key ring exists.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "key_ring_id": schema.StringAttribute{ + Description: "The root resource for Google Cloud KMS keys and key versions.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + }, + }, + }, + Description: "Generates a new TFE HYOK Configuration.", + } +} + +func (r *resourceTFEHYOKConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceTFEHYOKConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan into the model + var plan modelTFEHYOKConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the organization name from resource or provider config + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.HYOKConfigurationsCreateOptions{ + KEKID: plan.KEKID.ValueString(), + Name: plan.Name.ValueString(), + KMSOptions: plan.KMSOptions.TFEKMSOptions(), + OIDCConfiguration: plan.TFEOIDCConfigurationTypeChoice(), + AgentPool: &tfe.AgentPool{ID: plan.AgentPoolID.ValueString()}, + } + + tflog.Debug(ctx, fmt.Sprintf("Create TFE HYOK Configuration for organization %s", orgName)) + hyok, err := r.config.Client.HYOKConfigurations.Create(ctx, orgName, options) + if err != nil { + resp.Diagnostics.AddError("Error creating TFE HYOK Configuration", err.Error()) + return + } + result := modelFromTFEHYOKConfiguration(hyok) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEHYOKConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform state into the model + var state modelTFEHYOKConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + hyokID := state.ID.ValueString() + opts := tfe.HYOKConfigurationsReadOptions{ + Include: []tfe.HYOKConfigurationsIncludeOpt{ + tfe.HYOKConfigurationsIncludeOIDCConfiguration, + }, + } + tflog.Debug(ctx, fmt.Sprintf("Read HYOK configuration: %s", hyokID)) + hyok, err := r.config.Client.HYOKConfigurations.Read(ctx, state.ID.ValueString(), &opts) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("HYOK configuration %s no longer exists", hyokID)) + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading HYOK configuration %s", hyokID), + err.Error(), + ) + } + return + } + result := modelFromTFEHYOKConfiguration(hyok) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEHYOKConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFEHYOKConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + var state modelTFEHYOKConfiguration + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.HYOKConfigurationsUpdateOptions{ + Name: plan.Name.ValueStringPointer(), + KEKID: plan.KEKID.ValueStringPointer(), + KMSOptions: plan.KMSOptions.TFEKMSOptions(), + AgentPool: &tfe.AgentPool{ID: plan.AgentPoolID.ValueString()}, + } + + hyokID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Update TFE HYOK Configuration %s", hyokID)) + hyok, err := r.config.Client.HYOKConfigurations.Update(ctx, hyokID, options) + if err != nil { + resp.Diagnostics.AddError("Error updating TFE HYOK Configuration", err.Error()) + return + } + + result := modelFromTFEHYOKConfiguration(hyok) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEHYOKConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFEHYOKConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + hyokID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Delete TFE HYOK configuration: %s", hyokID)) + err := r.config.Client.HYOKConfigurations.Delete(ctx, hyokID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("TFE HYOK configuration %s no longer exists", hyokID)) + return + } + + resp.Diagnostics.AddError("Error deleting TFE HYOK Configuration", err.Error()) + return + } +} + +func modelFromTFEHYOKConfiguration(p *tfe.HYOKConfiguration) modelTFEHYOKConfiguration { + var kmsOptions *modelTFEKMSOptions + if p.KMSOptions != nil { + kmsOptions = &modelTFEKMSOptions{ + KeyRegion: types.StringValue(p.KMSOptions.KeyRegion), + KeyLocation: types.StringValue(p.KMSOptions.KeyLocation), + KeyRingID: types.StringValue(p.KMSOptions.KeyRingID), + } + } + + model := modelTFEHYOKConfiguration{ + ID: types.StringValue(p.ID), + Name: types.StringValue(p.Name), + KEKID: types.StringValue(p.KEKID), + Organization: types.StringValue(p.Organization.Name), + AgentPoolID: types.StringValue(p.AgentPool.ID), + KMSOptions: kmsOptions, + } + + switch { + case p.OIDCConfiguration.AWSOIDCConfiguration != nil: + model.OIDCConfigurationID = types.StringValue(p.OIDCConfiguration.AWSOIDCConfiguration.ID) + model.OIDCConfigurationType = types.StringValue(OIDCConfigurationTypeAWS) + case p.OIDCConfiguration.GCPOIDCConfiguration != nil: + model.OIDCConfigurationID = types.StringValue(p.OIDCConfiguration.GCPOIDCConfiguration.ID) + model.OIDCConfigurationType = types.StringValue(OIDCConfigurationTypeGCP) + case p.OIDCConfiguration.AzureOIDCConfiguration != nil: + model.OIDCConfigurationID = types.StringValue(p.OIDCConfiguration.AzureOIDCConfiguration.ID) + model.OIDCConfigurationType = types.StringValue(OIDCConfigurationTypeAzure) + case p.OIDCConfiguration.VaultOIDCConfiguration != nil: + model.OIDCConfigurationID = types.StringValue(p.OIDCConfiguration.VaultOIDCConfiguration.ID) + model.OIDCConfigurationType = types.StringValue(OIDCConfigurationTypeVault) + } + + return model +} diff --git a/internal/provider/resource_tfe_hyok_configuration_test.go b/internal/provider/resource_tfe_hyok_configuration_test.go new file mode 100644 index 000000000..27f4ad20e --- /dev/null +++ b/internal/provider/resource_tfe_hyok_configuration_test.go @@ -0,0 +1,354 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "os" + "testing" +) + +func TestAccTFEHYOKConfiguration_basic(t *testing.T) { + skipUnlessHYOKEnabled(t) + + orgName := os.Getenv("HYOK_ORGANIZATION_NAME") + + if orgName == "" { + t.Skip("Test skipped: HYOK_ORGANIZATION_NAME environment variable is not set") + } + + state := &tfe.HYOKConfiguration{} + + // With AWS OIDC configuration + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEAWSHYOKConfigurationConfig(orgName, "apple", "arn:aws:kms:us-east-1:123456789012:key/key1", "us-east-1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTFEHYOKConfigurationExists("tfe_hyok_configuration.hyok", state), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "apple"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "aws"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "arn:aws:kms:us-east-1:123456789012:key/key1"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kms_options.key_region", "us-east-1"), + ), + }, + // Import + { + ResourceName: "tfe_hyok_configuration.hyok", + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + Config: testAccTFEAWSHYOKConfigurationConfig(orgName, "orange", "arn:aws:kms:us-east-1:123456789012:key/key2", "us-east-2"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "orange"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "aws"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "arn:aws:kms:us-east-1:123456789012:key/key2"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kms_options.key_region", "us-east-2"), + ), + }, + // Delete - must first revoke configuration to avoid dangling resources + { + PreConfig: func() { revokeHYOKConfiguration(t, state.ID) }, + Config: testAccTFEHYOKConfigurationDestroyConfig(orgName), + }, + }, + }) + + // With Vault OIDC configuration + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEVaultHYOKConfigurationConfig(orgName, "peach", "key1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTFEHYOKConfigurationExists("tfe_hyok_configuration.hyok", state), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "peach"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "vault"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "key1"), + ), + }, + // Import + { + ResourceName: "tfe_hyok_configuration.hyok", + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + Config: testAccTFEVaultHYOKConfigurationConfig(orgName, "strawberry", "key2"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "strawberry"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "vault"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "key2"), + ), + }, + // Delete - must first revoke configuration to avoid dangling resources + { + PreConfig: func() { revokeHYOKConfiguration(t, state.ID) }, + Config: testAccTFEHYOKConfigurationDestroyConfig(orgName), + }, + }, + }) + + // With GCP OIDC configuration + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEGCPHYOKConfigurationConfig(orgName, "cucumber", "key1", "global", "key-ring-1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTFEHYOKConfigurationExists("tfe_hyok_configuration.hyok", state), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "cucumber"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "gcp"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "key1"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kms_options.key_location", "global"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kms_options.key_ring_id", "key-ring-1"), + ), + }, + // Import + { + ResourceName: "tfe_hyok_configuration.hyok", + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + Config: testAccTFEGCPHYOKConfigurationConfig(orgName, "tomato", "key2", "global", "key-ring-2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTFEHYOKConfigurationExists("tfe_hyok_configuration.hyok", state), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "tomato"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "gcp"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "key2"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kms_options.key_location", "global"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kms_options.key_ring_id", "key-ring-2"), + ), + }, + // Delete - must first revoke configuration to avoid dangling resources + { + PreConfig: func() { revokeHYOKConfiguration(t, state.ID) }, + Config: testAccTFEHYOKConfigurationDestroyConfig(orgName), + }, + }, + }) + + // With Azure OIDC configuration + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEAzureHYOKConfigurationConfig(orgName, "banana", "https://random.vault.azure.net/keys/key1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTFEHYOKConfigurationExists("tfe_hyok_configuration.hyok", state), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "banana"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "azure"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "https://random.vault.azure.net/keys/key1"), + ), + }, + // Import + { + ResourceName: "tfe_hyok_configuration.hyok", + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + Config: testAccTFEAzureHYOKConfigurationConfig(orgName, "blueberry", "https://random.vault.azure.net/keys/key2"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "name", "blueberry"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "oidc_configuration_type", "azure"), + resource.TestCheckResourceAttrSet("tfe_hyok_configuration.hyok", "oidc_configuration_id"), + resource.TestCheckResourceAttr("tfe_hyok_configuration.hyok", "kek_id", "https://random.vault.azure.net/keys/key2"), + ), + }, + // Delete - must first revoke configuration to avoid dangling resources + { + PreConfig: func() { revokeHYOKConfiguration(t, state.ID) }, + Config: testAccTFEHYOKConfigurationDestroyConfig(orgName), + }, + }, + }) +} + +func revokeHYOKConfiguration(t *testing.T, id string) { + err := testAccConfiguredClient.Client.HYOKConfigurations.Revoke(ctx, id) + if err != nil { + t.Fatalf("failed to revoke HYOK configuration: %v", err) + } + + // Wait for configuration to be in the revoked status + _, err = retryFn(10, 1, func() (any, error) { + hyok, err := testAccConfiguredClient.Client.HYOKConfigurations.Read(ctx, id, nil) + if err != nil { + t.Fatalf("failed to read HYOK configuration: %v", err) + } + + if hyok.Status != tfe.HYOKConfigurationRevoked { + return nil, fmt.Errorf("expected HYOK configuration to be revoked, got %s", hyok.Status) + } + return nil, nil + }) + + if err != nil { + t.Fatal(err) + } +} + +func testAccCheckTFEHYOKConfigurationExists(n string, hyokConfig *tfe.HYOKConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no instance ID is set") + } + + result, err := testAccConfiguredClient.Client.HYOKConfigurations.Read(ctx, rs.Primary.ID, nil) + if err != nil { + return err + } + + *hyokConfig = *result + + return nil + } +} + +func testAccTFEAWSHYOKConfigurationConfig(orgName string, name string, kekID string, keyRegion string) string { + return fmt.Sprintf(` +resource "tfe_agent_pool" "pool" { + name = "hyok-pool" + organization = "%s" +} + +resource "tfe_aws_oidc_configuration" "aws_oidc_config" { + role_arn = "arn:aws:iam::111111111111:role/example-role-arn" + organization = "%s" +} + +resource "tfe_hyok_configuration" "hyok" { + organization = "%s" + name = "%s" + kek_id = "%s" + agent_pool_id = resource.tfe_agent_pool.pool.id + oidc_configuration_id = resource.tfe_aws_oidc_configuration.aws_oidc_config.id + oidc_configuration_type = "aws" + kms_options { + key_region = "%s" + } +} +`, orgName, orgName, orgName, name, kekID, keyRegion) +} + +func testAccTFEVaultHYOKConfigurationConfig(orgName string, name string, kekID string) string { + return fmt.Sprintf(` +resource "tfe_agent_pool" "pool" { + name = "hyok-pool" + organization = "%s" +} + +resource "tfe_vault_oidc_configuration" "vault_oidc_config" { + address = "https://vault.example.com" + role_name = "vault-role-name" + namespace = "admin" + organization = "%s" +} + +resource "tfe_hyok_configuration" "hyok" { + organization = "%s" + name = "%s" + kek_id = "%s" + agent_pool_id = resource.tfe_agent_pool.pool.id + oidc_configuration_id = resource.tfe_vault_oidc_configuration.vault_oidc_config.id + oidc_configuration_type = "vault" +} +`, orgName, orgName, orgName, name, kekID) +} + +func testAccTFEGCPHYOKConfigurationConfig(orgName string, name string, kekID string, keyLocation string, keyRingID string) string { + return fmt.Sprintf(` +resource "tfe_agent_pool" "pool" { + name = "hyok-pool" + organization = "%s" +} + +resource "tfe_gcp_oidc_configuration" "gcp_oidc_config" { + service_account_email = "myemail@gmail.com" + project_number = "11111111" + workload_provider_name = "projects/1/locations/global/workloadIdentityPools/1/providers/1" + organization = "%s" +} + +resource "tfe_hyok_configuration" "hyok" { + organization = "%s" + name = "%s" + kek_id = "%s" + agent_pool_id = resource.tfe_agent_pool.pool.id + oidc_configuration_id = resource.tfe_gcp_oidc_configuration.gcp_oidc_config.id + oidc_configuration_type = "gcp" + kms_options { + key_location = "%s" + key_ring_id = "%s" + } +} +`, orgName, orgName, orgName, name, kekID, keyLocation, keyRingID) +} + +func testAccTFEAzureHYOKConfigurationConfig(orgName string, name string, kekID string) string { + return fmt.Sprintf(` +resource "tfe_agent_pool" "pool" { + name = "hyok-pool" + organization = "%s" +} + +resource "tfe_azure_oidc_configuration" "azure_oidc_config" { + client_id = "application-id-1" + subscription_id = "subscription-id-1" + tenant_id = "tenant-id1" + organization = "%s" +} + +resource "tfe_hyok_configuration" "hyok" { + organization = "%s" + name = "%s" + kek_id = "%s" + agent_pool_id = resource.tfe_agent_pool.pool.id + oidc_configuration_id = resource.tfe_azure_oidc_configuration.azure_oidc_config.id + oidc_configuration_type = "azure" +} +`, orgName, orgName, orgName, name, kekID) +} + +func testAccTFEHYOKConfigurationDestroyConfig(orgName string) string { + return fmt.Sprintf(` +resource "tfe_agent_pool" "pool" { + name = "hyok-pool" + organization = "%s" +} +`, orgName) +} diff --git a/internal/provider/resource_tfe_vault_oidc_configuration.go b/internal/provider/resource_tfe_vault_oidc_configuration.go new file mode 100644 index 000000000..7fd47b038 --- /dev/null +++ b/internal/provider/resource_tfe_vault_oidc_configuration.go @@ -0,0 +1,241 @@ +// // Copyright (c) HashiCorp, Inc. +// // SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + + tfe "github.com/hashicorp/go-tfe" + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + _ resource.ResourceWithConfigure = &resourceTFEVaultOIDCConfiguration{} + _ resource.ResourceWithImportState = &resourceTFEVaultOIDCConfiguration{} +) + +func NewVaultOIDCConfigurationResource() resource.Resource { + return &resourceTFEVaultOIDCConfiguration{} +} + +type resourceTFEVaultOIDCConfiguration struct { + config ConfiguredClient +} + +type modelTFEVaultOIDCConfiguration struct { + ID types.String `tfsdk:"id"` + Address types.String `tfsdk:"address"` + RoleName types.String `tfsdk:"role_name"` + Namespace types.String `tfsdk:"namespace"` + JWTAuthPath types.String `tfsdk:"auth_path"` + TLSCACertificate types.String `tfsdk:"encoded_cacert"` + Organization types.String `tfsdk:"organization"` +} + +func (r *resourceTFEVaultOIDCConfiguration) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + 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 *resourceTFEVaultOIDCConfiguration) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vault_oidc_configuration" +} + +func (r *resourceTFEVaultOIDCConfiguration) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the Vault OIDC configuration.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "address": schema.StringAttribute{ + Description: "The full address of your Vault instance.", + Required: true, + }, + "role_name": schema.StringAttribute{ + Description: "The name of a role in your Vault JWT auth path, with permission to encrypt and decrypt with a Transit secrets engine key.", + Required: true, + }, + "namespace": schema.StringAttribute{ + Description: "The namespace your JWT auth path is mounted in.", + Required: true, + }, + "auth_path": schema.StringAttribute{ + Description: `The mounting path of JWT auth path of JWT auth. Defaults to "jwt".`, + Optional: true, + Computed: true, + Default: stringdefault.StaticString("jwt"), + }, + "encoded_cacert": schema.StringAttribute{ + Description: "A base64 encoded certificate which can be used to authenticate your Vault certificate. Only needed for self-hosted Vault Enterprise instances with a self-signed certificate.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization to which the TFE Vault OIDC configuration belongs.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Description: "Generates a new TFE Vault OIDC Configuration.", + } +} + +func (r *resourceTFEVaultOIDCConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceTFEVaultOIDCConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan into the model + var plan modelTFEVaultOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the organization name from resource or provider config + var orgName string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.VaultOIDCConfigurationCreateOptions{ + Address: plan.Address.ValueString(), + RoleName: plan.RoleName.ValueString(), + Namespace: plan.Namespace.ValueString(), + JWTAuthPath: plan.JWTAuthPath.ValueString(), + TLSCACertificate: plan.TLSCACertificate.ValueString(), + } + + tflog.Debug(ctx, fmt.Sprintf("Create TFE Vault OIDC Configuration for organization %s", orgName)) + oidc, err := r.config.Client.VaultOIDCConfigurations.Create(ctx, orgName, options) + if err != nil { + resp.Diagnostics.AddError("Error creating TFE Vault OIDC Configuration", err.Error()) + return + } + result := modelFromTFEVaultOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEVaultOIDCConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform state into the model + var state modelTFEVaultOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Read Vault OIDC configuration: %s", oidcID)) + oidc, err := r.config.Client.VaultOIDCConfigurations.Read(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("Vault OIDC configuration %s no longer exists", oidcID)) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading Vault OIDC configuration %s", oidcID), + err.Error(), + ) + return + } + result := modelFromTFEVaultOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEVaultOIDCConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFEVaultOIDCConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + var state modelTFEVaultOIDCConfiguration + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + options := tfe.VaultOIDCConfigurationUpdateOptions{ + Address: plan.Address.ValueStringPointer(), + RoleName: plan.RoleName.ValueStringPointer(), + Namespace: plan.Namespace.ValueStringPointer(), + JWTAuthPath: plan.JWTAuthPath.ValueStringPointer(), + TLSCACertificate: plan.TLSCACertificate.ValueStringPointer(), + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Update TFE Vault OIDC Configuration %s", oidcID)) + oidc, err := r.config.Client.VaultOIDCConfigurations.Update(ctx, oidcID, options) + if err != nil { + resp.Diagnostics.AddError("Error updating TFE Vault OIDC Configuration", err.Error()) + return + } + + result := modelFromTFEVaultOIDCConfiguration(oidc) + resp.Diagnostics.Append(resp.State.Set(ctx, result)...) +} + +func (r *resourceTFEVaultOIDCConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFEVaultOIDCConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + oidcID := state.ID.ValueString() + tflog.Debug(ctx, fmt.Sprintf("Delete TFE Vault OIDC configuration: %s", oidcID)) + err := r.config.Client.VaultOIDCConfigurations.Delete(ctx, oidcID) + if err != nil { + if errors.Is(err, tfe.ErrResourceNotFound) { + tflog.Debug(ctx, fmt.Sprintf("TFE Vault OIDC configuration %s no longer exists", oidcID)) + return + } + + resp.Diagnostics.AddError("Error deleting TFE Vault OIDC Configuration", err.Error()) + return + } +} + +func modelFromTFEVaultOIDCConfiguration(p *tfe.VaultOIDCConfiguration) modelTFEVaultOIDCConfiguration { + return modelTFEVaultOIDCConfiguration{ + ID: types.StringValue(p.ID), + Address: types.StringValue(p.Address), + RoleName: types.StringValue(p.RoleName), + Namespace: types.StringValue(p.Namespace), + JWTAuthPath: types.StringValue(p.JWTAuthPath), + TLSCACertificate: types.StringValue(p.TLSCACertificate), + Organization: types.StringValue(p.Organization.Name), + } +} diff --git a/internal/provider/resource_tfe_vault_oidc_configuration_test.go b/internal/provider/resource_tfe_vault_oidc_configuration_test.go new file mode 100644 index 000000000..e609001b6 --- /dev/null +++ b/internal/provider/resource_tfe_vault_oidc_configuration_test.go @@ -0,0 +1,99 @@ +package provider + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccTFEVaultOIDCConfiguration_basic(t *testing.T) { + skipUnlessHYOKEnabled(t) + + orgName := os.Getenv("HYOK_ORGANIZATION_NAME") + if orgName == "" { + t.Skip("Skipping test. Set HYOK_ORGANIZATION_NAME environment to enable test.") + } + + originalAddress := "https://vault.example.com" + updatedAddress := "https://vault.example2.com" + originalRoleName := "role-name-1" + updatedRoleName := "role-name-2" + originalNamespace := "admin-1" + updatedNamespace := "admin-2" + originalAuthPath := "jwt" + updatedAuthPath := "jwt2" + originalCACert := "" + updatedCACert := "some-cert" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEVaultOIDCConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEVaultOIDCConfigurationConfig(orgName, originalAddress, originalRoleName, originalNamespace, originalAuthPath, originalCACert), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_vault_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "address", originalAddress), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "role_name", originalRoleName), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "namespace", originalNamespace), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "auth_path", originalAuthPath), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "encoded_cacert", originalCACert), + ), + }, + // Import + { + ResourceName: "tfe_vault_oidc_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + Config: testAccTFEVaultOIDCConfigurationConfig(orgName, updatedAddress, updatedRoleName, updatedNamespace, updatedAuthPath, updatedCACert), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("tfe_vault_oidc_configuration.test", "id"), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "address", updatedAddress), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "role_name", updatedRoleName), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "namespace", updatedNamespace), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "auth_path", updatedAuthPath), + resource.TestCheckResourceAttr("tfe_vault_oidc_configuration.test", "encoded_cacert", updatedCACert), + ), + }, + }, + }) +} + +func testAccTFEVaultOIDCConfigurationConfig(orgName string, address string, roleName string, namespace string, authPath string, cacert string) string { + return fmt.Sprintf(` +resource "tfe_vault_oidc_configuration" "test" { + address = "%s" + role_name = "%s" + namespace = "%s" + auth_path = "%s" + encoded_cacert = "%s" + organization = "%s" +} +`, address, roleName, namespace, authPath, cacert, orgName) +} + +func testAccCheckTFEVaultOIDCConfigurationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_vault_oidc_configuration" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no instance ID is set") + } + + _, err := testAccConfiguredClient.Client.VaultOIDCConfigurations.Read(ctx, rs.Primary.ID) + if err == nil { + return fmt.Errorf("TFE Vault OIDC Configuration %s still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/website/docs/d/hyok_customer_key_version.markdown b/website/docs/d/hyok_customer_key_version.markdown new file mode 100644 index 000000000..f25377cf8 --- /dev/null +++ b/website/docs/d/hyok_customer_key_version.markdown @@ -0,0 +1,35 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_hyok_customer_key_version" +description: |- + Get information on a HYOK customer key version. +--- + +# Data Source: tfe_hyok_customer_key_version + +Use this data source to get information about a Hold Your Own Keys (HYOK) customer key version. + +## Example Usage + +```hcl +data "tfe_hyok_customer_key_version" "tfe_hyok_customer_key_version1" { + id = "keyv-123" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - (Required) The ID of the HYOK customer key version. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `created_at` - The time when the customer key version was created. +* `error` - Any error message associated with the customer key version. +* `id` - The ID of the customer key version. +* `key_version` - The version number of the customer key. +* `status` - The status of the customer key version. +* `workspaces_secured` - The number of workspaces securefd by this customer key version. diff --git a/website/docs/d/hyok_encrypted_data_key.markdown b/website/docs/d/hyok_encrypted_data_key.markdown new file mode 100644 index 000000000..1226c0208 --- /dev/null +++ b/website/docs/d/hyok_encrypted_data_key.markdown @@ -0,0 +1,33 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_hyok_encrypted_data_key" +description: |- + Get information on a HYOK encrypted data key. +--- + +# Data Source: tfe_hyok_encrypted_data_key + +Use this data source to get information about a Hold Your Own Keys (HYOK) encrypted data key. + +## Example Usage + +```hcl +data "tfe_hyok_encrypted_data_key" "tfe_hyok_encrypted_data_key1" { + id = "dek-123" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - (Required) The ID of the HYOK encrypted data key. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `created_at` - The time when the encrypted data key was created. +* `customer_key_name` - The name of the customer key used to encrypt the data key. +* `encrypted_dek` - The encrypted data encryption key (DEK). +* `id` - The ID of the encrypted data key. \ No newline at end of file diff --git a/website/docs/r/aws_oidc_configuration.html.markdown b/website/docs/r/aws_oidc_configuration.html.markdown new file mode 100644 index 000000000..82750e2cb --- /dev/null +++ b/website/docs/r/aws_oidc_configuration.html.markdown @@ -0,0 +1,46 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_aws_oidc_configuration" +description: |- + Manages AWS OIDC configurations. +--- + +# tfe_aws_oidc_configuration + +Defines an AWS OIDC configuration resource. + +~> **NOTE:** This resource requires using the provider with HCP Terraform on the HCP Terraform Premium edition. Refer to [HCP Terraform pricing](https://www.hashicorp.com/en/pricing?product_intent=terraform&tab=terraform) for details. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_aws_oidc_configuration" "example" { + role_arn = "arn:aws:iam::111111111111:role/example-role-arn" + organization = "my-org-name" +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `role_arn` - (Required) The AWS ARN of your role.. +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The AWS OIDC configuration ID. + +## Import +AWS OIDC configurations can be imported by ID. + +Example: + +```shell +terraform import tfe_aws_oidc_configuration.example awsoidc-DXmy3B2emVHysnbq +``` diff --git a/website/docs/r/azure_oidc_configuration.html.markdown b/website/docs/r/azure_oidc_configuration.html.markdown new file mode 100644 index 000000000..57f964801 --- /dev/null +++ b/website/docs/r/azure_oidc_configuration.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_azure_oidc_configuration" +description: |- + Manages Azure OIDC configurations. +--- + +# tfe_azure_oidc_configuration + +Defines an Azure OIDC configuration resource. + +~> **NOTE:** This resource requires using the provider with HCP Terraform on the HCP Terraform Premium edition. Refer to [HCP Terraform pricing](https://www.hashicorp.com/en/pricing?product_intent=terraform&tab=terraform) for details. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_azure_oidc_configuration" "example" { + client_id = "application-id1" + subscription_id = "subscription-id1" + tenant_id = "tenant-id1" + organization = "my-org-name" +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `client_id` - (Required) The Client (or Application) ID of your Entra ID application. +* `subscription_id` - (Required) The ID of your Azure subscription. +* `tenant_id` - (Required) The Tenant (or Directory) ID of your Entra ID application. +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The Azure OIDC configuration ID. + +## Import +Azure OIDC configurations can be imported by ID. + +Example: + +```shell +terraform import tfe_azure_oidc_configuration.example azoidc-8DCgwEV2GbMcQjk8 +``` diff --git a/website/docs/r/gcp_oidc_configuration.html.markdown b/website/docs/r/gcp_oidc_configuration.html.markdown new file mode 100644 index 000000000..667d787fe --- /dev/null +++ b/website/docs/r/gcp_oidc_configuration.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_gcp_oidc_configuration" +description: |- + Manages GCP OIDC configurations. +--- + +# tfe_gcp_oidc_configuration + +Defines a GCP OIDC configuration resource. + +~> **NOTE:** This resource requires using the provider with HCP Terraform on the HCP Terraform Premium edition. Refer to [HCP Terraform pricing](https://www.hashicorp.com/en/pricing?product_intent=terraform&tab=terraform) for details. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_gcp_oidc_configuration" "example" { + service_account_email = "myemail@gmail.com" + project_number = "11111111" + workload_provider_name = "projects/1/locations/global/workloadIdentityPools/1/providers/1" + organization = "my-org-name" +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `service_account_email` - (Required) The email of your GCP service account, with permissions to encrypt and decrypt using a Cloud KMS key. +* `project_number` - (Required) The GCP Project containing the workload provider and service account. +* `workload_provider_name` - (Required) The fully qualified workload provider path. This should be in the format `projects/{project_number}/locations/global/workloadIdentityPools/{workload_identity_pool_id}/providers/{workload_identity_pool_provider_id}`. +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The GCP OIDC configuration ID. + +## Import +GCP OIDC configurations can be imported by ID. + +Example: + +```shell +terraform import tfe_gcp_oidc_configuration.example gcpoidc-PuXEeRoSaK3ENGj9 +``` diff --git a/website/docs/r/hyok_configuration.html.markdown b/website/docs/r/hyok_configuration.html.markdown new file mode 100644 index 000000000..e66288d9c --- /dev/null +++ b/website/docs/r/hyok_configuration.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_hyok_configuration" +description: |- + Manages HYOK configurations. +--- + +# tfe_hyok_configuration + +Defines a HYOK configuration resource. + +~> **NOTE:** This resource requires using the provider with HCP Terraform on the HCP Terraform Premium edition. Refer to [HCP Terraform pricing](https://www.hashicorp.com/en/pricing?product_intent=terraform&tab=terraform) for details. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_hyok_configuration" "example" { + organization = "my-hyok-org" + name = "my-key-name" + kek_id = "key1" + agent_pool_id = "apool-MFtsuFxHkC9pCRgB" + oidc_configuration_id = "gcpoidc-PuXEeRoSaK3ENGj9" + oidc_configuration_type = "gcp" + + kms_options { + key_location = "global" + key_ring_id = "example-key-ring" + } +} +``` + + +## Argument Reference + +The following arguments are supported: +* `name` - (Required) Label for the HYOK configuration to be used within HCP Terraform. +* `kek_id` - (Required) Refers to the name of your key encryption key stored in your key management service. +* `agent_pool_id` - (Required) The ID of the agent-pool to associate with the HYOK configuration. +* `oidc_configuration_id` - (Required) The ID of the TFE OIDC configuration. +* `oidc_configuration_type` - (Required) The type of OIDC configuration. Valid values are `vault`, `aws`, `gcp`, and `azure`. +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +The `kms_options` block is optional, and is used to specify additional fields for some key management services. Supported arguments are: +* `key_region` - (Optional) The AWS region where your key is located. +* `key_location` - (Optional) The location in which the GCP key ring exists. +* `key_ring_id` - (Optional) The root resource for Google Cloud KMS keys and key versions. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The HYOK configuration ID. + +## Import +HYOK configurations can be imported by ID. + +Example: + +```shell +terraform import tfe_hyok_configuration.gcp_example hyokc-XqYizSPQmeiG1aHJ +``` diff --git a/website/docs/r/vault_oidc_configuration.html.markdown b/website/docs/r/vault_oidc_configuration.html.markdown new file mode 100644 index 000000000..75716ba07 --- /dev/null +++ b/website/docs/r/vault_oidc_configuration.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_vault_oidc_configuration" +description: |- + Manages Vault OIDC configurations. +--- + +# tfe_vault_oidc_configuration + +Defines a Vault OIDC configuration resource. + +~> **NOTE:** This resource requires using the provider with HCP Terraform on the HCP Terraform Premium edition. Refer to [HCP Terraform pricing](https://www.hashicorp.com/en/pricing?product_intent=terraform&tab=terraform) for details. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_vault_oidc_configuration" "example" { + address = "https://my-vault-cluster-public-vault-659decf3.b8298d98.z1.hashicorp.cloud:8200" + role_name = "vault-role-name" + namespace = "admin" + auth_path = "jwt-auth-path" + encoded_cacert = "" + organization = "my-org-name" +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `address` - (Required) The full address of your Vault instance. +* `role_name` - (Required) The name of a role in your Vault JWT auth path, with permission to encrypt and decrypt with a Transit secrets engine key. +* `namespace` - (Required) The namespace your JWT auth path is mounted in. +* `auth_path` - (Optional) The mounting path of JWT auth path of JWT auth. Defaults to `"jwt"`. +* `encoded_cacert` - (Optional) A base64 encoded certificate which can be used to authenticate your Vault certificate. Only needed for self-hosted Vault Enterprise instances with a self-signed certificate. +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The Vault OIDC configuration ID. + +## Import +Vault OIDC configurations can be imported by ID. + +Example: + +```shell +terraform import tfe_vault_oidc_configuration.example voidc-AV61VxigiRvkkvPd +```