Skip to content

[Feature] Terrafron Provider for Open Cloud Datacenter API - #205

Open
sameera-87 wants to merge 2 commits into
wso2:terraform-provider-dcapifrom
sameera-87:terraform-provider-dcapi
Open

[Feature] Terrafron Provider for Open Cloud Datacenter API#205
sameera-87 wants to merge 2 commits into
wso2:terraform-provider-dcapifrom
sameera-87:terraform-provider-dcapi

Conversation

@sameera-87

@sameera-87 sameera-87 commented Jun 30, 2026

Copy link
Copy Markdown

What this changes

There was no Terraform provider for DC-API — provisioning was imperative, via dcctl and direct REST calls, with no plan step, no dependency ordering, and no drift detection. This adds a provider (registry.terraform.io/wso2/dcapi) that manages DC-API resources declaratively through Terraform's plan/apply lifecycle. Design and scope were agreed in #200.

Behaviour

  • Implemented set (20 resources, 12 data sources): the Tenant → Project → VNet → Subnet → VirtualMachine hierarchy, plus Cluster/NodePool, RouteTable/RouteTableAssociation, NetworkSecurityGroup/NSGAttachment, VNetPeering, Bastion, ServiceAccount, KeyVault/KeyVaultSecret, PrivateDnsZone/DnsRecord, PrivateEndpoint, and TenantMember — all support full CRUD plus import. Read-only data sources additionally cover Region and Image lookups.
  • Async lifecycle (202 + poll): create polls via resource.StateChangeConf until the resource reaches a known ready state; only known terminal-failure statuses error, unknown/intermediate statuses keep polling. d.SetId() is set only after create is confirmed, so a failed poll produces a clean failure rather than a tainted resource in state.
  • Deletes: poll until the API returns 404, treating 404 as success (already gone). A 409 on VNet/Project delete while children still exist is surfaced as an actionable error rather than a silent retry.
  • Credential resolution: endpoint/token resolve from the provider block, falling back to DCAPI_ENDPOINT/DCAPI_TOKEN env vars; missing either fails configuration with an explicit diagnostic. The provider never becomes a store of long-lived secrets.
  • State and dependencies: parent IDs are persisted into state (composite path IDs, e.g. tenant/project/vnet_uuid/subnet_uuid) so attribute references resolve and Terraform Core can infer create/destroy ordering. Sensitive outputs (private_key, console_password) are marked sensitive.
  • Hardening in this pass: nil-response guards added on VM/VNet/Subnet create responses so a malformed 202 body fails cleanly instead of panicking; node pool path segments are now url.PathEscaped.
  • Additive: no change to the DC-API server or dcctl.

