From 83ec24d4a9f657434e77c47d9b7b60411c7bfe0e Mon Sep 17 00:00:00 2001 From: lornakelly Date: Tue, 12 Nov 2024 21:45:58 +0000 Subject: [PATCH] ICD: Promote read replicas (#5738) * Promote read replicas * Add tests * Fixes * Docs * Fixes * Add my sql and edb tests * Update tests * Fixes * Update mysql, edb tests * Fix to tests --------- Co-authored-by: Lorna-Kelly --- ibm/service/database/resource_ibm_database.go | 66 ++++++++- .../resource_ibm_database_edb_test.go | 136 ++++++++++++++++++ .../resource_ibm_database_mysql_test.go | 106 ++++++++++++++ .../resource_ibm_database_postgresql_test.go | 88 ++++++++++++ website/docs/r/database.html.markdown | 3 +- 5 files changed, 392 insertions(+), 7 deletions(-) diff --git a/ibm/service/database/resource_ibm_database.go b/ibm/service/database/resource_ibm_database.go index 50198a8145..a0d2ab72a0 100644 --- a/ibm/service/database/resource_ibm_database.go +++ b/ibm/service/database/resource_ibm_database.go @@ -140,7 +140,8 @@ func ResourceIBMDatabaseInstance() *schema.Resource { CustomizeDiff: customdiff.All( resourceIBMDatabaseInstanceDiff, validateGroupsDiff, - validateUsersDiff), + validateUsersDiff, + validateRemoteLeaderIDDiff), Importer: &schema.ResourceImporter{}, @@ -258,10 +259,14 @@ func ResourceIBMDatabaseInstance() *schema.Resource { Optional: true, }, "remote_leader_id": { - Description: "The CRN of leader database", - Type: schema.TypeString, - Optional: true, - DiffSuppressFunc: flex.ApplyOnce, + Description: "The CRN of leader database", + Type: schema.TypeString, + Optional: true, + }, + "skip_initial_backup": { + Description: "Option to skip the initial backup when promoting a read-only replica. Skipping the initial backup means that your replica becomes available more quickly, but there is no immediate backup available.", + Type: schema.TypeBool, + Optional: true, }, "key_protect_instance": { Description: "The CRN of Key protect instance", @@ -840,6 +845,7 @@ func ResourceIBMDatabaseInstance() *schema.Resource { }, } } + func ResourceIBMICDValidator() *validate.ResourceValidator { validateSchema := make([]validate.ValidateSchema, 0) @@ -1597,7 +1603,6 @@ func resourceIBMDatabaseInstanceRead(context context.Context, d *schema.Resource if endpoint, ok := instance.Parameters["service-endpoints"]; ok { d.Set("service_endpoints", endpoint) } - } d.Set(flex.ResourceName, *instance.Name) @@ -2136,6 +2141,37 @@ func resourceIBMDatabaseInstanceUpdate(context context.Context, d *schema.Resour } } + if d.HasChange("remote_leader_id") { + remoteLeaderId := d.Get("remote_leader_id").(string) + + if remoteLeaderId == "" { + skipInitialBackup := false + if skip, ok := d.GetOk("skip_initial_backup"); ok { + skipInitialBackup = skip.(bool) + } + + promoteReadOnlyReplicaOptions := &clouddatabasesv5.PromoteReadOnlyReplicaOptions{ + ID: &instanceID, + Promotion: map[string]interface{}{ + "skip_initial_backup": skipInitialBackup, + }, + } + + promoteReadReplicaResponse, response, err := cloudDatabasesClient.PromoteReadOnlyReplica(promoteReadOnlyReplicaOptions) + + if err != nil { + return diag.FromErr(fmt.Errorf("[ERROR] Error promoting read replica: %s\n%s", err, response)) + } + + taskID := *promoteReadReplicaResponse.Task.ID + _, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return diag.FromErr(fmt.Errorf("[ERROR] Error promoting read replica: %s", err)) + } + } + } + return resourceIBMDatabaseInstanceRead(context, d, meta) } @@ -3039,6 +3075,24 @@ func expandUserChanges(_oldUsers []interface{}, _newUsers []interface{}) (userCh return userChanges } +func validateRemoteLeaderIDDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) (err error) { + _, remoteLeaderIdOk := diff.GetOk("remote_leader_id") + service := diff.Get("service").(string) + crn := diff.Get("resource_crn").(string) + + if remoteLeaderIdOk && (service != "databases-for-postgresql" && service != "databases-for-mysql" && service != "databases-for-enterprisedb") { + return fmt.Errorf("[ERROR] remote_leader_id is only supported for databases-for-postgresql, databases-for-enterprisedb and databases-for-mysql") + } + + oldValue, newValue := diff.GetChange("remote_leader_id") + + if crn != "" && oldValue == "" && newValue != "" { + return fmt.Errorf("[ERROR] You cannot convert an existing instance to a read replica") + } + + return nil +} + func (c *userChange) isDelete() bool { return c.Old != nil && c.New == nil } diff --git a/ibm/service/database/resource_ibm_database_edb_test.go b/ibm/service/database/resource_ibm_database_edb_test.go index 7936460bf4..bf15266c83 100644 --- a/ibm/service/database/resource_ibm_database_edb_test.go +++ b/ibm/service/database/resource_ibm_database_edb_test.go @@ -85,6 +85,93 @@ func TestAccIBMEDBDatabaseInstanceBasic(t *testing.T) { }) } +func TestAccIBMDatabaseInstanceEDBReadReplicaPromotion(t *testing.T) { + t.Parallel() + + databaseResourceGroup := "default" + + var sourceInstanceCRN string + var replicaInstanceCRN string + + serviceName := fmt.Sprintf("tf-edb-%d", acctest.RandIntRange(10, 100)) + readReplicaName := serviceName + "-replica" + + sourceResource := "ibm_database." + serviceName + replicaReplicaResource := "ibm_database." + readReplicaName + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMDatabaseInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMDatabaseInstanceEDBMinimal(databaseResourceGroup, serviceName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(sourceResource, &sourceInstanceCRN), + resource.TestCheckResourceAttr(sourceResource, "name", serviceName), + resource.TestCheckResourceAttr(sourceResource, "service", "databases-for-enterprisedb"), + resource.TestCheckResourceAttr(sourceResource, "plan", "standard"), + resource.TestCheckResourceAttr(sourceResource, "location", acc.Region()), + ), + }, + { + Config: acc.ConfigCompose( + testAccCheckIBMDatabaseInstanceEDBMinimal(databaseResourceGroup, serviceName), + testAccCheckIBMDatabaseInstanceEDBMinimal_ReadReplica(databaseResourceGroup, serviceName)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(replicaReplicaResource, &replicaInstanceCRN), + resource.TestCheckResourceAttr(replicaReplicaResource, "name", readReplicaName), + resource.TestCheckResourceAttr(replicaReplicaResource, "service", "databases-for-enterprisedb"), + resource.TestCheckResourceAttr(replicaReplicaResource, "plan", "standard"), + resource.TestCheckResourceAttr(replicaReplicaResource, "location", acc.Region()), + ), + }, + { + Config: acc.ConfigCompose( + testAccCheckIBMDatabaseInstanceEDBMinimal(databaseResourceGroup, serviceName), + testAccCheckIBMDatabaseInstanceEDBReadReplicaPromote(databaseResourceGroup, readReplicaName)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(sourceResource, &sourceInstanceCRN), + testAccCheckIBMDatabaseInstanceExists(replicaReplicaResource, &replicaInstanceCRN), + resource.TestCheckResourceAttr(replicaReplicaResource, "name", readReplicaName), + resource.TestCheckResourceAttr(replicaReplicaResource, "service", "databases-for-enterprisedb"), + resource.TestCheckResourceAttr(replicaReplicaResource, "plan", "standard"), + resource.TestCheckResourceAttr(replicaReplicaResource, "location", acc.Region()), + resource.TestCheckResourceAttr(replicaReplicaResource, "remote_leader_id", ""), + resource.TestCheckResourceAttr(replicaReplicaResource, "skip_initial_backup", "true"), + ), + }, + }, + }) +} + +func testAccCheckIBMDatabaseInstanceEDBMinimal(databaseResourceGroup string, name string) string { + return fmt.Sprintf(` + data "ibm_resource_group" "test_acc" { + is_default = true + # name = "%[1]s" + } + + resource "ibm_database" "%[2]s" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s" + service = "databases-for-enterprisedb" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + group { + group_id = "member" + host_flavor { + id = "b3c.4x16.encrypted" + } + disk { + allocation_mb = 20480 + } + } + } + `, databaseResourceGroup, name, acc.Region()) +} + func testAccCheckIBMDatabaseInstanceEDBBasic(databaseResourceGroup string, name string) string { return fmt.Sprintf(` data "ibm_resource_group" "test_acc" { @@ -207,3 +294,52 @@ func testAccCheckIBMDatabaseInstanceEDBReduced(databaseResourceGroup string, nam } `, databaseResourceGroup, name, acc.Region()) } + +func testAccCheckIBMDatabaseInstanceEDBMinimal_ReadReplica(databaseResourceGroup string, name string) string { + return fmt.Sprintf(` + resource "ibm_database" "%[2]s-replica" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s-replica" + service = "databases-for-enterprisedb" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + remote_leader_id = ibm_database.%[2]s.id + + group { + group_id = "member" + host_flavor { + id = "b3c.4x16.encrypted" + } + disk { + allocation_mb = 20480 + } + } + } + `, databaseResourceGroup, name, acc.Region()) +} + +func testAccCheckIBMDatabaseInstanceEDBReadReplicaPromote(databaseResourceGroup string, readReplicaName string) string { + return fmt.Sprintf(` + resource "ibm_database" "%[2]s" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s" + service = "databases-for-enterprisedb" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + remote_leader_id = "" + skip_initial_backup = true + + group { + group_id = "member" + host_flavor { + id = "b3c.4x16.encrypted" + } + disk { + allocation_mb = 20480 + } + } + } + `, databaseResourceGroup, readReplicaName, acc.Region()) +} diff --git a/ibm/service/database/resource_ibm_database_mysql_test.go b/ibm/service/database/resource_ibm_database_mysql_test.go index 301ca3570c..af0bb87ebb 100644 --- a/ibm/service/database/resource_ibm_database_mysql_test.go +++ b/ibm/service/database/resource_ibm_database_mysql_test.go @@ -62,6 +62,112 @@ func TestAccIBMMysqlDatabaseInstanceBasic(t *testing.T) { }) } +func TestAccIBMDatabaseInstanceMySQLReadReplicaPromotion(t *testing.T) { + t.Parallel() + + databaseResourceGroup := "default" + + var sourceInstanceCRN string + var replicaInstanceCRN string + + serviceName := fmt.Sprintf("tf-mysql-%d", acctest.RandIntRange(10, 100)) + readReplicaName := serviceName + "-replica" + + sourceResource := "ibm_database." + serviceName + replicaReplicaResource := "ibm_database." + readReplicaName + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMDatabaseInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMDatabaseInstanceMySQLMinimal(databaseResourceGroup, serviceName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(sourceResource, &sourceInstanceCRN), + resource.TestCheckResourceAttr(sourceResource, "name", serviceName), + resource.TestCheckResourceAttr(sourceResource, "service", "databases-for-mysql"), + resource.TestCheckResourceAttr(sourceResource, "plan", "standard"), + resource.TestCheckResourceAttr(sourceResource, "location", acc.Region()), + ), + }, + { + Config: acc.ConfigCompose( + testAccCheckIBMDatabaseInstanceMySQLMinimal(databaseResourceGroup, serviceName), + testAccCheckIBMDatabaseInstanceMySQLMinimal_ReadReplica(databaseResourceGroup, serviceName)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(replicaReplicaResource, &replicaInstanceCRN), + resource.TestCheckResourceAttr(replicaReplicaResource, "name", readReplicaName), + resource.TestCheckResourceAttr(replicaReplicaResource, "service", "databases-for-mysql"), + resource.TestCheckResourceAttr(replicaReplicaResource, "plan", "standard"), + resource.TestCheckResourceAttr(replicaReplicaResource, "location", acc.Region()), + ), + }, + { + Config: acc.ConfigCompose( + testAccCheckIBMDatabaseInstanceMySQLMinimal(databaseResourceGroup, serviceName), + testAccCheckIBMDatabaseInstanceMySQLReadReplicaPromote(databaseResourceGroup, readReplicaName)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(replicaReplicaResource, &replicaInstanceCRN), + resource.TestCheckResourceAttr(replicaReplicaResource, "name", readReplicaName), + resource.TestCheckResourceAttr(replicaReplicaResource, "service", "databases-for-mysql"), + resource.TestCheckResourceAttr(replicaReplicaResource, "plan", "standard"), + resource.TestCheckResourceAttr(replicaReplicaResource, "location", acc.Region()), + resource.TestCheckResourceAttr(replicaReplicaResource, "remote_leader_id", ""), + resource.TestCheckResourceAttr(replicaReplicaResource, "skip_initial_backup", "true"), + ), + }, + }, + }) +} + +func testAccCheckIBMDatabaseInstanceMySQLMinimal(databaseResourceGroup string, name string) string { + return fmt.Sprintf(` + data "ibm_resource_group" "test_acc" { + is_default = true + # name = "%[1]s" + } + + resource "ibm_database" "%[2]s" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s" + service = "databases-for-mysql" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + } + `, databaseResourceGroup, name, acc.Region()) +} + +func testAccCheckIBMDatabaseInstanceMySQLMinimal_ReadReplica(databaseResourceGroup string, name string) string { + return fmt.Sprintf(` + resource "ibm_database" "%[2]s-replica" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s-replica" + service = "databases-for-mysql" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + remote_leader_id = ibm_database.%[2]s.id + } + `, databaseResourceGroup, name, acc.Region()) +} + +func testAccCheckIBMDatabaseInstanceMySQLReadReplicaPromote(databaseResourceGroup string, readReplicaName string) string { + return fmt.Sprintf(` + resource "ibm_database" "%[2]s" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s" + service = "databases-for-mysql" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + remote_leader_id = "" + skip_initial_backup = true + } + `, databaseResourceGroup, readReplicaName, acc.Region()) +} + func testAccCheckIBMDatabaseInstanceMysqlBasic(databaseResourceGroup string, name string) string { return fmt.Sprintf(` data "ibm_resource_group" "test_acc" { diff --git a/ibm/service/database/resource_ibm_database_postgresql_test.go b/ibm/service/database/resource_ibm_database_postgresql_test.go index 8d7ce0890f..a8c6453c0a 100644 --- a/ibm/service/database/resource_ibm_database_postgresql_test.go +++ b/ibm/service/database/resource_ibm_database_postgresql_test.go @@ -256,6 +256,65 @@ func TestAccIBMDatabaseInstancePostgresPITR(t *testing.T) { }) } +func TestAccIBMDatabaseInstancePostgresReadReplicaPromotion(t *testing.T) { + t.Parallel() + + databaseResourceGroup := "default" + + var sourceInstanceCRN string + var replicaInstanceCRN string + + serviceName := fmt.Sprintf("tf-Pgress-%d", acctest.RandIntRange(10, 100)) + readReplicaName := serviceName + "-replica" + + sourceResource := "ibm_database." + serviceName + replicaReplicaResource := "ibm_database." + readReplicaName + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMDatabaseInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMDatabaseInstancePostgresMinimal(databaseResourceGroup, serviceName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(sourceResource, &sourceInstanceCRN), + resource.TestCheckResourceAttr(sourceResource, "name", serviceName), + resource.TestCheckResourceAttr(sourceResource, "service", "databases-for-postgresql"), + resource.TestCheckResourceAttr(sourceResource, "plan", "standard"), + resource.TestCheckResourceAttr(sourceResource, "location", acc.Region()), + ), + }, + { + Config: acc.ConfigCompose( + testAccCheckIBMDatabaseInstancePostgresMinimal(databaseResourceGroup, serviceName), + testAccCheckIBMDatabaseInstancePostgresMinimal_ReadReplica(databaseResourceGroup, serviceName)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(replicaReplicaResource, &replicaInstanceCRN), + resource.TestCheckResourceAttr(replicaReplicaResource, "name", readReplicaName), + resource.TestCheckResourceAttr(replicaReplicaResource, "service", "databases-for-postgresql"), + resource.TestCheckResourceAttr(replicaReplicaResource, "plan", "standard"), + resource.TestCheckResourceAttr(replicaReplicaResource, "location", acc.Region()), + ), + }, + { + Config: acc.ConfigCompose( + testAccCheckIBMDatabaseInstancePostgresMinimal(databaseResourceGroup, serviceName), + testAccCheckIBMDatabaseInstancePostgresReadReplicaPromote(databaseResourceGroup, readReplicaName)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMDatabaseInstanceExists(replicaReplicaResource, &replicaInstanceCRN), + resource.TestCheckResourceAttr(replicaReplicaResource, "name", readReplicaName), + resource.TestCheckResourceAttr(replicaReplicaResource, "service", "databases-for-postgresql"), + resource.TestCheckResourceAttr(replicaReplicaResource, "plan", "standard"), + resource.TestCheckResourceAttr(replicaReplicaResource, "location", acc.Region()), + resource.TestCheckResourceAttr(replicaReplicaResource, "remote_leader_id", ""), + resource.TestCheckResourceAttr(replicaReplicaResource, "skip_initial_backup", "true"), + ), + }, + }, + }) +} + func testAccCheckIBMDatabaseInstanceDestroy(s *terraform.State) error { rsContClient, err := acc.TestAccProvider.Meta().(conns.ClientSession).ResourceControllerV2API() if err != nil { @@ -736,3 +795,32 @@ func testAccCheckIBMDatabaseInstancePostgresMinimal_PITR(databaseResourceGroup s } `, databaseResourceGroup, name, acc.Region()) } + +func testAccCheckIBMDatabaseInstancePostgresMinimal_ReadReplica(databaseResourceGroup string, name string) string { + return fmt.Sprintf(` + resource "ibm_database" "%[2]s-replica" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s-replica" + service = "databases-for-postgresql" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + remote_leader_id = ibm_database.%[2]s.id + } + `, databaseResourceGroup, name, acc.Region()) +} + +func testAccCheckIBMDatabaseInstancePostgresReadReplicaPromote(databaseResourceGroup string, readReplicaName string) string { + return fmt.Sprintf(` + resource "ibm_database" "%[2]s" { + resource_group_id = data.ibm_resource_group.test_acc.id + name = "%[2]s" + service = "databases-for-postgresql" + plan = "standard" + location = "%[3]s" + service_endpoints = "public-and-private" + remote_leader_id = "" + skip_initial_backup = true + } + `, databaseResourceGroup, readReplicaName, acc.Region()) +} diff --git a/website/docs/r/database.html.markdown b/website/docs/r/database.html.markdown index 83d8a80b8f..f72636b3f1 100644 --- a/website/docs/r/database.html.markdown +++ b/website/docs/r/database.html.markdown @@ -680,7 +680,8 @@ Review the argument reference that you can specify for your resource. - `plan` - (Required, Forces new resource, String) The name of the service plan that you choose for your instance. All databases use `standard`. `enterprise` is supported only for elasticsearch (`databases-for-elasticsearch`), and mongodb(`databases-for-mongodb`). `platinum` is supported for elasticsearch (`databases-for-elasticsearch`). - `point_in_time_recovery_deployment_id` - (Optional, String) The ID of the source deployment that you want to recover back to. - `point_in_time_recovery_time` - (Optional, String) The timestamp in UTC format that you want to restore to. To retrieve the timestamp, run the `ibmcloud cdb postgresql earliest-pitr-timestamp ` command. To restore to the latest available time, use a blank string `""` as the timestamp. For more information, see [Point-in-time Recovery](https://cloud.ibm.com/docs/databases-for-postgresql?topic=databases-for-postgresql-pitr). -- `remote_leader_id` - (Optional, String) A CRN of the leader database to make the replica(read-only) deployment. The leader database is created by a database deployment with the same service ID. A read-only replica is set up to replicate all of your data from the leader deployment to the replica deployment by using asynchronous replication. For more information, see [Configuring Read-only Replicas](https://cloud.ibm.com/docs/databases-for-postgresql?topic=databases-for-postgresql-read-only-replicas). +- `remote_leader_id` - (Optional, String) A CRN of the leader database to make the replica(read-only) deployment. The leader database is created by a database deployment with the same service ID. A read-only replica is set up to replicate all of your data from the leader deployment to the replica deployment by using asynchronous replication. Removing the `remote_leader_id` attribute from an existing read-only replica will promote the deployment to a standalone deployment. The deployment will restart and break its connection with the leader. This will disable all database users associated with this deployment. For more information, see [Configuring Read-only Replicas](https://cloud.ibm.com/docs/databases-for-postgresql?topic=databases-for-postgresql-read-only-replicas). +- `skip_initial_backup` - (Optional, Boolean) Should only be set when promoting a read-only replica. By setting this value to `true`, you skip the initial backup that would normally be taken upon promotion. Skipping the initial backup means that your replica becomes available more quickly, but there is no immediate backup available. The default is `false`. For more information, see [Configuring Read-only Replicas] - `resource_group_id` - (Optional, Forces new resource, String) The ID of the resource group where you want to create the instance. To retrieve this value, run `ibmcloud resource groups` or use the `ibm_resource_group` data source. If no value is provided, the `default` resource group is used. - `service` - (Required, Forces new resource, String) The type of Cloud Databases that you want to create. Only the following services are currently accepted: `databases-for-etcd`, `databases-for-postgresql`, `databases-for-redis`, `databases-for-elasticsearch`, `messages-for-rabbitmq`,`databases-for-mongodb`,`databases-for-mysql`, and `databases-for-enterprisedb`. - `service_endpoints` - (Required, String) Specify whether you want to enable the public, private, or both service endpoints. Supported values are `public`, `private`, or `public-and-private`.