From 2530494ede2837d2ffc1361f94fdf4a89d2e52af Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Thu, 29 Aug 2024 08:57:18 -0400 Subject: [PATCH 1/4] * implemented HVS AWS Integration resource - removed test secrets * generate doc + changelog + added import instructions * lint * anonymized example variables - remove compact summary to view changes * patch doc --- .changelog/1078.txt | 3 + .../vault_secrets_integration_aws.md | 81 ++++ .../import.sh | 4 + .../resource.tf | 17 + go.mod | 2 +- go.sum | 4 +- internal/clients/client.go | 15 + internal/clients/vault_secrets_preview.go | 18 +- internal/provider/provider.go | 1 + ...ource_vault_secrets_dynamic_secret_test.go | 12 +- .../vaultsecrets/integration_utils.go | 109 ++++++ .../resource_vault_secrets_integration_aws.go | 347 ++++++++++++++++++ ...urce_vault_secrets_integration_aws_test.go | 185 ++++++++++ 13 files changed, 784 insertions(+), 14 deletions(-) create mode 100644 .changelog/1078.txt create mode 100644 docs/resources/vault_secrets_integration_aws.md create mode 100644 examples/resources/hcp_vault_secrets_integration_aws/import.sh create mode 100644 examples/resources/hcp_vault_secrets_integration_aws/resource.tf create mode 100644 internal/provider/vaultsecrets/integration_utils.go create mode 100644 internal/provider/vaultsecrets/resource_vault_secrets_integration_aws.go create mode 100644 internal/provider/vaultsecrets/resource_vault_secrets_integration_aws_test.go diff --git a/.changelog/1078.txt b/.changelog/1078.txt new file mode 100644 index 000000000..18cbc73e0 --- /dev/null +++ b/.changelog/1078.txt @@ -0,0 +1,3 @@ +```release-note:feature +add vault_secrets_integration_aws resource +``` diff --git a/docs/resources/vault_secrets_integration_aws.md b/docs/resources/vault_secrets_integration_aws.md new file mode 100644 index 000000000..5de0d4b39 --- /dev/null +++ b/docs/resources/vault_secrets_integration_aws.md @@ -0,0 +1,81 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "hcp_vault_secrets_integration_aws Resource - terraform-provider-hcp" +subcategory: "" +description: |- + The Vault Secrets AWS integration resource manages an AWS integration. +--- + +# hcp_vault_secrets_integration_aws (Resource) + +The Vault Secrets AWS integration resource manages an AWS integration. + +## Example Usage + +```terraform +resource "hcp_vault_secrets_integration_aws" "example_with_access_keys" { + name = "my-aws-1" + capabilities = ["DYNAMIC", "ROTATION"] + access_keys = { + access_key_id = "AKIA..." + secret_access_key = "rgUK..." + } +} + +resource "hcp_vault_secrets_integration_aws" "example_with_identity_federation" { + name = "my-aws-1" + capabilities = ["DYNAMIC", "ROTATION"] + federated_workload_identity = { + role_arn = "arn:aws:iam:::role/>" + audience = "" + } +} +``` + + +## Schema + +### Required + +- `capabilities` (Set of String) Capabilities enabled for the integration. See the Vault Secrets documentation for the list of supported capabilities per provider. +- `name` (String) The Vault Secrets integration name. + +### Optional + +- `access_keys` (Attributes) AWS IAM key pair used to authenticate against the target AWS account. Cannot be used with `federated_workload_identity`. (see [below for nested schema](#nestedatt--access_keys)) +- `federated_workload_identity` (Attributes) (Recommended) Federated identity configuration to authenticate against the target AWS account. Cannot be used with `access_keys`. (see [below for nested schema](#nestedatt--federated_workload_identity)) +- `project_id` (String) HCP project ID that owns the HCP Vault Secrets integration. Inferred from the provider configuration if omitted. + +### Read-Only + +- `organization_id` (String) HCP organization ID that owns the HCP Vault Secrets integration. +- `resource_id` (String) Resource ID used to uniquely identify the integration instance on the HCP platform. +- `resource_name` (String) Resource name used to uniquely identify the integration instance on the HCP platform. + + +### Nested Schema for `access_keys` + +Required: + +- `access_key_id` (String) Key ID used with the secret key to authenticate against the target AWS account. +- `secret_access_key` (String, Sensitive) Secret key used with the key ID to authenticate against the target AWS account. + + + +### Nested Schema for `federated_workload_identity` + +Required: + +- `audience` (String) Audience configured on the AWS IAM identity provider to federate access with HCP. +- `role_arn` (String) AWS IAM role ARN the integration will assume to carry operations for the appropriate capabilities. + +## Import + +Import is supported using the following syntax: + +```shell +# Vault Secrets AWS Integration can be imported by specifying the name of the integration +# Note that since the AWS secret access key is never returned on the Vault Secrets API, +# the next plan or apply will show a diff for that field if using the access keys authentication method. +terraform import hcp_vault_secrets_integration_aws.example my-aws-1 +``` diff --git a/examples/resources/hcp_vault_secrets_integration_aws/import.sh b/examples/resources/hcp_vault_secrets_integration_aws/import.sh new file mode 100644 index 000000000..b47687b15 --- /dev/null +++ b/examples/resources/hcp_vault_secrets_integration_aws/import.sh @@ -0,0 +1,4 @@ +# Vault Secrets AWS Integration can be imported by specifying the name of the integration +# Note that since the AWS secret access key is never returned on the Vault Secrets API, +# the next plan or apply will show a diff for that field if using the access keys authentication method. +terraform import hcp_vault_secrets_integration_aws.example my-aws-1 diff --git a/examples/resources/hcp_vault_secrets_integration_aws/resource.tf b/examples/resources/hcp_vault_secrets_integration_aws/resource.tf new file mode 100644 index 000000000..0bfeca5d0 --- /dev/null +++ b/examples/resources/hcp_vault_secrets_integration_aws/resource.tf @@ -0,0 +1,17 @@ +resource "hcp_vault_secrets_integration_aws" "example_with_access_keys" { + name = "my-aws-1" + capabilities = ["DYNAMIC", "ROTATION"] + access_keys = { + access_key_id = "AKIA..." + secret_access_key = "rgUK..." + } +} + +resource "hcp_vault_secrets_integration_aws" "example_with_identity_federation" { + name = "my-aws-1" + capabilities = ["DYNAMIC", "ROTATION"] + federated_workload_identity = { + role_arn = "arn:aws:iam:::role/>" + audience = "" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index ce32395c6..c4206007f 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hcp-sdk-go v0.105.0 + github.com/hashicorp/hcp-sdk-go v0.108.0 github.com/hashicorp/terraform-plugin-docs v0.19.4 github.com/hashicorp/terraform-plugin-framework v1.5.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 diff --git a/go.sum b/go.sum index f127c6a6e..5a6d42f42 100644 --- a/go.sum +++ b/go.sum @@ -122,8 +122,8 @@ github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC16 github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= -github.com/hashicorp/hcp-sdk-go v0.105.0 h1:KKqOBi13+wMEvMEG65brBJIXzvZcxjehVzk6vipaaSE= -github.com/hashicorp/hcp-sdk-go v0.105.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.108.0 h1:MgP7vGTx5A34l9HpktvQSY4nNfwCBpIXRJhJCHCxcyQ= +github.com/hashicorp/hcp-sdk-go v0.108.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= diff --git a/internal/clients/client.go b/internal/clients/client.go index 4a7d208e4..1edefc8f3 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/hcp-sdk-go/auth" "github.com/hashicorp/hcp-sdk-go/auth/workload" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" cloud_billing "github.com/hashicorp/hcp-sdk-go/clients/cloud-billing/preview/2020-11-05/client" @@ -276,3 +277,17 @@ func (cl *Client) GetProjectID() string { } return cl.Config.ProjectID } + +// Location returns the organization and project ID to use for a given resource +// The resource project ID takes precedence over the provider project ID +func (cl *Client) Location(resourceProjectID types.String) (string, string) { + orgID := cl.Config.OrganizationID + projID := cl.Config.ProjectID + + // project ID defined in the resource schema has precedence over the project ID from the provider + if !resourceProjectID.IsUnknown() { + projID = resourceProjectID.ValueString() + } + + return orgID, projID +} diff --git a/internal/clients/vault_secrets_preview.go b/internal/clients/vault_secrets_preview.go index 10e4487f9..6a6b76014 100644 --- a/internal/clients/vault_secrets_preview.go +++ b/internal/clients/vault_secrets_preview.go @@ -102,9 +102,12 @@ func GetRotatingSecretState(ctx context.Context, client *Client, loc *sharedmode // CreateMongoDBAtlasRotationIntegration NOTE: currently just needed for tests func CreateMongoDBAtlasRotationIntegration(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, integrationName, mongodbAtlasPublicKey, mongodbAtlasPrivateKey string) (*secretmodels.Secrets20231128MongoDBAtlasIntegration, error) { body := secretmodels.SecretServiceCreateMongoDBAtlasIntegrationBody{ - IntegrationName: integrationName, - MongodbAPIPublicKey: mongodbAtlasPublicKey, - MongodbAPIPrivateKey: mongodbAtlasPrivateKey, + Name: integrationName, + Capabilities: []*secretmodels.Secrets20231128Capability{secretmodels.Secrets20231128CapabilityROTATION.Pointer()}, + StaticCredentialDetails: &secretmodels.Secrets20231128MongoDBAtlasStaticCredentialsRequest{ + APIPrivateKey: mongodbAtlasPrivateKey, + APIPublicKey: mongodbAtlasPublicKey, + }, } params := secret_service.NewCreateMongoDBAtlasIntegrationParamsWithContext(ctx). WithOrganizationID(loc.OrganizationID). @@ -124,7 +127,7 @@ func DeleteMongoDBAtlasRotationIntegration(ctx context.Context, client *Client, params := secret_service.NewDeleteMongoDBAtlasIntegrationParamsWithContext(ctx). WithOrganizationID(loc.OrganizationID). WithProjectID(loc.ProjectID). - WithIntegrationName(integrationName) + WithName(integrationName) _, err := client.VaultSecretsPreview.DeleteMongoDBAtlasIntegration(params, nil) if err != nil { @@ -157,11 +160,12 @@ func CreateMongoDBAtlasRotatingSecret( } // CreateAwsIntegration NOTE: currently just needed for tests -func CreateAwsIntegration(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, name, roleArn string) (*secretmodels.Secrets20231128AwsIntegration, error) { +func CreateAwsIntegration(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, name, roleArn, audience string, capabilities []*secretmodels.Secrets20231128Capability) (*secretmodels.Secrets20231128AwsIntegration, error) { body := secretmodels.SecretServiceCreateAwsIntegrationBody{ - Name: name, + Name: name, + Capabilities: capabilities, FederatedWorkloadIdentity: &secretmodels.Secrets20231128AwsFederatedWorkloadIdentityRequest{ - Audience: loc.OrganizationID, + Audience: audience, RoleArn: roleArn, }, } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 722975008..80138dc2b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -153,6 +153,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res vaultsecrets.NewVaultSecretsSecretResource, vaultsecrets.NewVaultSecretsAppIAMPolicyResource, vaultsecrets.NewVaultSecretsAppIAMBindingResource, + vaultsecrets.NewVaultSecretsIntegrationAWSResource, // IAM iam.NewServicePrincipalResource, iam.NewServicePrincipalKeyResource, diff --git a/internal/provider/vaultsecrets/data_source_vault_secrets_dynamic_secret_test.go b/internal/provider/vaultsecrets/data_source_vault_secrets_dynamic_secret_test.go index 12a4f5b43..9c73d1007 100644 --- a/internal/provider/vaultsecrets/data_source_vault_secrets_dynamic_secret_test.go +++ b/internal/provider/vaultsecrets/data_source_vault_secrets_dynamic_secret_test.go @@ -9,6 +9,7 @@ import ( "testing" sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-hcp/internal/clients" @@ -19,6 +20,7 @@ var ctx = context.Background() func TestAcc_dataSourceVaultSecretsDynamicSecret(t *testing.T) { integrationRoleArn := checkRequiredEnvVarOrFail(t, "AWS_INTEGRATION_ROLE_ARN") + integrationAudience := checkRequiredEnvVarOrFail(t, "AWS_INTEGRATION_AUDIENCE") secretRoleArn := checkRequiredEnvVarOrFail(t, "AWS_SECRET_ROLE_ARN") appName := generateRandomSlug() @@ -38,7 +40,7 @@ func TestAcc_dataSourceVaultSecretsDynamicSecret(t *testing.T) { { PreConfig: func() { createTestApp(t, appName) - createTestAwsIntegration(t, integrationName, integrationRoleArn) + createTestAwsIntegration(t, integrationName, integrationRoleArn, integrationAudience, []*secretmodels.Secrets20231128Capability{secretmodels.Secrets20231128CapabilityDYNAMIC.Pointer()}) createTestAwsDynamicSecret(t, appName, integrationName, secretName, secretRoleArn) }, Config: tfconfig, @@ -65,7 +67,7 @@ func TestAcc_dataSourceVaultSecretsDynamicSecret(t *testing.T) { }) } -func createTestAwsIntegration(t *testing.T, name, roleArn string) { +func createTestAwsIntegration(t *testing.T, name, roleArn, audience string, capabilities []*secretmodels.Secrets20231128Capability) { t.Helper() client := acctest.HCPClients(t) @@ -74,13 +76,13 @@ func createTestAwsIntegration(t *testing.T, name, roleArn string) { ProjectID: client.Config.ProjectID, } - _, err := clients.CreateAwsIntegration(ctx, client, loc, name, roleArn) + _, err := clients.CreateAwsIntegration(ctx, client, loc, name, roleArn, audience, capabilities) if err != nil { t.Fatal(err) } } -func deleteTestAwsIntegration(t *testing.T, name string) { +func deleteTestAwsIntegration(t *testing.T, name string) resource.TestCheckFunc { t.Helper() client := acctest.HCPClients(t) @@ -93,6 +95,8 @@ func deleteTestAwsIntegration(t *testing.T, name string) { if err != nil { t.Error(err) } + + return nil } func createTestAwsDynamicSecret(t *testing.T, appName, secretName, integrationName, roleArn string) { diff --git a/internal/provider/vaultsecrets/integration_utils.go b/internal/provider/vaultsecrets/integration_utils.go new file mode 100644 index 000000000..2431378c0 --- /dev/null +++ b/internal/provider/vaultsecrets/integration_utils.go @@ -0,0 +1,109 @@ +package vaultsecrets + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +// sharedIntegrationAttributes are the attributes shared between all the Vault Secrets integrations +var sharedIntegrationAttributes = map[string]schema.Attribute{ + "organization_id": schema.StringAttribute{ + Description: "HCP organization ID that owns the HCP Vault Secrets integration.", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "HCP project ID that owns the HCP Vault Secrets integration. Inferred from the provider configuration if omitted.", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "resource_id": schema.StringAttribute{ + Description: "Resource ID used to uniquely identify the integration instance on the HCP platform.", + Computed: true, + }, + "resource_name": schema.StringAttribute{ + Description: "Resource name used to uniquely identify the integration instance on the HCP platform.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "The Vault Secrets integration name.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[-\da-zA-Z]{3,36}$`), + "must contain only letters, numbers or hyphens", + ), + }, + }, + "capabilities": schema.SetAttribute{ + ElementType: types.StringType, + Description: "Capabilities enabled for the integration. See the Vault Secrets documentation for the list of supported capabilities per provider.", + Required: true, + }, +} + +// resourceFunc is used to get the appropriate Terraform Vault Secrets integration representation either from the plan (create, update) or the state (read, delete) +type resourceFunc func(ctx context.Context, target interface{}) diag.Diagnostics + +// operationFunc performs the desired operation (read, create, update, delete) on the Vault Secrets backend +type operationFunc func(i integration) (any, error) + +// integration abstracts the conversion between Terraform and HVS domains +type integration interface { + projectID() types.String + initModel(ctx context.Context, orgID, projID string) diag.Diagnostics + fromModel(ctx context.Context, orgID, projID string, model any) diag.Diagnostics +} + +// decorateOperation abstracts all the conversion between the Terraform and HVS domain, +// as well as all the statefile management when performing operations (read, create, update, delete) +func decorateOperation[T integration](ctx context.Context, c *clients.Client, state *tfsdk.State, resourceFunc resourceFunc, operation string, operationFunc operationFunc) diag.Diagnostics { + diags := diag.Diagnostics{} + + var concreteIntegration T + diags.Append(resourceFunc(ctx, &concreteIntegration)...) + if diags.HasError() { + return diags + } + + orgID, projID := c.Location(concreteIntegration.projectID()) + diags.Append(concreteIntegration.initModel(ctx, orgID, projID)...) + if diags.HasError() { + return diags + } + + model, err := operationFunc(concreteIntegration) + if err != nil { + diags.AddError(fmt.Sprintf("Error %s Vault Secrets integration", operation), err.Error()) + return diags + } + if model == nil { + state.RemoveResource(ctx) + return diags + } + + diags.Append(concreteIntegration.fromModel(ctx, orgID, projID, model)...) + if diags.HasError() { + return diags + } + + diags.Append(state.Set(ctx, &concreteIntegration)...) + + return diags +} diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_integration_aws.go b/internal/provider/vaultsecrets/resource_vault_secrets_integration_aws.go new file mode 100644 index 000000000..f2df24a5c --- /dev/null +++ b/internal/provider/vaultsecrets/resource_vault_secrets_integration_aws.go @@ -0,0 +1,347 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vaultsecrets + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/modifiers" + "golang.org/x/exp/maps" +) + +type IntegrationAWS struct { + // Input fields + ProjectID types.String `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` + Capabilities types.Set `tfsdk:"capabilities"` + AccessKeys types.Object `tfsdk:"access_keys"` + FederatedWorkloadIdentity types.Object `tfsdk:"federated_workload_identity"` + + // Computed fields + OrganizationID types.String `tfsdk:"organization_id"` + ResourceID types.String `tfsdk:"resource_id"` + ResourceName types.String `tfsdk:"resource_name"` + + // Inner API-compatible models derived from the Terraform fields + capabilities []*secretmodels.Secrets20231128Capability `tfsdk:"-"` + accessKeys *secretmodels.Secrets20231128AwsAccessKeysRequest `tfsdk:"-"` + federatedWorkloadIdentity *secretmodels.Secrets20231128AwsFederatedWorkloadIdentityRequest `tfsdk:"-"` +} + +// Helper structs to help populate concrete targets from types.Object fields +type accessKeys struct { + AccessKeyID types.String `tfsdk:"access_key_id"` + SecretAccessKey types.String `tfsdk:"secret_access_key"` +} + +type federatedWorkloadIdentity struct { + RoleARN types.String `tfsdk:"role_arn"` + Audience types.String `tfsdk:"audience"` +} + +var _ resource.Resource = &resourceVaultSecretsIntegrationAWS{} +var _ resource.ResourceWithConfigure = &resourceVaultSecretsIntegrationAWS{} +var _ resource.ResourceWithModifyPlan = &resourceVaultSecretsIntegrationAWS{} +var _ resource.ResourceWithImportState = &resourceVaultSecretsIntegrationAWS{} + +func NewVaultSecretsIntegrationAWSResource() resource.Resource { + return &resourceVaultSecretsIntegrationAWS{} +} + +type resourceVaultSecretsIntegrationAWS struct { + client *clients.Client +} + +func (r *resourceVaultSecretsIntegrationAWS) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vault_secrets_integration_aws" +} + +func (r *resourceVaultSecretsIntegrationAWS) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "access_keys": schema.SingleNestedAttribute{ + Description: "AWS IAM key pair used to authenticate against the target AWS account. Cannot be used with `federated_workload_identity`.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "access_key_id": schema.StringAttribute{ + Description: "Key ID used with the secret key to authenticate against the target AWS account.", + Required: true, + }, + "secret_access_key": schema.StringAttribute{ + Description: "Secret key used with the key ID to authenticate against the target AWS account.", + Required: true, + Sensitive: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("federated_workload_identity"), + }...), + }, + }, + "federated_workload_identity": schema.SingleNestedAttribute{ + Description: "(Recommended) Federated identity configuration to authenticate against the target AWS account. Cannot be used with `access_keys`.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "role_arn": schema.StringAttribute{ + Description: "AWS IAM role ARN the integration will assume to carry operations for the appropriate capabilities.", + Required: true, + }, + "audience": schema.StringAttribute{ + Description: "Audience configured on the AWS IAM identity provider to federate access with HCP.", + Required: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("access_keys"), + }...), + }, + }, + } + + maps.Copy(attributes, sharedIntegrationAttributes) + + resp.Schema = schema.Schema{ + MarkdownDescription: "The Vault Secrets AWS integration resource manages an AWS integration.", + Attributes: attributes, + } +} + +func (r *resourceVaultSecretsIntegrationAWS) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*clients.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.client = client +} + +func (r *resourceVaultSecretsIntegrationAWS) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + modifiers.ModifyPlanForDefaultProjectChange(ctx, r.client.Config.ProjectID, req.State, req.Config, req.Plan, resp) +} + +func (r *resourceVaultSecretsIntegrationAWS) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationAWS](ctx, r.client, &resp.State, req.State.Get, "reading", func(i integration) (any, error) { + integration, ok := i.(*IntegrationAWS) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + response, err := r.client.VaultSecretsPreview.GetAwsIntegration( + secret_service.NewGetAwsIntegrationParamsWithContext(ctx). + WithOrganizationID(integration.OrganizationID.ValueString()). + WithProjectID(integration.ProjectID.ValueString()). + WithName(integration.Name.ValueString()), nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + if response == nil || response.Payload == nil { + return nil, nil + } + return response.Payload.Integration, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationAWS) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationAWS](ctx, r.client, &resp.State, req.Plan.Get, "creating", func(i integration) (any, error) { + integration, ok := i.(*IntegrationAWS) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + response, err := r.client.VaultSecretsPreview.CreateAwsIntegration(&secret_service.CreateAwsIntegrationParams{ + Body: &secretmodels.SecretServiceCreateAwsIntegrationBody{ + AccessKeys: integration.accessKeys, + Capabilities: integration.capabilities, + FederatedWorkloadIdentity: integration.federatedWorkloadIdentity, + Name: integration.Name.ValueString(), + }, + OrganizationID: integration.OrganizationID.ValueString(), + ProjectID: integration.ProjectID.ValueString(), + }, nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + if response == nil || response.Payload == nil { + return nil, nil + } + return response.Payload.Integration, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationAWS) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationAWS](ctx, r.client, &resp.State, req.Plan.Get, "updating", func(i integration) (any, error) { + integration, ok := i.(*IntegrationAWS) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + response, err := r.client.VaultSecretsPreview.UpdateAwsIntegration(&secret_service.UpdateAwsIntegrationParams{ + Body: &secretmodels.SecretServiceUpdateAwsIntegrationBody{ + AccessKeys: integration.accessKeys, + Capabilities: integration.capabilities, + FederatedWorkloadIdentity: integration.federatedWorkloadIdentity, + }, + Name: integration.Name.ValueString(), + OrganizationID: integration.OrganizationID.ValueString(), + ProjectID: integration.ProjectID.ValueString(), + }, nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + if response == nil || response.Payload == nil { + return nil, nil + } + return response.Payload.Integration, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationAWS) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationAWS](ctx, r.client, &resp.State, req.State.Get, "deleting", func(i integration) (any, error) { + integration, ok := i.(*IntegrationAWS) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + _, err := r.client.VaultSecretsPreview.DeleteAwsIntegration( + secret_service.NewDeleteAwsIntegrationParamsWithContext(ctx). + WithOrganizationID(integration.OrganizationID.ValueString()). + WithProjectID(integration.ProjectID.ValueString()). + WithName(integration.Name.ValueString()), nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + return nil, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationAWS) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // The Vault Secrets API does not return sensitive values like the secret access key, so they will be initialized to an empty value + // It means the first plan/apply after a successful import will always show a diff for the secret access key + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), r.client.Config.OrganizationID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), r.client.Config.ProjectID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), req.ID)...) +} + +var _ integration = &IntegrationAWS{} + +func (i *IntegrationAWS) projectID() types.String { + return i.ProjectID + +} +func (i *IntegrationAWS) initModel(ctx context.Context, orgID, projID string) diag.Diagnostics { + // Init fields that depend on the Terraform provider configuration + i.OrganizationID = types.StringValue(orgID) + i.ProjectID = types.StringValue(projID) + + // Init the HVS domain models from the Terraform domain models + var capabilities []types.String + diags := i.Capabilities.ElementsAs(ctx, &capabilities, false) + if diags.HasError() { + return diags + } + for _, c := range capabilities { + i.capabilities = append(i.capabilities, secretmodels.Secrets20231128Capability(c.ValueString()).Pointer()) + } + + if !i.AccessKeys.IsNull() { + ak := accessKeys{} + diags = i.AccessKeys.As(ctx, &ak, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return diags + } + + i.accessKeys = &secretmodels.Secrets20231128AwsAccessKeysRequest{ + AccessKeyID: ak.AccessKeyID.ValueString(), + SecretAccessKey: ak.SecretAccessKey.ValueString(), + } + } + + if !i.FederatedWorkloadIdentity.IsNull() { + fwi := federatedWorkloadIdentity{} + diags = i.FederatedWorkloadIdentity.As(ctx, &fwi, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return diags + } + + i.federatedWorkloadIdentity = &secretmodels.Secrets20231128AwsFederatedWorkloadIdentityRequest{ + RoleArn: fwi.RoleARN.ValueString(), + Audience: fwi.Audience.ValueString(), + } + } + + return diag.Diagnostics{} +} + +func (i *IntegrationAWS) fromModel(ctx context.Context, orgID, projID string, model any) diag.Diagnostics { + diags := diag.Diagnostics{} + + integrationModel, ok := model.(*secretmodels.Secrets20231128AwsIntegration) + if !ok { + diags.AddError("Invalid model type, this is a bug on the provider.", fmt.Sprintf("Expected *secretmodels.Secrets20231128AwsIntegration, got: %T", model)) + return diags + } + + i.OrganizationID = types.StringValue(orgID) + i.ProjectID = types.StringValue(projID) + i.ResourceID = types.StringValue(integrationModel.ResourceID) + i.ResourceName = types.StringValue(integrationModel.ResourceName) + i.Name = types.StringValue(integrationModel.Name) + + var values []attr.Value + for _, c := range integrationModel.Capabilities { + values = append(values, types.StringValue(string(*c))) + } + i.Capabilities, diags = types.SetValue(types.StringType, values) + if diags.HasError() { + return diags + } + + if integrationModel.AccessKeys != nil { + // The secret key is not returned by the API, so we use an empty value (e.g. for imports) or the state value (e.g. for updates) + secretAccessKey := "" + if i.accessKeys != nil { + secretAccessKey = i.accessKeys.SecretAccessKey + } + + i.AccessKeys, diags = types.ObjectValue(i.AccessKeys.AttributeTypes(ctx), map[string]attr.Value{ + "access_key_id": types.StringValue(integrationModel.AccessKeys.AccessKeyID), + "secret_access_key": types.StringValue(secretAccessKey), + }) + if diags.HasError() { + return diags + } + } + + if integrationModel.FederatedWorkloadIdentity != nil { + i.FederatedWorkloadIdentity, diags = types.ObjectValue(i.FederatedWorkloadIdentity.AttributeTypes(ctx), map[string]attr.Value{ + "role_arn": types.StringValue(integrationModel.FederatedWorkloadIdentity.RoleArn), + "audience": types.StringValue(integrationModel.FederatedWorkloadIdentity.Audience), + }) + if diags.HasError() { + return diags + } + } + + return diags +} diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_integration_aws_test.go b/internal/provider/vaultsecrets/resource_vault_secrets_integration_aws_test.go new file mode 100644 index 000000000..b389a97eb --- /dev/null +++ b/internal/provider/vaultsecrets/resource_vault_secrets_integration_aws_test.go @@ -0,0 +1,185 @@ +package vaultsecrets_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" +) + +func TestAccVaultSecretsResourceIntegrationAWS(t *testing.T) { + accessKeyID := checkRequiredEnvVarOrFail(t, "AWS_ACCESS_KEY_ID") + secretAccessKey := checkRequiredEnvVarOrFail(t, "AWS_SECRET_ACCESS_KEY") + roleArn := checkRequiredEnvVarOrFail(t, "AWS_INTEGRATION_ROLE_ARN") + audience := checkRequiredEnvVarOrFail(t, "AWS_INTEGRATION_AUDIENCE") + + integrationName1 := generateRandomSlug() + integrationName2 := generateRandomSlug() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create initial integration with access keys + { + Config: fmt.Sprintf(` + resource "hcp_vault_secrets_integration_aws" "acc_test" { + name = %q + capabilities = ["DYNAMIC", "ROTATION"] + access_keys = { + access_key_id = %q + secret_access_key = %q + } + }`, integrationName1, accessKeyID, secretAccessKey), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "organization_id"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "project_id", os.Getenv("HCP_PROJECT_ID")), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_id"), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_name"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "name", integrationName1), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.#", "2"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.0", "DYNAMIC"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.1", "ROTATION"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "access_keys.access_key_id", accessKeyID), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "access_keys.secret_access_key", secretAccessKey), + ), + }, + // Changing the name forces a recreation + { + Config: fmt.Sprintf(` + resource "hcp_vault_secrets_integration_aws" "acc_test" { + name = %q + capabilities = ["DYNAMIC", "ROTATION"] + access_keys = { + access_key_id = %q + secret_access_key = %q + } + }`, integrationName2, accessKeyID, secretAccessKey), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "organization_id"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "project_id", os.Getenv("HCP_PROJECT_ID")), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_id"), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_name"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "name", integrationName2), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.#", "2"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.0", "DYNAMIC"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.1", "ROTATION"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "access_keys.access_key_id", accessKeyID), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "access_keys.secret_access_key", secretAccessKey), + ), + }, + // Modifying mutable fields causes an update + { + Config: fmt.Sprintf(` + resource "hcp_vault_secrets_integration_aws" "acc_test" { + name = %q + capabilities = ["DYNAMIC"] + federated_workload_identity = { + role_arn = %q + audience = %q + } + }`, integrationName2, roleArn, audience), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "organization_id"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "project_id", os.Getenv("HCP_PROJECT_ID")), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_id"), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_name"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "name", integrationName2), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.#", "1"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.0", "DYNAMIC"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "federated_workload_identity.role_arn", roleArn), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "federated_workload_identity.audience", audience), + ), + }, + // Deleting the integration out of band causes a recreation + { + PreConfig: func() { + deleteTestAwsIntegration(t, integrationName2) + }, + Config: fmt.Sprintf(` + resource "hcp_vault_secrets_integration_aws" "acc_test" { + name = %q + capabilities = ["DYNAMIC"] + federated_workload_identity = { + role_arn = %q + audience = %q + } + }`, integrationName2, roleArn, audience), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "organization_id"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "project_id", os.Getenv("HCP_PROJECT_ID")), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_id"), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_name"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "name", integrationName2), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.#", "1"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.0", "DYNAMIC"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "federated_workload_identity.role_arn", roleArn), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "federated_workload_identity.audience", audience), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + // Pre-existing integration can be imported + { + PreConfig: func() { + createTestAwsIntegration(t, integrationName2, roleArn, audience, []*secretmodels.Secrets20231128Capability{secretmodels.Secrets20231128CapabilityDYNAMIC.Pointer()}) + }, + Config: fmt.Sprintf(` + resource "hcp_vault_secrets_integration_aws" "acc_test" { + name = %q + capabilities = ["DYNAMIC"] + federated_workload_identity = { + role_arn = %q + audience = %q + } + }`, integrationName2, roleArn, audience), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "organization_id"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "project_id", os.Getenv("HCP_PROJECT_ID")), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_id"), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_aws.acc_test", "resource_name"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "name", integrationName2), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.#", "1"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "capabilities.0", "DYNAMIC"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "federated_workload_identity.role_arn", roleArn), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_aws.acc_test", "federated_workload_identity.audience", audience), + ), + ResourceName: "hcp_vault_secrets_integration_aws.acc_test", + ImportStateId: integrationName2, + ImportState: true, + }, + }, + CheckDestroy: func(_ *terraform.State) error { + if awsIntegrationExists(t, integrationName1) { + return fmt.Errorf("test aws integration %s was not destroyed", integrationName1) + } + if awsIntegrationExists(t, integrationName2) { + return fmt.Errorf("test aws integration %s was not destroyed", integrationName2) + } + return nil + }, + }) +} + +func awsIntegrationExists(t *testing.T, name string) bool { + t.Helper() + + client := acctest.HCPClients(t) + + response, err := client.VaultSecretsPreview.GetAwsIntegration( + secret_service.NewGetAwsIntegrationParamsWithContext(ctx). + WithOrganizationID(client.Config.OrganizationID). + WithProjectID(client.Config.ProjectID). + WithName(name), nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + t.Fatal(err) + } + + return !clients.IsResponseCodeNotFound(err) && response != nil && response.Payload != nil && response.Payload.Integration != nil +} From 304bbf04ea4ccfa00c24635975d3144874aaf73e Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Fri, 30 Aug 2024 16:23:52 -0400 Subject: [PATCH 2/4] + implemented HVS Twilio Integration resource --- .../vault_secrets_integration_twilio.md | 64 ++++ .../import.sh | 4 + .../resource.tf | 9 + internal/provider/provider.go | 1 + ...source_vault_secrets_integration_twilio.go | 303 ++++++++++++++++++ ...e_vault_secrets_integration_twilio_test.go | 156 +++++++++ 6 files changed, 537 insertions(+) create mode 100644 docs/resources/vault_secrets_integration_twilio.md create mode 100644 examples/resources/hcp_vault_secrets_integration_twilio/import.sh create mode 100644 examples/resources/hcp_vault_secrets_integration_twilio/resource.tf create mode 100644 internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go create mode 100644 internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio_test.go diff --git a/docs/resources/vault_secrets_integration_twilio.md b/docs/resources/vault_secrets_integration_twilio.md new file mode 100644 index 000000000..96a307057 --- /dev/null +++ b/docs/resources/vault_secrets_integration_twilio.md @@ -0,0 +1,64 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "hcp_vault_secrets_integration_twilio Resource - terraform-provider-hcp" +subcategory: "" +description: |- + The Vault Secrets Twilio integration resource manages a Twilio integration. +--- + +# hcp_vault_secrets_integration_twilio (Resource) + +The Vault Secrets Twilio integration resource manages a Twilio integration. + +## Example Usage + +```terraform +resource "hcp_vault_secrets_integration_twilio" "example" { + name = "my-twilio-1" + capabilities = ["ROTATION"] + static_credential_details = { + account_sid = "AC7..." + api_key_sid = "TKa..." + api_key_secret = "6aG..." + } +} +``` + + +## Schema + +### Required + +- `capabilities` (Set of String) Capabilities enabled for the integration. See the Vault Secrets documentation for the list of supported capabilities per provider. +- `name` (String) The Vault Secrets integration name. + +### Optional + +- `project_id` (String) HCP project ID that owns the HCP Vault Secrets integration. Inferred from the provider configuration if omitted. +- `static_credential_details` (Attributes) Twilio API key parts used to authenticate against the target Twilio account. (see [below for nested schema](#nestedatt--static_credential_details)) + +### Read-Only + +- `organization_id` (String) HCP organization ID that owns the HCP Vault Secrets integration. +- `resource_id` (String) Resource ID used to uniquely identify the integration instance on the HCP platform. +- `resource_name` (String) Resource name used to uniquely identify the integration instance on the HCP platform. + + +### Nested Schema for `static_credential_details` + +Required: + +- `account_sid` (String) Account SID for the target Twilio account. +- `api_key_secret` (String, Sensitive) Api key secret used with the api key SID to authenticate against the target Twilio account. +- `api_key_sid` (String) Api key SID to authenticate against the target Twilio account. + +## Import + +Import is supported using the following syntax: + +```shell +# Vault Secrets Twilio Integration can be imported by specifying the name of the integration +# Note that since the Api Key secret is never returned on the Vault Secrets API, +# the next plan or apply will show a diff for that field. +terraform import hcp_vault_secrets_integration_twilio.example my-twilio-1 +``` diff --git a/examples/resources/hcp_vault_secrets_integration_twilio/import.sh b/examples/resources/hcp_vault_secrets_integration_twilio/import.sh new file mode 100644 index 000000000..c7b740f58 --- /dev/null +++ b/examples/resources/hcp_vault_secrets_integration_twilio/import.sh @@ -0,0 +1,4 @@ +# Vault Secrets Twilio Integration can be imported by specifying the name of the integration +# Note that since the Api Key secret is never returned on the Vault Secrets API, +# the next plan or apply will show a diff for that field. +terraform import hcp_vault_secrets_integration_twilio.example my-twilio-1 diff --git a/examples/resources/hcp_vault_secrets_integration_twilio/resource.tf b/examples/resources/hcp_vault_secrets_integration_twilio/resource.tf new file mode 100644 index 000000000..72ba89e06 --- /dev/null +++ b/examples/resources/hcp_vault_secrets_integration_twilio/resource.tf @@ -0,0 +1,9 @@ +resource "hcp_vault_secrets_integration_twilio" "example" { + name = "my-twilio-1" + capabilities = ["ROTATION"] + static_credential_details = { + account_sid = "AC7..." + api_key_sid = "TKa..." + api_key_secret = "6aG..." + } +} \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 80138dc2b..7242f699d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -154,6 +154,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res vaultsecrets.NewVaultSecretsAppIAMPolicyResource, vaultsecrets.NewVaultSecretsAppIAMBindingResource, vaultsecrets.NewVaultSecretsIntegrationAWSResource, + vaultsecrets.NewVaultSecretsIntegrationTwilioResource, // IAM iam.NewServicePrincipalResource, iam.NewServicePrincipalKeyResource, diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go b/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go new file mode 100644 index 000000000..955cdc41c --- /dev/null +++ b/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go @@ -0,0 +1,303 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vaultsecrets + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/modifiers" + "golang.org/x/exp/maps" +) + +type IntegrationTwilio struct { + // Input fields + ProjectID types.String `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` + Capabilities types.Set `tfsdk:"capabilities"` + StaticCredentialDetails types.Object `tfsdk:"static_credential_details"` + + // Computed fields + OrganizationID types.String `tfsdk:"organization_id"` + ResourceID types.String `tfsdk:"resource_id"` + ResourceName types.String `tfsdk:"resource_name"` + + // Inner API-compatible models derived from the Terraform fields + capabilities []*secretmodels.Secrets20231128Capability `tfsdk:"-"` + staticCredentialDetails *secretmodels.Secrets20231128TwilioStaticCredentialsRequest `tfsdk:"-"` +} + +// Helper structs to help populate concrete targets from types.Object fields +type staticCredentialDetails struct { + AccountSID types.String `tfsdk:"account_sid"` + APIKeySID types.String `tfsdk:"api_key_sid"` + APIKeySecret types.String `tfsdk:"api_key_secret"` +} + +var _ resource.Resource = &resourceVaultSecretsIntegrationTwilio{} +var _ resource.ResourceWithConfigure = &resourceVaultSecretsIntegrationTwilio{} +var _ resource.ResourceWithModifyPlan = &resourceVaultSecretsIntegrationTwilio{} +var _ resource.ResourceWithImportState = &resourceVaultSecretsIntegrationTwilio{} + +func NewVaultSecretsIntegrationTwilioResource() resource.Resource { + return &resourceVaultSecretsIntegrationTwilio{} +} + +type resourceVaultSecretsIntegrationTwilio struct { + client *clients.Client +} + +func (r *resourceVaultSecretsIntegrationTwilio) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vault_secrets_integration_twilio" +} + +func (r *resourceVaultSecretsIntegrationTwilio) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "static_credential_details": schema.SingleNestedAttribute{ + Description: "Twilio API key parts used to authenticate against the target Twilio account.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "account_sid": schema.StringAttribute{ + Description: "Account SID for the target Twilio account.", + Required: true, + }, + "api_key_sid": schema.StringAttribute{ + Description: "Api key SID to authenticate against the target Twilio account.", + Required: true, + }, + "api_key_secret": schema.StringAttribute{ + Description: "Api key secret used with the api key SID to authenticate against the target Twilio account.", + Required: true, + Sensitive: true, + }, + }, + }, + } + + maps.Copy(attributes, sharedIntegrationAttributes) + + resp.Schema = schema.Schema{ + MarkdownDescription: "The Vault Secrets Twilio integration resource manages a Twilio integration.", + Attributes: attributes, + } +} + +func (r *resourceVaultSecretsIntegrationTwilio) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*clients.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.client = client +} + +func (r *resourceVaultSecretsIntegrationTwilio) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + modifiers.ModifyPlanForDefaultProjectChange(ctx, r.client.Config.ProjectID, req.State, req.Config, req.Plan, resp) +} + +func (r *resourceVaultSecretsIntegrationTwilio) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.State.Get, "reading", func(i integration) (any, error) { + integration, ok := i.(*IntegrationTwilio) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + response, err := r.client.VaultSecretsPreview.GetTwilioIntegration( + secret_service.NewGetTwilioIntegrationParamsWithContext(ctx). + WithOrganizationID(integration.OrganizationID.ValueString()). + WithProjectID(integration.ProjectID.ValueString()). + WithName(integration.Name.ValueString()), nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + if response == nil || response.Payload == nil { + return nil, nil + } + return response.Payload.Integration, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationTwilio) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.Plan.Get, "creating", func(i integration) (any, error) { + integration, ok := i.(*IntegrationTwilio) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + response, err := r.client.VaultSecretsPreview.CreateTwilioIntegration(&secret_service.CreateTwilioIntegrationParams{ + Body: &secretmodels.SecretServiceCreateTwilioIntegrationBody{ + Capabilities: integration.capabilities, + StaticCredentialDetails: integration.staticCredentialDetails, + Name: integration.Name.ValueString(), + }, + OrganizationID: integration.OrganizationID.ValueString(), + ProjectID: integration.ProjectID.ValueString(), + }, nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + if response == nil || response.Payload == nil { + return nil, nil + } + return response.Payload.Integration, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationTwilio) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.Plan.Get, "updating", func(i integration) (any, error) { + integration, ok := i.(*IntegrationTwilio) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + response, err := r.client.VaultSecretsPreview.UpdateTwilioIntegration(&secret_service.UpdateTwilioIntegrationParams{ + Body: &secretmodels.SecretServiceUpdateTwilioIntegrationBody{ + Capabilities: integration.capabilities, + StaticCredentialDetails: integration.staticCredentialDetails, + }, + Name: integration.Name.ValueString(), + OrganizationID: integration.OrganizationID.ValueString(), + ProjectID: integration.ProjectID.ValueString(), + }, nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + if response == nil || response.Payload == nil { + return nil, nil + } + return response.Payload.Integration, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationTwilio) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.State.Get, "deleting", func(i integration) (any, error) { + integration, ok := i.(*IntegrationTwilio) + if !ok { + return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + } + + _, err := r.client.VaultSecretsPreview.DeleteTwilioIntegration( + secret_service.NewDeleteTwilioIntegrationParamsWithContext(ctx). + WithOrganizationID(integration.OrganizationID.ValueString()). + WithProjectID(integration.ProjectID.ValueString()). + WithName(integration.Name.ValueString()), nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + return nil, err + } + return nil, nil + })...) +} + +func (r *resourceVaultSecretsIntegrationTwilio) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // The Vault Secrets API does not return sensitive values like the secret access key, so they will be initialized to an empty value + // It means the first plan/apply after a successful import will always show a diff for the secret access key + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), r.client.Config.OrganizationID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), r.client.Config.ProjectID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), req.ID)...) +} + +var _ integration = &IntegrationTwilio{} + +func (i *IntegrationTwilio) projectID() types.String { + return i.ProjectID + +} +func (i *IntegrationTwilio) initModel(ctx context.Context, orgID, projID string) diag.Diagnostics { + // Init fields that depend on the Terraform provider configuration + i.OrganizationID = types.StringValue(orgID) + i.ProjectID = types.StringValue(projID) + + // Init the HVS domain models from the Terraform domain models + var capabilities []types.String + diags := i.Capabilities.ElementsAs(ctx, &capabilities, false) + if diags.HasError() { + return diags + } + for _, c := range capabilities { + i.capabilities = append(i.capabilities, secretmodels.Secrets20231128Capability(c.ValueString()).Pointer()) + } + + if !i.StaticCredentialDetails.IsNull() { + scd := staticCredentialDetails{} + diags = i.StaticCredentialDetails.As(ctx, &scd, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return diags + } + + i.staticCredentialDetails = &secretmodels.Secrets20231128TwilioStaticCredentialsRequest{ + AccountSid: scd.AccountSID.ValueString(), + APIKeySecret: scd.APIKeySecret.ValueString(), + APIKeySid: scd.APIKeySID.ValueString(), + } + } + + return diag.Diagnostics{} +} + +func (i *IntegrationTwilio) fromModel(ctx context.Context, orgID, projID string, model any) diag.Diagnostics { + diags := diag.Diagnostics{} + + integrationModel, ok := model.(*secretmodels.Secrets20231128TwilioIntegration) + if !ok { + diags.AddError("Invalid model type, this is a bug on the provider.", fmt.Sprintf("Expected *secretmodels.Secrets20231128AwsIntegration, got: %T", model)) + return diags + } + + i.OrganizationID = types.StringValue(orgID) + i.ProjectID = types.StringValue(projID) + // TODO These fields are not returned by the API on updates, so we use the state value if they are blank on the model as a stopgap until it gets fixed on HVS + if integrationModel.ResourceID != "" { + i.ResourceID = types.StringValue(integrationModel.ResourceID) + } + if integrationModel.ResourceName != "" { + i.ResourceName = types.StringValue(integrationModel.ResourceName) + } + if integrationModel.Name != "" { + i.Name = types.StringValue(integrationModel.Name) + } + + var values []attr.Value + for _, c := range integrationModel.Capabilities { + values = append(values, types.StringValue(string(*c))) + } + i.Capabilities, diags = types.SetValue(types.StringType, values) + if diags.HasError() { + return diags + } + + if integrationModel.StaticCredentialDetails != nil { + // The secret key is not returned by the API, so we use an empty value (e.g. for imports) or the state value (e.g. for updates) + apiKeySecret := "" + if i.staticCredentialDetails != nil { + apiKeySecret = i.staticCredentialDetails.APIKeySecret + } + + i.StaticCredentialDetails, diags = types.ObjectValue(i.StaticCredentialDetails.AttributeTypes(ctx), map[string]attr.Value{ + "account_sid": types.StringValue(integrationModel.StaticCredentialDetails.AccountSid), + "api_key_sid": types.StringValue(integrationModel.StaticCredentialDetails.APIKeySid), + "api_key_secret": types.StringValue(apiKeySecret), + }) + if diags.HasError() { + return diags + } + } + + return diags +} diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio_test.go b/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio_test.go new file mode 100644 index 000000000..a3fc9b617 --- /dev/null +++ b/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio_test.go @@ -0,0 +1,156 @@ +package vaultsecrets_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" +) + +func TestAccVaultSecretsResourceIntegrationTwilio(t *testing.T) { + accountSID := checkRequiredEnvVarOrFail(t, "TWILIO_ACCOUNT_SID") + apiKeySID := checkRequiredEnvVarOrFail(t, "TWILIO_API_KEY_SID") + apiKeySecret := checkRequiredEnvVarOrFail(t, "TWILIO_API_KEY_SECRET") + + integrationName1 := generateRandomSlug() + integrationName2 := generateRandomSlug() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create initial integration with access keys + { + Config: config(integrationName1, accountSID, apiKeySID, apiKeySecret), + Check: resource.ComposeTestCheckFunc( + checkFuncs(integrationName1, accountSID, apiKeySID, apiKeySecret)..., + ), + }, + // Changing the name forces a recreation + { + Config: config(integrationName2, accountSID, apiKeySID, apiKeySecret), + Check: resource.ComposeTestCheckFunc( + checkFuncs(integrationName2, accountSID, apiKeySID, apiKeySecret)..., + ), + }, + // Modifying mutable fields causes an update + { + Config: config(integrationName2, accountSID, apiKeySID, apiKeySecret), + Check: resource.ComposeTestCheckFunc( + checkFuncs(integrationName2, accountSID, apiKeySID, apiKeySecret)..., + ), + }, + // Deleting the integration out of band causes a recreation + { + PreConfig: func() { + t.Helper() + client := acctest.HCPClients(t) + _, err := client.VaultSecretsPreview.DeleteTwilioIntegration(&secret_service.DeleteTwilioIntegrationParams{ + Name: integrationName2, + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + }, nil) + if err != nil { + t.Fatal(err) + } + }, + Config: config(integrationName2, accountSID, apiKeySID, apiKeySecret), + Check: resource.ComposeTestCheckFunc( + checkFuncs(integrationName2, accountSID, apiKeySID, apiKeySecret)..., + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + // Pre-existing integration can be imported + { + PreConfig: func() { + t.Helper() + client := acctest.HCPClients(t) + _, err := client.VaultSecretsPreview.CreateTwilioIntegration(&secret_service.CreateTwilioIntegrationParams{ + Body: &secretmodels.SecretServiceCreateTwilioIntegrationBody{ + Capabilities: []*secretmodels.Secrets20231128Capability{secretmodels.Secrets20231128CapabilityROTATION.Pointer()}, + StaticCredentialDetails: &secretmodels.Secrets20231128TwilioStaticCredentialsRequest{ + AccountSid: accountSID, + APIKeySid: apiKeySID, + APIKeySecret: apiKeySecret, + }, + Name: integrationName2, + }, + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + }, nil) + if err != nil { + t.Fatal(err) + } + }, + Config: config(integrationName2, accountSID, apiKeySID, apiKeySecret), + Check: resource.ComposeTestCheckFunc( + checkFuncs(integrationName2, accountSID, apiKeySID, apiKeySecret)..., + ), + ResourceName: "hcp_vault_secrets_integration_twilio.acc_test", + ImportStateId: integrationName2, + ImportState: true, + }, + }, + CheckDestroy: func(_ *terraform.State) error { + if twilioIntegrationExists(t, integrationName1) { + return fmt.Errorf("test twilio integration %s was not destroyed", integrationName1) + } + if twilioIntegrationExists(t, integrationName2) { + return fmt.Errorf("test twilio integration %s was not destroyed", integrationName2) + } + return nil + }, + }) +} + +func config(integrationName, accountSID, apiKeySID, apiKeySecret string) string { + return fmt.Sprintf(` + resource "hcp_vault_secrets_integration_twilio" "acc_test" { + name = %q + capabilities = ["ROTATION"] + static_credential_details = { + account_sid = %q + api_key_sid = %q + api_key_secret = %q + } + }`, integrationName, accountSID, apiKeySID, apiKeySecret) +} + +func checkFuncs(integrationName, accountSID, apiKeySID, apiKeySecret string) []resource.TestCheckFunc { + return []resource.TestCheckFunc{ + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_twilio.acc_test", "organization_id"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_twilio.acc_test", "project_id", os.Getenv("HCP_PROJECT_ID")), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_twilio.acc_test", "resource_id"), + resource.TestCheckResourceAttrSet("hcp_vault_secrets_integration_twilio.acc_test", "resource_name"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_twilio.acc_test", "name", integrationName), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_twilio.acc_test", "capabilities.#", "1"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_twilio.acc_test", "capabilities.0", "ROTATION"), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_twilio.acc_test", "static_credential_details.account_sid", accountSID), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_twilio.acc_test", "static_credential_details.api_key_secret", apiKeySecret), + resource.TestCheckResourceAttr("hcp_vault_secrets_integration_twilio.acc_test", "static_credential_details.api_key_sid", apiKeySID), + } +} + +func twilioIntegrationExists(t *testing.T, name string) bool { + t.Helper() + + client := acctest.HCPClients(t) + + response, err := client.VaultSecretsPreview.GetTwilioIntegration( + secret_service.NewGetTwilioIntegrationParamsWithContext(ctx). + WithOrganizationID(client.Config.OrganizationID). + WithProjectID(client.Config.ProjectID). + WithName(name), nil) + if err != nil && !clients.IsResponseCodeNotFound(err) { + t.Fatal(err) + } + + return !clients.IsResponseCodeNotFound(err) && response != nil && response.Payload != nil && response.Payload.Integration != nil +} From ad31060a441f7e6d79db8952931545d94cdeaecf Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Fri, 30 Aug 2024 16:28:31 -0400 Subject: [PATCH 3/4] + changelog --- .changelog/1081.txt | 3 +++ .../resource_vault_secrets_integration_twilio.go | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 .changelog/1081.txt diff --git a/.changelog/1081.txt b/.changelog/1081.txt new file mode 100644 index 000000000..91f0a2782 --- /dev/null +++ b/.changelog/1081.txt @@ -0,0 +1,3 @@ +```release-note:feature +add vault_secrets_integration_twilio resource +``` diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go b/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go index 955cdc41c..0fcc5c6e6 100644 --- a/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go +++ b/internal/provider/vaultsecrets/resource_vault_secrets_integration_twilio.go @@ -116,7 +116,7 @@ func (r *resourceVaultSecretsIntegrationTwilio) Read(ctx context.Context, req re resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.State.Get, "reading", func(i integration) (any, error) { integration, ok := i.(*IntegrationTwilio) if !ok { - return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i) } response, err := r.client.VaultSecretsPreview.GetTwilioIntegration( @@ -138,7 +138,7 @@ func (r *resourceVaultSecretsIntegrationTwilio) Create(ctx context.Context, req resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.Plan.Get, "creating", func(i integration) (any, error) { integration, ok := i.(*IntegrationTwilio) if !ok { - return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i) } response, err := r.client.VaultSecretsPreview.CreateTwilioIntegration(&secret_service.CreateTwilioIntegrationParams{ @@ -164,7 +164,7 @@ func (r *resourceVaultSecretsIntegrationTwilio) Update(ctx context.Context, req resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.Plan.Get, "updating", func(i integration) (any, error) { integration, ok := i.(*IntegrationTwilio) if !ok { - return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i) } response, err := r.client.VaultSecretsPreview.UpdateTwilioIntegration(&secret_service.UpdateTwilioIntegrationParams{ @@ -190,7 +190,7 @@ func (r *resourceVaultSecretsIntegrationTwilio) Delete(ctx context.Context, req resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.State.Get, "deleting", func(i integration) (any, error) { integration, ok := i.(*IntegrationTwilio) if !ok { - return nil, fmt.Errorf("invalid integration type, expected *IntegrationAWS, got: %T, this is a bug on the provider", i) + return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i) } _, err := r.client.VaultSecretsPreview.DeleteTwilioIntegration( @@ -256,7 +256,7 @@ func (i *IntegrationTwilio) fromModel(ctx context.Context, orgID, projID string, integrationModel, ok := model.(*secretmodels.Secrets20231128TwilioIntegration) if !ok { - diags.AddError("Invalid model type, this is a bug on the provider.", fmt.Sprintf("Expected *secretmodels.Secrets20231128AwsIntegration, got: %T", model)) + diags.AddError("Invalid model type, this is a bug on the provider.", fmt.Sprintf("Expected *secretmodels.Secrets20231128TwilioIntegration, got: %T", model)) return diags } From 984ca54509d6d0ca93bf208821eb575976cdb10a Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Wed, 4 Sep 2024 11:03:22 -0400 Subject: [PATCH 4/4] * fix merge --- internal/provider/provider.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index dbd248c1f..7242f699d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -155,7 +155,6 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res vaultsecrets.NewVaultSecretsAppIAMBindingResource, vaultsecrets.NewVaultSecretsIntegrationAWSResource, vaultsecrets.NewVaultSecretsIntegrationTwilioResource, - vaultsecrets.NewVaultSecretsIntegrationAWSResource, // IAM iam.NewServicePrincipalResource, iam.NewServicePrincipalKeyResource,