Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate additional regions from primary region #217

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/data-sources/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)


6 changes: 4 additions & 2 deletions docs/resources/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
```

Expand All @@ -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))

Expand Down
3 changes: 2 additions & 1 deletion examples/resources/astra_database/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
15 changes: 10 additions & 5 deletions internal/provider/data_source_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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,
Expand Down Expand Up @@ -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,
},
Expand Down
105 changes: 54 additions & 51 deletions internal/provider/resource_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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,
Expand Down Expand Up @@ -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<len(additionalRegions); i++ {
additionalRegions[i] = regions[i+1].(string)
}
}

resp, err := client.CreateDatabaseWithResponse(ctx, astra.CreateDatabaseJSONRequestBody{
Name: name,
Expand Down Expand Up @@ -274,17 +267,16 @@ func resourceDatabaseDelete(ctx context.Context, resourceData *schema.ResourceDa
alreadyDeleted := false

// get the list of regions and delete any extra regions/datacenters first
regions := (resourceData.Get("regions")).([]interface{})
if len(regions) > 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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -420,15 +414,15 @@ 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)
if err != nil {
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 {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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)
Expand Down