Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/4319.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/tencentcloud_cynosdb_readonly_instance: support `instance_name` modification via `ModifyInstanceName` API
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-17
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# add-cynosdb-readonly-instance-name-modify

Support instance_name modification for tencentcloud_cynosdb_readonly_instance via ModifyInstanceName API
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## Context

`tencentcloud_cynosdb_readonly_instance` 资源当前的 `instance_name` 字段为 `ForceNew`,修改实例名称会触发资源销毁重建。腾讯云 TDSQL-C MySQL 提供了 `ModifyInstanceName` 接口(cynosdb v20190107),支持对已存在的实例修改名称,请求参数为 `InstanceId` 与 `InstanceName`,无需异步轮询(同步返回)。

现有代码结构:
- 资源文件:`tencentcloud/services/cynosdb/resource_tc_cynosdb_readonly_instance.go`
- 服务层:`tencentcloud/services/cynosdb/service_tencentcloud_cynosdb.go`(已有 `ModifyClusterName` 等同类方法可参考)
- 测试:`tencentcloud/services/cynosdb/resource_tc_cynosdb_readonly_instance_test.go`(现有 Terraform acc 测试套件)

## Goals / Non-Goals

**Goals:**
- 使 `instance_name` 字段支持就地更新,调用 `ModifyInstanceName` API 完成修改。
- 在服务层封装 `ModifyInstanceName` 调用,遵循 `WriteRetryTimeout` 重试与 `tccommon.RetryError` 错误包装的现有模式。
- 保持向后兼容:不改变现有 state 格式与 ID 规则。

**Non-Goals:**
- 不修改 `instance_name` 的 `Required` 属性。
- 不调整其它字段(如 `vpc_id`/`subnet_id` 仍不支持变更)。
- 不引入新的 schema 字段。

## Decisions

### Decision 1: 移除 instance_name 的 ForceNew

**选择**:将 `instance_name` 的 `ForceNew: true` 删除,使其成为可更新字段。

**理由**:`ModifyInstanceName` API 支持修改实例名称,无需重建资源。移除 `ForceNew` 是接入该 API 的前提。

**替代方案**:保留 `ForceNew` 并新增一个独立字段——会导致语义重复且用户体验割裂,弃用。

### Decision 2: 在 Update 方法中增加 instance_name 变更分支

**选择**:在 `resourceTencentCloudCynosdbReadonlyInstanceUpdate` 中新增 `if d.HasChange("instance_name")` 分支,调用 `cynosdbService.ModifyInstanceName(ctx, instanceId, instanceName)`。

**理由**:与现有 `instance_cpu_core`/`instance_memory_size`、维护窗口等分支保持一致的 `d.HasChange` 模式。

### Decision 3: 服务层方法签名与重试策略

**选择**:在 `service_tencentcloud_cynosdb.go` 中新增:
```go
func (me *CynosdbService) ModifyInstanceName(ctx context.Context, instanceId string, instanceName string) (errRet error)
```
内部使用 `resource.Retry(tccommon.WriteRetryTimeout, ...)` + `tccommon.RetryError(err)`,与 `ModifyClusterName` 方法保持一致。

**理由**:写操作遵循 provider 现有重试模式;请求参数仅 `InstanceId` 与 `InstanceName`,无需额外封装。

### Decision 4: 单元测试使用 gomonkey mock

**选择**:在 `resource_tc_cynosdb_readonly_instance_test.go` 中新增 `TestUnitCynosdbReadonlyInstance_UpdateInstanceName`,使用 gomonkey mock `UseCynosdbClient`、`ModifyInstanceName`、`DescribeInstances`(用于 Read)。

**理由**:本变更新增了 CRUD 逻辑分支,按照代码生成要求使用 mock 进行业务逻辑单元测试,可独立运行无需云资源。

## Risks / Trade-offs

- **[风险] 旧 state 中 instance_name 为 ForceNew,升级后行为变化** → 缓解:仅是从"重建"变为"就地更新",对用户而言是行为增强,不会破坏已有配置;首次 apply 不会触发任何变更。
- **[风险] ModifyInstanceName 接口偶发失败** → 缓解:通过 `WriteRetryTimeout` 重试机制覆盖。
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

