Skip to content

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

Open
sameera-87 wants to merge 1 commit 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 1 commit 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; this is the first contribution, shipping the core resource set as 0.1.0.

Behaviour

  • Implemented set (14 of 19 resources): the Tenant → Project → VNet → Subnet → VirtualMachine hierarchy, plus Cluster/NodePool, RouteTable/RouteTableAssociation, NetworkSecurityGroup/NSGAttachment, VNetPeering, Bastion, and ServiceAccount. All support full CRUD plus import.
  • Async lifecycle (202 + poll): create polls 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 delete while child subnets still exist is surfaced as an actionable error rather than a silent retry.
  • Credential resolution, not storage: credentials resolve at runtime via provider block → DCAPI_TOKENdcctl credentials file; endpoint from DCAPI_ENDPOINT. The provider never becomes a second store of long-lived secrets.
  • State and dependencies: parent IDs (tenant_id, project_id) are persisted into state so attribute references resolve and Terraform Core can infer create/destroy ordering. Sensitive outputs (private_key, console_password) are marked sensitive.
  • 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 / dcctl).
  • docs/examples: generated resource docs and a runnable example covering the tenant → project → vnet → subnet → vm flow with outputs.

Out of scope

The five deferred resources — PrivateEndpoint, KeyVault, PrivateDnsZone, DnsRecord, TenantMember — deliberately aren't implemented here; they follow in subsequent PRs once the core hierarchy is merged. The SDKv2 → Plugin Framework migration (planned via terraform-plugin-mux) is also a separate, later change.

Verification

Race-enabled unit tests using httptest to fake the DC-API server (DTO decode, CRUD handler logic, state surfacing). Acceptance tests covering create/read/update/delete/import were run against a local DC-API server (make run). The full acceptance suite against a shared/staging DC-API instance wasn't run here.

Summary by CodeRabbit

  • New Features

    • Added a full Terraform provider for DC API with support for tenants, projects, networks, clusters, VMs, bastions, service accounts, route tables, and security groups.
    • Added end-to-end examples for common deployment flows, including VM, cluster, subnet, VNet, bastion, and service account setups.
    • Added user-facing documentation covering provider configuration, resource behavior, and usage patterns.
  • Bug Fixes

    • Improved handling of asynchronous create/delete operations and resource drift detection during refresh.

@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 (terraform-provider-dcapi) for the DC-API. It adds the Go module, HTTP client with error handling, CRUD resource implementations for 13 resource types (tenant, project, vnet, subnet, VM, bastion, cluster, node pool, NSG, NSG attachment, route table, route table association, service account), provider wiring, usage examples, and documentation.

DC-API Terraform Provider

