From ba78d2c193f775b8e65be2dfc41cc13426727a63 Mon Sep 17 00:00:00 2001 From: ruwenqiang123 Date: Tue, 19 Nov 2024 17:48:39 +0800 Subject: [PATCH] feat(dbss): add new resource to manage RDS database bind DBSS instance (#5877) --- docs/resources/dbss_rds_database.md | 131 +++++++ huaweicloud/provider.go | 1 + ...urce_huaweicloud_dbss_rds_database_test.go | 158 ++++++++ .../resource_huaweicloud_dbss_rds_database.go | 351 ++++++++++++++++++ 4 files changed, 641 insertions(+) create mode 100644 docs/resources/dbss_rds_database.md create mode 100644 huaweicloud/services/acceptance/dbss/resource_huaweicloud_dbss_rds_database_test.go create mode 100644 huaweicloud/services/dbss/resource_huaweicloud_dbss_rds_database.go diff --git a/docs/resources/dbss_rds_database.md b/docs/resources/dbss_rds_database.md new file mode 100644 index 0000000000..e86a9a73ea --- /dev/null +++ b/docs/resources/dbss_rds_database.md @@ -0,0 +1,131 @@ +--- +subcategory: "Database Security Service (DBSS)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_dbss_rds_database" +description: |- + Manage the resource of adding RDS database to DBS instance within HuaweiCloud. +--- + +# huaweicloud_dbss_rds_database + +Manage the resource of adding RDS database to DBS instance within HuaweiCloud. + +-> Before adding the RDS database to the DBSS instance, the DBSS instance `status` must be **ACTIVE**. + +## Example Usage + +```hcl +variable "instance_id" {} +variable "rds_id" {} +variable "type" {} + +resource "huaweicloud_dbss_rds_database" "test" { + instance_id = var.instance_id + rds_id = var.rds_id + type = var.type +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region in which to create the resource. + If omitted, the provider-level region will be used. Changing this parameter will create a new resource. + +* `instance_id` - (Required, String, ForceNew) Specifies the DBSS instance ID. + Changing this parameter will create a new resource. + +* `rds_id` - (Required, String, ForceNew) Specifies the RDS instance ID. + Changing this parameter will create a new resource. + +* `type` - (Required, String, ForceNew) Specifies the RDS database type. + The valid values are as follows: + + **MYSQL** + + **ORACLE** + + **POSTGRESQL** + + **SQLSERVER** + + **DAMENG** + + **TAURUS** + + **DWS** + + **KINGBASE** + + **MARIADB** + + **GAUSSDBOPENGAUSS** + + Changing this parameter will create a new resource. + +* `status` - (Optional, String) Specifies the audit status of the RDS database. + The valid values are as follows: + + **ON** + + **OFF** + + After an RDS database is associated with the DBSS instance, the audit status is **OFF** by default. + +* `lts_audit_switch` - (Optional, Int) Specifies whether to disable LTS audit. + The valid values are as follows: + + `1`: Indicates disable. + + `0`: Remain unchanged. (In this case, the value can also be an integer other than `1`). + + -> This parameter is used in the DWS database scenario. If you do not need to close it, + there is no need to set this field. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID. + +* `db_id` - The database ID. + +* `name` - The database name. + +* `version` - The database version. + +* `charset` - The database character set. + The value can be **GBK** or **UTF8** + +* `ip` - The database IP address. + +* `port` - The database port. + +* `os` - The database operation system. + +* `instance_name` - The database instance name. + +* `audit_status` - The database running status. + The value can be **ACTIVE**, **SHUTOFF** or **ERROR**. + +* `agent_url` - The unique ID of the agent. + +* `db_classification` - The classification of the database. + The value can be **RDS** (RDS database) or **ECS** (self-built database). + +* `rds_audit_switch_mismatch` - Whether the audit switch status of the RDS instance is match. + When the database audit function is enabled and the log upload function on RDS is disabled, the value is **true**. + +## Import + +The resource can be imported using the related `instance_id` and their `id`, separated by a slash (/), e.g. + +```bash +$ terraform import huaweicloud_dbss_rds_database.test / +``` + +Note that the imported state may not be identical to your resource definition, due to some attributes missing from the +API response. +The missing attributes include: `lts_audit_switch`. +It is generally recommended running `terraform plan` after importing the resource. +You can then decide if changes should be applied to the instance, or the resource definition should be updated to align +with the instance. Also, you can ignore changes as below. + +```hcl +resource "huaweicloud_dbss_rds_database" "test" { + ... + + lifecycle { + ignore_changes = [ + lts_audit_switch, + ] + } +} +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 2c1661184b..64818d9be7 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1461,6 +1461,7 @@ func Provider() *schema.Provider { "huaweicloud_dbss_audit_risk_rule_action": dbss.ResourceRiskRuleAction(), "huaweicloud_dbss_instance": dbss.ResourceInstance(), + "huaweicloud_dbss_rds_database": dbss.ResourceAddRdsDatabase(), "huaweicloud_dc_virtual_gateway": dc.ResourceVirtualGateway(), "huaweicloud_dc_virtual_interface": dc.ResourceVirtualInterface(), diff --git a/huaweicloud/services/acceptance/dbss/resource_huaweicloud_dbss_rds_database_test.go b/huaweicloud/services/acceptance/dbss/resource_huaweicloud_dbss_rds_database_test.go new file mode 100644 index 0000000000..699fdd77a1 --- /dev/null +++ b/huaweicloud/services/acceptance/dbss/resource_huaweicloud_dbss_rds_database_test.go @@ -0,0 +1,158 @@ +package dbss + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/dbss" +) + +func getAddRdsDatabaseFunc(conf *config.Config, state *terraform.ResourceState) (interface{}, error) { + client, err := conf.NewServiceClient("dbss", acceptance.HW_REGION_NAME) + if err != nil { + return nil, fmt.Errorf("error creating DBSS client: %s", err) + } + return dbss.GetDatabaseList(client, state.Primary.Attributes["instance_id"], state.Primary.ID) +} + +func TestAccAddRdsDatabase_basic(t *testing.T) { + var ( + addRdsDatabase interface{} + rName = "huaweicloud_dbss_rds_database.test" + name = acceptance.RandomAccResourceName() + ) + + rc := acceptance.InitResourceCheck( + rName, + &addRdsDatabase, + getAddRdsDatabaseFunc, + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccAddRdsDatabase_basic(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(rName, "instance_id", "huaweicloud_dbss_instance.test", "instance_id"), + resource.TestCheckResourceAttrPair(rName, "rds_id", "huaweicloud_rds_instance.test", "id"), + resource.TestCheckResourceAttr(rName, "type", "MYSQL"), + resource.TestCheckResourceAttr(rName, "status", "ON"), + resource.TestCheckResourceAttrSet(rName, "db_id"), + resource.TestCheckResourceAttrSet(rName, "name"), + resource.TestCheckResourceAttrSet(rName, "version"), + resource.TestCheckResourceAttrSet(rName, "charset"), + resource.TestCheckResourceAttrSet(rName, "ip"), + resource.TestCheckResourceAttrSet(rName, "port"), + resource.TestCheckResourceAttrSet(rName, "os"), + resource.TestCheckResourceAttrSet(rName, "instance_name"), + resource.TestCheckResourceAttrSet(rName, "db_classification"), + ), + }, + { + Config: testAccAddRdsDatabase_update(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "status", "OFF"), + ), + }, + { + ResourceName: rName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "lts_audit_switch", + }, + ImportStateIdFunc: testAccAddRdsDatabaseImportState(rName), + }, + }, + }) +} + +// Before test, you need to set the default security group rule, enable `3306` port +func testAccAddRdsDatabase_base(name string) string { + return fmt.Sprintf(` +%[1]s + +data "huaweicloud_rds_flavors" "test" { + db_type = "MySQL" + db_version = "8.0" + instance_mode = "single" + group_type = "dedicated" + vcpus = 4 +} + +resource "huaweicloud_rds_instance" "test" { + name = "%[2]s" + flavor = data.huaweicloud_rds_flavors.test.flavors[0].name + security_group_id = data.huaweicloud_networking_secgroup.test.id + subnet_id = data.huaweicloud_vpc_subnet.test.id + vpc_id = data.huaweicloud_vpc.test.id + availability_zone = [data.huaweicloud_availability_zones.test.names[0]] + + db { + type = "MySQL" + version = "8.0" + } + + volume { + type = "CLOUDSSD" + size = 100 + } +} +`, testInstance_basic(name), name) +} + +func testAccAddRdsDatabase_basic(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_dbss_rds_database" "test" { + instance_id = huaweicloud_dbss_instance.test.instance_id + rds_id = huaweicloud_rds_instance.test.id + type = "MYSQL" + status = "ON" +} +`, testAccAddRdsDatabase_base(name)) +} + +func testAccAddRdsDatabase_update(name string) string { + return fmt.Sprintf(` +%[1]s + +resource "huaweicloud_dbss_rds_database" "test" { + instance_id = huaweicloud_dbss_instance.test.instance_id + rds_id = huaweicloud_rds_instance.test.id + type = "MYSQL" + status = "OFF" +} +`, testAccAddRdsDatabase_base(name)) +} + +func testAccAddRdsDatabaseImportState(rName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + var instanceId, rdsId string + rs, ok := s.RootModule().Resources[rName] + if !ok { + return "", fmt.Errorf("resource (%s) not found", rName) + } + + instanceId = rs.Primary.Attributes["instance_id"] + rdsId = rs.Primary.ID + if instanceId == "" || rdsId == "" { + return "", fmt.Errorf("invalid format specified for import ID, want '/', but got '%s/%s'", + instanceId, rdsId) + } + return fmt.Sprintf("%s/%s", instanceId, rdsId), nil + } +} diff --git a/huaweicloud/services/dbss/resource_huaweicloud_dbss_rds_database.go b/huaweicloud/services/dbss/resource_huaweicloud_dbss_rds_database.go new file mode 100644 index 0000000000..5e8a1283da --- /dev/null +++ b/huaweicloud/services/dbss/resource_huaweicloud_dbss_rds_database.go @@ -0,0 +1,351 @@ +package dbss + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/chnsz/golangsdk" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API DBSS POST /v2/{project_id}/{instance_id}/audit/databases/rds +// @API DBSS GET /v1/{project_id}/{instance_id}/dbss/audit/databases +// @API DBSS POST /v2/{project_id}/{instance_id}/audit/databases/switch +// @API DBSS DELETE /v2/{project_id}/{instance_id}/audit/databases/{db_id} +func ResourceAddRdsDatabase() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAddRdsDatabaseCreate, + ReadContext: resourceAddRdsDatabaseRead, + UpdateContext: resourceAddRdsDatabaseUpdate, + DeleteContext: resourceAddRdsDatabaseDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceAddRdsDatabaseImportState, + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "instance_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "rds_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "status": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "lts_audit_switch": { + Type: schema.TypeInt, + Optional: true, + }, + "db_id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeString, + Computed: true, + }, + "charset": { + Type: schema.TypeString, + Computed: true, + }, + "ip": { + Type: schema.TypeString, + Computed: true, + }, + "port": { + Type: schema.TypeString, + Computed: true, + }, + "os": { + Type: schema.TypeString, + Computed: true, + }, + "instance_name": { + Type: schema.TypeString, + Computed: true, + }, + "audit_status": { + Type: schema.TypeString, + Computed: true, + }, + "agent_url": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "db_classification": { + Type: schema.TypeString, + Computed: true, + }, + "rds_audit_switch_mismatch": { + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func resourceAddRdsDatabaseCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + httpUrl := "v2/{project_id}/{instance_id}/audit/databases/rds" + client, err := cfg.NewServiceClient("dbss", region) + if err != nil { + return diag.Errorf("error creating DBSS client: %s", err) + } + + instanceId := d.Get("instance_id").(string) + createPath := client.Endpoint + httpUrl + createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID) + createPath = strings.ReplaceAll(createPath, "{instance_id}", instanceId) + createOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + JSONBody: buildAddRdsDatabaseParams(d), + } + resp, err := client.Request("POST", createPath, &createOpt) + if err != nil { + return diag.Errorf("error adding RDS database to the DBSS instance: %s", err) + } + + respBody, err := utils.FlattenResponse(resp) + if err != nil { + return diag.FromErr(err) + } + + resourceId := utils.PathSearch("ret_list[0].id", respBody, "").(string) + if resourceId == "" { + return diag.Errorf("error adding RDS database to the DBSS instance: ID is not found in API response") + } + + d.SetId(resourceId) + + status := d.Get("status").(string) + if status == "ON" { + databaseInfo, err := GetDatabaseList(client, instanceId, d.Id()) + if err != nil { + return diag.Errorf("error retrieving DBSS audit databases in creation") + } + databaseId := utils.PathSearch("database.id", databaseInfo, "").(string) + if databaseId == "" { + return diag.Errorf("failed to enable the audit status: 'db_id' is not found in API response") + } + + err = updateAuditStatus(client, d, instanceId, databaseId) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceAddRdsDatabaseRead(ctx, d, meta) +} + +func buildAddRdsDatabaseParams(d *schema.ResourceData) map[string]interface{} { + params := map[string]interface{}{ + "databases": []map[string]interface{}{ + { + "id": d.Get("rds_id"), + "type": d.Get("type"), + }, + }, + } + + return params +} + +func resourceAddRdsDatabaseRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + client, err := cfg.NewServiceClient("dbss", region) + if err != nil { + return diag.Errorf("error creating DBSS client: %s", err) + } + + instanceId := d.Get("instance_id").(string) + databaseInfo, err := GetDatabaseList(client, instanceId, d.Id()) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving DBSS audit databases") + } + + mErr := multierror.Append(nil, + d.Set("region", region), + d.Set("instance_id", instanceId), + d.Set("rds_id", utils.PathSearch("database.rds_id", databaseInfo, nil)), + d.Set("db_id", utils.PathSearch("database.id", databaseInfo, nil)), + d.Set("type", utils.PathSearch("database.type", databaseInfo, nil)), + d.Set("status", utils.PathSearch("database.status", databaseInfo, nil)), + d.Set("name", utils.PathSearch("database.name", databaseInfo, nil)), + d.Set("version", utils.PathSearch("database.version", databaseInfo, nil)), + d.Set("charset", utils.PathSearch("database.charset", databaseInfo, nil)), + d.Set("ip", utils.PathSearch("database.ip", databaseInfo, nil)), + d.Set("port", utils.PathSearch("database.port", databaseInfo, nil)), + d.Set("os", utils.PathSearch("database.os", databaseInfo, nil)), + d.Set("instance_name", utils.PathSearch("database.instance_name", databaseInfo, nil)), + d.Set("audit_status", utils.PathSearch("database.audit_status", databaseInfo, nil)), + d.Set("agent_url", utils.PathSearch("database.agent_url", databaseInfo, nil)), + d.Set("db_classification", utils.PathSearch("database.db_classification", databaseInfo, nil)), + d.Set("rds_audit_switch_mismatch", utils.PathSearch("database.rds_audit_switch_mismatch", databaseInfo, nil)), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func GetDatabaseList(client *golangsdk.ServiceClient, instanceId, rdsId string) (interface{}, error) { + httpUrl := "v1/{project_id}/{instance_id}/dbss/audit/databases?limit=100" + getPath := client.Endpoint + httpUrl + getPath = strings.ReplaceAll(getPath, "{project_id}", client.ProjectID) + getPath = strings.ReplaceAll(getPath, "{instance_id}", instanceId) + + getOpts := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + offset := 0 + for { + getPathWithOffset := fmt.Sprintf("%s&offset=%d", getPath, offset) + resp, err := client.Request("GET", getPathWithOffset, &getOpts) + if err != nil { + return nil, err + } + + respBody, err := utils.FlattenResponse(resp) + if err != nil { + return nil, err + } + + databaseList := utils.PathSearch("databases", respBody, make([]interface{}, 0)).([]interface{}) + if len(databaseList) == 0 { + break + } + + database := utils.PathSearch(fmt.Sprintf("[?database.rds_id=='%s']|[0]", rdsId), databaseList, nil) + if database != nil { + return database, nil + } + offset += len(databaseList) + } + + return nil, golangsdk.ErrDefault404{} +} + +func updateAuditStatus(client *golangsdk.ServiceClient, d *schema.ResourceData, instanceId, databaseId string) error { + httpUrl := "v2/{project_id}/{instance_id}/audit/databases/switch" + requestPath := client.Endpoint + httpUrl + requestPath = strings.ReplaceAll(requestPath, "{project_id}", client.ProjectID) + requestPath = strings.ReplaceAll(requestPath, "{instance_id}", instanceId) + requestOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + JSONBody: buildUpdateaAuditStatusBodyParams(d, databaseId), + } + + _, err := client.Request("POST", requestPath, &requestOpt) + if err != nil { + return fmt.Errorf("error updating the database audit status: %s", err) + } + + return nil +} + +func buildUpdateaAuditStatusBodyParams(d *schema.ResourceData, databaseId string) map[string]interface{} { + params := map[string]interface{}{ + "id": databaseId, + "status": d.Get("status"), + "lts_audit_switch": utils.ValueIgnoreEmpty(d.Get("lts_audit_switch")), + } + + return params +} + +func resourceAddRdsDatabaseUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + region := cfg.GetRegion(d) + client, err := cfg.NewServiceClient("dbss", region) + if err != nil { + return diag.Errorf("error creating DBSS client: %s", err) + } + + instanceId := d.Get("instance_id").(string) + databaseId := d.Get("db_id").(string) + + if d.HasChanges("status", "lts_audit_switch") { + if databaseId == "" { + diag.Errorf("edit audit status is not currently supported: 'db_id' is not found in API response") + } + err := updateAuditStatus(client, d, instanceId, databaseId) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceAddRdsDatabaseRead(ctx, d, meta) +} + +func resourceAddRdsDatabaseDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + client, err := cfg.NewServiceClient("dbss", cfg.GetRegion(d)) + if err != nil { + return diag.Errorf("error creating DBSS client: %s", err) + } + instanceId := d.Get("instance_id").(string) + databaseId := d.Get("db_id").(string) + + httpUrl := "v2/{project_id}/{instance_id}/audit/databases/{db_id}" + deletePath := client.Endpoint + httpUrl + deletePath = strings.ReplaceAll(deletePath, "{project_id}", client.ProjectID) + deletePath = strings.ReplaceAll(deletePath, "{instance_id}", instanceId) + deletePath = strings.ReplaceAll(deletePath, "{db_id}", databaseId) + + deleteOpts := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + _, err = client.Request("DELETE", deletePath, &deleteOpts) + if err != nil { + return common.CheckDeletedDiag(d, err, "error removing the RDS database from the DBSS instance") + } + return nil +} + +func resourceAddRdsDatabaseImportState(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, + error) { + importedId := d.Id() + parts := strings.Split(importedId, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid format specified for import ID, want '/', but got '%s'", + importedId) + } + d.SetId(parts[1]) + + mErr := multierror.Append(nil, + d.Set("instance_id", parts[0]), + ) + + return []*schema.ResourceData{d}, mErr.ErrorOrNil() +}