Surface

  • provider: source registry.terraform.io/wso2/dcapi; resources named dcapi_<resource> (e.g. dcapi_vnet, dcapi_virtual_machine).
  • config: required_providers block, provider configuration with optional explicit endpoint/token (both default to env vars).
  • docs/examples: resource docs (docs/dc-api-reference.md) and a runnable example per resource under examples/<resource>/main.tf, covering the full tenant → project → vnet → subnet → vm flow plus each standalone resource type.
  • build: Go toolchain bumped to 1.25; dependency versions refreshed (golang.org/x/*, google.golang.org/grpc, etc.).

Verification

go build ./... and go vet ./... pass. Manual exercise via make install against a local DC-API server (make run), covering create/read/update/delete/import for the core hierarchy.

No automated test suite exists in this repo yet — there are currently no _test.go files. Unit tests (httptest-backed fakes of DC-API for DTO decode / CRUD handler logic) and acceptance tests are still outstanding and should land as a follow-up before this is relied on beyond manual verification.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a complete Terraform provider for the DC-API, including Go module/build setup, HTTP client methods, Terraform resources and data sources, provider wiring, asynchronous lifecycle handling, examples, tests, and extensive architecture and API documentation.

Changes

DC-API Terraform Provider

Layer / File(s) Summary
Provider bootstrap and HTTP client
go.mod, GNUmakefile, main.go, internal/client/client.go, internal/provider/provider.go
Adds module/build configuration, plugin startup, provider authentication settings, resource/data-source registration, and shared HTTP request/error handling.
DC-API client operations
internal/client/*.go
Adds typed request/response models and client methods for compute, networking, identity, DNS, Key Vault, secrets, images, and regions.
Terraform resources
internal/resources/*.go
Adds CRUD implementations with composite state IDs, async polling, drift handling, full-replacement updates, dependency-aware deletion errors, and shown-once sensitive outputs.
Terraform data sources
internal/datasources/*.go
Adds read-only lookup data sources using collection listing and name or identifier matching.
Examples and validation configurations
examples/*, test/main.tf
Adds provider usage examples for resources and data sources, including chained infrastructure and sensitive outputs.
Architecture and API documentation
Architecture.md, docs/*
Documents provider wiring, API endpoints and schemas, lifecycle states, state ID formats, implementation guidance, OpenAPI definitions, and Terraform protocol conversions.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is informative but does not follow the required template sections Summary, Changes, Testing, and Checklist. Rewrite the description using the repo template with Summary, Changes, Testing, and Checklist sections, and include the missing checklist items.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and correctly points to the new DC-API Terraform provider, despite a typo.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (6)
docs/dc-api-reference.md-27-27 (1)

27-27: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the broken PrivateDnsZone table-of-contents link.

Line 27 points to #resource-privatednszoned, so Markdown renderers will not jump to the PrivateDnsZone section.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/dc-api-reference.md` at line 27, The table-of-contents entry for
PrivateDnsZone uses a broken anchor, so update the markdown link target to match
the actual section heading slug generated for PrivateDnsZone. Fix the TOC entry
itself rather than the section, and verify the anchor resolves correctly in the
docs renderer.

Source: Linters/SAST tools

examples/vm/main.tf-82-85 (1)

82-85: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use subnet_uuid here as well.

This output is documented as the subnet UUID, but it currently returns dcapi_subnet.app.id. The same file already uses dcapi_subnet.app.subnet_uuid on Line 60 for the actual API identifier, so this output will otherwise expose the wrong value to downstream consumers.

Suggested fix
 output "subnet_id" {
-  value       = dcapi_subnet.app.id
+  value       = dcapi_subnet.app.subnet_uuid
   description = "UUID of the created Subnet."
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vm/main.tf` around lines 82 - 85, The subnet output is returning the
Terraform resource ID instead of the API UUID, so update the output block named
subnet_id in main.tf to use dcapi_subnet.app.subnet_uuid just like the existing
subnet_uuid reference in the same configuration. Keep the description aligned
with the value being exported so downstream consumers receive the actual subnet
UUID rather than the provider-generated id.
examples/subnet/main.tf-49-52 (1)

49-52: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Output the subnet UUID instead of Terraform's state ID.

This block says it returns the created subnet UUID, but dcapi_subnet.app.id is the Terraform resource ID. The provider already exposes the API UUID separately (subnet_uuid), and the VM example consumes that field for downstream wiring. Returning .id here will hand callers the wrong identifier shape.

Suggested fix
 output "subnet_id" {
-  value       = dcapi_subnet.app.id
+  value       = dcapi_subnet.app.subnet_uuid
   description = "UUID of the created Subnet."
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/subnet/main.tf` around lines 49 - 52, The subnet output is currently
returning Terraform’s resource ID instead of the API UUID. Update the `output
"subnet_id"` block in the subnet example to use the
`dcapi_subnet.app.subnet_uuid` attribute, matching the provider’s UUID field and
the way the VM example wires subnet values downstream. Keep the output name and
description as-is, but ensure the value references the UUID-exposing symbol on
`dcapi_subnet.app`.
internal/resources/vnet.go-253-260 (1)

253-260: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The 409 guidance points users at the wrong attribute.

dcapi_subnet.vnet_id expects the bare UUID, but this message tells users to reference dcapi_vnet.example.id, which is the composite tenant/project/uuid state ID. Following that advice will build an invalid subnet path; the example should use dcapi_vnet.example.vnet_uuid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/vnet.go` around lines 253 - 260, The 409 delete guidance
in the VNet error path is pointing users to the wrong reference attribute.
Update the message in the VNet deletion logic (the strings.Contains(err.Error(),
"HTTP 409") branch) so the example Terraform reference uses
dcapi_vnet.example.vnet_uuid instead of dcapi_vnet.example.id, since
dcapi_subnet.vnet_id requires the bare UUID and not the composite state ID.
internal/resources/tenant.go-14-19 (1)

14-19: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add Importer wiring for these resources. schema.Resource doesn’t set Importer here, so terraform import won’t work for dcapi_tenant or the other new resources in internal/resources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/tenant.go` around lines 14 - 19, The tenant resource is
missing import support, so terraform import cannot work for this and the other
new resources. Update ResourceTenant and the corresponding Resource constructors
in internal/resources to wire schema.Resource.Importer, using the appropriate
StateContext importer helper already used by other resources. Make sure the
import setup is added alongside CreateContext, ReadContext, UpdateContext, and
DeleteContext so dcapi_tenant and related resources can be imported
consistently.
internal/resources/node_pool.go-276-281 (1)

276-281: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat HTTP 404 as a successful delete.

internal/client/node_pool.go:103-111 bubbles any DELETE error, and Lines 276-281 return immediately on that error before the poller runs. If the pool was already removed out of band, destroy fails instead of converging on the desired absent state. Normalize HTTP 404 to success in the delete path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/node_pool.go` around lines 276 - 281, Treat HTTP 404 as a
successful delete in the node pool destroy flow. Update the DeleteNodePool path
used by node pool deletion so a not-found response is normalized to nil instead
of bubbling an error, and make the delete handler in
internal/client/node_pool.go return success when the pool is already gone. Keep
the existing waitForNodePoolDeleted poller behavior in
internal/resources/node_pool.go so the resource still converges on absent state
when DeleteNodePool is called.
🧹 Nitpick comments (4)
docs/dc-api-reference.md (1)

419-429: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Mark the VM and Cluster networking selectors as user inputs, not computed fields.

network_name, vnet_id, and subnet_id appear in the create request bodies and must be set by configuration for the chosen networking mode. Labeling them COMPUTED_OPTIONAL flips that contract and makes these tables inconsistent with the examples above.

Also applies to: 661-663

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/dc-api-reference.md` around lines 419 - 429, The field classification
table is labeling VM and Cluster networking selectors as computed, but
`network_name`, `vnet_id`, and `subnet_id` are user-provided inputs in the
create request bodies. Update the Field Classification entries in the relevant
documentation sections so these selectors are marked as user inputs for their
respective networking modes, keeping the wording consistent with the request
examples and the related Cluster table.
Architecture.md (1)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update this architecture doc to match the resource set shipped in 0.1.0.

These sections still describe only the tenant/project/vnet/subnet/vm slice, but this PR also ships Bastion, Cluster, NodePool, RouteTable, RouteTableAssociation, NetworkSecurityGroup, NSGAttachment, VNetPeering, and ServiceAccount. The resource hierarchy is also incomplete for VPC-backed workloads. That makes the architecture doc stale on merge day.

Also applies to: 11-28, 30-46, 370-399

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Architecture.md` at line 3, Update the architecture documentation to reflect
the full 0.1.0 resource set instead of only the tenant/project/vnet/subnet/vm
slice. In the Architecture.md sections currently describing the provider scope
and resource hierarchy, add Bastion, Cluster, NodePool, RouteTable,
RouteTableAssociation, NetworkSecurityGroup, NSGAttachment, VNetPeering, and
ServiceAccount, and expand the VPC-backed workload hierarchy accordingly. Use
the existing architecture headings and resource overview areas in
Architecture.md so the doc stays aligned with the shipped provider surface.
internal/client/project.go (1)

87-92: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Stop classifying 404s by parsing error strings.

doRequest only returns formatted errors today, so the read paths are forced to inspect message text. Add a typed/status-bearing error and branch on that instead of strings.Contains(err.Error(), "HTTP 404"); otherwise a wording change or unrelated wrapped error can misclassify drift. The same pattern exists in internal/client/vnet.go, internal/client/subnet.go, internal/client/service_account.go, internal/client/vm.go, internal/client/nsg.go, internal/client/route_table.go, internal/client/node_pool.go, internal/client/cluster.go, and internal/client/bastion.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/project.go` around lines 87 - 92, The 404 handling in
GetProjectByID is still parsing err.Error() text, which should be replaced with
a typed/status-bearing error from doRequest and checked directly in the read
path. Update doRequest to return an error that exposes the HTTP status, then
change GetProjectByID to branch on that status instead of
strings.Contains(err.Error(), "HTTP 404"), preserving the nil,nil behavior for
not-found. Apply the same pattern to the other client read methods that
currently inspect error strings, including the ones in vnet, subnet,
service_account, vm, nsg, route_table, node_pool, cluster, and bastion.
internal/client/vm.go (1)

91-96: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use a status-based helper for 404 handling. GetVM and the other getters in internal/client all depend on doRequest emitting "HTTP 404" in its error text. A small shared helper in internal/client/client.go would make the 404→nil contract explicit and avoid breaking drift detection if the formatter changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/vm.go` around lines 91 - 96, The 404 handling in GetVM
currently depends on matching the text from doRequest, which is brittle across
getters in internal/client. Add a shared status-based helper in
internal/client/client.go that detects a 404 from the request error or status
and returns the nil/not-found result explicitly, then update GetVM and the other
getter methods to use that helper instead of strings.Contains on err.Error().
Keep the existing nil-on-404 contract and preserve the existing wrapping for
non-404 errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/dc-api-reference.md`:
- Line 66: The auth example in docs/dc-api-reference.md still shows unsupported
provider arguments (`tenant_id` and `project_id`) even though the provider
examples only take `endpoint` and `token`; update the example to use the same
supported auth fields as the rest of the docs. Also tighten the credential
guidance in the Terraform section by removing the “member” minimum for tenant
operations and reflecting that tenant actions are admin-only, using the relevant
provider example blocks and tenant auth text as the source of truth.

In `@GNUmakefile`:
- Around line 4-12: The install target currently copies the provider as
terraform-provider-dcapi, but Terraform discovers plugins by the versioned
binary name; update the GNUmakefile install flow so the copy step in install
uses the Terraform versioned filename for the built artifact, while keeping
BINARY and INSTALL_DIR aligned with the provider version. Use the existing build
and install targets to locate the change and ensure the installed filename
includes v0.1.0.

In `@go.mod`:
- Around line 70-78: The dependency graph still contains vulnerable pinned
versions in go.mod, so refresh the affected modules before release. Update the
transitive dependencies pulled in through terraform-plugin-sdk/v2 that bring in
golang.org/x/crypto and google.golang.org/grpc, then run go mod tidy to
regenerate the module graph and lock in the upgraded versions.

In `@internal/client/client.go`:
- Around line 6-14: The shared HTTP client used by doRequest currently relies on
the default client with no timeout, which can leave plan/apply blocked on hung
connections. Update the client initialization in client.go to use a bounded
timeout, or accept a timeout from provider configuration and thread it into the
client used by doRequest. Keep the change centered around the shared HTTP client
path and the request flow that all resource calls use.

In `@internal/client/node_pool.go`:
- Around line 28-35: The NodePoolUpdateRequest shape in node_pool.go cannot use
nil slices/maps to mean “omit” because encoding/json will emit null for Taints
and Labels, which can unintentionally clear server state on count-only updates.
Update the request model and its PATCH serialization path so omitted fields are
truly excluded from the JSON payload, or add custom marshaling/pointer fields in
NodePoolUpdateRequest to preserve the intended omit-vs-clear behavior for Taints
and Labels.
- Around line 72-73: The node-pool request path is interpolating poolName
directly in DCAPIClient.GetNodePool, which can break GET/PATCH/DELETE when the
name contains reserved characters. Update the node-pool URL construction to
escape the poolName segment before formatting the path, and apply the same fix
anywhere the node-pool path is built so the full name remains intact even
without a ValidateFunc on name.

In `@internal/client/subnet.go`:
- Around line 51-56: CreateSubnet currently returns wrapper.Resource without
verifying it was actually present, which can let a missing 202 response body
slip through as (nil, nil). Update CreateSubnet in internal/client/subnet.go to
validate the unmarshaled SubnetCreateResponse has a non-nil Resource before
returning, and if it is missing, return an explicit error from the CreateSubnet
path instead of nil; keep the existing JSON parsing flow but add the check right
after json.Unmarshal and before returning wrapper.Resource.

In `@internal/client/vm.go`:
- Around line 77-82: The CreateVM response handling currently returns a parsed
VMCreateResponse even when the accepted payload is structurally incomplete.
After json.Unmarshal in CreateVM, validate that the returned resp includes a
non-empty resource.id before returning it, and if it is missing, fail fast with
a clear error instead of returning a nil/empty state that will break later
polling.

In `@internal/client/vnet.go`:
- Around line 49-54: The CreateVNet response handling currently returns
wrapper.Resource unchecked, which can turn an empty 202 response into (nil,
nil). Update the CreateVNet logic in vnet.go to validate that the unmarshaled
VNetCreateResponse contains a non-nil Resource before returning; if it is
missing, return a descriptive error from this path instead of a nil result so
callers get a clear diagnostic.

In `@internal/provider/provider.go`:
- Around line 45-59: The provider registration in ResourcesMap is missing the
dcapi_vnet_peering resource, so add the corresponding entry alongside the other
dcapi_* resources in provider initialization. Use the existing resource factory
pattern from symbols like ResourceVNet, ResourceCluster, and
ResourceRouteTableAssociation, and ensure the new map key exactly matches
dcapi_vnet_peering so the resource can be declared and imported.
- Around line 31-38: The provider token resolution currently only uses the
provider block and DCAPI_TOKEN env fallback, but the advertised dcctl credential
fallback is missing. Update the token handling in provider initialization and
related validation paths in the provider schema/config code so that when token
is empty it also attempts to load existing dcctl credentials before failing. Use
the existing token schema field and the provider setup/validation logic
referenced by the token attribute to wire this fallback in cleanly.

In `@internal/resources/cluster.go`:
- Around line 84-89: The system_pool.disk_gb schema in the cluster resource is
causing perpetual ForceNew diffs when omitted from config because Read writes
the API value into state without a matching config value. Update the disk_gb
field definition in the cluster schema to either mark it as Computed or set an
explicit default that matches the API behavior, and make sure the resource’s
Read/update flow stays consistent with that choice so omitted config does not
trigger replacement.

In `@internal/resources/node_pool.go`:
- Around line 74-79: The `disk_gb` handling in `node_pool.go` is silently
treating invalid values like 0 as “unset,” which causes the API default to be
applied instead of failing. Update the `disk_gb` schema on the node pool
resource to validate a minimum of 40, and adjust the create/update path in
`nodePool` so `disk_gb` is only omitted when the attribute is truly not set
rather than when it is non-positive. Make sure the logic around the current
`disk_gb` handling preserves explicit user input and does not coerce invalid
values into defaults.

In `@internal/resources/route_table.go`:
- Around line 79-92: The route table schema allows an invalid
`virtual_appliance` route to proceed without `next_hop_ip`, and
`routeTableResource` only copies `next_hop_ip` opportunistically during apply.
Add validation in the `routeTableResource`/schema path so `next_hop_ip` is
required when `next_hop_type` is `virtual_appliance`, and reject it for the
non-`virtual_appliance` hop types as well. Keep the check close to the existing
`next_hop_type` and `next_hop_ip` fields so the bad combination is caught before
apply.

In `@internal/resources/service_account.go`:
- Around line 19-25: Add import support to the Resource schemas by setting an
Importer on ResourceServiceAccount and the other affected resource constructors
(node_pool, nsg, nsg_attachment, route_table, route_table_association). Also
update each Read handler, especially resourceServiceAccountRead, to parse the
composite import ID and repopulate every ForceNew parent field from it (for
example both tenant_id and project_id) so imported state is complete.

In `@internal/resources/tenant.go`:
- Around line 229-233: The tenant delete path currently reports success while
only removing the resource from Terraform state, which leaves the remote tenant
orphaned. Update resourceTenantDelete to stop pretending deletion succeeded:
instead of clearing the ID and returning nil, return a diagnostic indicating
tenant deletion is unsupported until the DC-API provides a real delete endpoint.
Keep the change localized to resourceTenantDelete so terraform destroy surfaces
a failure rather than silently succeeding.
- Around line 51-67: The quota cap schema fields in tenant resource are causing
perpetual diffs because the Terraform defaults of 0 conflict with the
API-resolved values written back into state. Update the resource schema entries
for cpu_cores_cap, memory_gb_cap, and storage_gb_cap in the tenant resource to
use Optional plus Computed (or add equivalent diff suppression/normalization) so
omitted or zero-valued configs do not constantly plan changes against the
resolved 80/256/2000 values.

In `@test/main.tf`:
- Around line 12-16: The dcapi_tenant resource example is using the wrong
attribute name, which causes the config to fail validation. Update the tenant
resource block in test/main.tf to use tenant_id instead of id, matching the
tenant examples and docs used elsewhere in this PR. Keep the rest of the
resource fields unchanged.

---

Minor comments:
In `@docs/dc-api-reference.md`:
- Line 27: The table-of-contents entry for PrivateDnsZone uses a broken anchor,
so update the markdown link target to match the actual section heading slug
generated for PrivateDnsZone. Fix the TOC entry itself rather than the section,
and verify the anchor resolves correctly in the docs renderer.

In `@examples/subnet/main.tf`:
- Around line 49-52: The subnet output is currently returning Terraform’s
resource ID instead of the API UUID. Update the `output "subnet_id"` block in
the subnet example to use the `dcapi_subnet.app.subnet_uuid` attribute, matching
the provider’s UUID field and the way the VM example wires subnet values
downstream. Keep the output name and description as-is, but ensure the value
references the UUID-exposing symbol on `dcapi_subnet.app`.

In `@examples/vm/main.tf`:
- Around line 82-85: The subnet output is returning the Terraform resource ID
instead of the API UUID, so update the output block named subnet_id in main.tf
to use dcapi_subnet.app.subnet_uuid just like the existing subnet_uuid reference
in the same configuration. Keep the description aligned with the value being
exported so downstream consumers receive the actual subnet UUID rather than the
provider-generated id.

In `@internal/resources/node_pool.go`:
- Around line 276-281: Treat HTTP 404 as a successful delete in the node pool
destroy flow. Update the DeleteNodePool path used by node pool deletion so a
not-found response is normalized to nil instead of bubbling an error, and make
the delete handler in internal/client/node_pool.go return success when the pool
is already gone. Keep the existing waitForNodePoolDeleted poller behavior in
internal/resources/node_pool.go so the resource still converges on absent state
when DeleteNodePool is called.

In `@internal/resources/tenant.go`:
- Around line 14-19: The tenant resource is missing import support, so terraform
import cannot work for this and the other new resources. Update ResourceTenant
and the corresponding Resource constructors in internal/resources to wire
schema.Resource.Importer, using the appropriate StateContext importer helper
already used by other resources. Make sure the import setup is added alongside
CreateContext, ReadContext, UpdateContext, and DeleteContext so dcapi_tenant and
related resources can be imported consistently.

In `@internal/resources/vnet.go`:
- Around line 253-260: The 409 delete guidance in the VNet error path is
pointing users to the wrong reference attribute. Update the message in the VNet
deletion logic (the strings.Contains(err.Error(), "HTTP 409") branch) so the
example Terraform reference uses dcapi_vnet.example.vnet_uuid instead of
dcapi_vnet.example.id, since dcapi_subnet.vnet_id requires the bare UUID and not
the composite state ID.

---

Nitpick comments:
In `@Architecture.md`:
- Line 3: Update the architecture documentation to reflect the full 0.1.0
resource set instead of only the tenant/project/vnet/subnet/vm slice. In the
Architecture.md sections currently describing the provider scope and resource
hierarchy, add Bastion, Cluster, NodePool, RouteTable, RouteTableAssociation,
NetworkSecurityGroup, NSGAttachment, VNetPeering, and ServiceAccount, and expand
the VPC-backed workload hierarchy accordingly. Use the existing architecture
headings and resource overview areas in Architecture.md so the doc stays aligned
with the shipped provider surface.

In `@docs/dc-api-reference.md`:
- Around line 419-429: The field classification table is labeling VM and Cluster
networking selectors as computed, but `network_name`, `vnet_id`, and `subnet_id`
are user-provided inputs in the create request bodies. Update the Field
Classification entries in the relevant documentation sections so these selectors
are marked as user inputs for their respective networking modes, keeping the
wording consistent with the request examples and the related Cluster table.

In `@internal/client/project.go`:
- Around line 87-92: The 404 handling in GetProjectByID is still parsing
err.Error() text, which should be replaced with a typed/status-bearing error
from doRequest and checked directly in the read path. Update doRequest to return
an error that exposes the HTTP status, then change GetProjectByID to branch on
that status instead of strings.Contains(err.Error(), "HTTP 404"), preserving the
nil,nil behavior for not-found. Apply the same pattern to the other client read
methods that currently inspect error strings, including the ones in vnet,
subnet, service_account, vm, nsg, route_table, node_pool, cluster, and bastion.

In `@internal/client/vm.go`:
- Around line 91-96: The 404 handling in GetVM currently depends on matching the
text from doRequest, which is brittle across getters in internal/client. Add a
shared status-based helper in internal/client/client.go that detects a 404 from
the request error or status and returns the nil/not-found result explicitly,
then update GetVM and the other getter methods to use that helper instead of
strings.Contains on err.Error(). Keep the existing nil-on-404 contract and
preserve the existing wrapping for non-404 errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0ead4a6d-bb46-4aef-b4da-9824a528de03

📥 Commits

Reviewing files that changed from the base of the PR and between 37bc76e and f6d6666.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (42)
  • Architecture.md
  • GNUmakefile
  • docs/dc-api-reference.md
  • examples/bastion/main.tf
  • examples/cluster/main.tf
  • examples/node_pool/main.tf
  • examples/project/main.tf
  • examples/provider/main.tf
  • examples/service_account/main.tf
  • examples/subnet/main.tf
  • examples/tenant/main.tf
  • examples/vm/main.tf
  • examples/vnet/main.tf
  • go.mod
  • internal/client/bastion.go
  • internal/client/client.go
  • internal/client/cluster.go
  • internal/client/node_pool.go
  • internal/client/nsg.go
  • internal/client/project.go
  • internal/client/route_table.go
  • internal/client/service_account.go
  • internal/client/subnet.go
  • internal/client/tenant.go
  • internal/client/vm.go
  • internal/client/vnet.go
  • internal/provider/provider.go
  • internal/resources/bastion.go
  • internal/resources/cluster.go
  • internal/resources/node_pool.go
  • internal/resources/nsg.go
  • internal/resources/nsg_attachment.go
  • internal/resources/project.go
  • internal/resources/route_table.go
  • internal/resources/route_table_association.go
  • internal/resources/service_account.go
  • internal/resources/subnet.go
  • internal/resources/tenant.go
  • internal/resources/vm.go
  • internal/resources/vnet.go
  • main.go
  • test/main.tf

Comment thread docs/dc-api-reference.md
Comment thread GNUmakefile
Comment thread go.mod
Comment thread internal/client/client.go
Comment thread internal/client/node_pool.go
Comment thread internal/resources/route_table.go
Comment thread internal/resources/service_account.go
Comment thread internal/resources/tenant.go
Comment thread internal/resources/tenant.go
Comment thread test/main.tf
@sameera-87 sameera-87 closed this Jul 21, 2026
@sameera-87 sameera-87 reopened this Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (18)
docs/Open-api-sprc.yaml (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Filename typo: Open-api-sprc.yamlOpen-api-spec.yaml.

"sprc" is a transposition of "spec". Renaming also requires updating the two references in docs/data-source-implementation-plan.md (lines 18 and 184).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/Open-api-sprc.yaml` at line 1, Rename the OpenAPI document from
Open-api-sprc.yaml to Open-api-spec.yaml, and update both references in
docs/data-source-implementation-plan.md to use the corrected filename.
docs/grpc-to-resourcedata.md (2)

21-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to the fenced blocks (markdownlint MD040).

Four fences have no language specified; text suits all of them.

📝 Example fix
-```
+```text
 Terraform Core  --gRPC (tfplugin5)-->  terraform-plugin-go  --> ...

Also applies to: 189-189, 199-199, 303-303

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/grpc-to-resourcedata.md` at line 21, Specify the text language on all
four unlabeled fenced code blocks in the documentation, including the blocks
near the referenced sections, by changing each opening fence to use text while
preserving their contents.

Source: Linters/SAST tools


142-144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid hard-coded SDK and cluster.go line numbers in the docs.

The SDK citations like helper/schema/schema.go:629 depend on the v2.34.0 pin, and provider calls like cluster.go:312, cluster.go:96-168, cluster.go:439, :456 will shift with future edits. Use function names such as resourceClusterRead, ResourceCluster, expandSystemPool, and expandWorkerPools unless the exact line is essential.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/grpc-to-resourcedata.md` around lines 142 - 144, Update the references
in docs/grpc-to-resourcedata.md to remove hard-coded SDK versions and file line
numbers, replacing them with stable function or symbol names such as
resourceClusterRead, ResourceCluster, expandSystemPool, and expandWorkerPools.
Retain a line citation only where the exact location is essential, and keep the
documented call flow unchanged.
docs/Open-api-spec.md (1)

3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the "uploaded file" framing and label the bare fences.

Line 3's "The uploaded file is an OpenAPI Specification…" reads as a leftover from a chat session; in-repo this should name the file directly. The server-URL fences at 135 and 141 also trip markdownlint MD040.

📝 Proposed fix
-The uploaded file is an **OpenAPI Specification (Swagger)** for the **DC-API**.
-
-Instead of the raw YAML, here's what it describes in plain English.
+`docs/Open-api-sprc.yaml` is the **OpenAPI Specification** for the **DC-API**.
+This document summarises it in plain English.
-```
+```text
 https://dcapi.lk.internal.wso2.com

Also applies to: 135-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/Open-api-spec.md` around lines 3 - 5, Update the opening description to
refer directly to the DC-API OpenAPI specification rather than an “uploaded
file.” Add an explicit text language tag to both bare server-URL fenced code
blocks near the server URL documentation, preserving their contents.

Source: Linters/SAST tools

examples/vm/vm-legacy.tf (1)

1-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Entire file is commented out — move it to its own example directory instead.

Terraform merges every .tf in a directory, so this can't be uncommented in place: it would collide with main.tf's terraform/provider blocks and its identically named vm_ip / vm_private_key / vm_console_password outputs. Shipping it fully commented means it is never parsed, formatted, or validated, and it will silently rot.

The legacy bridge path is worth documenting (it demonstrates the network_name vs vnet_id+subnet_id mutual exclusivity from docs/Open-api-sprc.yaml 719-722), so relocating beats deleting: move it to examples/vm_legacy/main.tf as live HCL, keeping the explanatory comment block at lines 21-25 as real comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vm/vm-legacy.tf` around lines 1 - 56, The entire commented-out
legacy VM example should be relocated rather than left inert. Create a
standalone examples/vm_legacy/main.tf containing live Terraform configuration
for the dcapi provider, project, dcapi_virtual_machine.web_legacy resource, and
its outputs; preserve the explanatory legacy bridge comments, and remove the
commented content from vm-legacy.tf to avoid collisions with the existing
main.tf.
docs/data-source-implementation-plan.md (1)

132-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Plan doc describes a pre-implementation state that this same PR already changes.

Line 134 states the provider "currently has no DataSourcesMap field at all", but internal/provider/provider.go:74-88 in this PR already wires all 12 entries listed here. Likewise §3.4 presents the appendSet sharing decision as open, while the PR ships internal/datasources/helpers.go. Shipping the plan as-is will read as current-state documentation and mislead.

Suggest either adding a status header (e.g. "Status: implemented in PR #X — retained for design rationale") or converting the present-tense claims to past tense.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/data-source-implementation-plan.md` around lines 132 - 176, The plan
document describes already-implemented provider and helper changes as pending
work. Update sections 3.3 and 3.4 to clearly mark them as implemented while
retaining design rationale, or rewrite the present-tense claims in past tense to
reflect the existing DataSourcesMap wiring in New and appendSet placement in
internal/datasources/helpers.go.
examples/routeTableAssociation/main.tf (1)

1-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

terraform fmt has not been run across the examples tree. Shared root cause: the example configs were hand-written without a formatting pass, leaving inconsistent indentation and trailing whitespace. Adding a fmt -check -recursive step to the GNUmakefile/CI would keep this from recurring.

  • examples/routeTableAssociation/main.tf#L1-L8: normalise the terraform block from 4/6-space to 2-space indentation to match the other examples, and strip trailing whitespace on lines 5 and 39.
  • examples/vm/main.tf#L41-L69: strip trailing whitespace on lines 22, 23, 32, 33, 34, 43, 44, 50, and 51.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/routeTableAssociation/main.tf` around lines 1 - 8, The example
Terraform files need consistent formatting and whitespace cleanup. In
examples/routeTableAssociation/main.tf lines 1-8, normalize the terraform block
indentation to two spaces and remove trailing whitespace on lines 5 and 39; in
examples/vm/main.tf lines 41-69, remove trailing whitespace on lines 22, 23, 32,
33, 34, 43, 44, 50, and 51. Add a recursive terraform fmt check to the
GNUmakefile/CI to prevent future drift.
examples/key_vault_secret/main.tf (1)

25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose kv_uuid from dcapi_key_vault. Both examples parse an internal composite Terraform state ID to obtain the API UUID; make that UUID a computed resource attribute instead.

  • examples/key_vault_secret/main.tf#L25-L27: replace split()/element() parsing with dcapi_key_vault.prod_secrets.kv_uuid.
  • examples/private_endpoint/main.tf#L71-L75: replace split()/element() parsing with dcapi_key_vault.pe-secrets-s87.kv_uuid.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/key_vault_secret/main.tf` around lines 25 - 27, Expose and use the
computed kv_uuid attribute instead of parsing composite state IDs: update
examples/key_vault_secret/main.tf lines 25-27 to reference
dcapi_key_vault.prod_secrets.kv_uuid, and examples/private_endpoint/main.tf
lines 71-75 to reference dcapi_key_vault.pe-secrets-s87.kv_uuid.
internal/resources/vnet_peering.go (1)

228-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace deprecated resource.StateChangeConf with helper/retry.StateChangeConf.

Same deprecated API (SA1019) as flagged in private_dns_zone.go — this file imports "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" and uses resource.StateChangeConf in both waitForVNetPeeringActive and waitForVNetPeeringDeleted. key_vault.go already demonstrates the drop-in helper/retry.StateChangeConf replacement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/vnet_peering.go` around lines 228 - 274, Replace the
deprecated resource.StateChangeConf usage with helper/retry.StateChangeConf in
both waitForVNetPeeringActive and waitForVNetPeeringDeleted. Update the import
accordingly, preserving each function’s existing Pending, Target, Refresh,
timeout, and error behavior.

Source: Linters/SAST tools

internal/resources/dns_record.go (2)

97-126: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Create is an upsert keyed on name+type — no existence check before adoption.

resourceDnsRecordCreate calls UpsertDnsRecord, which per builds the records collection URL and POSTs the DnsRecordUpsertRequest, returning the parsed record response keyed on name+type (per the function's own doc comment). If a record with the same name+type already exists (created by another dcapi_dns_record resource or outside Terraform), this "create" call will silently overwrite its values and this resource will adopt that record's ID, breaking Terraform's create-should-fail-if-exists invariant and risking two state entries fighting over one record.

Consider a pre-create existence check (GetDnsRecord by name+type, if such a lookup exists) that fails fast with a clear "already exists — use terraform import" error, or document this upsert caveat explicitly for users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/dns_record.go` around lines 97 - 126, Update
resourceDnsRecordCreate to check for an existing DNS record by name and type
before calling UpsertDnsRecord; if found, return a clear error instructing users
to use terraform import, while preserving the create flow when no record exists.
Reuse an existing GetDnsRecord lookup if available and distinguish not-found
errors from other lookup failures.

58-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Enforce the documented type enum with validation.StringInSlice.

The description enumerates the valid values ("A"|"AAAA"|"CNAME"|"SRV"|"TXT"|"MX") but nothing validates them client-side, unlike the analogous role field in tenant_member.go (line 50) which uses validation.StringInSlice. Adding the same here catches typos at plan time instead of a round-trip API error.

💡 Proposed fix
 			"type": {
-				Type:        schema.TypeString,
-				Required:    true,
-				ForceNew:    true,
-				Description: "Record type: \"A\"|\"AAAA\"|\"CNAME\"|\"SRV\"|\"TXT\"|\"MX\". Part of the record's upsert identity — immutable.",
+				Type:         schema.TypeString,
+				Required:     true,
+				ForceNew:     true,
+				ValidateFunc: validation.StringInSlice([]string{"A", "AAAA", "CNAME", "SRV", "TXT", "MX"}, false),
+				Description:  "Record type: \"A\"|\"AAAA\"|\"CNAME\"|\"SRV\"|\"TXT\"|\"MX\". Part of the record's upsert identity — immutable.",
 			},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/dns_record.go` around lines 58 - 63, Update the `type`
schema field in the DNS record resource to add `validation.StringInSlice` with
the documented allowed values `A`, `AAAA`, `CNAME`, `SRV`, `TXT`, and `MX`,
matching the validation pattern used by the analogous `role` field in
`tenant_member.go`.
internal/resources/key_vault_secret.go (1)

91-117: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Create is a PUT upsert keyed on key — no existence check before adoption.

resourceKeyVaultSecretCreate calls WriteKeyVaultSecret, which per PUTs to the keyvault secret URL and returns the parsed KeyVaultSecretResponse (create or update). If a secret with the same key already exists under this KeyVault, Create silently overwrites its value/metadata and adopts it into this resource's state instead of failing, the same collision risk flagged in dns_record.go's Create handler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/key_vault_secret.go` around lines 91 - 117, Update
resourceKeyVaultSecretCreate to check whether the secret identified by tenantID,
projectID, keyVaultID, and key already exists before calling
WriteKeyVaultSecret. Return a diagnostic error for an existing secret, and only
perform the PUT upsert and set the composite resource ID when no matching secret
is found.
internal/resources/private_dns_zone.go (1)

201-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace deprecated resource.StateChangeConf with helper/retry.StateChangeConf.

Static analysis flags resource.StateChangeConf as deprecated (SA1019: use helper/retry instead). key_vault.go in this same PR already uses the non-deprecated helper/retry.StateChangeConf with an identical field API (Pending/Target/Timeout/MinTimeout/Refresh/WaitForStateContext), so this should be a straightforward import swap.

♻️ Proposed fix
 import (
 	"context"
 	"fmt"
 	"strings"
 	"time"

 	"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/retry"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"terraform-provider-dcapi/internal/client"
 )
...
-	conf := &resource.StateChangeConf{
+	conf := &retry.StateChangeConf{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resources/private_dns_zone.go` around lines 201 - 247, Replace the
deprecated resource.StateChangeConf usage in waitForPrivateDnsZoneActive and
waitForPrivateDnsZoneDeleted with helper/retry.StateChangeConf, updating the
import accordingly. Preserve the existing Pending, Target, Timeout, MinTimeout,
Refresh, and WaitForStateContext behavior.

Source: Linters/SAST tools

internal/datasources/dns_record.go (1)

93-106: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No ambiguity check on (name, type) match.

The loop returns on the first record matching (name, type) without checking whether more than one record matches. If the API ever returns duplicates for this composite key, the wrong record is silently selected. See consolidated comment for the shared fix across similar data sources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/datasources/dns_record.go` around lines 93 - 106, The record lookup
loop must detect duplicate matches instead of returning the first record for a
given (name, type) pair. Update the matching logic in the records loop to count
or collect all matching records, return an appropriate diagnostic when more than
one matches, and retain the existing field population only for the unique match.
internal/datasources/vnet_peering.go (1)

105-123: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No ambiguity check on name match.

First match on name wins silently. See consolidated comment for the shared fix across similar data sources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/datasources/vnet_peering.go` around lines 105 - 123, Update the
peering lookup loop in the data source read method to detect when multiple
entries share the requested name instead of returning the first match. Track
matches while iterating, return an explicit ambiguity diagnostic when more than
one match is found, and only populate the resource fields and return success for
a unique match.
internal/datasources/vnet.go (1)

99-117: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No ambiguity check on name match.

First match on name wins silently; vnet_uuid from a wrong match would misdirect child resources like dcapi_subnet.vnet_id. See consolidated comment for the shared fix across similar data sources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/datasources/vnet.go` around lines 99 - 117, The vnet lookup loop
must reject ambiguous name matches instead of returning the first result. Update
the surrounding data-source read logic to count all VNETs matching name, return
an error when more than one matches, and only set the ID and fields from the
unique match; preserve the not-found behavior for zero matches and use the
existing diagnostic/error conventions.
internal/datasources/key_vault.go (1)

104-122: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No ambiguity check on name match.

First match on name wins silently; kv_uuid from a wrong match would misdirect dcapi_private_endpoint.kv_id. See consolidated comment for the shared fix across similar data sources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/datasources/key_vault.go` around lines 104 - 122, Update the
name-matching logic in the key-vault read function to detect and reject
ambiguous results when multiple entries share the requested name, rather than
returning the first match. Only populate the resource fields and return
diagnostics after confirming exactly one matching key vault; preserve the
existing not-found behavior and diagnostic handling.
internal/datasources/image.go (1)

58-69: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No ambiguity check on display_name match.

First match on display_name wins silently. See consolidated comment for the shared fix across similar data sources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/datasources/image.go` around lines 58 - 69, Update the image lookup
loop in the data source read function to detect multiple images with the same
display_name instead of returning the first match. Track the matching image
count, return the existing not-found behavior when none match, and emit an
ambiguity diagnostic/error when more than one matches; only set the resource
fields and return after confirming exactly one match.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/data-source-implementation-plan.md`:
- Around line 192-198: Update the implementation plan steps 6 and 8 to reference
documentation that actually exists in the repository, replacing the missing
docs/manual-testing-plan.md and unavailable per-resource README sections;
alternatively, add and track those missing documents before retaining the
references. Keep the smoke-test and README-update requirements intact, anchored
to steps 6 and 8.

In `@docs/Open-api-sprc.yaml`:
- Around line 724-725: Update the VM, bastion, and VNet endpoint prose in
docs/Open-api-sprc.yaml at lines 724-725, 1061-1062, 2801-2802, 761, 1086, 2828,
and 2844 to use tenant/project-scoped paths, or consistently use the established
abbreviated convention. Also update the four VM endpoint headings in
docs/Open-api-spec.md lines 265-310 to the fully scoped
/v1/tenants/{tenant_id}/projects/{project_id}/virtual-machines[/{id}] forms.
- Around line 2346-2366: Resolve the service-account scope consistently across
the OpenAPI specification, using the
`/v1/tenants/{tenant_id}/projects/{project_id}/service-accounts` path and its
project scope as the source of truth unless the implementation proves otherwise.
Update createServiceAccount and listServiceAccounts descriptions, the
introductory and bearerAuth references, and listTenantRoleAssignments
cross-references to use the same scope and path. Align the provider’s
dcapi_service_account identity requirements with the confirmed scope, including
project_id when applicable.

In `@examples/cluster/main.tf`:
- Line 76: Update the kubeconfig retrieval command comment to instruct users to
create the output file with restrictive permissions, using umask 077 or install
-m 600 before writing the sensitive Terraform output.

In `@examples/key_vault_secret/main.tf`:
- Line 30: Replace the committed secret literal in the key vault secret resource
with a sensitive input variable that has no default value. Update the example to
consume that variable and ensure its state backend is configured securely,
without embedding production secret values in HCL.

In `@examples/nsg/main.tf`:
- Around line 71-81: Remove the duplicate NSG rule named allow-ssh from the
rules block in main.tf, unless it serves a distinct purpose; in that case,
update its name and corresponding rule settings to represent that purpose while
preserving unique rule names.
- Around line 59-69: Update the inbound “allow-ssh” rule to replace
source_address_prefix = "*" with the narrowly scoped administrator CIDR input
exposed by the example configuration, preserving the existing SSH port and rule
behavior.

In `@examples/routeTableAssociation/main.tf`:
- Around line 72-77: Add route validation for the routes block so
virtual_appliance next_hop_ip must belong to a subnet within the parent VNet,
and reject destination_cidr values that are unreachable from that VNet. Ensure
the routeTableAssociation example is rejected during apply for both 10.0.1.5 and
192.168.1.0/24.

In `@examples/tenant_member/main.tf`:
- Around line 15-17: Update dcapi_tenant_member and its example to use the
DC-API contract field role_definition backed by the role catalog, removing the
obsolete role/member tenant-rank and owner/member/viewer auth0 placeholder
usage. Replace user_sub with the same Asgardeo-compatible subject format used by
the other examples, and remove trailing whitespace from the role-assignment
line.

In `@examples/vnet_peering/main.tf`:
- Around line 41-64: Remove the dcapi_vnet_peering resource named b-to-a-s87,
keeping a-to-b-s87 as the sole peering for the VNet pair. Update the
b_to_a_status expectation or reference to reflect that createPeering installs
reciprocal routes through the remaining resource, without defining a second
peering.

In `@internal/client/private_dns_zone.go`:
- Around line 46-50: Reject create responses that omit the resource instead of
returning nil without an error. In internal/client/private_dns_zone.go:46-50,
update the create response handling after json.Unmarshal to return an error when
wrapper.Resource is nil; apply the same validation in
internal/client/vnet_peering.go:50-54. Preserve successful responses that
contain a resource.

In `@internal/client/tenant_member.go`:
- Around line 67-71: Update DeleteTenantMember to apply url.PathEscape to
principalID before interpolating it into the DELETE route, while leaving
tenantID handling and the existing endpoint structure unchanged.

In `@internal/datasources/project.go`:
- Around line 31-35: Rename the required lookup schema field from “id” to
“project_id” in the project data source schema, and update the associated lookup
logic to read project_id. Preserve the separate composite tenant_id/project_id
state identity and do not redefine Terraform’s implicit id field.

---

Nitpick comments:
In `@docs/data-source-implementation-plan.md`:
- Around line 132-176: The plan document describes already-implemented provider
and helper changes as pending work. Update sections 3.3 and 3.4 to clearly mark
them as implemented while retaining design rationale, or rewrite the
present-tense claims in past tense to reflect the existing DataSourcesMap wiring
in New and appendSet placement in internal/datasources/helpers.go.

In `@docs/grpc-to-resourcedata.md`:
- Line 21: Specify the text language on all four unlabeled fenced code blocks in
the documentation, including the blocks near the referenced sections, by
changing each opening fence to use text while preserving their contents.
- Around line 142-144: Update the references in docs/grpc-to-resourcedata.md to
remove hard-coded SDK versions and file line numbers, replacing them with stable
function or symbol names such as resourceClusterRead, ResourceCluster,
expandSystemPool, and expandWorkerPools. Retain a line citation only where the
exact location is essential, and keep the documented call flow unchanged.

In `@docs/Open-api-spec.md`:
- Around line 3-5: Update the opening description to refer directly to the
DC-API OpenAPI specification rather than an “uploaded file.” Add an explicit
text language tag to both bare server-URL fenced code blocks near the server URL
documentation, preserving their contents.

In `@docs/Open-api-sprc.yaml`:
- Line 1: Rename the OpenAPI document from Open-api-sprc.yaml to
Open-api-spec.yaml, and update both references in
docs/data-source-implementation-plan.md to use the corrected filename.

In `@examples/key_vault_secret/main.tf`:
- Around line 25-27: Expose and use the computed kv_uuid attribute instead of
parsing composite state IDs: update examples/key_vault_secret/main.tf lines
25-27 to reference dcapi_key_vault.prod_secrets.kv_uuid, and
examples/private_endpoint/main.tf lines 71-75 to reference
dcapi_key_vault.pe-secrets-s87.kv_uuid.

In `@examples/routeTableAssociation/main.tf`:
- Around line 1-8: The example Terraform files need consistent formatting and
whitespace cleanup. In examples/routeTableAssociation/main.tf lines 1-8,
normalize the terraform block indentation to two spaces and remove trailing
whitespace on lines 5 and 39; in examples/vm/main.tf lines 41-69, remove
trailing whitespace on lines 22, 23, 32, 33, 34, 43, 44, 50, and 51. Add a
recursive terraform fmt check to the GNUmakefile/CI to prevent future drift.

In `@examples/vm/vm-legacy.tf`:
- Around line 1-56: The entire commented-out legacy VM example should be
relocated rather than left inert. Create a standalone examples/vm_legacy/main.tf
containing live Terraform configuration for the dcapi provider, project,
dcapi_virtual_machine.web_legacy resource, and its outputs; preserve the
explanatory legacy bridge comments, and remove the commented content from
vm-legacy.tf to avoid collisions with the existing main.tf.

In `@internal/datasources/dns_record.go`:
- Around line 93-106: The record lookup loop must detect duplicate matches
instead of returning the first record for a given (name, type) pair. Update the
matching logic in the records loop to count or collect all matching records,
return an appropriate diagnostic when more than one matches, and retain the
existing field population only for the unique match.

In `@internal/datasources/image.go`:
- Around line 58-69: Update the image lookup loop in the data source read
function to detect multiple images with the same display_name instead of
returning the first match. Track the matching image count, return the existing
not-found behavior when none match, and emit an ambiguity diagnostic/error when
more than one matches; only set the resource fields and return after confirming
exactly one match.

In `@internal/datasources/key_vault.go`:
- Around line 104-122: Update the name-matching logic in the key-vault read
function to detect and reject ambiguous results when multiple entries share the
requested name, rather than returning the first match. Only populate the
resource fields and return diagnostics after confirming exactly one matching key
vault; preserve the existing not-found behavior and diagnostic handling.

In `@internal/datasources/vnet_peering.go`:
- Around line 105-123: Update the peering lookup loop in the data source read
method to detect when multiple entries share the requested name instead of
returning the first match. Track matches while iterating, return an explicit
ambiguity diagnostic when more than one match is found, and only populate the
resource fields and return success for a unique match.

In `@internal/datasources/vnet.go`:
- Around line 99-117: The vnet lookup loop must reject ambiguous name matches
instead of returning the first result. Update the surrounding data-source read
logic to count all VNETs matching name, return an error when more than one
matches, and only set the ID and fields from the unique match; preserve the
not-found behavior for zero matches and use the existing diagnostic/error
conventions.

In `@internal/resources/dns_record.go`:
- Around line 97-126: Update resourceDnsRecordCreate to check for an existing
DNS record by name and type before calling UpsertDnsRecord; if found, return a
clear error instructing users to use terraform import, while preserving the
create flow when no record exists. Reuse an existing GetDnsRecord lookup if
available and distinguish not-found errors from other lookup failures.
- Around line 58-63: Update the `type` schema field in the DNS record resource
to add `validation.StringInSlice` with the documented allowed values `A`,
`AAAA`, `CNAME`, `SRV`, `TXT`, and `MX`, matching the validation pattern used by
the analogous `role` field in `tenant_member.go`.

In `@internal/resources/key_vault_secret.go`:
- Around line 91-117: Update resourceKeyVaultSecretCreate to check whether the
secret identified by tenantID, projectID, keyVaultID, and key already exists
before calling WriteKeyVaultSecret. Return a diagnostic error for an existing
secret, and only perform the PUT upsert and set the composite resource ID when
no matching secret is found.

In `@internal/resources/private_dns_zone.go`:
- Around line 201-247: Replace the deprecated resource.StateChangeConf usage in
waitForPrivateDnsZoneActive and waitForPrivateDnsZoneDeleted with
helper/retry.StateChangeConf, updating the import accordingly. Preserve the
existing Pending, Target, Timeout, MinTimeout, Refresh, and WaitForStateContext
behavior.

In `@internal/resources/vnet_peering.go`:
- Around line 228-274: Replace the deprecated resource.StateChangeConf usage
with helper/retry.StateChangeConf in both waitForVNetPeeringActive and
waitForVNetPeeringDeleted. Update the import accordingly, preserving each
function’s existing Pending, Target, Refresh, timeout, and error behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e41821fd-5be2-485e-a301-a730b2a97307

📥 Commits

Reviewing files that changed from the base of the PR and between f6d6666 and f5bea90.

⛔ Files ignored due to path filters (1)
  • examples/project/tf-debug-plan.log is excluded by !**/*.log
📒 Files selected for processing (77)
  • .gitignore
  • README.md
  • dc-api/dc-api
  • docs/Open-api-spec.md
  • docs/Open-api-sprc.yaml
  • docs/data-source-implementation-plan.md
  • docs/grpc-to-resourcedata.md
  • examples/bastion/main.tf
  • examples/cluster/main.tf
  • examples/data_sources/main.tf
  • examples/dns_record/main.tf
  • examples/key_vault/main.tf
  • examples/key_vault_secret/main.tf
  • examples/node_pool/main.tf
  • examples/nsg/main.tf
  • examples/private_dns_zone/main.tf
  • examples/private_endpoint/main.tf
  • examples/project/main.tf
  • examples/provider/main.tf
  • examples/routeTable/main.tf
  • examples/routeTableAssociation/main.tf
  • examples/subnet/main.tf
  • examples/tenant/main.tf
  • examples/tenant_member/main.tf
  • examples/vm/main.tf
  • examples/vm/vm-legacy.tf
  • examples/vnet/main.tf
  • examples/vnet_peering/main.tf
  • internal/client/bastion.go
  • internal/client/client.go
  • internal/client/dns_record.go
  • internal/client/image.go
  • internal/client/key_vault.go
  • internal/client/key_vault_secret.go
  • internal/client/nsg.go
  • internal/client/private_dns_zone.go
  • internal/client/private_endpoint.go
  • internal/client/region.go
  • internal/client/route_table.go
  • internal/client/subnet.go
  • internal/client/tenant.go
  • internal/client/tenant_member.go
  • internal/client/vm.go
  • internal/client/vnet.go
  • internal/client/vnet_peering.go
  • internal/datasources/dns_record.go
  • internal/datasources/helpers.go
  • internal/datasources/image.go
  • internal/datasources/key_vault.go
  • internal/datasources/nsg.go
  • internal/datasources/private_dns_zone.go
  • internal/datasources/project.go
  • internal/datasources/region.go
  • internal/datasources/route_table.go
  • internal/datasources/subnet.go
  • internal/datasources/tenant.go
  • internal/datasources/vnet.go
  • internal/datasources/vnet_peering.go
  • internal/provider/provider.go
  • internal/resources/bastion.go
  • internal/resources/cluster.go
  • internal/resources/dns_record.go
  • internal/resources/helpers.go
  • internal/resources/key_vault.go
  • internal/resources/key_vault_secret.go
  • internal/resources/node_pool.go
  • internal/resources/private_dns_zone.go
  • internal/resources/private_endpoint.go
  • internal/resources/project.go
  • internal/resources/service_account.go
  • internal/resources/subnet.go
  • internal/resources/tenant.go
  • internal/resources/tenant_member.go
  • internal/resources/vm.go
  • internal/resources/vnet.go
  • internal/resources/vnet_peering.go
  • main.go
💤 Files with no reviewable changes (2)
  • examples/provider/main.tf
  • README.md
🚧 Files skipped from review as they are similar to previous changes (21)
  • main.go
  • examples/vnet/main.tf
  • examples/bastion/main.tf
  • examples/node_pool/main.tf
  • examples/project/main.tf
  • internal/client/subnet.go
  • examples/subnet/main.tf
  • internal/resources/service_account.go
  • internal/resources/subnet.go
  • internal/client/route_table.go
  • internal/resources/bastion.go
  • internal/client/nsg.go
  • internal/client/tenant.go
  • internal/resources/vm.go
  • internal/resources/tenant.go
  • internal/client/vm.go
  • internal/resources/project.go
  • internal/client/bastion.go
  • internal/resources/vnet.go
  • internal/resources/cluster.go
  • internal/resources/node_pool.go

Comment thread docs/data-source-implementation-plan.md
Comment thread docs/Open-api-sprc.yaml
Comment thread docs/Open-api-sprc.yaml
Comment thread examples/cluster/main.tf
description = "Current cluster status."
}