Layer / File(s) Summary
Module bootstrap and entry point
go.mod, GNUmakefile, main.go
go.mod declares the module with terraform-plugin-sdk/v2; GNUmakefile provides build/install/clean targets; main.go wires plugin.Serve to provider.New.
HTTP client core
internal/client/client.go
DCAPIClient struct and NewClient constructor; doRequest handles bearer auth, JSON encoding, and non-2xx errors including quota-exceeded 400 parsing with detailed CPU/memory/storage fields.
Resource-specific client DTOs and methods
internal/client/tenant.go, internal/client/project.go, internal/client/vnet.go, internal/client/subnet.go, internal/client/vm.go, internal/client/bastion.go, internal/client/cluster.go, internal/client/node_pool.go, internal/client/nsg.go, internal/client/route_table.go, internal/client/service_account.go
Request/response structs and CRUD methods for all resource types; all methods call doRequest and treat HTTP 404 errors as (nil, nil) drift signals.
Provider schema and configuration wiring
internal/provider/provider.go
New() builds the schema.Provider with endpoint/token fields and ResourcesMap registering all 13 resource types; configureProvider validates credentials and returns a DCAPIClient as provider meta.
Tenant, project, vnet, subnet, and VM resources
internal/resources/tenant.go, internal/resources/project.go, internal/resources/vnet.go, internal/resources/subnet.go, internal/resources/vm.go
CRUD handlers: tenant delete is state-only; project supports partial quota update; vnet and subnet use async polling (202 + StateChangeConf) and convert 409 to descriptive errors; VM validates networking mode exclusivity and preserves shown-once private_key/console_password across reads.
Bastion, cluster, node pool, NSG, route table, and service account resources
internal/resources/bastion.go, internal/resources/cluster.go, internal/resources/node_pool.go, internal/resources/nsg.go, internal/resources/nsg_attachment.go, internal/resources/route_table.go, internal/resources/route_table_association.go, internal/resources/service_account.go
CRUD implementations for remaining resource types: bastion/cluster use async polling with kubeconfig fetch; node pool supports full-replace taints/labels on update; NSG uses full-replace rules via PUT; route table association reads by scanning parent route table's association list; service account preserves token from create across reads.
Usage examples
examples/*/main.tf, test/main.tf
HCL examples for each resource type showing chained resource dependencies (tenant → project → vnet → subnet → VM/bastion/cluster) and sensitive output handling.
Architecture and API reference docs
Architecture.md, docs/dc-api-reference.md
Architecture.md documents plugin wiring, async lifecycle, composite state ID encoding, and development checklist; docs/dc-api-reference.md covers all endpoint specs, field classifications, async polling semantics, and Terraform implementation notes for all 20+ resource types.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 A brand new provider hops into the scene,
With tenants and projects and VMs so keen,
We POST and we PATCH and we poll for ACTIVE,
Secrets are one-time — keep your state native!
From bastion to cluster, the stack stands tall,
This bunny built terraform to manage it all! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly identifies the Terraform provider addition, despite a minor typo.
Description check ✅ Passed The description covers summary, changes, testing, and scope, but it doesn't follow the template headings or include the checklist.
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.
✨ 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
Login endpoint: GET /v1/auth/login (browser/BFF flow only — not suitable for provider use)
```

**For Terraform**: use a service account token. It is long-lived, never expires unless the SA is deleted, and requires no OIDC flow. Create a dedicated SA with at minimum `member` role; use `owner` role only if the provider needs to manage members or service accounts.

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

Remove unsupported provider arguments from the auth example and clarify the required credential scope.

Lines 2185-2186 document tenant_id and project_id as provider arguments, but every other provider example in this PR only accepts endpoint/token; users copying this block will hit unsupported-argument errors. This section also overstates the “member service account” guidance even though Line 151 documents tenant operations as admin-only.

Proposed fix
 provider "dcapi" {
   endpoint   = "https://dcapi.example.com"
   token      = var.dcapi_sa_token   # dcapi_sa_<lookup_id>_<secret>
-  tenant_id  = "my-org"
-  project_id = "my-project"
 }

Also applies to: 151-151, 2177-2187

🤖 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 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.

Comment thread GNUmakefile
Comment on lines +4 to +12
BINARY = terraform-provider-dcapi
INSTALL_DIR = $(HOME)/.terraform.d/plugins/registry.terraform.io/wso2/dcapi/0.1.0/$(OS)_$(ARCH)

build:
go build -o $(BINARY) .

install: build
mkdir -p $(INSTALL_DIR)
cp $(BINARY) $(INSTALL_DIR)/$(BINARY)

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

printf 'Repo files of interest:\n'
git ls-files | rg '(^|/)(GNUmakefile|README|go\.mod|main\.go|provider|terraform)' || true

printf '\nGNUmakefile:\n'
cat -n GNUmakefile

printf '\nSearch for provider name/version references:\n'
rg -n "terraform-provider-dcapi|registry\.terraform\.io|filesystem mirror|mirror|terraform init|v0\.1\.0|versioned provider" .

Repository: wso2/open-cloud-datacenter

Length of output: 4695


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'README install-related excerpt:\n'
sed -n '35,130p' README.md | cat -n

printf '\nArchitecture install-related excerpt:\n'
sed -n '100,130p' Architecture.md | cat -n

printf '\nProvider package metadata:\n'
sed -n '1,220p' main.go | cat -n

Repository: wso2/open-cloud-datacenter

Length of output: 6055


🌐 Web query:

Terraform filesystem mirror provider binary filename versioned executable local install "terraform-provider-" "_v" registry.terraform.io filesystem_mirror

💡 Result:

To install a Terraform provider via a local filesystem mirror, you must follow a specific directory structure and binary naming convention that Terraform CLI expects [1][2][3]. Directory Structure The filesystem mirror must adhere to a rigid path layout that mirrors the registry address [2][3]. The required structure is: /////_/ Example: /opt/terraform/providers/registry.terraform.io/hashicorp/aws/5.31.0/linux_amd64/ Binary Naming Convention Inside the version/platform directory, the provider executable must follow this naming pattern [3][4]: terraform-provider-_v For example, a provider for the "aws" service at version "5.31.0" would be named: terraform-provider-aws_v5.31.0 If the provider is for Windows, the filename must also include the appropriate extension (e.g., terraform-provider-aws_v5.31.0.exe) [3]. Important Implementation Notes: 1. File Packaging: Terraform typically expects providers in a filesystem mirror to be provided as.zip archives [1][2]. However, Terraform CLI can also discover and use executable binaries placed directly in the correct directory if they follow the required naming convention [3][4]. 2. Configuration: You must configure the provider_installation block in your CLI configuration file (~/.terraformrc or terraform.rc) to point to the root [5][6]: provider_installation { filesystem_mirror { path = "/opt/terraform/providers" include = ["registry.terraform.io//"] } } 3. Automation: The most reliable way to populate this directory is using the terraform providers mirror command, which automatically downloads the necessary providers and creates the correct directory structure and naming, including the required.zip archives [1][7][2]. Manual placement is generally reserved for custom or in-house providers [3][8].

Citations:


Rename the installed provider binary to Terraform’s versioned name.

install copies terraform-provider-dcapi into the mirror path, but Terraform expects terraform-provider-dcapi_v0.1.0 there, so terraform init won’t discover the plugin. Adjust the copied filename to include the version.

🤖 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 `@GNUmakefile` around lines 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.

Comment thread go.mod
Comment on lines +70 to +78
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/grpc v1.63.2 // indirect

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod excerpt =="
nl -ba go.mod | sed -n '60,90p'

echo
echo "== dependency references =="
rg -n 'golang.org/x/crypto|google.golang.org/grpc|terraform-plugin-sdk/v2' go.mod go.sum

echo
echo "== module graph (if available) =="
if command -v go >/dev/null 2>&1; then
  go list -m all 2>/dev/null | rg 'golang.org/x/crypto|google.golang.org/grpc|terraform-plugin-sdk/v2' || true
else
  echo "go tool not available"
fi

Repository: wso2/open-cloud-datacenter

Length of output: 225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod excerpt =="
sed -n '60,90p' go.mod

echo
echo "== dependency references in go.mod/go.sum =="
grep -nE 'golang.org/x/crypto|google.golang.org/grpc|terraform-plugin-sdk/v2' go.mod go.sum || true

echo
echo "== available go toolchain check =="
command -v go >/dev/null 2>&1 && go version || echo "go tool not available"

Repository: wso2/open-cloud-datacenter

Length of output: 2404


🌐 Web query:

golang.org/x/crypto v0.23.0 advisory OSV vulnerability google.golang.org/grpc v1.63.2 advisory OSV vulnerability

💡 Result:

Regarding the specified versions of the Go modules, both are associated with historical security advisories tracked in the OSV database. golang.org/x/crypto v0.23.0 This version is listed on pkg.go.dev as having numerous security advisories [1]. While v0.23.0 is a specific point-in-time release, it is affected by several identified vulnerabilities documented in the OSV database, such as GO-2024-3321 (related to CVE-2024-45337, an authorization bypass in SSH server configurations) [2][1] and GO-2026-5023 (an issue involving permissions skip in VerifiedPublicKeyCallback) [3]. Because security patches are ongoing, users are encouraged to consult the official OSV reports linked on the package's pkg.go.dev page to determine if their specific usage is impacted and to update to the latest available version [1]. google.golang.org/grpc v1.63.2 This version is associated with security vulnerabilities tracked in the OSV database, including GHSA-p77j-4mvh-x3m3 (also identified as CVE-2026-33186) [4][5][6]. This specific vulnerability is an authorization bypass resulting from improper input validation of the HTTP/2:path pseudo-header [7][8]. The issue affects gRPC-Go servers that use path-based authorization interceptors and have security policies with specific "deny" rules [7][6]. The vulnerability was addressed in version 1.79.3, which ensures requests missing a leading slash in the:path are rejected [7][6][8]. Users of v1.63.2 should upgrade to 1.79.3 or later to mitigate this risk [7][8].

Citations:


Refresh the dependency graph before release.

golang.org/x/crypto v0.23.0 and google.golang.org/grpc v1.63.2 are still pinned in go.mod and are associated with OSV advisories. Upgrade the affected modules (likely via terraform-plugin-sdk/v2) and run go mod tidy before shipping.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Misuse of connection.serverAuthenticate may cause authorization bypass in golang.org/x/crypto

(GO-2024-3321)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Potential denial of service in golang.org/x/crypto

(GO-2025-3487)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Potential denial of service in golang.org/x/crypto/ssh/agent

(GO-2025-4116)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Unbounded memory consumption in golang.org/x/crypto/ssh

(GO-2025-4134)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Malformed constraint may cause denial of service in golang.org/x/crypto/ssh/agent

(GO-2025-4135)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking key constraints not enforced in golang.org/x/crypto/ssh/agent

(GO-2026-5005)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking agent constraints dropped when forwarding keys in golang.org/x/crypto/ssh/agent

(GO-2026-5006)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking byte arithmetic causes underflow and panic in golang.org/x/crypto/ssh

(GO-2026-5013)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking bypass of certificate restrictions in golang.org/x/crypto/ssh

(GO-2026-5014)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking server panic during CheckHostKey/Authenticate in golang.org/x/crypto/ssh

(GO-2026-5015)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking memory leak when rejecting channels can lead to DoS in golang.org/x/crypto/ssh

(GO-2026-5016)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking client can cause server deadlock on unexpected responses in golang.org/x/crypto/ssh

(GO-2026-5017)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking pathological RSA/DSA parameters may cause DoS in golang.org/x/crypto/ssh

(GO-2026-5018)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking bypass of FIDO/U2F security keys physical interaction in golang.org/x/crypto/ssh

(GO-2026-5019)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking infinite loop on large channel writes in golang.org/x/crypto/ssh

(GO-2026-5020)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking auth bypass via unenforced @revoked status in golang.org/x/crypto/ssh/knownhosts

(GO-2026-5021)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking VerifiedPublicKeyCallback permissions skip enforcement in golang.org/x/crypto/ssh

(GO-2026-5023)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Invoking pathological inputs can lead to client panic in golang.org/x/crypto/ssh/agent

(GO-2026-5033)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: golang.org/x/crypto/ssh/agent vulnerable to panic if message is malformed due to out of bounds read

(GHSA-f6x5-jh6r-wrfv)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: golang.org/x/crypto Vulnerable to Denial of Service (DoS) via Slow or Incomplete Key Exchange

(GHSA-hcg3-q754-cr77)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: golang.org/x/crypto/ssh allows an attacker to cause unbounded memory consumption

(GHSA-j5w8-q4qc-rx2x)


[CRITICAL] 70-70: golang.org/x/crypto 0.23.0: Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass in golang.org/x/crypto

(GHSA-v778-237x-gjrc)


[CRITICAL] 78-78: google.golang.org/grpc 1.63.2: Authorization bypass in gRPC-Go via missing leading slash in :path in google.golang.org/grpc

(GO-2026-4762)


[CRITICAL] 78-78: google.golang.org/grpc 1.63.2: gRPC-Go has an authorization bypass via missing leading slash in :path

(GHSA-p77j-4mvh-x3m3)

🤖 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 `@go.mod` around lines 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.

Source: Linters/SAST tools

Comment thread internal/client/client.go
Comment on lines +6 to +14
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

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

Set a non-zero timeout on the shared HTTP client.

Every resource call funnels through doRequest, and this provider polls long-running operations. With http.DefaultClient, a hung TCP connection can block a single request indefinitely and stall plan/apply; give the client a bounded timeout or inject one from provider configuration.

Suggested fix
 import (
 	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"strings"
+	"time"
 )
@@
 func NewClient(baseURL, token string) (*DCAPIClient, error) {
 	return &DCAPIClient{
 		baseURL:    strings.TrimRight(baseURL, "/"),
 		token:      token,
-		httpClient: http.DefaultClient,
+		httpClient: &http.Client{Timeout: 30 * time.Second},
 	}, nil
 }

Also applies to: 29-34

🤖 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/client.go` around lines 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.

Comment on lines +28 to +35
// NodePoolUpdateRequest maps to PATCH .../clusters/{clusterID}/node-pools/{poolName}.
// Taints and Labels use full-replace semantics — send the complete desired state.
// An empty slice/map clears the existing values; omitting (nil) leaves them unchanged.
type NodePoolUpdateRequest struct {
Count int `json:"count"`
Taints []NodePoolTaint `json:"taints"`
Labels map[string]string `json:"labels"`
}

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

nil cannot mean “omit” with this request shape.

Lines 29-30 say nil taints/labels should leave server state unchanged, but encoding/json will serialize these fields as null, not omit them. A count-only update can therefore clear or invalidate taints/labels instead of leaving them untouched.

Suggested fix
 type NodePoolUpdateRequest struct {
-	Count  int               `json:"count"`
-	Taints []NodePoolTaint   `json:"taints"`
-	Labels map[string]string `json:"labels"`
+	Count  int                  `json:"count"`
+	Taints *[]NodePoolTaint     `json:"taints,omitempty"`
+	Labels *map[string]string   `json:"labels,omitempty"`
 }
📝 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
// NodePoolUpdateRequest maps to PATCH .../clusters/{clusterID}/node-pools/{poolName}.
// Taints and Labels use full-replace semantics — send the complete desired state.
// An empty slice/map clears the existing values; omitting (nil) leaves them unchanged.
type NodePoolUpdateRequest struct {
Count int `json:"count"`
Taints []NodePoolTaint `json:"taints"`
Labels map[string]string `json:"labels"`
}
// NodePoolUpdateRequest maps to PATCH .../clusters/{clusterID}/node-pools/{poolName}.
// Taints and Labels use full-replace semantics — send the complete desired state.
// An empty slice/map clears the existing values; omitting (nil) leaves them unchanged.
type NodePoolUpdateRequest struct {
Count int `json:"count"`
Taints *[]NodePoolTaint `json:"taints,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
}
🤖 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/node_pool.go` around lines 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.

Comment on lines +79 to +92
"next_hop_type": {
Type: schema.TypeString,
Required: true,
Description: "\"vnet_local\"|\"internet\"|\"virtual_appliance\"|\"none\".",
ValidateFunc: validation.StringInSlice([]string{
"vnet_local", "internet", "virtual_appliance", "none",
}, false),
},
"next_hop_ip": {
Type: schema.TypeString,
Optional: true,
Description: "Next-hop IP. Required when next_hop_type is \"virtual_appliance\".",
ValidateFunc: validation.IsIPAddress,
},

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

Enforce the virtual_appliance/next_hop_ip contract before apply.

The schema says next_hop_ip is required for virtual_appliance, but nothing validates that combination and Lines 245-247 only copy the field when it happens to be present. That lets a known-invalid route survive planning and fail later at apply time. Please reject missing next_hop_ip for virtual_appliance routes, and ideally reject it for the other hop types too.

Also applies to: 232-247

🤖 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/route_table.go` around lines 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.

Comment on lines +19 to +25
func ResourceServiceAccount() *schema.Resource {
return &schema.Resource{
// No UpdateContext — every field is ForceNew; changes require destroy + recreate.
CreateContext: resourceServiceAccountCreate,
ReadContext: resourceServiceAccountRead,
DeleteContext: resourceServiceAccountDelete,

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

printf '\n== service_account ==\n'
ast-grep outline internal/resources/service_account.go --view expanded || true
printf '\n== node_pool ==\n'
ast-grep outline internal/resources/node_pool.go --view expanded || true
printf '\n== nsg ==\n'
ast-grep outline internal/resources/nsg.go --view expanded || true
printf '\n== nsg_attachment ==\n'
ast-grep outline internal/resources/nsg_attachment.go --view expanded || true
printf '\n== route_table ==\n'
ast-grep outline internal/resources/route_table.go --view expanded || true
printf '\n== route_table_association ==\n'
ast-grep outline internal/resources/route_table_association.go --view expanded || true

printf '\n== importer search ==\n'
rg -n "Importer|ImportStatePassthroughContext|ImportState" internal/resources/{service_account.go,node_pool.go,nsg.go,nsg_attachment.go,route_table.go,route_table_association.go} || true

printf '\n== force-new fields and read IDs ==\n'
rg -n "ForceNew: true|Set\\(|Parse|Split|tenant_id|project_id|cluster_id|nsg_id|route_table_id|association_id" internal/resources/{service_account.go,node_pool.go,nsg.go,nsg_attachment.go,route_table.go,route_table_association.go} || true

Repository: wso2/open-cloud-datacenter

Length of output: 17095


Import support is missing for these resources

  • internal/resources/service_account.go, node_pool.go, nsg.go, nsg_attachment.go, route_table.go, and route_table_association.go do not set an Importer, so terraform import will fail.
  • Their Read handlers also do not restore every ForceNew parent field from the composite ID; for example, service_account repopulates tenant_id but not project_id, so imported state would still be incomplete.
🤖 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/service_account.go` around lines 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.

Comment on lines +51 to +67
"cpu_cores_cap": {
Type: schema.TypeInt,
Optional: true,
Default: 0,
Description: "CPU core quota ceiling. 0 = platform default (80). Updatable.",
},
"memory_gb_cap": {
Type: schema.TypeInt,
Optional: true,
Default: 0,
Description: "Memory quota ceiling in GB. 0 = platform default (256). Updatable.",
},
"storage_gb_cap": {
Type: schema.TypeInt,
Optional: true,
Default: 0,
Description: "Storage quota ceiling in GB. 0 = platform default (2000). Updatable.",

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

# Inspect the target file and nearby logic.
git ls-files internal/resources/tenant.go
wc -l internal/resources/tenant.go
cat -n internal/resources/tenant.go | sed -n '1,280p'

# Find related schema/state handling for these fields elsewhere.
rg -n '"cpu_cores_cap"|"memory_gb_cap"|"storage_gb_cap"|cpu_cores_cap|memory_gb_cap|storage_gb_cap' internal -S

# Inspect any helpers that might normalize defaults or suppress diffs.
rg -n 'Default: *0|StateFunc|DiffSuppress|CustomizeDiff|SetNew|ForceNew|Computed|Optional: *true' internal -S

Repository: wso2/open-cloud-datacenter

Length of output: 26981


Avoid perpetual diffs on defaulted quota caps. Default: 0 conflicts with writing the API-resolved 80/256/2000 values back into state, so omitted/zeroed configs will keep planning an update. Use Optional: true + Computed: true (or normalize/suppress the diff) for these fields.

🤖 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 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.

Comment on lines +229 to +233
// resourceTenantDelete removes the resource from Terraform state only.
// The DC-API has no delete endpoint for tenants — the real tenant continues to exist.
func resourceTenantDelete(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics {
d.SetId("")
return 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.

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

Delete should not succeed without deleting the remote tenant.

This makes terraform destroy drop the tenant from state while the real tenant keeps existing. That leaves an orphaned remote object, and a later apply of the same tenant_id is likely to fail on create. Until the API supports deletion, returning a diagnostic is safer than reporting success.

🤖 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 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.

Comment thread test/main.tf
Comment on lines +12 to +16
resource "dcapi_tenant" "test" {
id = "example-tenant"
name = "Example Tenant"
description = "Testing the Terraform provider"
}

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

Use tenant_id, not id, in the tenant resource.

Line 13 uses an argument name that does not match the rest of this PR's tenant examples/docs. As written, this config will fail with an unsupported argument instead of creating the tenant.

Proposed fix
 resource "dcapi_tenant" "test" {
-  id          = "example-tenant"
+  tenant_id   = "example-tenant"
   name        = "Example Tenant"
   description = "Testing the Terraform provider"
 }
📝 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
resource "dcapi_tenant" "test" {
id = "example-tenant"
name = "Example Tenant"
description = "Testing the Terraform provider"
}
resource "dcapi_tenant" "test" {
tenant_id = "example-tenant"
name = "Example Tenant"
description = "Testing the Terraform provider"
}
🤖 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 `@test/main.tf` around lines 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.

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.

2 participants