`tencentcloud_cynosdb_readonly_instance` 资源当前的 `instance_name` 字段被标记为 `ForceNew`,用户修改实例名称时会导致资源重建。腾讯云 TDSQL-C MySQL 提供了 `ModifyInstanceName` 接口用于修改实例名称,应在 Terraform 中接入该接口,使 `instance_name` 支持就地更新,避免不必要的重建。

## What Changes

- 移除 `tencentcloud_cynosdb_readonly_instance` 资源中 `instance_name` 字段的 `ForceNew`,使其支持更新。
- 在 `resourceTencentCloudCynosdbReadonlyInstanceUpdate` 中新增 `d.HasChange("instance_name")` 分支,调用云 API `ModifyInstanceName` 完成实例名称修改。
- 在 `service_tencentcloud_cynosdb.go` 中新增 `ModifyInstanceName` 服务方法,封装 `ModifyInstanceName` API 调用(含 `WriteRetryTimeout` 重试)。
- 更新文档 `website/docs/r/cynosdb_readonly_instance.html.markdown`,将 `instance_name` 的 `ForceNew` 标记移除。
- 更新 `.changelog/4319.txt`,将变更描述从"update instance_name field description"改为支持 `instance_name` 修改的增强说明。
- 在 `resource_tc_cynosdb_readonly_instance_test.go` 中补充 `instance_name` 更新的单元测试用例���使用 gomonkey mock 云 API)。

## Capabilities

### New Capabilities
- `cynosdb-readonly-instance-name-modify`: 支持通过 `ModifyInstanceName` 接口就地修改 `tencentcloud_cynosdb_readonly_instance` 的 `instance_name` 字段。

### Modified Capabilities
<!-- 无需修改已有 spec 层面的行为契约 -->

## Impact

- **代码文件**:
- `tencentcloud/services/cynosdb/resource_tc_cynosdb_readonly_instance.go`(schema、update)
- `tencentcloud/services/cynosdb/service_tencentcloud_cynosdb.go`(新增服务方法)
- `tencentcloud/services/cynosdb/resource_tc_cynosdb_readonly_instance_test.go`(单元测试)
- **文档文件**:`website/docs/r/cynosdb_readonly_instance.html.markdown`
- **changelog**:`.changelog/4319.txt`
- **API**:`ModifyInstanceName`(cynosdb v20190107)
- **向后兼容**:仅将 `ForceNew` 字段改为可更新,原有 state 与配置完全兼容,不会触发重建。
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## ADDED Requirements

### Requirement: instance_name supports in-place update

The `tencentcloud_cynosdb_readonly_instance` resource SHALL allow the `instance_name` field to be updated in-place without recreating the resource. When `instance_name` changes, the provider SHALL call the `ModifyInstanceName` API to update the instance name on the cloud, then refresh state.

#### Scenario: Update instance_name triggers ModifyInstanceName API

- **WHEN** the user updates `instance_name` in the Terraform configuration for an existing `tencentcloud_cynosdb_readonly_instance`
- **THEN** the provider SHALL call `ModifyInstanceName` with the instance ID and the new instance name, and SHALL NOT recreate the resource

#### Scenario: instance_name is no longer ForceNew

- **WHEN** the schema for `instance_name` is inspected
- **THEN** the field SHALL be `Required` and `String`, and SHALL NOT have `ForceNew` set

#### Scenario: Read reflects updated instance_name

- **WHEN** `ModifyInstanceName` succeeds and a subsequent Read is performed
- **THEN** the provider SHALL set `instance_name` in state to the value returned by the `DescribeInstanceById` API

#### Scenario: ModifyInstanceName failure is retried

- **WHEN** the `ModifyInstanceName` API returns an error
- **THEN** the provider SHALL wrap the error with `tccommon.RetryError` and retry within `WriteRetryTimeout`
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## 1. 服务层方法