# Retrieve with: terraform output -raw kubeconfig > ~/.kube/prod.yaml

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Create the kubeconfig with restrictive permissions.

This command can write credentials to a shell-default-permission file. Use umask 077 (or install -m 600) before redirecting the sensitive output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cluster/main.tf` at line 76, Update the kubeconfig retrieval command
comment to instruct users to create the output file with restrictive
permissions, using umask 077 or install -m 600 before writing the sensitive
Terraform output.

key_vault_id = element(split("/", dcapi_key_vault.prod_secrets.id), 2)

key = "db-password"
value = "super-secret-value" # sensitive — updating this bumps `version` in place

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not model secrets as committed HCL literals.

Use a sensitive input variable with no default (and a secured state backend) so copied examples do not normalize committing production secret values.

Proposed change
-  value = "super-secret-value" # sensitive — updating this bumps `version` in place
+  value = var.db_password # sensitive — updating this bumps `version` in place
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/key_vault_secret/main.tf` at line 30, Replace the committed secret
literal in the key vault secret resource with a sensitive input variable that
has no default value. Update the example to consume that variable and ensure its
state backend is configured securely, without embedding production secret values
in HCL.

Comment on lines +15 to +17
user_sub = "auth0|abc123"
role = "member"
display_alias = "Alice"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'tenant_member.go' internal/resources internal/client --exec rg -n '"role"|role_definition|RoleDefinition' {}

