From eb9541e2c03b00e7256a456470bc2f31542a916d Mon Sep 17 00:00:00 2001 From: houpeng80 <114376095+houpeng80@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:48:43 +0800 Subject: [PATCH] feat(GaussDB): gaussdb opengauss instance support update flavor (#6021) --- docs/resources/gaussdb_opengauss_instance.md | 9 +- ...eicloud_gaussdb_opengauss_instance_test.go | 48 +++++++--- ..._huaweicloud_gaussdb_opengauss_instance.go | 94 ++++++++++++++++++- 3 files changed, 132 insertions(+), 19 deletions(-) diff --git a/docs/resources/gaussdb_opengauss_instance.md b/docs/resources/gaussdb_opengauss_instance.md index 8dfed3e91b..83886679af 100644 --- a/docs/resources/gaussdb_opengauss_instance.md +++ b/docs/resources/gaussdb_opengauss_instance.md @@ -93,8 +93,7 @@ The following arguments are supported: The value must be `4` to `64` characters in length and start with a letter. It is case-sensitive and can contain only letters, digits, hyphens (-), and underscores (_). -* `flavor` - (Required, String, ForceNew) Specifies the instance specifications. Please reference the API docs for valid - options. Changing this parameter will create a new resource. +* `flavor` - (Required, String) Specifies the instance specifications. * `password` - (Required, String) Specifies the database password. The value must be `8` to `32` characters in length, including uppercase and lowercase letters, digits, and special characters, such as **~!@#%^*-_=+?**. You are advised @@ -293,7 +292,7 @@ The `nodes` block contains: This resource provides the following timeouts configuration options: * `create` - Default is 120 minutes. -* `update` - Default is 90 minutes. +* `update` - Default is 150 minutes. * `delete` - Default is 45 minutes. ## Import @@ -317,8 +316,8 @@ resource "huaweicloud_gaussdb_opengauss_instance" "test" { lifecycle { ignore_changes = [ - password, ha.0.mode, ha.0.instance_mode, configuration_id, disk_encryption_id, enable_force_switch, - enable_single_float_ip, period_unit, period, auto_renew, + password, configuration_id, disk_encryption_id, enable_force_switch, enable_single_float_ip, period_unit, period, + auto_renew, ] } } diff --git a/huaweicloud/services/acceptance/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance_test.go b/huaweicloud/services/acceptance/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance_test.go index 8ed22c97c1..ff2abf9006 100644 --- a/huaweicloud/services/acceptance/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance_test.go +++ b/huaweicloud/services/acceptance/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance_test.go @@ -85,12 +85,13 @@ func TestAccOpenGaussInstance_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "subnet_id", "huaweicloud_vpc_subnet.test", "id"), resource.TestCheckResourceAttrPair(resourceName, "security_group_id", "huaweicloud_networking_secgroup.test", "id"), - resource.TestCheckResourceAttr(resourceName, "flavor", "gaussdb.opengauss.ee.dn.m6.2xlarge.8.in"), + resource.TestCheckResourceAttrPair(resourceName, "flavor", + "data.huaweicloud_gaussdb_opengauss_flavors.test", "flavors.0.spec_code"), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "password", password), resource.TestCheckResourceAttr(resourceName, "ha.0.mode", "enterprise"), resource.TestCheckResourceAttr(resourceName, "ha.0.replication_mode", "sync"), - resource.TestCheckResourceAttr(resourceName, "ha.0.consistency", "strong"), + resource.TestCheckResourceAttr(resourceName, "ha.0.consistency", "eventual"), resource.TestCheckResourceAttr(resourceName, "volume.0.type", "ULTRAHIGH"), resource.TestCheckResourceAttr(resourceName, "volume.0.size", "40"), resource.TestCheckResourceAttr(resourceName, "sharding_num", "1"), @@ -105,6 +106,8 @@ func TestAccOpenGaussInstance_basic(t *testing.T) { Config: testAccOpenGaussInstance_update(rName, newPassword, 3), Check: resource.ComposeTestCheckFunc( rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(resourceName, "flavor", + "data.huaweicloud_gaussdb_opengauss_flavors.test", "flavors.1.spec_code"), resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("%s-update", rName)), resource.TestCheckResourceAttr(resourceName, "password", newPassword), resource.TestCheckResourceAttr(resourceName, "sharding_num", "2"), @@ -151,7 +154,8 @@ func TestAccOpenGaussInstance_haModeCentralized(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "subnet_id", "huaweicloud_vpc_subnet.test", "id"), resource.TestCheckResourceAttrPair(resourceName, "security_group_id", "huaweicloud_networking_secgroup.test", "id"), - resource.TestCheckResourceAttr(resourceName, "flavor", "gaussdb.bs.s6.xlarge.x864.ha"), + resource.TestCheckResourceAttrPair(resourceName, "flavor", + "data.huaweicloud_gaussdb_opengauss_flavors.test", "flavors.0.spec_code"), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "ha.0.mode", "centralization_standard"), resource.TestCheckResourceAttr(resourceName, "ha.0.replication_mode", "sync"), @@ -165,6 +169,8 @@ func TestAccOpenGaussInstance_haModeCentralized(t *testing.T) { Config: testAccOpenGaussInstance_haModeCentralizedUpdate(rName, newPassword), Check: resource.ComposeTestCheckFunc( rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(resourceName, "flavor", + "data.huaweicloud_gaussdb_opengauss_flavors.test", "flavors.1.spec_code"), resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("%s-update", rName)), resource.TestCheckResourceAttr(resourceName, "volume.0.size", "80"), resource.TestCheckResourceAttr(resourceName, "backup_strategy.0.start_time", "08:00-09:00"), @@ -205,6 +211,11 @@ func testAccOpenGaussInstance_basic(rName, password string, replicaNum int) stri return fmt.Sprintf(` %[1]s +data "huaweicloud_gaussdb_opengauss_flavors" "test" { + version = "8.201" + ha_mode = "enterprise" +} + resource "huaweicloud_gaussdb_opengauss_instance" "test" { depends_on = [ huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss, @@ -215,7 +226,7 @@ resource "huaweicloud_gaussdb_opengauss_instance" "test" { subnet_id = huaweicloud_vpc_subnet.test.id security_group_id = huaweicloud_networking_secgroup.test.id - flavor = "gaussdb.bs.s6.xlarge.x864.ha" + flavor = data.huaweicloud_gaussdb_opengauss_flavors.test.flavors[0].spec_code name = "%[2]s" password = "%[3]s" sharding_num = 1 @@ -228,10 +239,10 @@ resource "huaweicloud_gaussdb_opengauss_instance" "test" { enterprise_project_id = "%[5]s" ha { - mode = "centralization_standard" + mode = "enterprise" replication_mode = "sync" consistency = "eventual" - instance_mode = "basic" + instance_mode = "enterprise" } volume { @@ -251,6 +262,11 @@ func testAccOpenGaussInstance_update(rName, password string, replicaNum int) str return fmt.Sprintf(` %[1]s +data "huaweicloud_gaussdb_opengauss_flavors" "test" { + version = "8.201" + ha_mode = "enterprise" +} + resource "huaweicloud_gaussdb_opengauss_instance" "test" { depends_on = [ huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss, @@ -261,7 +277,7 @@ resource "huaweicloud_gaussdb_opengauss_instance" "test" { subnet_id = huaweicloud_vpc_subnet.test.id security_group_id = huaweicloud_networking_secgroup.test.id - flavor = "gaussdb.bs.s6.xlarge.x864.ha" + flavor = data.huaweicloud_gaussdb_opengauss_flavors.test.flavors[1].spec_code name = "%[2]s-update" password = "%[3]s" sharding_num = 2 @@ -274,10 +290,10 @@ resource "huaweicloud_gaussdb_opengauss_instance" "test" { enterprise_project_id = "%[5]s" ha { - mode = "centralization_standard" + mode = "enterprise" replication_mode = "sync" consistency = "eventual" - instance_mode = "basic" + instance_mode = "enterprise" } volume { @@ -297,6 +313,11 @@ func testAccOpenGaussInstance_haModeCentralized(rName, password string) string { return fmt.Sprintf(` %[1]s +data "huaweicloud_gaussdb_opengauss_flavors" "test" { + version = "8.201" + ha_mode = "centralization_standard" +} + resource "huaweicloud_gaussdb_opengauss_instance" "test" { depends_on = [ huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss, @@ -306,7 +327,7 @@ resource "huaweicloud_gaussdb_opengauss_instance" "test" { subnet_id = huaweicloud_vpc_subnet.test.id security_group_id = huaweicloud_networking_secgroup.test.id - flavor = "gaussdb.bs.s6.xlarge.x864.ha" + flavor = data.huaweicloud_gaussdb_opengauss_flavors.test.flavors[0].spec_code name = "%[2]s" password = "%[3]s" replica_num = 3 @@ -340,6 +361,11 @@ func testAccOpenGaussInstance_haModeCentralizedUpdate(rName, password string) st return fmt.Sprintf(` %[1]s +data "huaweicloud_gaussdb_opengauss_flavors" "test" { + version = "8.201" + ha_mode = "centralization_standard" +} + resource "huaweicloud_gaussdb_opengauss_instance" "test" { depends_on = [ huaweicloud_networking_secgroup_rule.in_v4_tcp_opengauss, @@ -350,7 +376,7 @@ resource "huaweicloud_gaussdb_opengauss_instance" "test" { subnet_id = huaweicloud_vpc_subnet.test.id security_group_id = huaweicloud_networking_secgroup.test.id - flavor = "gaussdb.bs.s6.xlarge.x864.ha" + flavor = data.huaweicloud_gaussdb_opengauss_flavors.test.flavors[1].spec_code name = "%[2]s-update" password = "%[3]s" replica_num = 3 diff --git a/huaweicloud/services/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance.go b/huaweicloud/services/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance.go index 3be52236e0..95a9611d9c 100644 --- a/huaweicloud/services/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance.go +++ b/huaweicloud/services/gaussdb/resource_huaweicloud_gaussdb_opengauss_instance.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "net/http" "strconv" "strings" "time" @@ -32,6 +33,7 @@ const ( // @API GaussDB POST /v3/{project_id}/instances/{instance_id}/password // @API GaussDB POST /v3/{project_id}/instances/{instance_id}/action // @API GaussDB PUT /v3/{project_id}/instances/{instance_id}/backups/policy +// @API GaussDB PUT /v3/{project_id}/instance/{instance_id}/flavor // @API GaussDB DELETE /v3/{project_id}/instances/{instance_id} // @API BSS GET /v2/orders/customer-orders/details/{order_id} // @API BSS POST /v2/orders/suscriptions/resources/query @@ -51,7 +53,7 @@ func ResourceOpenGaussInstance() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(120 * time.Minute), - Update: schema.DefaultTimeout(90 * time.Minute), + Update: schema.DefaultTimeout(150 * time.Minute), Delete: schema.DefaultTimeout(45 * time.Minute), }, CustomizeDiff: func(_ context.Context, d *schema.ResourceDiff, v interface{}) error { @@ -75,7 +77,6 @@ func ResourceOpenGaussInstance() *schema.Resource { "flavor": { Type: schema.TypeString, Required: true, - ForceNew: true, }, "availability_zone": { Type: schema.TypeString, @@ -619,7 +620,7 @@ func resourceOpenGaussInstanceRead(_ context.Context, d *schema.ResourceData, me func flattenGaussDBOpenGaussResponseBodyHa(instance interface{}) []interface{} { rst := []interface{}{ map[string]interface{}{ - "mode": utils.PathSearch("type", instance, nil), + "mode": strings.ToLower(utils.PathSearch("type", instance, "").(string)), "replication_mode": utils.PathSearch("ha.replication_mode", instance, nil), "consistency": utils.PathSearch("ha.consistency", instance, nil), "instance_mode": utils.PathSearch("instance_mode", instance, nil), @@ -775,6 +776,12 @@ func resourceOpenGaussInstanceUpdate(ctx context.Context, d *schema.ResourceData } } + if d.HasChange("flavor") { + if err = updateInstanceFlavor(ctx, d, client, bssClient); err != nil { + return diag.FromErr(err) + } + } + if d.HasChange("auto_renew") { if err = common.UpdateAutoRenew(bssClient, d.Get("auto_renew").(string), d.Id()); err != nil { return diag.Errorf("error updating the auto-renew of the instance (%s): %s", d.Id(), err) @@ -1046,6 +1053,87 @@ func buildUpdateInstanceBackupStrategyBodyParams(d *schema.ResourceData) map[str return bodyParams } +func updateInstanceFlavor(ctx context.Context, d *schema.ResourceData, client, bssClient *golangsdk.ServiceClient) error { + var ( + httpUrl = "v3/{project_id}/instance/{instance_id}/flavor" + ) + + updatePath := client.Endpoint + httpUrl + updatePath = strings.ReplaceAll(updatePath, "{project_id}", client.ProjectID) + updatePath = strings.ReplaceAll(updatePath, "{instance_id}", d.Id()) + + updateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + updateOpt.JSONBody = utils.RemoveNil(buildUpdateInstanceFlavorBodyParams(d)) + retryFunc := func() (interface{}, bool, error) { + res, err := client.Request("PUT", updatePath, &updateOpt) + retry, err := handleMultiOperationsError(err) + return res, retry, err + } + r, err := common.RetryContextWithWaitForState(&common.RetryContextWithWaitForStateParam{ + Ctx: ctx, + RetryFunc: retryFunc, + WaitFunc: instanceStateRefreshFunc(client, d.Id()), + WaitTarget: []string{"ACTIVE"}, + Timeout: d.Timeout(schema.TimeoutUpdate), + DelayTimeout: 10 * time.Second, + PollInterval: 10 * time.Second, + }) + if err != nil { + return fmt.Errorf("error updating GaussDB OpenGauss instance (%s) flavor: %s", d.Id(), err) + } + + updateRespBody, err := utils.FlattenResponse(r.(*http.Response)) + if err != nil { + return err + } + + if v, ok := d.GetOk("charging_mode"); ok && v.(string) == "prePaid" { + orderId := utils.PathSearch("order_id", updateRespBody, nil) + if orderId == nil { + return fmt.Errorf("error updating GaussDB OpenGauss instance flavor: order_id is not found in API response") + } + // wait for order success + err = common.WaitOrderComplete(ctx, bssClient, orderId.(string), d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + } else { + jobId := utils.PathSearch("job_id", updateRespBody, nil) + if jobId == nil { + return fmt.Errorf("error updating GaussDB OpenGauss instance flavor: job_id is not found in API response") + } + err = checkGaussDBOpenGaussJobFinish(ctx, client, jobId.(string), 180, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"MODIFYING", "BACKING UP"}, + Target: []string{"ACTIVE"}, + Refresh: instanceStateRefreshFunc(client, d.Id()), + Timeout: d.Timeout(schema.TimeoutUpdate), + PollInterval: 10 * time.Second, + } + + _, err = stateConf.WaitForStateContext(ctx) + if err != nil { + return fmt.Errorf("error waiting for instance (%s) to become ready: %s", d.Id(), err) + } + + return nil +} + +func buildUpdateInstanceFlavorBodyParams(d *schema.ResourceData) map[string]interface{} { + bodyParams := map[string]interface{}{ + "flavor_ref": d.Get("flavor"), + "is_auto_pay": true, + } + return bodyParams +} + func resourceOpenGaussInstanceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { cfg := meta.(*config.Config) region := cfg.GetRegion(d)