- [x] 1.1 在 `tencentcloud/services/cynosdb/service_tencentcloud_cynosdb.go` 中新增 `ModifyInstanceName(ctx, instanceId, instanceName)` 方法,使用 `cynosdb.NewModifyInstanceNameRequest()`,设置 `InstanceId` 与 `InstanceName`,内部使用 `resource.Retry(tccommon.WriteRetryTimeout, ...)` + `tccommon.RetryError(err)`,参考 `ModifyClusterName` 方法实现

## 2. 资源 Schema 与 Update 逻辑

- [x] 2.1 在 `tencentcloud/services/cynosdb/resource_tc_cynosdb_readonly_instance.go` 的 `ResourceTencentCloudCynosdbReadonlyInstance()` 中,移除 `instance_name` 字段的 `ForceNew: true`
- [x] 2.2 在 `resourceTencentCloudCynosdbReadonlyInstanceUpdate` 中,新增 `if d.HasChange("instance_name")` 分支,读取新值并调用 `cynosdbService.ModifyInstanceName(ctx, instanceId, instanceName)`,处理 error

## 3. 单元测试

- [x] 3.1 在 `tencentcloud/services/cynosdb/resource_tc_cynosdb_readonly_instance_test.go` 中新增 `TestUnitCynosdbReadonlyInstance_UpdateInstanceName`,使用 gomonkey mock `UseCynosdbClient`、`ModifyInstanceName`、`DescribeInstances`,验证 Update 路径调用 ModifyInstanceName 且不报错
- [x] 3.2 执行 `go test ./tencentcloud/services/cynosdb/ -run "TestUnitCynosdbReadonlyInstance_UpdateInstanceName" -v -count=1 -gcflags="all=-l"` 确保测试通过

## 4. 文档与 changelog

- [x] 4.1 更新 `website/docs/r/cynosdb_readonly_instance.html.markdown`,移除 `instance_name` 的 `ForceNew` 标记(由 `make doc` 在收尾阶段生成,本任务仅准备 .md 源文件)
- [x] 4.2 更新 `.changelog/4319.txt`,将变更描述改为 `resource/tencentcloud_cynosdb_readonly_instance: support instance_name modification via ModifyInstanceName API`(enhancement)

## 5. 验证

- [x] 5.1 确认所有修改的 Go 文件可正确编译(由后续流程的 go build 验证)

## 6. 额外修复

- [x] 6.1 修复 `resource_tc_cynosdb_cluster_v2_test.go` 中 sweeper 名称冲突(master 预先存在的问题:与 `resource_tc_cynosdb_cluster_test.go` 注册了同名 `tencentcloud_cynosdb` sweeper,导致 `log.Fatalf` 阻断整个包的测试运行),将 v2 的 sweeper 重命名为 `tencentcloud_cynosdb_v2`
29 changes: 29 additions & 0 deletions openspec/specs/cynosdb-readonly-instance-name-modify/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# cynosdb-readonly-instance-name-modify Specification

## Purpose
TBD - created by archiving change add-cynosdb-readonly-instance-name-modify. Update Purpose after archive.
## Requirements
### Requirement: instance_name supports in-place update

The `tencentcloud_cynosdb_readonly_instance` resource SHALL allow the `instance_name` field to be updated in-place without recreating the resource. When `instance_name` changes, the provider SHALL call the `ModifyInstanceName` API to update the instance name on the cloud, then refresh state.

#### Scenario: Update instance_name triggers ModifyInstanceName API

- **WHEN** the user updates `instance_name` in the Terraform configuration for an existing `tencentcloud_cynosdb_readonly_instance`
- **THEN** the provider SHALL call `ModifyInstanceName` with the instance ID and the new instance name, and SHALL NOT recreate the resource

#### Scenario: instance_name is no longer ForceNew

- **WHEN** the schema for `instance_name` is inspected
- **THEN** the field SHALL be `Required` and `String`, and SHALL NOT have `ForceNew` set

#### Scenario: Read reflects updated instance_name

- **WHEN** `ModifyInstanceName` succeeds and a subsequent Read is performed
- **THEN** the provider SHALL set `instance_name` in state to the value returned by the `DescribeInstanceById` API

#### Scenario: ModifyInstanceName failure is retried

