From b986502a5e6c1c99e56da85d3139d01312580bc1 Mon Sep 17 00:00:00 2001 From: Erik Merkle Date: Fri, 1 Dec 2023 11:36:45 -0600 Subject: [PATCH] Add BYOK resource --- docs/data-sources/cloud_accounts.md | 35 ++++ docs/data-sources/customer_key.md | 27 +++ docs/data-sources/customer_keys.md | 31 ++++ docs/data-sources/database.md | 2 +- docs/data-sources/databases.md | 2 +- docs/resources/customer_key.md | 27 +++ .../provider/data_source_cloud_accounts.go | 101 +++++++++++ internal/provider/data_source_customer_key.go | 72 ++++++++ .../provider/data_source_customer_keys.go | 89 ++++++++++ internal/provider/data_source_database.go | 2 +- internal/provider/data_source_databases.go | 2 +- internal/provider/provider.go | 4 + internal/provider/resource_customer_key.go | 168 ++++++++++++++++++ 13 files changed, 558 insertions(+), 4 deletions(-) create mode 100644 docs/data-sources/cloud_accounts.md create mode 100644 docs/data-sources/customer_key.md create mode 100644 docs/data-sources/customer_keys.md create mode 100644 docs/resources/customer_key.md create mode 100644 internal/provider/data_source_cloud_accounts.go create mode 100644 internal/provider/data_source_customer_key.go create mode 100644 internal/provider/data_source_customer_keys.go create mode 100644 internal/provider/resource_customer_key.go diff --git a/docs/data-sources/cloud_accounts.md b/docs/data-sources/cloud_accounts.md new file mode 100644 index 00000000..1fb2aab2 --- /dev/null +++ b/docs/data-sources/cloud_accounts.md @@ -0,0 +1,35 @@ +--- +# 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 + + + + +## 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 00000000..27dbcadc --- /dev/null +++ b/docs/data-sources/customer_key.md @@ -0,0 +1,27 @@ +--- +# 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 + + + + +## 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 00000000..f2ed397d --- /dev/null +++ b/docs/data-sources/customer_keys.md @@ -0,0 +1,31 @@ +--- +# 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 + + + + +## 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 29d2ea67..e36825e6 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 3a22388b..d0c50e01 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 00000000..8d2c1a26 --- /dev/null +++ b/docs/resources/customer_key.md @@ -0,0 +1,27 @@ +--- +# 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). +--- + +# astra_customer_key (Resource) + +`astra_customer_key` provides a Customer Key resource for Astra's Bring Your Own Key (BYOK). + + + + +## Schema + +### Required + +- `cloud_provider` (String) The cloud provider where the Customer Key exists (Currently supported: aws, gcp) +- `key_id` (String) Customer Key ID. +- `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). diff --git a/internal/provider/data_source_cloud_accounts.go b/internal/provider/data_source_cloud_accounts.go new file mode 100644 index 00000000..3be19efc --- /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 00000000..05ff8456 --- /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/%s/%s", orgId, cloudProvider, region)) + 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 00000000..27a91e74 --- /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 dcb462b7..4d6ead5a 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 75a439f6..ecd78907 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 3539b382..83e5c615 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 00000000..56046da9 --- /dev/null +++ b/internal/provider/resource_customer_key.go @@ -0,0 +1,168 @@ +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).", + 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.", + 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) + + // build the create Key request + createKeyReq := &astra.ExternalKMS{} + 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.StatusOK { + return diag.Errorf("Unexpected error creating Customer Key. Status: %d, Message: %s", resp.StatusCode(), string(resp.Body)) + } + orgId, err := getOrgId(ctx, client) + if err != nil { + return diag.FromErr(err) + } + // 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 nil +} + +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: /// + d.SetId(fmt.Sprintf("%s/%s/%s/%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 + } + return resp.JSON200.Id, nil +} + +func parseCustomerKeyId(id string) (string, string, string, string, error) { + re := regexp.MustCompile(`(?P.*)/(?P.*)/(?P.*)/(?P.*)`) + if !re.MatchString(id) { + return "", "", "", "", errors.New("invalid customer key id format: expected ///") + } + 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