Repository: wso2/open-cloud-datacenter

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -t f 'tenant_member.go|Open-api-sprc.yaml|main.tf' . | sed 's#^\./##' | sort

echo
echo "== examples/tenant_member/main.tf =="
cat -n examples/tenant_member/main.tf

echo
echo "== internal/resources tenant member outline/content =="
for f in $(fd -t f 'tenant_member.go' internal/resources internal/client); do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,220p' "$f" | cat -n
done

echo
echo "== relevant OpenAPI lines =="
if [ -f docs/Open-api-sprc.yaml ]; then
  wc -l docs/Open-api-sprc.yaml
  sed -n '6880,7040p' docs/Open-api-sprc.yaml | cat -n
  echo "--- role_definition occurrences ---"
  rg -n -i 'role_definition|reader|contributor|owner/member/viewer|user_sub|01abc123' docs/Open-api-sprc.yaml -C 2
else
  echo "docs/Open-api-sprc.yaml not found"
fi

echo
echo "== all user_sub examples in docs/main.tf =="
rg -n -i 'user_sub|auth0\||01abc123' docs examples -C 2

Repository: wso2/open-cloud-datacenter

Length of output: 44023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== docs/dc-api-reference.md tenant member section =="
sed -n '1720,1820p' docs/dc-api-reference.md | cat -n