- **WHEN** the `ModifyInstanceName` API returns an error
- **THEN** the provider SHALL wrap the error with `tccommon.RetryError` and retry within `WriteRetryTimeout`

Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import (
)

func init() {
resource.AddTestSweepers("tencentcloud_cynosdb", &resource.Sweeper{
Name: "tencentcloud_cynosdb",
resource.AddTestSweepers("tencentcloud_cynosdb_v2", &resource.Sweeper{
Name: "tencentcloud_cynosdb_v2",
F: func(r string) error {
logId := tccommon.GetLogId(tccommon.ContextNil)
ctx := context.WithValue(context.TODO(), tccommon.LogIdKey, logId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ func ResourceTencentCloudCynosdbReadonlyInstance() *schema.Resource {
"instance_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Name of instance.",
Description: "Instance name.",
},
"force_delete": {
Type: schema.TypeBool,
Expand Down Expand Up @@ -223,6 +222,14 @@ func resourceTencentCloudCynosdbReadonlyInstanceUpdate(d *schema.ResourceData, m

d.Partial(true)

if d.HasChange("instance_name") {
instanceName := d.Get("instance_name").(string)
err := cynosdbService.ModifyInstanceName(ctx, instanceId, instanceName)
if err != nil {
return err
}
}

if d.HasChange("instance_cpu_core") || d.HasChange("instance_memory_size") {
cpu := int64(d.Get("instance_cpu_core").(int))
memory := int64(d.Get("instance_memory_size").(int))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,36 @@ package cynosdb_test
import (
tcacctest "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/acctest"
tccommon "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/common"
"github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/connectivity"
svccynosdb "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/services/cynosdb"

"context"
"fmt"
"testing"

"github.com/agiledragon/gomonkey/v2"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/assert"
cynosdb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cynosdb/v20190107"
"github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/internal/helper"
)

type mockMetaCynosdbReadonlyInstance struct {
client *connectivity.TencentCloudClient
}

func (m *mockMetaCynosdbReadonlyInstance) GetAPIV3Conn() *connectivity.TencentCloudClient {
return m.client
}

var _ tccommon.ProviderMeta = &mockMetaCynosdbReadonlyInstance{}

func newMockMetaCynosdbReadonlyInstance() *mockMetaCynosdbReadonlyInstance {
return &mockMetaCynosdbReadonlyInstance{client: &connectivity.TencentCloudClient{Region: "ap-guangzhou"}}
}

func TestAccTencentCloudCynosdbReadonlyInstanceResource(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
Expand Down Expand Up @@ -244,3 +264,118 @@ resource "tencentcloud_cynosdb_readonly_instance" "foo" {
]
}
`

// go test ./tencentcloud/services/cynosdb/ -run "TestUnitCynosdbReadonlyInstance_UpdateInstanceName" -v -count=1 -gcflags="all=-l"
func TestUnitCynosdbReadonlyInstance_UpdateInstanceName(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()

meta := newMockMetaCynosdbReadonlyInstance()
cynosdbClient := &cynosdb.Client{}
patches.ApplyMethodReturn(meta.client, "UseCynosdbClient", cynosdbClient)

modifyCalled := false
patches.ApplyMethodFunc(cynosdbClient, "ModifyInstanceName", func(request *cynosdb.ModifyInstanceNameRequest) (*cynosdb.ModifyInstanceNameResponse, error) {
assert.Equal(t, "cynosdbmysql-ins-abcdefgh", *request.InstanceId)
assert.Equal(t, "tf-cynosdb-readonly-instance-new", *request.InstanceName)
modifyCalled = true
resp := &cynosdb.ModifyInstanceNameResponse{}
resp.Response = &cynosdb.ModifyInstanceNameResponseParams{
RequestId: helper.String("fake-request-id"),
}
return resp, nil
})

// mock DescribeInstances used by DescribeInstanceById in Read
patches.ApplyMethodFunc(cynosdbClient, "DescribeInstances", func(request *cynosdb.DescribeInstancesRequest) (*cynosdb.DescribeInstancesResponse, error) {
resp := &cynosdb.DescribeInstancesResponse{}
resp.Response = &cynosdb.DescribeInstancesResponseParams{
TotalCount: helper.IntInt64(1),
InstanceSet: []*cynosdb.CynosdbInstance{
{
ClusterId: helper.String("cynosdbmysql-12345678"),
InstanceId: helper.String("cynosdbmysql-ins-abcdefgh"),
InstanceName: helper.String("tf-cynosdb-readonly-instance-new"),
Status: helper.String("running"),
Cpu: helper.IntInt64(2),
Memory: helper.IntInt64(4),
VpcId: helper.String("vpc-m0d2dbnn"),
SubnetId: helper.String("subnet-j10lsueq"),
},
},
RequestId: helper.String("fake-request-id"),
}
return resp, nil
})

// mock DescribeInstanceDetail used by DescribeInstanceById in Read
patches.ApplyMethodFunc(cynosdbClient, "DescribeInstanceDetail", func(request *cynosdb.DescribeInstanceDetailRequest) (*cynosdb.DescribeInstanceDetailResponse, error) {
resp := &cynosdb.DescribeInstanceDetailResponse{}
resp.Response = &cynosdb.DescribeInstanceDetailResponseParams{
Detail: &cynosdb.CynosdbInstanceDetail{
ClusterId: helper.String("cynosdbmysql-12345678"),
InstanceId: helper.String("cynosdbmysql-ins-abcdefgh"),
InstanceName: helper.String("tf-cynosdb-readonly-instance-new"),
Status: helper.String("running"),
Cpu: helper.IntInt64(2),
Memory: helper.IntInt64(4),
Storage: helper.IntInt64(50),
VpcId: helper.String("vpc-m0d2dbnn"),
SubnetId: helper.String("subnet-j10lsueq"),
},
RequestId: helper.String("fake-request-id"),
}
return resp, nil
})

// mock DescribeMaintainPeriod used by Read
patches.ApplyMethodFunc(cynosdbClient, "DescribeMaintainPeriod", func(request *cynosdb.DescribeMaintainPeriodRequest) (*cynosdb.DescribeMaintainPeriodResponse, error) {
resp := &cynosdb.DescribeMaintainPeriodResponse{}
resp.Response = &cynosdb.DescribeMaintainPeriodResponseParams{
MaintainWeekDays: []*string{helper.String("Mon"), helper.String("Tue")},
MaintainStartTime: helper.IntInt64(10800),
MaintainDuration: helper.IntInt64(3600),
RequestId: helper.String("fake-request-id"),
}
return resp, nil
})

res := svccynosdb.ResourceTencentCloudCynosdbReadonlyInstance()

// Build a prior state where instance_name is the old value, and a new config
// where only instance_name changes, so that d.HasChange("instance_name")
// returns true while other fields remain unchanged.
state := &terraform.InstanceState{
ID: "cynosdbmysql-ins-abcdefgh",
Attributes: map[string]string{
"id": "cynosdbmysql-ins-abcdefgh",
"cluster_id": "cynosdbmysql-12345678",
"instance_name": "tf-cynosdb-readonly-instance-old",
"instance_cpu_core": "2",
"instance_memory_size": "4",
"instance_maintain_duration": "3600",
"instance_maintain_start_time": "10800",
},
}

rawConfig := terraform.NewResourceConfigRaw(map[string]interface{}{
"cluster_id": "cynosdbmysql-12345678",
"instance_name": "tf-cynosdb-readonly-instance-new",
"instance_cpu_core": 2,
"instance_memory_size": 4,
"instance_maintain_duration": 3600,
"instance_maintain_start_time": 10800,
})

diff, err := res.Diff(nil, state, rawConfig, meta)
assert.NoError(t, err)
assert.NotNil(t, diff)

d, err := schema.InternalMap(res.Schema).Data(state, diff)
assert.NoError(t, err)

err = res.Update(d, meta)
assert.NoError(t, err)
assert.True(t, modifyCalled)
assert.Equal(t, "tf-cynosdb-readonly-instance-new", d.Get("instance_name"))
}
Loading
Loading