diff --git a/docs/data-sources/database.md b/docs/data-sources/database.md index 0e67a32e..47865535 100644 --- a/docs/data-sources/database.md +++ b/docs/data-sources/database.md @@ -28,6 +28,7 @@ data "astra_database" "db" { ### Read-Only - `additional_keyspaces` (List of String) Additional keyspaces +- `additional_regions` (Set of String) Additional Datacenter Cloud regions for multi-region Database deployments. - `cloud_provider` (String) Cloud provider (AWS, GCP, AZURE) - `cqlsh_url` (String) URL for cqlsh web - `data_endpoint_url` (String) REST API URL @@ -38,11 +39,11 @@ 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) Org 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 +- `region` (String) Primary Datacenter Cloud region - `replication_factor` (Number) Replication Factor (not relevant for serverless databases) - `status` (String) Database status -- `total_storage` (Number) Storage Capacity (not relevant for serverelss databases) +- `total_storage` (Number) Storage Capacity (not relevant for serverless databases) diff --git a/docs/resources/database.md b/docs/resources/database.md index f38624f3..1e0cae54 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -17,7 +17,8 @@ resource "astra_database" "example" { name = "name" keyspace = "keyspace" cloud_provider = "gcp" - regions = ["us-east1"] + region = "us-east1" + additional_regions = ["us-east4", "us-central1"] } ``` @@ -29,10 +30,11 @@ resource "astra_database" "example" { - `cloud_provider` (String) The cloud provider to launch the database. (Currently supported: aws, azure, gcp) - `keyspace` (String) Initial keyspace name. For additional keyspaces, use the astra_keyspace resource. - `name` (String) Astra database name. -- `regions` (List of String) Cloud regions to launch the database. (see https://docs.datastax.com/en/astra/docs/database-regions.html for supported regions) +- `region` (String) Primary Cloud region to launch the database. (see https://docs.datastax.com/en/astra/docs/database-regions.html for supported regions) ### Optional +- `additional_regions` (Set of String) Additional Cloud regions for multi-region Database deployment. (see https://docs.datastax.com/en/astra/docs/database-regions.html for supported regions) - `deletion_protection` (Boolean) Whether or not to allow Terraform to destroy the instance. Unless this field is set to false in Terraform state, a `terraform destroy` or `terraform apply` command that deletes the instance will fail. Defaults to `true`. - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) diff --git a/examples/resources/astra_database/resource.tf b/examples/resources/astra_database/resource.tf index 352f8f3d..327681ee 100644 --- a/examples/resources/astra_database/resource.tf +++ b/examples/resources/astra_database/resource.tf @@ -2,5 +2,6 @@ resource "astra_database" "example" { name = "name" keyspace = "keyspace" cloud_provider = "gcp" - regions = ["us-east1"] + region = "us-east1" + additional_regions = ["us-east4", "us-central1"] } diff --git a/internal/provider/data_source_database.go b/internal/provider/data_source_database.go index bfaae3dd..e7913785 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: "Org id (system generated)", Type: schema.TypeString, Computed: true, }, @@ -45,9 +45,14 @@ func dataSourceDatabase() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "regions": { - Description: "Cloud provider region. Get list of supported regions from regions data-source", - Type: schema.TypeList, + "region": { + Description: "Primary Datacenter Cloud region", + Type: schema.TypeString, + Computed: true, + }, + "additional_regions": { + Description: "Additional Datacenter Cloud regions for multi-region Database deployments.", + Type: schema.TypeSet, Computed: true, Elem: &schema.Schema{ Type: schema.TypeString, @@ -94,7 +99,7 @@ func dataSourceDatabase() *schema.Resource { Computed: true, }, "total_storage": { - Description: "Storage Capacity (not relevant for serverelss databases)", + Description: "Storage Capacity (not relevant for serverless databases)", Type: schema.TypeInt, Computed: true, }, diff --git a/internal/provider/resource_database.go b/internal/provider/resource_database.go index 8ea3f1ca..35ad80ab 100644 --- a/internal/provider/resource_database.go +++ b/internal/provider/resource_database.go @@ -53,7 +53,7 @@ func resourceDatabase() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: validation.StringMatch(regexp.MustCompile("^.{2,}"), "name must be atleast 2 characters"), + ValidateFunc: validation.StringMatch(regexp.MustCompile("^.{2,}"), "name must be at least 2 characters"), }, "keyspace": { Description: "Initial keyspace name. For additional keyspaces, use the astra_keyspace resource.", @@ -70,10 +70,16 @@ func resourceDatabase() *schema.Resource { ValidateFunc: validation.StringInSlice(availableCloudProviders, true), DiffSuppressFunc: ignoreCase, }, - "regions": { - Description: "Cloud regions to launch the database. (see https://docs.datastax.com/en/astra/docs/database-regions.html for supported regions)", - Type: schema.TypeList, + "region": { + Description: "Primary Cloud region to launch the database. (see https://docs.datastax.com/en/astra/docs/database-regions.html for supported regions)", + Type: schema.TypeString, Required: true, + ForceNew: true, + }, + "additional_regions": { + Description: "Additional Cloud regions for multi-region Database deployment. (see https://docs.datastax.com/en/astra/docs/database-regions.html for supported regions)", + Type: schema.TypeSet, + Optional: true, ForceNew: false, Elem: &schema.Schema{ Type: schema.TypeString, @@ -163,26 +169,13 @@ func resourceDatabaseCreate(ctx context.Context, resourceData *schema.ResourceDa name := resourceData.Get("name").(string) keyspace := resourceData.Get("keyspace").(string) cloudProvider := resourceData.Get("cloud_provider").(string) - regions := resourceData.Get("regions").([]interface{}) - - if len(regions) < 1 { - return diag.Errorf("\"region\" array must have at least 1 region specified") - } + region := resourceData.Get("region").(string) + additionalRegions := (resourceData.Get("additional_regions").(*schema.Set)).List() // Make sure all regions are valid if err := ensureValidRegions(ctx, client, resourceData); err != nil { return err } - // get the first region in the list to use as the region in which to create the database - region := regions[0].(string) - - // make an array of additonal regions to add if more than one specified - additionalRegions := make([]string, len(regions) -1) - if len(additionalRegions) > 0 { - for i:=0; i 1 { - primaryRegion := []interface{}{regions[0].(string)} - _, regionsToDelete := getRegionUpdates(regions, primaryRegion) - tflog.Debug(ctx, fmt.Sprintf("Multiple regions found. Must delete all additional regions first: %v, regions to delete: %v", regions, regionsToDelete)) + regionsToDelete := (resourceData.Get("additional_regions").(*schema.Set)).List() + + if len(regionsToDelete) > 1 { + tflog.Debug(ctx, fmt.Sprintf("Multiple regions found. Must delete all additional regions first: %v", regionsToDelete)) cloudProvider := resourceData.Get("cloud_provider").(string) if err := deleteRegionsFromDatabase(ctx, resourceData, client, regionsToDelete, databaseID, cloudProvider); err != nil { return err } } else { - tflog.Debug(ctx, fmt.Sprintf("Single region found %v", regions)) + tflog.Debug(ctx, fmt.Sprintf("Single region found %v", resourceData.Get("region"))) } if err := resource.RetryContext(ctx, resourceData.Timeout(schema.TimeoutDelete), func() *resource.RetryError { @@ -365,9 +357,9 @@ func resourceDatabaseUpdate(ctx context.Context, resourceData *schema.ResourceDa databaseID := resourceData.Id() cloudProvider := resourceData.Get("cloud_provider").(string) - if resourceData.HasChange("regions") { + if resourceData.HasChange("additional_regions") { // get regions to add and delete - regionsToAdd, regionsToDelete := getRegionUpdates(resourceData.GetChange("regions")) + regionsToAdd, regionsToDelete := getRegionUpdates(resourceData.GetChange("additional_regions")) if len(regionsToAdd) > 0 { // add any regions to add first if err := addRegionsToDatabase(ctx, resourceData, client, regionsToAdd, databaseID, cloudProvider); err != nil { @@ -384,33 +376,35 @@ func resourceDatabaseUpdate(ctx context.Context, resourceData *schema.ResourceDa return nil } -func getRegionUpdates(oldRegions interface{}, newRegions interface{}) ([]string, []string){ +func getRegionUpdates(oldRegions interface{}, newRegions interface{}) ([]interface{}, []interface{}){ mOld := map[string]bool{} mNew := map[string]bool{} - var regionsToAdd []string - var regionsToDelete []string + var regionsToAdd []interface{} + var regionsToDelete []interface{} + oldRegionsList := (oldRegions.(*schema.Set)).List() + newRegionsList := (newRegions.(*schema.Set)).List() // find any regions to add - for _, v := range oldRegions.([]interface{}) { + for _, v := range oldRegionsList { mOld[v.(string)] = true } - for _, v := range newRegions.([]interface{}) { + for _, v := range newRegionsList { mNew[v.(string)] = true } - for _, v := range oldRegions.([]interface{}) { + for _, v := range oldRegionsList { if !mNew[v.(string)] { - regionsToDelete = append(regionsToDelete, v.(string)) + regionsToDelete = append(regionsToDelete, v) } } - for _, v := range newRegions.([]interface{}) { + for _, v := range newRegionsList { if !mOld[v.(string)] { - regionsToAdd = append(regionsToAdd, v.(string)) + regionsToAdd = append(regionsToAdd, v) } } return regionsToAdd, regionsToDelete } -func addRegionsToDatabase(ctx context.Context, resourceData *schema.ResourceData, client *astra.ClientWithResponses, regions []string, databaseID string, cloudProvider string) diag.Diagnostics { +func addRegionsToDatabase(ctx context.Context, resourceData *schema.ResourceData, client *astra.ClientWithResponses, regions []interface{}, databaseID, cloudProvider string) diag.Diagnostics { // make sure the regions are valid if err := ensureValidRegions(ctx, client, resourceData); err != nil { return err @@ -420,7 +414,7 @@ func addRegionsToDatabase(ctx context.Context, resourceData *schema.ResourceData datacenters := make([]astra.Datacenter, 1) datacenters[0] = astra.Datacenter { CloudProvider: astra.CloudProvider(cloudProvider), - Region: region, + Region: region.(string), Tier: "serverless", } resp, err := client.AddDatacentersWithResponse(ctx, astra.DatabaseIdParam(databaseID), datacenters) @@ -428,7 +422,7 @@ func addRegionsToDatabase(ctx context.Context, resourceData *schema.ResourceData return diag.FromErr(err) } if resp.StatusCode() != http.StatusCreated { - return diag.FromErr(fmt.Errorf("Unexpected response addinng Regions: %s", string(resp.Body))) + return diag.FromErr(fmt.Errorf("Unexpected response adding Regions: %s", string(resp.Body))) } // Wait for the database to be ACTIVE then set resource data if err := waitForDatabaseAndUpdateResource(ctx, resourceData, client, databaseID); err != nil { @@ -438,8 +432,8 @@ func addRegionsToDatabase(ctx context.Context, resourceData *schema.ResourceData return nil } -func deleteRegionsFromDatabase(ctx context.Context, resourceData *schema.ResourceData, client *astra.ClientWithResponses, regions []string, databaseID string, cloudProvider string) diag.Diagnostics { - // get all the datacenetrs for the Datbase ID +func deleteRegionsFromDatabase(ctx context.Context, resourceData *schema.ResourceData, client *astra.ClientWithResponses, regions []interface{}, databaseID, cloudProvider string) diag.Diagnostics { + // get all the datacenters for the Database ID dcListResp, err := client.ListDatacentersWithResponse(ctx, astra.DatabaseIdParam(databaseID), &astra.ListDatacentersParams{}) if err != nil { return diag.FromErr(err) @@ -455,7 +449,7 @@ func deleteRegionsFromDatabase(ctx context.Context, resourceData *schema.Resourc } // delete each region that exists for _, v := range regions { - if dc := regionDcMap[v]; dc.Id != nil { + if dc := regionDcMap[v.(string)]; dc.Id != nil { termResp, err := client.TerminateDatacenterWithResponse(ctx, astra.DatabaseIdParam(databaseID), astra.DatacenterIdParam(*dc.Id)) if err != nil { return diag.FromErr(err) @@ -464,7 +458,7 @@ func deleteRegionsFromDatabase(ctx context.Context, resourceData *schema.Resourc return diag.Errorf("Error terminating datacenter for region \"%s\": Insufficient permissions.", v) } if termResp.StatusCode() != http.StatusAccepted { - return diag.Errorf("Error terminating datacenter for region \"%s\": Response %d, mesage = %s", v, termResp.StatusCode(), string(termResp.Body)) + return diag.Errorf("Error terminating datacenter for region \"%s\": Response %d, message = %s", v, termResp.StatusCode(), string(termResp.Body)) } // Wait for the database to be ACTIVE then set resource data if err := waitForDatabaseAndUpdateResource(ctx, resourceData, client, databaseID); err != nil { @@ -539,7 +533,8 @@ func flattenDatabase(db *astra.Database) map[string]interface{} { "data_endpoint_url": astra.StringValue(db.DataEndpointUrl), "cqlsh_url": astra.StringValue(db.CqlshUrl), "cloud_provider": "", - "regions": []string{astra.StringValue(db.Info.Region)}, + "region": astra.StringValue(db.Info.Region), + "additional_regions": []string{}, "keyspace": astra.StringValue(db.Info.Keyspace), "additional_keyspaces": astra.StringSlice(db.Info.AdditionalKeyspaces), "node_count": db.Storage.NodeCount, @@ -553,23 +548,27 @@ func flattenDatabase(db *astra.Database) map[string]interface{} { flatDB["cloud_provider"] = string(cloudProvider) } - if db.Info.Datacenters != nil { - regions := make([]string, len(*db.Info.Datacenters)) + if db.Info.Datacenters != nil && len(*db.Info.Datacenters) > 1 { + regions := make([]string, len(*db.Info.Datacenters) - 1) datacenters := make(map[string]interface{}, len(*db.Info.Datacenters)) - for index, dc := range *db.Info.Datacenters { - regions[index] = dc.Region + regionIndex := 0 + for _, dc := range *db.Info.Datacenters { + if dc.Region != flatDB["region"].(string) { + regions[regionIndex] = dc.Region + regionIndex++ + } // make a datacenter key of cloud_provider.region dcKey := flatDB["cloud_provider"].(string) + "." + dc.Region datacenters[dcKey] = *dc.Id } - flatDB["regions"] = regions + flatDB["additional_regions"] = regions flatDB["datacenters"] = datacenters } return flatDB } func ensureValidRegions(ctx context.Context, client *astra.ClientWithResponses, resourceData *schema.ResourceData) diag.Diagnostics { - // get the list of serveless regions + // get the list of serverless regions regionsResp, err := client.ListServerlessRegionsWithResponse(ctx) if err != nil { return diag.FromErr(err) @@ -578,7 +577,11 @@ func ensureValidRegions(ctx context.Context, client *astra.ClientWithResponses, } // make sure all of the regions are valid cloudProvider := resourceData.Get("cloud_provider").(string) - regions := resourceData.Get("regions").([] interface{}) + primaryRegion := resourceData.Get("region").(string) + if findMatchingRegion(cloudProvider, primaryRegion, "serverless", *regionsResp.JSON200) == nil { + return diag.Errorf("cloud provider and Primary region combination not available: %s/%s", cloudProvider, primaryRegion) + } + regions := (resourceData.Get("additional_regions").(*schema.Set)).List() for _, r := range regions { region := r.(string) dbRegion := findMatchingRegion(cloudProvider, region, "serverless", *regionsResp.JSON200) diff --git a/internal/provider/resource_database_test.go b/internal/provider/resource_database_test.go index 364298fe..ef56e381 100644 --- a/internal/provider/resource_database_test.go +++ b/internal/provider/resource_database_test.go @@ -2,8 +2,10 @@ package provider import ( "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func TestDatabase(t *testing.T){ @@ -25,7 +27,7 @@ resource "astra_database" "dev" { name = "puppies" keyspace = "puppies" cloud_provider = "gcp" - regions = ["us-east1"] + region = "us-east1" } data "astra_secure_connect_bundle_url" "dev" { @@ -37,8 +39,9 @@ data "astra_secure_connect_bundle_url" "dev" { func TestGetRegionUpdatesOnlyDeletes(t *testing.T) { oldData := []interface{} {"region1", "region2", "region3", "region4", "region5"} newData := []interface{} {"region1", "region2", "region3"} - - regionsToAdd, regionsToDelete := getRegionUpdates(oldData, newData) + oldDataSet := schema.NewSet(schema.HashString, oldData) + newDataSet := schema.NewSet(schema.HashString, newData) + regionsToAdd, regionsToDelete := getRegionUpdates(oldDataSet, newDataSet) testFailed := false // verify no adds and 2 deletes @@ -55,7 +58,7 @@ func TestGetRegionUpdatesOnlyDeletes(t *testing.T) { expectedMap["region4"] = true expectedMap["region5"] = true for _, v := range regionsToDelete { - if !expectedMap[v] { + if !expectedMap[v.(string)] { testFailed = true t.Logf("Unexpected region to delete: %s", v) } @@ -70,8 +73,9 @@ func TestGetRegionUpdatesOnlyDeletes(t *testing.T) { func TestGetRegionUpdatesOnlyAdds(t *testing.T) { oldData := []interface{} {"region1", "region2", "region3"} newData := []interface{} {"region1", "region2", "region3", "region4", "region5"} - - regionsToAdd, regionsToDelete := getRegionUpdates(oldData, newData) + oldDataSet := schema.NewSet(schema.HashString, oldData) + newDataSet := schema.NewSet(schema.HashString, newData) + regionsToAdd, regionsToDelete := getRegionUpdates(oldDataSet, newDataSet) testFailed := false // verify no deletes and 2 adds @@ -84,7 +88,7 @@ func TestGetRegionUpdatesOnlyAdds(t *testing.T) { expectedMap["region4"] = true expectedMap["region5"] = true for _, v := range regionsToAdd { - if !expectedMap[v] { + if !expectedMap[v.(string)] { testFailed = true t.Logf("Unexpected region to add: %s", v) } @@ -103,8 +107,9 @@ func TestGetRegionUpdatesOnlyAdds(t *testing.T) { func TestGetRegionUpdatesAddsAndDeletes(t *testing.T) { oldData := []interface{} {"region1", "region3", "region5"} newData := []interface{} {"region1", "region2", "region4"} - - regionsToAdd, regionsToDelete := getRegionUpdates(oldData, newData) + oldDataSet := schema.NewSet(schema.HashString, oldData) + newDataSet := schema.NewSet(schema.HashString, newData) + regionsToAdd, regionsToDelete := getRegionUpdates(oldDataSet, newDataSet) testFailed := false // verify 2 adds and 2 deletes @@ -117,7 +122,7 @@ func TestGetRegionUpdatesAddsAndDeletes(t *testing.T) { expectedMap["region2"] = true expectedMap["region4"] = true for _, v := range regionsToAdd { - if !expectedMap[v] { + if !expectedMap[v.(string)] { testFailed = true t.Logf("Unexpected region to add: %s", v) } @@ -132,7 +137,7 @@ func TestGetRegionUpdatesAddsAndDeletes(t *testing.T) { expectedMap["region3"] = true expectedMap["region5"] = true for _, v := range regionsToDelete { - if !expectedMap[v] { + if !expectedMap[v.(string)] { testFailed = true t.Logf("Unexpected region to delete: %s", v) } @@ -141,4 +146,4 @@ func TestGetRegionUpdatesAddsAndDeletes(t *testing.T) { if testFailed { t.Fail() } -} \ No newline at end of file +}