echo
echo "== terminal whitespace check in examples/tenant_member/main.tf =="
python3 - <<'PY'
from pathlib import Path
p=Path("examples/tenant_member/main.tf")
for i,l in enumerate(p.read_text().splitlines(),1):
    if l != l.rstrip() or line_no in [15,16]:
        if i in [15,16]:
            print(f"{i}: {l!r} len={len(l)} rstripped={len(l.rstrip())}")
            print(f"  has whitespace: {'tabs' if chr(9) in l else ''} {'trailing_spaces' if l.endswith(' ') else ''}")
PY
echo "end whitespace check"

echo
echo "== tenant member provider endpoint refs =="
rg -n 'tenant.*member|/members|tenant_member|CreateTenantMember|ResourceTenantMember|dcapi_tenant_member|role_definition|role' terraform-provider-dcapi internal docs examples -C 2

Repository: wso2/open-cloud-datacenter

Length of output: 3709


Align the tenant member example with the DC-API role-assignment contract.

dcapi_tenant_member currently passes the role value as JSON role, but the matching OpenAPI contract uses user_sub + role_definition; the member tenant rank is also documented as removed. Update this provider/resource to accept a role_definition catalog key and remove the obsolete owner/member/viewer user_sub/auth0|... placeholder, while keeping the example user_sub compatible with the Asgardeo-sub examples elsewhere. Also remove the trailing whitespace on line 16.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/tenant_member/main.tf` around lines 15 - 17, Update
dcapi_tenant_member and its example to use the DC-API contract field
role_definition backed by the role catalog, removing the obsolete role/member
tenant-rank and owner/member/viewer auth0 placeholder usage. Replace user_sub
with the same Asgardeo-compatible subject format used by the other examples, and
remove trailing whitespace from the role-assignment line.

Comment on lines +41 to +64
# Direction 1: routes FROM vnet-a TOWARDS vnet-b.
resource "dcapi_vnet_peering" "a-to-b-s87" {
tenant_id = dcapi_vnet.peer-vnet-a-s87.tenant_id
project_id = dcapi_vnet.peer-vnet-a-s87.project_id
vnet_id = dcapi_vnet.peer-vnet-a-s87.vnet_uuid

name = "a-to-b-s87"
peer_vnet_id = dcapi_vnet.peer-vnet-b-s87.vnet_uuid

allow_forwarded_traffic = false
}

# Direction 2: routes FROM vnet-b TOWARDS vnet-a — required for full bidirectional
# connectivity. Without this second resource, only vnet-a could reach vnet-b.
resource "dcapi_vnet_peering" "b-to-a-s87" {
tenant_id = dcapi_vnet.peer-vnet-b-s87.tenant_id
project_id = dcapi_vnet.peer-vnet-b-s87.project_id
vnet_id = dcapi_vnet.peer-vnet-b-s87.vnet_uuid

name = "b-to-a-s87"
peer_vnet_id = dcapi_vnet.peer-vnet-a-s87.vnet_uuid

allow_forwarded_traffic = false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## locate relevant files"
fd -a . . | sed 's#^\./##' | rg '(^|/)(main\.tf|Open-api-sprc\.ya?ml)$' || true

echo
echo "## git status/stat"
git diff --stat || true

echo
echo "## inspect examples/vnet_peering/main.tf around peering"
if [ -f examples/vnet_peering/main.tf ]; then
  nl -ba examples/vnet_peering/main.tf | sed -n '1,140p'
fi

echo
echo "## inspect OpenAPI peer spec sections"
if [ -f docs/Open-api-sprc.yaml ]; then
  echo "## lines around createPeering"
  nl -ba docs/Open-api-sprc.yaml | sed -n '3330,3390p'
fi

echo
echo "## search for vnet peering docs/spec/provider comments"
rg -n "createPeering| Only one peering|mutual|reciprocal|bidi|bidirectional|forwarded_traffic|vnet_peering" . --glob '!*.lock' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -200

Repository: wso2/open-cloud-datacenter

Length of output: 1368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re
import yaml

root = Path('.')
print("## YAML parse check")
p = root / 'docs' / 'Open-api-sprc.yaml'
if p.exists():
    data = yaml.safe_load(p.read_text())
    print("root keys sample:", list(data.keys())[:10])
    op = data.get("paths", {}).get("/dc/api/v1/tenants/{tenant_id}/vpcs/{vpc_id}/peerings", {})
    post = op.get("post")
    print("createPeering path exists:", "/dc/api/v1/tenants/{tenant_id}/vpcs/{vpc_id}/peerings" in data.get("paths", {}))
    if post:
        print("createPeering summary:", post.get("operationId"), post.get("summary"))
        txt = (post.get("description") or "").strip()
        print("--- createPeering description ---")
        for i, line in enumerate(txt.splitlines(),1):
            print(f"{i:03}: {line}")
        code = post.get("responses", {}).get(409)
        print("has 409 response:", code is not None)
        print("409 description:", (code.get("description") or "" if code else ""))
        print("has 400 response:", post.get("responses", {}).get(400))
        print("400 description:", (post.get("responses", {}).get(400, {}).get("description") or ""))
PY

Repository: wso2/open-cloud-datacenter

Length of output: 309


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## files matching vnet peering names"
find . -maxdepth 5 -type f \( -iname '*vnet*peer*' -o -iname 'main.tf' \) | sort

echo
echo "## example file stats/contents"
wc -l examples/vnet_peering/main.tf docs/Open-api-sprc.yaml
awk 'NR>=1 && NR<=220 {printf "%6d  %s\n", NR, $0}' examples/vnet_peering/main.tf

echo
echo "## search candidate file paths in OpenAPI"
rg -n 'dc/api/v1/tenants/.*/vpcs/.*/peerings|createPeering|create.*Peer|peerings|peering|mutual|reciprocal|only one|one peering|409' docs/Open-api-sprc.yaml

echo
echo "## broader repo search for vnet peering guidance"
rg -n 'b-to-a|a-to-b|vnet peering|peerings|create.*Peer|mutual|reciprocal|only one|one peering|bidirectional|forwarded_traffic' . --glob '!**/.git/**' --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' | head -200

Repository: wso2/open-cloud-datacenter

Length of output: 15550


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re
import yaml

for p in [Path('docs/Open-api-sprc.yaml')]:
    if not p.exists():
        print(f"missing {p}")
        continue
    data = yaml.safe_load(p.read_text())
    print(f"## paths containing peer")
    for path, methods in data.get("paths", {}).items():
        if "peer" in path.lower():
            print(path)
            for m, details in methods.items():
                print(f"  {m.upper()}: {details.get('operationId')}")
                txt = (details.get("description") or details.get("summary") or "")
                # print first 20 lines for peer paths
                for i, line in enumerate(txt.splitlines(), 1):
                    if i > 20: break
                    print(f"    {i}: {line.strip()}")

print("## parse peer resource names in example")
p = Path('examples/vnet_peering/main.tf')
if p.exists():
    res = re.findall(r'resource\s+"([^"]+)"\s+"([^"]+)"', p.read_text())
    print(res)
PY

Repository: wso2/open-cloud-datacenter

Length of output: 387


Remove the second peering resource

createPeering installs reciprocal routes and the API allows only one peering per VNet pair. Defining both a-to-b-s87 and b-to-a-s87 will reject the second creation with 409 Conflict; keep one resource and update b_to_a_status accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/vnet_peering/main.tf` around lines 41 - 64, Remove the
dcapi_vnet_peering resource named b-to-a-s87, keeping a-to-b-s87 as the sole
peering for the VNet pair. Update the b_to_a_status expectation or reference to
reflect that createPeering installs reciprocal routes through the remaining
resource, without defining a second peering.

