diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a06cb73f..d6e9fe9aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ FEATURES: * **New resource**: `r/tfe_aws_oidc_configuration` for managing AWS OIDC configurations. [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1835) * **New resource**: `r/tfe_gcp_oidc_configuration` for managing GCP OIDC configurations. [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1835) * **New resource**: `r/tfe_azure_oidc_configuration` for managing Azure OIDC configurations. [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1835) +* **New resource**: `r/tfe_hyok_configuration` for managing HYOK configurations. [#1835](https://github.com/hashicorp/terraform-provider-tfe/pull/1841) ## v0.70.0 diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index 7e9520d44..7a945fc2f 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -176,6 +176,7 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res NewGCPOIDCConfigurationResource, NewAzureOIDCConfigurationResource, NewVaultOIDCConfigurationResource, + NewHYOKConfigurationResource, } } 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..000649c61 --- /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) { + skipIfEnterprise(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/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 +```