diff --git a/docs/data-sources/cloud_accounts.md b/docs/data-sources/cloud_accounts.md new file mode 100644 index 0000000..6d66215 --- /dev/null +++ b/docs/data-sources/cloud_accounts.md @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "astra_cloud_accounts Data Source - terraform-provider-astra" +subcategory: "" +description: |- + Retrieve a list of Cloud Accounts within an Organization +--- + +# astra_cloud_accounts (Data Source) + +Retrieve a list of Cloud Accounts within an Organization + +## Example Usage + +```terraform +# AWS example +data "astra_cloud_accounts" "awsaccounts" { + cloud_provider = "aws" + region = "us-east-1" +} + +# GCP example +data "astra_cloud_accounts" "gcpaccounts" { + cloud_provider = "gcp" + region = "us-east1" +} +``` + + +## Schema + +### Required + +- `cloud_provider` (String) The cloud provider where the Customer Key exists (Currently supported: aws, gcp) +- `region` (String) Cloud provider region + +### Read-Only + +- `id` (String) The ID of this resource. +- `results` (List of Object) The list of Cloud Accounts for the given Organization. (see [below for nested schema](#nestedatt--results)) + + +### Nested Schema for `results` + +Read-Only: + +- `organization_id` (String) +- `provider` (String) +- `provider_id` (String) diff --git a/docs/data-sources/customer_key.md b/docs/data-sources/customer_key.md new file mode 100644 index 0000000..0c847e3 --- /dev/null +++ b/docs/data-sources/customer_key.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "astra_customer_key Data Source - terraform-provider-astra" +subcategory: "" +description: |- + Retrieve a Customer Key for a given cloud provider and region +--- + +# astra_customer_key (Data Source) + +Retrieve a Customer Key for a given cloud provider and region + +## Example Usage + +```terraform +# Read in a customer key for a given cloud provider and region +data "astra_customer_key" "key" { + cloud_provider = "aws" + region = "us-east-1" +} +``` + + +## Schema + +### Required + +- `cloud_provider` (String) The cloud provider where the Customer Key exists (Currently supported: aws, gcp) +- `region` (String) Cloud provider region + +### Read-Only + +- `id` (String) The ID of this resource. +- `key_id` (String) The Customer Key ID +- `organization_id` (String) Organization ID diff --git a/docs/data-sources/customer_keys.md b/docs/data-sources/customer_keys.md new file mode 100644 index 0000000..a998dd7 --- /dev/null +++ b/docs/data-sources/customer_keys.md @@ -0,0 +1,37 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "astra_customer_keys Data Source - terraform-provider-astra" +subcategory: "" +description: |- + Retrieve a list of Customer Keys within an Organization +--- + +# astra_customer_keys (Data Source) + +Retrieve a list of Customer Keys within an Organization + +## Example Usage + +```terraform +# Read in all customer keys for the given Organization +data "astra_customer_keys" "keys" { +} +``` + + +## Schema + +### Read-Only + +- `id` (String) The ID of this resource. +- `results` (List of Object) The list of Customer Keys for the given Organization. (see [below for nested schema](#nestedatt--results)) + + +### Nested Schema for `results` + +Read-Only: + +- `cloud_provider` (String) +- `key_id` (String) +- `organization_id` (String) +- `region` (String) diff --git a/docs/data-sources/database.md b/docs/data-sources/database.md index 29d2ea6..e36825e 100644 --- a/docs/data-sources/database.md +++ b/docs/data-sources/database.md @@ -39,7 +39,7 @@ data "astra_database" "db" { - `keyspace` (String) Initial keyspace - `name` (String) Database name (user provided) - `node_count` (Number) Node count (not relevant for serverless databases) -- `organization_id` (String) Ordg id (system generated) +- `organization_id` (String) Organization id (system generated) - `owner_id` (String) Owner id (system generated) - `regions` (List of String) Cloud provider region. Get list of supported regions from regions data-source - `replication_factor` (Number) Replication Factor (not relevant for serverless databases) diff --git a/docs/data-sources/databases.md b/docs/data-sources/databases.md index 3a22388..d0c50e0 100644 --- a/docs/data-sources/databases.md +++ b/docs/data-sources/databases.md @@ -28,7 +28,7 @@ output "existing_dbs" { ### Optional - `cloud_provider` (String) The cloud provider -- `status` (String) Status flter. Only return databases with matching status, if supplied. Otherwise return all databases matching other requirements +- `status` (String) Status filter. Only return databases with matching status, if supplied. Otherwise return all databases matching other requirements ### Read-Only diff --git a/docs/resources/customer_key.md b/docs/resources/customer_key.md new file mode 100644 index 0000000..b6f232e --- /dev/null +++ b/docs/resources/customer_key.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "astra_customer_key Resource - terraform-provider-astra" +subcategory: "" +description: |- + astra_customer_key provides a Customer Key resource for Astra's Bring Your Own Key (BYOK). Note that DELETE is not supported through Terraform currently. A support ticket must be created to delete Customer Keys in Astra. WARNING: Deleting a key from Astra will result in an outage. Please see https://docs.datastax.com/en/astra-db-serverless/administration/delete-customer-keys.html for more information. +--- + +# astra_customer_key (Resource) + +`astra_customer_key` provides a Customer Key resource for Astra's Bring Your Own Key (BYOK). Note that DELETE is not supported through Terraform currently. A support ticket must be created to delete Customer Keys in Astra. WARNING: Deleting a key from Astra will result in an outage. Please see https://docs.datastax.com/en/astra-db-serverless/administration/delete-customer-keys.html for more information. + +## Example Usage + +```terraform +# AWS example +resource "astra_customer_key" "customerkey" { + cloud_provider = "aws" + region = "us-east-1" + key_id = "arn:aws:kms:us-east-1:123456789012:key/1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d" +} + +# GCP example +resource "astra_customer_key" "customerKey" { + cloud_provider = "gcp" + region = "us-east1" + key_id = "projects/my-project/locations/us-east1/keyRings/my-key-ring/cryptoKeys/my-key" +} +``` + + +## Schema + +### Required + +- `cloud_provider` (String) The cloud provider where the Customer Key exists (Currently supported: aws, gcp) +- `key_id` (String) Customer Key ID. This is cloud provider specific. +- `region` (String) Region in which the Customer Key exists. + +### Read-Only + +- `id` (String) The ID of this resource. +- `organization_id` (String) The Astra organization ID (this is derived from the token used to create the Customer Key). + +## Import + +Import is supported using the following syntax: + +```shell +# the import id is in the fomrat of /cloudProvider//region//keyId/ +terraform import astra_customer_key.customerkey 4d3c2b1a-5e6f-1a2b-3c4d-5e6f1a2b3c4d/cloudProvider/aws/region/us-east-1/keyId/arn:aws:kms:us-east-1:123456789012:key/1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d +``` diff --git a/examples/data-sources/astra_cloud_accounts/data-source.tf b/examples/data-sources/astra_cloud_accounts/data-source.tf new file mode 100644 index 0000000..f0a702c --- /dev/null +++ b/examples/data-sources/astra_cloud_accounts/data-source.tf @@ -0,0 +1,11 @@ +# AWS example +data "astra_cloud_accounts" "awsaccounts" { + cloud_provider = "aws" + region = "us-east-1" +} + +# GCP example +data "astra_cloud_accounts" "gcpaccounts" { + cloud_provider = "gcp" + region = "us-east1" +} diff --git a/examples/data-sources/astra_customer_key/data-source.tf b/examples/data-sources/astra_customer_key/data-source.tf new file mode 100644 index 0000000..098e7a0 --- /dev/null +++ b/examples/data-sources/astra_customer_key/data-source.tf @@ -0,0 +1,5 @@ +# Read in a customer key for a given cloud provider and region +data "astra_customer_key" "key" { + cloud_provider = "aws" + region = "us-east-1" +} diff --git a/examples/data-sources/astra_customer_keys/data-source.tf b/examples/data-sources/astra_customer_keys/data-source.tf new file mode 100644 index 0000000..b386596 --- /dev/null +++ b/examples/data-sources/astra_customer_keys/data-source.tf @@ -0,0 +1,3 @@ +# Read in all customer keys for the given Organization +data "astra_customer_keys" "keys" { +} diff --git a/examples/resources/astra_customer_key/import.sh b/examples/resources/astra_customer_key/import.sh new file mode 100644 index 0000000..dfcdb58 --- /dev/null +++ b/examples/resources/astra_customer_key/import.sh @@ -0,0 +1,2 @@ +# the import id is in the fomrat of /cloudProvider//region//keyId/ +terraform import astra_customer_key.customerkey 4d3c2b1a-5e6f-1a2b-3c4d-5e6f1a2b3c4d/cloudProvider/aws/region/us-east-1/keyId/arn:aws:kms:us-east-1:123456789012:key/1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d \ No newline at end of file diff --git a/examples/resources/astra_customer_key/resource.tf b/examples/resources/astra_customer_key/resource.tf new file mode 100644 index 0000000..a2affc0 --- /dev/null +++ b/examples/resources/astra_customer_key/resource.tf @@ -0,0 +1,13 @@ +# AWS example +resource "astra_customer_key" "customerkey" { + cloud_provider = "aws" + region = "us-east-1" + key_id = "arn:aws:kms:us-east-1:123456789012:key/1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d" +} + +# GCP example +resource "astra_customer_key" "customerKey" { + cloud_provider = "gcp" + region = "us-east1" + key_id = "projects/my-project/locations/us-east1/keyRings/my-key-ring/cryptoKeys/my-key" +} \ No newline at end of file diff --git a/go.mod b/go.mod index a5edf31..29c369d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.22.0 require ( - github.com/datastax/astra-client-go/v2 v2.2.53 + github.com/datastax/astra-client-go/v2 v2.2.54 github.com/datastax/pulsar-admin-client-go v0.0.0-20230707040954-1a4745e07587 github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 diff --git a/go.sum b/go.sum index d85265e..c786d30 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/datastax/astra-client-go/v2 v2.2.53 h1:qWCBksV9rWi9WmSBW71IGhy3mL/QwkEw1BMG42ph540= -github.com/datastax/astra-client-go/v2 v2.2.53/go.mod h1:zxXWuqDkYia7PzFIL3T7RmjChc9LN81UnfI2yB4kE7M= +github.com/datastax/astra-client-go/v2 v2.2.54 h1:R2k9ek9zaU15cLD96np5gsj12oZhK3Z5/tSytjQagO8= +github.com/datastax/astra-client-go/v2 v2.2.54/go.mod h1:zxXWuqDkYia7PzFIL3T7RmjChc9LN81UnfI2yB4kE7M= github.com/datastax/pulsar-admin-client-go v0.0.0-20230707040954-1a4745e07587 h1:3jv+O0hWcz3oj3sZ9/Ov9/m1Vaqx8Ql8jp5ZeA13O5A= github.com/datastax/pulsar-admin-client-go v0.0.0-20230707040954-1a4745e07587/go.mod h1:guL8YZ5gJINN+h5Kmja1AnuzhxLU3sHQL8o/8HYLtqk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/provider/data_source_cloud_accounts.go b/internal/provider/data_source_cloud_accounts.go new file mode 100644 index 0000000..3be19ef --- /dev/null +++ b/internal/provider/data_source_cloud_accounts.go @@ -0,0 +1,101 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + + "github.com/datastax/astra-client-go/v2/astra" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceCloudAccounts() *schema.Resource { + return &schema.Resource{ + Description: "Retrieve a list of Cloud Accounts within an Organization", + + ReadContext: dataSourceCloudAccountsRead, + + Schema: map[string]*schema.Schema{ + // Required inputs + "cloud_provider": { + Description: "The cloud provider where the Customer Key exists (Currently supported: aws, gcp)", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(availableBYOKCloudProviders, true), + DiffSuppressFunc: ignoreCase, + }, + "region": { + Description: "Cloud provider region", + Type: schema.TypeString, + Required: true, + }, + // Computed outputs + "results": { + Type: schema.TypeList, + Description: "The list of Cloud Accounts for the given Organization.", + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "organization_id": { + Description: "Organization ID", + Type: schema.TypeString, + Computed: true, + }, + "provider": { + Description: "The cloud provider", + Type: schema.TypeString, + Required: true, + }, + "provider_id": { + Description: "The provider account ID", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceCloudAccountsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(astraClients).astraClient.(*astra.ClientWithResponses) + provider := d.Get("cloud_provider").(string) + region := d.Get("region").(string) + + cloudAccounts, err := listCloudAccounts(ctx, client, provider, region) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("results", cloudAccounts); err != nil { + return diag.FromErr(err) + } + + d.SetId(id.UniqueId()) + return nil +} + +func listCloudAccounts(ctx context.Context, client *astra.ClientWithResponses, cloudProvider, region string) ([]map[string]interface{}, error) { + resp, err := client.GetCloudAccountsWithResponse(ctx, cloudProvider, region) + if err != nil { + return nil, err + } + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("Error fetching Customer Keys. Status: %d, Message: %s", resp.StatusCode(), (resp.Body)) + } + cloudAccounts := resp.JSON200 + result := make([]map[string]interface{}, 0, len(*cloudAccounts)) + for _, account := range *cloudAccounts { + result = append(result, map[string]interface{}{ + "organization_id" : account.OrganizationId, + "provider" : account.Provider, + "provider_id" : account.ProviderId, + }) + } + return result, nil +} \ No newline at end of file diff --git a/internal/provider/data_source_customer_key.go b/internal/provider/data_source_customer_key.go new file mode 100644 index 0000000..9bf75d0 --- /dev/null +++ b/internal/provider/data_source_customer_key.go @@ -0,0 +1,72 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/datastax/astra-client-go/v2/astra" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceCustomerKey() *schema.Resource { + return &schema.Resource{ + Description: "Retrieve a Customer Key for a given cloud provider and region", + + ReadContext: dataSourceCustomerKeyRead, + + Schema: map[string]*schema.Schema{ + // Required inputs + "cloud_provider": { + Description: "The cloud provider where the Customer Key exists (Currently supported: aws, gcp)", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(availableBYOKCloudProviders, true), + DiffSuppressFunc: ignoreCase, + }, + "region": { + Description: "Cloud provider region", + Type: schema.TypeString, + Required: true, + }, + // Computed outputs + "organization_id": { + Description: "Organization ID", + Type: schema.TypeString, + Computed: true, + }, + "key_id": { + Description: "The Customer Key ID", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceCustomerKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(astraClients).astraClient.(*astra.ClientWithResponses) + cloudProvider := d.Get("cloud_provider").(string) + region := d.Get("region").(string) + + customerKeys, err := listCustomerKeys(ctx, client) + if err != nil { + return diag.FromErr(err) + } + for _, key := range customerKeys { + if strings.EqualFold(cloudProvider, key["cloud_provider"].(string)) && + region == key["region"].(string) { + orgId := key["organization_id"].(string) + keyId := key["key_id"].(string) + d.Set("organization_id", orgId) + d.Set("key_id", keyId) + d.SetId(fmt.Sprintf("%s/cloudProvider/%s/region/%s/keyId/%s", orgId, cloudProvider, region, keyId)) + return nil + } + } + // key not found + return diag.Errorf("No Customer Key found for provider: %s, region: %s", cloudProvider, region) +} diff --git a/internal/provider/data_source_customer_keys.go b/internal/provider/data_source_customer_keys.go new file mode 100644 index 0000000..ddfe862 --- /dev/null +++ b/internal/provider/data_source_customer_keys.go @@ -0,0 +1,89 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + + "github.com/datastax/astra-client-go/v2/astra" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCustomerKeys() *schema.Resource { + return &schema.Resource{ + Description: "Retrieve a list of Customer Keys within an Organization", + + ReadContext: dataSourceCustomerKeysRead, + + Schema: map[string]*schema.Schema{ + "results": { + Type: schema.TypeList, + Description: "The list of Customer Keys for the given Organization.", + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "organization_id": { + Description: "Organization ID", + Type: schema.TypeString, + Computed: true, + }, + "cloud_provider": { + Description: "The cloud provider", + Type: schema.TypeString, + Computed: true, + }, + "key_id": { + Description: "The Customer Key ID", + Type: schema.TypeString, + Computed: true, + }, + "region": { + Description: "The cloud provider region", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceCustomerKeysRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(astraClients).astraClient.(*astra.ClientWithResponses) + + customerKeys, err := listCustomerKeys(ctx, client) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("results", customerKeys); err != nil { + return diag.FromErr(err) + } + + d.SetId(id.UniqueId()) + return nil +} + +func listCustomerKeys(ctx context.Context, client *astra.ClientWithResponses) ([]map[string]interface{}, error) { + resp, err := client.ListKeysWithResponse(ctx) + if err != nil { + return nil, err + } + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("Error fetching Customer Keys: %s", string(resp.Body)) + } + customerKeys := resp.JSON200 + result := make([]map[string]interface{}, 0, len(*customerKeys)) + for _, key := range *customerKeys { + result = append(result, map[string]interface{}{ + "organization_id" : *key.OrganizationID, + "cloud_provider" : *key.CloudProvider, + "region" : *key.Region, + "key_id" : *key.KeyID, + }) + } + return result, nil +} diff --git a/internal/provider/data_source_database.go b/internal/provider/data_source_database.go index dcb462b..4d6ead5 100644 --- a/internal/provider/data_source_database.go +++ b/internal/provider/data_source_database.go @@ -36,7 +36,7 @@ func dataSourceDatabase() *schema.Resource { Computed: true, }, "organization_id": { - Description: "Ordg id (system generated)", + Description: "Organization id (system generated)", Type: schema.TypeString, Computed: true, }, diff --git a/internal/provider/data_source_databases.go b/internal/provider/data_source_databases.go index 75a439f..ecd7890 100644 --- a/internal/provider/data_source_databases.go +++ b/internal/provider/data_source_databases.go @@ -19,7 +19,7 @@ func dataSourceDatabases() *schema.Resource { // Optional "status": { Type: schema.TypeString, - Description: "Status flter. Only return databases with matching status, if supplied. Otherwise return all databases matching other requirements", + Description: "Status filter. Only return databases with matching status, if supplied. Otherwise return all databases matching other requirements", Optional: true, }, "cloud_provider": { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 3539b38..83e5c61 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -72,6 +72,9 @@ func NewSDKProvider(version string) func() *schema.Provider { "astra_roles": dataSourceRoles(), "astra_users": dataSourceUsers(), "astra_streaming_tenant_tokens": dataSourceStreamingTenantTokens(), + "astra_customer_keys": dataSourceCustomerKeys(), + "astra_customer_key": dataSourceCustomerKey(), + "astra_cloud_accounts": dataSourceCloudAccounts(), }, ResourcesMap: map[string]*schema.Resource{ "astra_database": resourceDatabase(), @@ -85,6 +88,7 @@ func NewSDKProvider(version string) func() *schema.Provider { "astra_streaming_tenant": resourceStreamingTenant(), "astra_streaming_sink": resourceStreamingSink(), "astra_table": resourceTable(), + "astra_customer_key": resourceCustomerKey(), }, Schema: map[string]*schema.Schema{ "token": { diff --git a/internal/provider/resource_customer_key.go b/internal/provider/resource_customer_key.go new file mode 100644 index 0000000..3f3955e --- /dev/null +++ b/internal/provider/resource_customer_key.go @@ -0,0 +1,183 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/datastax/astra-client-go/v2/astra" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +var availableBYOKCloudProviders = []string{ + "aws", + "gcp", +} + +func resourceCustomerKey() *schema.Resource { + return &schema.Resource{ + Description: "`astra_customer_key` provides a Customer Key resource for Astra's Bring Your Own Key (BYOK). " + + "Note that DELETE is not supported through Terraform currently. " + + "A support ticket must be created to delete Customer Keys in Astra. " + + "WARNING: Deleting a key from Astra will result in an outage. " + + "Please see https://docs.datastax.com/en/astra-db-serverless/administration/delete-customer-keys.html for more information.", + CreateContext: resourceCustomerKeyCreate, + ReadContext: resourceCustomerKeyRead, + DeleteContext: resourceCustomerKeyDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + // Required + "cloud_provider": { + Description: "The cloud provider where the Customer Key exists (Currently supported: aws, gcp)", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(availableBYOKCloudProviders, true), + DiffSuppressFunc: ignoreCase, + }, + "key_id": { + Description: "Customer Key ID. This is cloud provider specific.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "region": { + Description: "Region in which the Customer Key exists.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + // Computed + "organization_id": { + Description: "The Astra organization ID (this is derived from the token used to create the Customer Key).", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCustomerKeyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(astraClients).astraClient.(*astra.ClientWithResponses) + cloudProvider := d.Get("cloud_provider").(string) + keyId := d.Get("key_id").(string) + region := d.Get("region").(string) + // Determine the orgId from the current context + orgId, err := getOrgId(ctx, client) + if err != nil { + return diag.FromErr(err) + } + + // build the create Key request + createKeyReq := &astra.ExternalKMS{ + OrgId: &orgId, + } + if strings.EqualFold("aws", cloudProvider) { + createKeyReq.Aws = buildAWSKms(region, keyId) + } else if strings.EqualFold("gcp", cloudProvider) { + createKeyReq.Gcp = buildGCPKms(region, keyId) + } + // create the Customer Key + resp, err := client.CreateKeyWithResponse(ctx, *createKeyReq) + if err != nil { + return diag.FromErr(err) + } + if resp.StatusCode() != http.StatusCreated { + return diag.Errorf("Unexpected error creating Customer Key. Status: %d, Message: %s", resp.StatusCode(), string(resp.Body)) + } + // set the data + if err := setCustomerKeyData(d, orgId, cloudProvider, region, keyId); err != nil { + return diag.FromErr(err) + } + return nil +} + +func resourceCustomerKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + id := d.Id() + + orgId, cloudProvider, region, keyId, err := parseCustomerKeyId(id) + if err != nil { + return diag.FromErr(err) + } + setCustomerKeyData(d, orgId, cloudProvider, region, keyId) + return nil +} + +func resourceCustomerKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Delete not yet supported via DevOps API + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Delete of Customer Key resource not supported", + Detail: "Please open a Support ticket to delete Customer Keys in Astra.", + }, + } +} + +func buildAWSKms(region, keyId string) *astra.AWSKMS { + return &astra.AWSKMS{ + KeyID: &keyId, + Region: ®ion, + } +} + +func buildGCPKms(region, keyId string) *astra.GCPKMS { + return &astra.GCPKMS{ + KeyID: &keyId, + Region: ®ion, + } +} + +func setCustomerKeyData(d *schema.ResourceData, orgId, cloudProvider, region, keyId string) error { + if err := d.Set("organization_id", orgId); err != nil { + return err + } + if err:= d.Set("cloud_provider", cloudProvider); err != nil { + return err + } + if err := d.Set("region", region); err != nil { + return err + } + if err := d.Set("key_id", keyId); err != nil { + return err + } + + // generate the resource ID + // format: /cloudProvider//region//keyId/ + d.SetId(fmt.Sprintf("%s/cloudProvider/%s/region/%s/keyId/%s", orgId, cloudProvider, region, keyId)) + return nil +} + +func getOrgId(ctx context.Context, client *astra.ClientWithResponses) (string, error) { + // get the current Org ID + resp, err := client.GetCurrentOrganizationWithResponse(ctx) + if err != nil { + return "", err + } + if resp.StatusCode() != http.StatusOK { + return "", fmt.Errorf("Error fetching current organization. Status: %d, Message: %s", resp.StatusCode(), string(resp.Body)) + } + return resp.JSON200.Id, nil +} + +func parseCustomerKeyId(id string) (string, string, string, string, error) { + re := regexp.MustCompile(`(?P.*)/cloudProvider/(?P.*)/region/(?P.*)/keyId/(?P.*)`) + if !re.MatchString(id) { + return "", "", "", "", errors.New("invalid customer key id format: expected /cloudProvider//region//keyId/") + } + matches := re.FindStringSubmatch(id) + orgIdIndex := re.SubexpIndex("orgid") + cloudProviderIndex := re.SubexpIndex("cloudprovider") + regionIndex := re.SubexpIndex("region") + keyIdIndex := re.SubexpIndex("keyid") + return matches[orgIdIndex], matches[cloudProviderIndex], matches[regionIndex], matches[keyIdIndex], nil +} \ No newline at end of file diff --git a/internal/provider/resource_customer_key_test.go b/internal/provider/resource_customer_key_test.go new file mode 100644 index 0000000..91114f7 --- /dev/null +++ b/internal/provider/resource_customer_key_test.go @@ -0,0 +1,30 @@ +package provider + +import ( + "testing" +) + +func TestCustomerKeyIdParser(t *testing.T) { + testId := "28b7d281-a2ae-4e5b-bc3f-9f80df5f5223/cloudProvider/aws/region/us-east-1/keyId/arn:aws:kms:us-east-1:388533891461:key/85e37e2b-d897-49f0-9d18-3c0daf4a7ff5" + orgId, cloudProvider, region, keyId, err := parseCustomerKeyId(testId) + if err != nil { + t.Logf("Customer Key ID failed to parse: \"%s\", %s", testId, err) + t.Fail() + } + if orgId != "28b7d281-a2ae-4e5b-bc3f-9f80df5f5223" { + t.Logf("Organization ID parsed from Customer Key ID: \"%s\", expected \"%s\"", orgId, "28b7d281-a2ae-4e5b-bc3f-9f80df5f5223") + t.Fail() + } + if cloudProvider != "aws" { + t.Logf("Cloud Provider parsed from Customer Key ID: \"%s\", expected \"%s\"", cloudProvider, "aws") + t.Fail() + } + if region != "us-east-1" { + t.Logf("Region parsed from Customer Key ID: \"%s\", expected \"%s\"", region, "us-east-1") + t.Fail() + } + if keyId != "arn:aws:kms:us-east-1:388533891461:key/85e37e2b-d897-49f0-9d18-3c0daf4a7ff5" { + t.Logf("Key ID parsed from Customer Key ID: \"%s\", expected \"%s\"", keyId, "arn:aws:kms:us-east-1:388533891461:key/85e37e2b-d897-49f0-9d18-3c0daf4a7ff5") + t.Fail() + } +} \ No newline at end of file