Comment on lines +46 to +50
var wrapper PrivateDnsZoneCreateResponse
if err := json.Unmarshal(respBytes, &wrapper); err != nil {
return nil, fmt.Errorf("CreatePrivateDnsZone: failed to parse response: %w", err)
}
return wrapper.Resource, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject create responses without resource.

Both methods return a nil resource with no error when the API omits resource; fail immediately instead.

  • internal/client/private_dns_zone.go#L46-L50: return an error when wrapper.Resource == nil.
  • internal/client/vnet_peering.go#L50-L54: return an error when wrapper.Resource == nil.
📍 Affects 2 files
  • internal/client/private_dns_zone.go#L46-L50 (this comment)
  • internal/client/vnet_peering.go#L50-L54
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/private_dns_zone.go` around lines 46 - 50, Reject create
responses that omit the resource instead of returning nil without an error. In
internal/client/private_dns_zone.go:46-50, update the create response handling
after json.Unmarshal to return an error when wrapper.Resource is nil; apply the
same validation in internal/client/vnet_peering.go:50-54. Preserve successful
responses that contain a resource.

Comment on lines +67 to +71
// DeleteTenantMember sends DELETE /v1/tenants/{tenantID}/members/{principalID}.
// principalID is the OIDC sub string (TenantMemberResponse.PrincipalID) — NOT the
// role_assignment UUID returned in the "id" field.
func (c *DCAPIClient) DeleteTenantMember(ctx context.Context, tenantID, principalID string) error {
path := fmt.Sprintf("/v1/tenants/%s/members/%s", tenantID, principalID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file =="
fd -a 'tenant_member\.go$' .

echo "== file excerpt =="
if [ -f internal/client/tenant_member.go ]; then
  nl -ba internal/client/tenant_member.go | sed -n '1,120p'
fi

echo "== related tenants/member endpoints/search =="
rg -n "DeleteTenantMember|/tenants/.*/members/|PrincipalID|TenantMemberResponse|members/" internal -S || true

echo "== Go url.PathEscape/import usage in client package =="
rg -n "net/url|PathEscape|fmt\\.Sprintf\\(\"/v1/tenants/%s/members/%s\"" internal/client || true

echo "== deterministic Go PathEscape behavior for subject-like values =="
python3 - <<'PY'
# Minimal illustration of characters that PathEscape would avoid embedding literally in paths.
samples = ["alice@example.com", "user/admin", "user?query=1", "user%hash", "user#frag", "user[slash]/child"]
encoding_table = {chr(i): None for i in range(32)} | {i: chr(i) for i in range(127, 256)}
import urllib.parse
for s in samples:
    print(f"{s!r}: {urllib.parse.quote(s, safe='')!r}")
PY

Repository: wso2/open-cloud-datacenter

Length of output: 351


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
awk '{printf "%6d\t%s\n", NR, $0}' internal/client/tenant_member.go | sed -n '1,120p'

echo "== related tenant member endpoint/search =="
rg -n "DeleteTenantMember|/tenants/.*/members/|PrincipalID|TenantMemberResponse|members/" internal -S || true

echo "== client imports and path construction =="
rg -n 'net/url|PathEscape|fmt\.Sprintf\("/v1/tenants/%s/members/%s"|fmt\.Sprintf\("/v1/tenants/%s/members/%s"' internal/client || true

echo "== deterministic PathEscape/quote behavior for subject-like values =="
awk '{printf "%6d\t%s\n", NR, $0}' /tmp/coderabbit-shell-logs/shell-output-*.log 2>/dev/null || true

cat <<'PY' | python3
import urllib.parse
samples = ["alice@example.com", "user\admin", "user?query=1", "user#frag", "user[slash]/child"]
for s in samples:
    print(f"{s!r}: {urllib.parse.quote(s, safe='')!r}")
PY

Repository: wso2/open-cloud-datacenter

Length of output: 6262


Escape the opaque OIDC subject before using it as a path segment.

principalID is interpolated directly into the DELETE URL and can contain URL-significant characters, so deletion can target a malformed route. Encode it with url.PathEscape(principalID) when building this path.

Proposed fix
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"net/url"
 )
 
-	path := fmt.Sprintf("/v1/tenants/%s/members/%s", tenantID, principalID)
+	path := fmt.Sprintf("/v1/tenants/%s/members/%s", tenantID, url.PathEscape(principalID))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// DeleteTenantMember sends DELETE /v1/tenants/{tenantID}/members/{principalID}.
// principalID is the OIDC sub string (TenantMemberResponse.PrincipalID) — NOT the
// role_assignment UUID returned in the "id" field.
func (c *DCAPIClient) DeleteTenantMember(ctx context.Context, tenantID, principalID string) error {
path := fmt.Sprintf("/v1/tenants/%s/members/%s", tenantID, principalID)
// DeleteTenantMember sends DELETE /v1/tenants/{tenantID}/members/{principalID}.
// principalID is the OIDC sub string (TenantMemberResponse.PrincipalID) — NOT the
// role_assignment UUID returned in the "id" field.
func (c *DCAPIClient) DeleteTenantMember(ctx context.Context, tenantID, principalID string) error {
path := fmt.Sprintf("/v1/tenants/%s/members/%s", tenantID, url.PathEscape(principalID))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/tenant_member.go` around lines 67 - 71, Update
DeleteTenantMember to apply url.PathEscape to principalID before interpolating
it into the DELETE route, while leaving tenantID handling and the existing
endpoint structure unchanged.

