Skip to content

Commit ca55b47

Browse files
committed
feat(teo): replace teo_bind_security_template read api and add unit tests
1 parent 11593ca commit ca55b47

11 files changed

Lines changed: 646 additions & 33 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-07-20
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# replace-teo-bind-security-template-read-api
2+
3+
Replace DescribeSecurityTemplateBindings with DescribeZones + DescribeWebSecurityTemplates for teo_bind_security_template read
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
## Context
2+
3+
The `tencentcloud_teo_bind_security_template` resource reads its state through `TeoService.DescribeTeoBindSecurityTemplateById`, which historically called `DescribeSecurityTemplateBindings(zoneId, templateId)` and scanned the returned `SecurityTemplate[0].TemplateScope[0].EntityStatus` for the matching `entity`. The binding lookup is unreliable, and the provider already has a supported alternative — `DescribeWebSecurityTemplates(zoneIds)` — which returns `SecurityPolicyTemplateInfo` objects including `BindDomains` (each with `Domain`, `ZoneId`, `Status`). This change swaps the read-path API without altering the resource schema or Terraform-facing behavior.
4+
5+
The existing codebase already contains a paged `DescribeTeoZonesByFilter` helper, but it returns `[]*Zone` and uses `UseTeoClient()`. To keep the new read path self-contained and consistent with the v20220901 client used by the rest of `DescribeTeoBindSecurityTemplateById`, a dedicated `describeTeoAllZoneIds` helper using `UseTeoV20220901Client().DescribeZones` is introduced.
6+
7+
## Goals / Non-Goals
8+
9+
**Goals:**
10+
- Replace `DescribeSecurityTemplateBindings` usage with `DescribeZones` + `DescribeWebSecurityTemplates` in the read path.
11+
- Respect the `DescribeWebSecurityTemplates` constraint of at most 100 zone IDs per request by batching.
12+
- Preserve the resource's user-facing schema, id format (`zoneId#templateId#entity`), and lifecycle (create/read/delete only).
13+
- Keep the create state-refresh polling safe against a nil `Status`.
14+
- Preserve the resource id in logs before clearing state on not-found.
15+
16+
**Non-Goals:**
17+
- Changing the resource schema or adding an update operation.
18+
- Migrating other TEO resources off `DescribeSecurityTemplateBindings`.
19+
- Reusing `DescribeTeoZonesByFilter` (different client wrapper / return type); a focused helper is added instead to avoid cross-coupling.
20+
21+
## Decisions
22+
23+
**Decision 1: Fetch all zone IDs via `DescribeZones`, then batch `DescribeWebSecurityTemplates`.**
24+
- Rationale: `DescribeWebSecurityTemplates` requires zone IDs as input (unlike `DescribeSecurityTemplateBindings` which took a template ID directly). The binding we need is identified by `templateId` + `entity`, and the zone is already part of the resource's composite id — but the template may be returned under any zone the account owns, so we enumerate all zones to be safe.
25+
- Alternative considered: Pass only the resource's own `zoneId` to `DescribeWebSecurityTemplates`. Rejected because the original implementation queried by template ID globally; limiting to a single zone could miss bindings reported under a different zone and cause spurious not-found / state drift.
26+
27+
**Decision 2: Batch size of 100 for `DescribeWebSecurityTemplates`.**
28+
- Rationale: The SDK model doc states "单次查询最多传入 100 个站点" (at most 100 zone IDs per request). A constant `batchSize = 100` enforces this.
29+
- Alternative considered: Smaller batch (e.g., 50). Rejected — would increase request count without benefit.
30+
31+
**Decision 3: `DescribeZones` paging uses `Limit=100` (documented maximum).**
32+
- Rationale: Maximizes pages-per-request efficiency; matches the existing `DescribeTeoZonesByFilter` convention.
33+
34+
**Decision 4: Return a synthesized `EntityStatus{Entity, Status}` instead of reusing the API's `EntityStatus` directly.**
35+
- Rationale: `DescribeWebSecurityTemplates` returns `BindDomainInfo{Domain, ZoneId, Status}`, not `EntityStatus`. To keep the resource Read / state-refresh code unchanged (it expects `*EntityStatus` with `Entity` and `Status`), the helper constructs an `EntityStatus` from the matched `BindDomainInfo`.
36+
37+
**Decision 5: Drop the `DescribeSecurityTemplateBindingsRequest` field from the extension state-refresh function.**
38+
- Rationale: The field was unused after the API swap and referenced a request type no longer constructed. Removing it keeps the extension clean and avoids dead code.
39+
40+
**Decision 6: Guard nil `Status` in the state-refresh function.**
41+
- Rationale: During create polling the freshly-created binding may briefly return a non-nil `EntityStatus` with a nil `Status`. The original code dereferenced `*resp.Status` and could panic. Returning `(resp, "", nil)` keeps the refresh in a pending state until a concrete status arrives.
42+
43+
## Risks / Trade-offs
44+
45+
- [More API calls per read] `DescribeZones` paging + multiple `DescribeWebSecurityTemplates` batches replace a single `DescribeSecurityTemplateBindings` call. → Mitigated: batching caps request count at `ceil(zoneCount/100) + ceil(zoneCount/100)`; accounts rarely have hundreds of zones, and each call is retried with `ReadRetryTimeout`. The trade-off is acceptable for correctness.
46+
- [Read latency increase for large zone counts] → Mitigated: each batch is independent and short; retry is per-batch so a transient failure does not restart all batches.
47+
- [Behavioral parity] The synthesized `EntityStatus.Status` comes from `BindDomainInfo.Status`, whose documented values (`process`/`online`/`fail`) match the original `EntityStatus.Status` values. → No state-machine change for the create `Target: ["online"]`.
48+
- [No migration needed] State id format and schema are unchanged; existing Terraform states remain valid.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## Why
2+
3+
The `tencentcloud_teo_bind_security_template` resource currently relies on the `DescribeSecurityTemplateBindings` API to read binding state. This API is being phased out / does not reliably return the binding status for all scenarios, causing read failures and state drift. The `DescribeWebSecurityTemplates` API provides the same binding information (template-to-domain associations with delivery status) and is the recommended replacement. Switching the read path to `DescribeZones` + `DescribeWebSecurityTemplates` keeps the resource's user-facing behavior unchanged while using a supported, more reliable API.
4+
5+
## What Changes
6+
7+
- Replace the `DescribeTeoBindSecurityTemplateById` implementation in `tencentcloud/services/teo/service_tencentcloud_teo.go`:
8+
- Remove the `DescribeSecurityTemplateBindings` API call.
9+
- Add a new helper `describeTeoAllZoneIds` that pages through `DescribeZones` (Limit=100) to collect all zone IDs under the account.
10+
- Call `DescribeWebSecurityTemplates` in batches of at most 100 zone IDs per request (the API's documented upper limit), filter the returned `SecurityPolicyTemplates` by `TemplateId` and the bound `BindDomains.Domain` (entity), and return the matching `EntityStatus` (entity + status).
11+
- Update `resource_tc_teo_bind_security_template_extension.go` state refresh function to drop the now-unused `DescribeSecurityTemplateBindingsRequest` reference and to return an empty state string when `resp.Status` is nil (avoids nil dereference during create polling).
12+
- Update `resource_tc_teo_bind_security_template.go` read method to print the resource id (`[CRUD] teo_bind_security_template id=%s`) before clearing the id when the binding is not found, preserving the id for log diagnostics.
13+
- Add gomonkey-based unit tests in `resource_tc_teo_bind_security_template_test.go` covering: read success, read not-found (id cleared), read with no zones, read with >100 zones (batching verified), and schema validation.
14+
- Update `.changelog/4261.txt` to reflect the behavior-preserving API replacement (bug-fix / enhancement to read reliability).
15+
16+
## Capabilities
17+
18+
### New Capabilities
19+
- `teo-bind-security-template-read-api`: Read path for the `tencentcloud_teo_bind_security_template` resource, using `DescribeZones` + `DescribeWebSecurityTemplates` to locate a template-to-domain binding and return its delivery status.
20+
21+
### Modified Capabilities
22+
<!-- None. The user-facing schema and lifecycle of the resource are unchanged. -->
23+
24+
## Impact
25+
26+
- Affected code:
27+
- `tencentcloud/services/teo/service_tencentcloud_teo.go` (`DescribeTeoBindSecurityTemplateById`, new `describeTeoAllZoneIds`)
28+
- `tencentcloud/services/teo/resource_tc_teo_bind_security_template.go` (read log ordering)
29+
- `tencentcloud/services/teo/resource_tc_teo_bind_security_template_extension.go` (state refresh cleanup)
30+
- `tencentcloud/services/teo/resource_tc_teo_bind_security_template_test.go` (new unit tests)
31+
- Cloud APIs: drops usage of `DescribeSecurityTemplateBindings`; uses `DescribeZones` and `DescribeWebSecurityTemplates` (both already used elsewhere in the provider).
32+
- Backward compatible: no schema changes, no state migration, no breaking changes to Terraform configurations.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Read locates binding via DescribeZones and DescribeWebSecurityTemplates
4+
The `DescribeTeoBindSecurityTemplateById` service function SHALL locate a template-to-domain binding by first paging through the `DescribeZones` API (with `Limit` set to the documented maximum of 100) to collect all zone IDs under the account, then calling `DescribeWebSecurityTemplates` in batches of at most 100 zone IDs per request. It SHALL NOT use the `DescribeSecurityTemplateBindings` API.
5+
6+
#### Scenario: Binding found in the first batch
7+
- **WHEN** the account has fewer than 100 zones and one of the returned `SecurityPolicyTemplates` matches the requested `templateId` and contains a `BindDomains` entry whose `Domain` equals the requested `entity`
8+
- **THEN** the function SHALL return an `EntityStatus` with `Entity` set to the requested entity and `Status` set to the matched `BindDomainInfo.Status`
9+
10+
#### Scenario: Binding found in a later batch
11+
- **WHEN** the account has more than 100 zones and the matching binding is only returned for a zone ID in the second (or later) `DescribeWebSecurityTemplates` batch
12+
- **THEN** the function SHALL issue at least two `DescribeWebSecurityTemplates` requests and return the matching `EntityStatus`
13+
14+
#### Scenario: API call retry on failure
15+
- **WHEN** a `DescribeZones` or `DescribeWebSecurityTemplates` API call fails with a retryable error
16+
- **THEN** the function SHALL retry the call with `tccommon.ReadRetryTimeout` duration using `resource.Retry`, and wrap failures with `tccommon.RetryError`
17+
18+
### Requirement: Read returns nil when no binding matches
19+
When no `SecurityPolicyTemplateInfo` matches the requested `templateId`, or the matched template has no `BindDomains` entry whose `Domain` equals the requested `entity`, the function SHALL return a nil `EntityStatus` (and nil error) so the resource Read method clears the Terraform state id.
20+
21+
#### Scenario: No zone available
22+
- **WHEN** `DescribeZones` returns no zones
23+
- **THEN** the function SHALL log `[DEBUG] ... no zone found when reading teo bind_security_template` and return nil without calling `DescribeWebSecurityTemplates`
24+
25+
#### Scenario: Template or entity not found
26+
- **WHEN** the returned `SecurityPolicyTemplates` do not contain the requested `templateId` or none of the `BindDomains` matches the `entity`
27+
- **THEN** the function SHALL return nil without error
28+
29+
### Requirement: Resource Read preserves id in logs before clearing state
30+
The `resourceTencentCloudTeoBindSecurityTemplateRead` function SHALL, when the service lookup returns nil (binding not found), print a `[CRUD] teo_bind_security_template id=%s` log line containing `d.Id()` BEFORE calling `d.SetId("")`, so the cleared id remains traceable in logs.
31+
32+
#### Scenario: Binding not found during read
33+
- **WHEN** the service function returns a nil `EntityStatus`
34+
- **THEN** the read function SHALL log the current id, then clear the id, then log a warning that the resource was not found
35+
36+
### Requirement: Create state refresh tolerates nil status
37+
The `resourceTeoBindSecurityTemplateCreateStateRefreshFunc_0_0` state refresh function SHALL return the response object with an empty state string when `resp.Status` is nil, instead of dereferencing a nil pointer.
38+
39+
#### Scenario: Status nil during create polling
40+
- **WHEN** the service function returns a non-nil `EntityStatus` whose `Status` field is nil
41+
- **THEN** the state refresh function SHALL return `(resp, "", nil)` without error
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## 1. Service layer
2+
3+
- [x] 1.1 Replace `DescribeTeoBindSecurityTemplateById` in `tencentcloud/services/teo/service_tencentcloud_teo.go` to drop `DescribeSecurityTemplateBindings` and use `DescribeZones` + `DescribeWebSecurityTemplates`
4+
- [x] 1.2 Add `describeTeoAllZoneIds` helper paging `DescribeZones` with `Limit=100` via `UseTeoV20220901Client()`
5+
- [x] 1.3 Batch `DescribeWebSecurityTemplates` by at most 100 zone IDs per request, filter by `TemplateId` and `BindDomains.Domain`, return synthesized `EntityStatus{Entity, Status}`
6+
- [x] 1.4 Wrap `DescribeZones` / `DescribeWebSecurityTemplates` calls with `resource.Retry(tccommon.ReadRetryTimeout, ...)` using `tccommon.RetryError`
7+
8+
## 2. Resource and extension code
9+
10+
- [x] 2.1 Update `resourceTencentCloudTeoBindSecurityTemplateRead` to log `[CRUD] teo_bind_security_template id=%s` with `d.Id()` before `d.SetId("")`
11+
- [x] 2.2 Clean up `resourceTeoBindSecurityTemplateCreateStateRefreshFunc_0_0` in `resource_tc_teo_bind_security_template_extension.go`: remove unused `DescribeSecurityTemplateBindingsRequest` field and guard nil `resp.Status`
12+
13+
## 3. Tests
14+
15+
- [x] 3.1 Add gomonkey-based unit tests in `resource_tc_teo_bind_security_template_test.go` for: read success, read not-found, read no-zone, read >100 zones (batching), and schema validation
16+
- [x] 3.2 Run `go test ./tencentcloud/services/teo/ -run "TestTeoBindSecurityTemplate_" -v -count=1 -gcflags="all=-l"` and confirm all pass
17+
18+
## 4. Docs and changelog
19+
20+
- [x] 4.1 Verify `resource_tc_teo_bind_security_template.md` example/import content is consistent (no schema change; no edit required beyond the existing docs commit)
21+
- [ ] 4.2 Update `.changelog/4261.txt` (or add a new changelog entry in the finalize phase) to describe the read-path API replacement as a bug-fix/enhancement
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## Requirements
2+
3+
### Requirement: Read locates binding via DescribeZones and DescribeWebSecurityTemplates
4+
The `DescribeTeoBindSecurityTemplateById` service function SHALL locate a template-to-domain binding by first paging through the `DescribeZones` API (with `Limit` set to the documented maximum of 100) to collect all zone IDs under the account, then calling `DescribeWebSecurityTemplates` in batches of at most 100 zone IDs per request. It SHALL NOT use the `DescribeSecurityTemplateBindings` API.
5+
6+
#### Scenario: Binding found in the first batch
7+
- **WHEN** the account has fewer than 100 zones and one of the returned `SecurityPolicyTemplates` matches the requested `templateId` and contains a `BindDomains` entry whose `Domain` equals the requested `entity`
8+
- **THEN** the function SHALL return an `EntityStatus` with `Entity` set to the requested entity and `Status` set to the matched `BindDomainInfo.Status`
9+
10+
#### Scenario: Binding found in a later batch
11+
- **WHEN** the account has more than 100 zones and the matching binding is only returned for a zone ID in the second (or later) `DescribeWebSecurityTemplates` batch
12+
- **THEN** the function SHALL issue at least two `DescribeWebSecurityTemplates` requests and return the matching `EntityStatus`
13+
14+
#### Scenario: API call retry on failure
15+
- **WHEN** a `DescribeZones` or `DescribeWebSecurityTemplates` API call fails with a retryable error
16+
- **THEN** the function SHALL retry the call with `tccommon.ReadRetryTimeout` duration using `resource.Retry`, and wrap failures with `tccommon.RetryError`
17+
18+
### Requirement: Read returns nil when no binding matches
19+
When no `SecurityPolicyTemplateInfo` matches the requested `templateId`, or the matched template has no `BindDomains` entry whose `Domain` equals the requested `entity`, the function SHALL return a nil `EntityStatus` (and nil error) so the resource Read method clears the Terraform state id.
20+
21+
#### Scenario: No zone available
22+
- **WHEN** `DescribeZones` returns no zones
23+
- **THEN** the function SHALL log `[DEBUG] ... no zone found when reading teo bind_security_template` and return nil without calling `DescribeWebSecurityTemplates`
24+
25+
#### Scenario: Template or entity not found
26+
- **WHEN** the returned `SecurityPolicyTemplates` do not contain the requested `templateId` or none of the `BindDomains` matches the `entity`
27+
- **THEN** the function SHALL return nil without error
28+
29+
### Requirement: Resource Read preserves id in logs before clearing state
30+
The `resourceTencentCloudTeoBindSecurityTemplateRead` function SHALL, when the service lookup returns nil (binding not found), print a `[CRUD] teo_bind_security_template id=%s` log line containing `d.Id()` BEFORE calling `d.SetId("")`, so the cleared id remains traceable in logs.
31+
32+
#### Scenario: Binding not found during read
33+
- **WHEN** the service function returns a nil `EntityStatus`
34+
- **THEN** the read function SHALL log the current id, then clear the id, then log a warning that the resource was not found
35+
36+
### Requirement: Create state refresh tolerates nil status
37+
The `resourceTeoBindSecurityTemplateCreateStateRefreshFunc_0_0` state refresh function SHALL return the response object with an empty state string when `resp.Status` is nil, instead of dereferencing a nil pointer.
38+
39+
#### Scenario: Status nil during create polling
40+
- **WHEN** the service function returns a non-nil `EntityStatus` whose `Status` field is nil
41+
- **THEN** the state refresh function SHALL return `(resp, "", nil)` without error

tencentcloud/services/teo/resource_tc_teo_bind_security_template.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,9 @@ func resourceTencentCloudTeoBindSecurityTemplateRead(d *schema.ResourceData, met
170170
}
171171

172172
if respData == nil {
173+
log.Printf("[CRUD] teo_bind_security_template id=%s", d.Id())
173174
d.SetId("")
174-
log.Printf("[WARN]%s resource `teo_bind_security_template` [%s] not found, please check if it has been deleted.\n", logId, d.Id())
175+
log.Printf("[WARN]%s resource `teo_bind_security_template` not found, please check if it has been deleted.\n", logId)
175176
return nil
176177
}
177178

tencentcloud/services/teo/resource_tc_teo_bind_security_template_extension.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,27 @@ import (
55
"fmt"
66

77
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8-
teov20220901 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
98
tccommon "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/common"
109
)
1110

1211
func resourceTeoBindSecurityTemplateCreateStateRefreshFunc_0_0(ctx context.Context, zoneId string, templateId string, entity string) resource.StateRefreshFunc {
13-
var req *teov20220901.DescribeSecurityTemplateBindingsRequest
1412
return func() (interface{}, string, error) {
1513
meta := tccommon.ProviderMetaFromContext(ctx)
16-
17-
service := TeoService{client: meta.(tccommon.ProviderMeta).GetAPIV3Conn()}
1814
if meta == nil {
1915
return nil, "", fmt.Errorf("resource data can not be nil")
2016
}
21-
if req == nil {
22-
d := tccommon.ResourceDataFromContext(ctx)
23-
if d == nil {
24-
return nil, "", fmt.Errorf("resource data can not be nil")
25-
}
26-
_ = d
27-
req = teov20220901.NewDescribeSecurityTemplateBindingsRequest()
28-
}
17+
18+
service := TeoService{client: meta.(tccommon.ProviderMeta).GetAPIV3Conn()}
2919
resp, err := service.DescribeTeoBindSecurityTemplateById(ctx, zoneId, templateId, entity)
3020
if err != nil {
3121
return nil, "", err
3222
}
3323
if resp == nil {
3424
return nil, "", nil
3525
}
26+
if resp.Status == nil {
27+
return resp, "", nil
28+
}
3629
return resp, *resp.Status, nil
3730
}
3831
}

0 commit comments

Comments
 (0)