Skip to content

Commit

Permalink
feat(dds): support primary standby switch
Browse files Browse the repository at this point in the history
  • Loading branch information
saf3dfsa committed Oct 28, 2024
1 parent 7564da4 commit e7fab91
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 0 deletions.
57 changes: 57 additions & 0 deletions docs/resources/dds_primary_standby_switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
subcategory: "Document Database Service (DDS)"
layout: "huaweicloud"
page_title: "HuaweiCloud: huaweicloud_dds_primary_standby_switch"
description: |-
Manages a DDS primary standby switch resource within HuaweiCloud.
---

# huaweicloud_dds_primary_standby_switch

Manages a DDS primary standby switch resource within HuaweiCloud.

## Example Usage

### Perform switch for a replica set instance

```hcl
variable "instance_id" {}
resource "huaweicloud_dds_primary_standby_switch" "test" {
instance_id = var.instance_id
}
```

### Promote standby node to primary for replica set node, shard node or config node

```hcl
variable "instance_id" {}
variable "node_id" {}
resource "huaweicloud_dds_primary_standby_switch" "test" {
instance_id = var.instance_id
node_id = var.node_id
}
```

## 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 creates a new resource.

* `instance_id` - (Required, String, ForceNew) Specifies the instance ID.
Changing this creates a new resource.

* `node_id` - (Optional, String, ForceNew) Specifies the ID of replica set node, shard node or config node.
Changing this creates a new resource.

-> If `node_id` is not specified, perform a primary/secondary switchover in a replica set instance.

## Attribute Reference

In addition to all arguments above, the following attributes are exported:

* `id` - The resource ID.
1 change: 1 addition & 0 deletions huaweicloud/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,7 @@ func Provider() *schema.Provider {
"huaweicloud_dds_instance_eip_associate": dds.ResourceDDSInstanceBindEIP(),
"huaweicloud_dds_instance_restore": dds.ResourceDDSInstanceRestore(),
"huaweicloud_dds_instance_parameters_modify": dds.ResourceDDSInstanceParametersModify(),
"huaweicloud_dds_primary_standby_switch": dds.ResourceDDSPrimaryStandbySwitch(),
"huaweicloud_dds_recycle_policy": dds.ResourceDDSRecyclePolicy(),

"huaweicloud_ddm_instance": ddm.ResourceDdmInstance(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dds

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"

"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance"
)

func TestAccDDSPrimaryStandbySwitch_basic(t *testing.T) {
rName := acceptance.RandomAccResourceName()

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.TestAccPreCheck(t) },
ProviderFactories: acceptance.TestAccProviderFactories,
CheckDestroy: nil,
Steps: []resource.TestStep{
{
Config: testAccDDSPrimaryStandbySwitch_Instance(rName),
Check: resource.ComposeTestCheckFunc(),
},
},
})
}

func testAccDDSPrimaryStandbySwitch_Instance(rName string) string {
return fmt.Sprintf(`
%s
resource "huaweicloud_dds_primary_standby_switch" "test" {
instance_id = huaweicloud_dds_instance.instance.id
}`, testAccDDSInstanceReplicaSetBasic(rName))
}

func TestAccDDSPrimaryStandbySwitch_node(t *testing.T) {
rName := acceptance.RandomAccResourceName()

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.TestAccPreCheck(t) },
ProviderFactories: acceptance.TestAccProviderFactories,
CheckDestroy: nil,
Steps: []resource.TestStep{
{
Config: testAccDDSPrimaryStandbySwitch_Node(rName),
Check: resource.ComposeTestCheckFunc(),
},
},
})
}

func testAccDDSPrimaryStandbySwitch_Node(rName string) string {
return fmt.Sprintf(`
%s
locals {
node_ids = [for node in huaweicloud_dds_instance.instance.nodes: node.id if node.role == "Secondary"]
}
resource "huaweicloud_dds_primary_standby_switch" "test" {
instance_id = huaweicloud_dds_instance.instance.id
node_id = local.node_ids[0]
lifecycle {
ignore_changes = [
node_id,
]
}
}`, testAccDDSInstanceV3Config_basic(rName, 8800))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package dds

import (
"context"
"fmt"
"strings"
"time"

"github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/chnsz/golangsdk"

"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils"
)

// @API DDS POST /v3/{project_id}/instances/{instance_id}/switchover
// @API DDS POST /v3/{project_id}/instances/{instance_id}/nodes/{node_id}/primary
// @API DDS GET /v3/{project_id}/jobs
func ResourceDDSPrimaryStandbySwitch() *schema.Resource {
return &schema.Resource{
CreateContext: resourceDDSPrimaryStandbySwitchCreate,
ReadContext: resourceDDSPrimaryStandbySwitchRead,
DeleteContext: resourceDDSPrimaryStandbySwitchDelete,

Schema: map[string]*schema.Schema{
"region": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"instance_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"node_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}

func resourceDDSPrimaryStandbySwitchCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
cfg := meta.(*config.Config)
region := cfg.GetRegion(d)
client, err := cfg.NewServiceClient("dds", region)
if err != nil {
return diag.Errorf("error creating DDS client: %s", err)
}

var jobID string

if _, ok := d.GetOk("node_id"); ok {
// switch by node, it's a synchronous task
// but need to wait the node become primary for a few seconds
err = promoteStandbyNodeToPrimary(client, d)
if err != nil {
return diag.FromErr(err)
}

// lintignore:R018
time.Sleep(30 * time.Second)
} else {
// switch instance, it's a asynchronous task
jobID, err = performSwitchForInstance(client, d)
if err != nil {
return diag.FromErr(err)
}
}

id, err := uuid.GenerateUUID()
if err != nil {
return diag.Errorf("unable to generate ID: %s", err)
}
d.SetId(id)

// wait for job complete
if jobID != "" {
stateConf := &resource.StateChangeConf{
Pending: []string{"Running"},
Target: []string{"Completed"},
Refresh: JobStateRefreshFunc(client, jobID),
Timeout: d.Timeout(schema.TimeoutUpdate),
Delay: 10 * time.Second,
PollInterval: 10 * time.Second,
}

_, err = stateConf.WaitForStateContext(ctx)
if err != nil {
return diag.Errorf("error waiting for the job (%s) completed: %s ", jobID, err)
}
}

return nil
}

func promoteStandbyNodeToPrimary(client *golangsdk.ServiceClient, d *schema.ResourceData) error {
createHttpUrl := "v3/{project_id}/instances/{instance_id}/nodes/{node_id}/primary"
createPath := client.Endpoint + createHttpUrl
createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID)
createPath = strings.ReplaceAll(createPath, "{instance_id}", d.Get("instance_id").(string))
createPath = strings.ReplaceAll(createPath, "{node_id}", d.Get("node_id").(string))
createOpt := golangsdk.RequestOpts{
KeepResponseBody: true,
MoreHeaders: map[string]string{
"Content-Type": "application/json",
},
}

_, err := client.Request("POST", createPath, &createOpt)
if err != nil {
return fmt.Errorf("error promoting standby node to primary: %s", err)
}

return nil
}

func performSwitchForInstance(client *golangsdk.ServiceClient, d *schema.ResourceData) (string, error) {
createHttpUrl := "v3/{project_id}/instances/{instance_id}/switchover"
createPath := client.Endpoint + createHttpUrl
createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID)
createPath = strings.ReplaceAll(createPath, "{instance_id}", d.Get("instance_id").(string))
createOpt := golangsdk.RequestOpts{
KeepResponseBody: true,
}

createResp, err := client.Request("POST", createPath, &createOpt)
if err != nil {
return "", fmt.Errorf("error performing primary standby switch for instance: %s", err)
}
createRespBody, err := utils.FlattenResponse(createResp)
if err != nil {
return "", fmt.Errorf("error flattening response: %s", err)
}

jobID := utils.PathSearch("job_id", createRespBody, "").(string)
if jobID == "" {
return "", fmt.Errorf("unable to find job ID in API response")
}

return jobID, nil
}

func resourceDDSPrimaryStandbySwitchRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
return nil
}

func resourceDDSPrimaryStandbySwitchDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
errorMsg := "Deleting primary standby switch resource is not supported. The resource is only removed from the state," +
" the instance remains in the cloud."
return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Warning,
Summary: errorMsg,
},
}
}

0 comments on commit e7fab91

Please sign in to comment.