Comment on lines +31 to +35
"id": {
Type: schema.TypeString,
Required: true,
Description: "Slug of the project to look up.",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sdk_version="$(awk '$1 == "github.com/hashicorp/terraform-plugin-sdk/v2" { print $2 }' go.mod)"
test -n "$sdk_version"

curl -fsSL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/${sdk_version}/helper/schema/core_schema.go" |
  rg -n 'implicitly required "id"|block.Attributes\["id"\]'

curl -fsSL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/${sdk_version}/helper/schema/resource.go" |
  rg -n 'Data source|SetId|validateResourceID'

Repository: wso2/open-cloud-datacenter

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod sdk version =="
awk '$1 == "github.com/hashicorp/terraform-plugin-sdk/v2" { print $2 }' go.mod || true
echo

echo "== locate project.go =="
fd -a 'project\.go$' . | sed 's#^\./##' | head -20
echo

echo "== inspect internal/datasources/project.go relevant sections =="
wc -l internal/datasources/project.go
sed -n '1,170p' internal/datasources/project.go | cat -n
echo

echo "== Search for SDK code/files if present =="
find . -path './vendor/**' -o -path './.terraform/**' | head -5 || true
rg -n 'core_schema|ImplicitlyRequireOptionalID|Read|SetId|create.*Schema|Data Source|DataSource' go.mod go.sum internal go 2>/dev/null | head -80 || true

Repository: wso2/open-cloud-datacenter

Length of output: 14040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod and go.sum provider blocks =="
sed -n '1,120p' go.mod
echo
grep -n 'terraform-provider-dcapi\|terraform-plugin-sdk/v2\|terraform-plugin-go' go.sum | head -40
echo

echo "== inspect other parent-scoped data sources for lookup naming pattern =="
fd -a '\.go$' internal/datasources internal | sort
rg -n 'DataSource[A-Z]|tenant_id|SetId\(|Required:    true|Computed:    true' internal/datasources internal/resources -g '*.go' | head -200 || true

Repository: wso2/open-cloud-datacenter

Length of output: 22598


🌐 Web query:

terraform-plugin-sdk v2 data source top-level id reserved SetId implicitly required

💡 Result:

In the terraform-plugin-sdk v2, the id field is not strictly reserved in the schema as a hard-coded language keyword, but it holds a special, implicit status due to the legacy design of the ResourceData type [1][2]. When using SetId(string) within the SDK, the framework performs two actions: it sets the internal resource identifier (InstanceState.ID) and implicitly writes or updates an id attribute in the resource state [1][2]. Because of this historical tight coupling, the id field has traditionally been treated as a mandatory attribute for managed resources and data sources to ensure proper state serialization and provider compatibility [3][2][4][5]. Key technical implications include: 1. Implicit Attribute Management: If you define an id attribute in your schema, the SDK's SetId() method will automatically populate it [1]. Historically, this led to requirements that the id attribute be marked as Computed: true in the schema if it were to be handled correctly without causing validation or consistency errors [3][5]. 2. Evolution and Deprecation: Over time, the SDK introduced methods like GetResourceId() and SetResourceId() to allow developers to manage the resource identifier independently of the id attribute, attempting to decouple the two [2][4]. 3. Best Practices: While SDK v2 continues to support this pattern for compatibility, modern development strongly encourages migrating to the terraform-plugin-framework [6][5]. In the framework, id is a standard, explicit attribute that must be managed like any other field, removing the implicit, "magic" behavior found in the legacy SDK [7]. If you are currently working within the v2 SDK, it is recommended to ensure your schema includes "id": { Type: schema.TypeString, Computed: true } if you are using SetId() to prevent state consistency issues [3][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SDK v2.34.0 core/resource data sources for id handling =="
curl -fsSL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/v2.34.0/helper/schema/resource_data.go" | rg -n -C 3 'func \[Mm]arkResourceTypeFullyComputedForDataSources|ImplicitlyRequireOptionalID|block.Attributes\["id"\]|SetResourceID|SetId|Read|ResourceData' || true

echo
curl -fsCL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/v2.34.0/helper/schema/core_schema.go" | rg -n -C 4 'ExplicitResourceID|ImplicitlyRequireOptionalID|top-level' || true

echo "== SDK v2.34.0 data source read path schema validation =="
curl -fsSL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/v2.34.0/helper/schema/resource.go" | rg -n -C 5 'Data Source|dataSource|SetStateID|Read|Validate|SetId|markResourceTypeFullyComputedForDataSources' || true

Repository: wso2/open-cloud-datacenter

Length of output: 27430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ReadDataApply implementation =="
sed -n '1028,1120p' "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/v2.34.0/helper/schema/resource.go" 2>/dev/null || curl -fsSL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/v2.34.0/helper/schema/resource.go" | sed -n '1028,1120p'

echo
echo "== SchemaMap DataData and SetResource implementations =="
curl -fsSL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/v2.34.0/helper/schema/schema.go" | rg -n -C 4 'func \(.*SchemaMap\).*Data|SetResource|SetResourceID|ReservedDataSourceFields|ImplicitlyRequireOptionalID|markResourceType' || true

echo
echo "== core_schema behavior with explicit id via Go parser/text check =="
curl -fsSL "https://raw.githubusercontent.com/hashicorp/terraform-plugin-sdk/v2.34.0/helper/schema/core_schema.go" | sed -n '1,260p' | nl

Repository: wso2/open-cloud-datacenter

Length of output: 2981


🌐 Web query:

github.com/hashicorp/terraform-plugin-sdk/v2.34.0 InternalValidate id required data source

💡 Result:

In the Terraform Plugin SDK (including version v2.34.0), the "id" attribute is implicitly treated as a reserved field for resources and data sources [1]. The SDK's InternalValidate method performs validation to ensure that any custom definition of the "id" attribute, if provided, adheres to specific requirements [2][3]. Key points regarding the "id" attribute and InternalValidate: 1. Implicit Handling: If an "id" attribute is not explicitly defined in your schema, the SDK implicitly adds it as Computed: true and Optional: true [4]. This is because the SDK relies on the "id" attribute to track state via SetId() [1][5]. 2. Validation Requirements: If you choose to explicitly define an "id" attribute in your schema (e.g., to add a custom description), InternalValidate will verify it [6][2]. If defined, it must typically be configured as Computed: true to align with the SDK's internal expectations [1]. Defining it as Required: true or with other conflicting constraints will cause InternalValidate to fail [1][2]. 3. Debugging Errors: If you encounter an "id" related error during InternalValidate, ensure that your schema definition does not conflict with the SDK's requirements [1][6]. Generally, you should not need to explicitly define "id" unless you are adding a custom description or specific documentation, in which case it should be marked as Computed: true [6]. 4. Recommended Practice: For general development, omit the "id" attribute from your schema definition entirely and let the SDK handle it implicitly [1][4]. If you are receiving a validation error, it is likely because your manual definition of "id" violates the requirements for resource identity [2]. Ensure any explicit "id" schema does not set Required: true [2].

Citations:


Do not define the data source’s lookup argument as Terraform’s id field.

id is reserved by the SDK for state identity; in a data source it is added implicitly as Optional: true/Computed: true and a custom Required: true schema field conflicts with that behavior. Use project_id for the lookup input and keep the composite tenant_id/project_id identity separate.

Proposed fix
-			"id": {
+			"project_id": {
 				Type:        schema.TypeString,
 				Required:    true,
 				Description: "Slug of the project to look up.",
 			},
...
-	id := d.Get("id").(string)
+	projectID := d.Get("project_id").(string)

-	project, err := c.GetProjectByID(ctx, tenantID, id)
+	project, err := c.GetProjectByID(ctx, tenantID, projectID)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"id": {
Type: schema.TypeString,
Required: true,
Description: "Slug of the project to look up.",
},
"project_id": {
Type: schema.TypeString,
Required: true,
Description: "Slug of the project to look up.",
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/datasources/project.go` around lines 31 - 35, Rename the required
lookup schema field from “id” to “project_id” in the project data source schema,
and update the associated lookup logic to read project_id. Preserve the separate
composite tenant_id/project_id state identity and do not redefine Terraform’s
implicit id field.

Comment thread main.go
func main() {

plugin.Serve(&plugin.ServeOpts{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove unintended new lines?

Comment thread dc-api/dc-api

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the binary

Comment thread internal/client/tenant.go
// included here with omitempty so GetTenantByID's list-and-scan still captures it without
// affecting the Create/Update unmarshal, which simply leaves it empty.
type TenantResponse struct {
ID string `json:"id"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why there is ID and UUID both?

@rajithacharith

Copy link
Copy Markdown

let's add golint for this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants