All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
pcd_host_cluster_role: importing a role no longer leaves a permanentwait_until_convergeddiff. Import set onlyid,host_idandrole, so the flag stayed null and the schema default plannednull -> falseon every imported role; applying that re-PUT the role assignment to resmgr for a change resmgr never sees. Import now writes the default, and an update whose only change iswait_until_convergedstores the value without calling resmgr and does not wait for convergence — including afalse -> trueflip on an already-assigned role, which previously PUT the role again and then waited. To block on an already-assigned role, taint it or runterraform apply -replace.host_clusterandbackendsare still not read back, so a configuration that sets them on an imported role plans the update it always did.
- Every import now says where its ID comes from (PCD-9784). Each resource's Import section
names the
pcdctlcommand (or resource-manager call) that prints the ID it needs, and a new Importing guide covers CLI setup,terraform importversusimportblocks with generated configuration, a per-resource lookup table, the resmgr host UUID, and a worked blueprint import. - A runnable Community Edition example at
examples/complete/community-edition/, rendered by the new Community Edition guide: from one prepared host to a running instance with an NFS-backed volume attached, in one apply. Validated end to end on a Community Edition 2026.4 lab. pcd_blockstorage_volume_typeandpcd_cluster_blueprintexplain howvolume_backend_name,storage_backends_json,image_library_storage, andpcd_host_cluster_role.backendsfit together; the README's PCD-native table addspcd_clusterandpcd_host_cluster_role.- Corrected
pcd_host_cluster_role.backends: it lists the second-level keys ofstorage_backends_json(the driver configuration names), not the top-level backend names as the description said. The top-level key is what becomesvolume_backend_nameon the host. Naming a top-level key inbackendsyields an empty backend set,cinder-volumecannot start, and the host is stuck converging while resmgr refuses every role change (409). The mistake was invisible while every example used the same name at both levels.
pcd_cluster_blueprint:terraform destroynow deletes the blueprint from PCD (PCD-9783). Destroy used to be a no-op that only dropped the resource from state, so Terraform reported the blueprint destroyed while it lived on in the region. It now issuesDELETE /resmgr/v2/blueprint/<name>; a blueprint that is already gone is success, and any other refusal fails the destroy instead of being hidden. Destroy whatever depends on the blueprint first (apcd_host_configreferencing it throughcluster_name, and the clusters and host roles built on it). To stop managing an imported blueprint without deleting it, useterraform state rm.
- Bumped the indirect
google.golang.org/grpcdependency to 1.83.1 (GHSA-vp52-pcj8-j9qc: heap memory exhaustion via HTTP/2 DATA frame fragmentation). As with the previous gRPC advisory, the provider's gRPC server only ever serves the local Terraform CLI over a private channel, so exposure was minimal.
pcd_host_config_assignment,pcd_host_cluster_roleandpcd_host_roleno longer disappear from state while a host is being deauthorised. resmgr answers the per-host endpoints with404for minutes after a host's last role is removed, whileGET /resmgr/v2/hostskeeps reporting the host, its roles and itshostconfig_idthroughout; the reads believed the404and removed resources that still existed, and the next apply then failed against the reality they never left —409 HostToHostconfigConflictre-creating an assignment,403 HostInAuthStatere-adding a role. A404is now checked against the host list before anything leaves state, and an unreadable list is an error rather than an absence. An ordinary read still costs one request.
- Bumped the indirect
google.golang.org/grpcdependency to 1.82.1 (GHSA: gRPC-Go xDS RBAC and HTTP/2 vulnerabilities). The provider's gRPC server only ever serves the local Terraform CLI over a private channel and does not use xDS, so exposure was minimal.
pcd_host_config: destroying one is now refused while any host is still assigned to it, and the check is fail-closed — if resmgr will not answer, the delete does not proceed. PCD accepts that delete, keeps the assignment, and from then on refuses to remove it (404) and refuses every re-assignment (409HostToHostconfigConflict); deleting the host record does not clear it and the id cannot be re-created, so the host can never be assigned a host configuration — never onboarded — again. Remove thepcd_host_config_assignmentfirst. The guard will be lifted once resmgr refuses the unsafe delete itself.pcd_host_config_assignment: destroying one now confirms the host has actually stopped reporting the host configuration instead of trusting the204, which resmgr returns whether or not it unbound anything. A binding Terraform believed was gone is what left a host strandable.
pcd_cluster,pcd_cluster_blueprint,pcd_host_configand the host reads behindpcd_host_config_assignment/pcd_host_role: an object resmgr no longer has is now removed from state. resmgr answersGETfor a deleted cluster with200and a body ofnullrather than a404; that decoded into a zero-value struct with no error, so Read reported the resource as present-but-blank and never calledRemoveResource— leaving Terraform convinced a destroyed region still existed and unable to rebuild it. A 200 that describes no object is now absence.pcd_compute_instance:availability_zone = "<az>:<host>"(admin host pin) no longer fails with "inconsistent result after apply" — the pin is preserved on read, as upstream does.
pcd_compute_instance:scheduler_hintsblock (group,different_host,same_host,additional_properties) — upstream parity; makespcd_compute_servergroupusable for affinity / anti-affinity placement.pcd_compute_instancegainsblock_device— Novablock_device_mapping_v2, mirroringopenstack_compute_instance_v2so configurations port unchanged. This unlocks every boot source the PCD UI's Deploy VM wizard offers beyond "Image": boot from a new volume (source_type = "image",destination_type = "volume",volume_size,volume_type,delete_on_termination— the wizard's default), an existing volume, a volume snapshot, and install from ISO (a blank root volume atboot_index = 0plus the ISO as adevice_type = "cdrom"volume atboot_index = 1).image_id/image_namebecome optional when ablock_devicewithboot_index = 0supplies the root disk. Extra data disks at boot areboot_index = -1. Create-only, like upstream; usepcd_compute_volume_attachfor day-2 attach/detach. When block devices are present the create call negotiates compute API microversion 2.67 (required forvolume_typein a block-device mapping; PCD 2026.4 serves up to 2.100); every other call keeps the provider's default so read paths are unchanged. Verified live against a 2026.4 CE: all four boot sources reach ACTIVE, the ISO path presents the installer as an IDEcdromdevice with the blank target on virtio.pcd_compute_instancegainsmigration_priority— how PCD's Dynamic Resource Rebalancing (DRR) service treats the VM:normal,low,high, ornever(excluded). Stored as themigration-priorityserver-metadata key, exactly as the PCD UI's "Set Migration Priority" dialog does; updatable in place,""clears. The key is reserved and kept out ofmetadataon read, so the two attributes never drift against each other, and configuring it directly inmetadatais rejected.pcd_networking_networkgainsport_security_enabled(defaulttrue), mirroringopenstack_networking_network_v2. Setting itfalseis what makes a Layer 2 / "Simple" network (the PCD UI's Simple Networking option: no subnet, no DHCP, no security groups — VMs manage their own addressing). Together withsegmentsand thesimple_networktag the UI keys on, an L2 network is now fully expressible in one resource. Note that Nova refuses to boot on a subnet-less network by network id ("requires a subnet in order to boot instances on"): attach the instance through apcd_networking_porton the network instead — the L2 model anyway. The example shows both halves. Also read back by thepcd_networking_networkdata source.
pcd_cluster_blueprintsilently acceptedvm_high_availabilityandauto_resource_rebalancing. These are cluster-scoped settings: the blueprint API does not store them (a POST carrying them returns an object without either key), and the PCD UI never sends them. A user who set them on the blueprint believed HA/DRR were enabled when nothing had happened. Both attributes are removed; they live onpcd_cluster, where they take effect. Not a breaking change for configurations that omitted them.networking_typeandenable_distributed_routingare now read-only. No product surface exposes them (the PCD UI hardcodesovn/true;pcdctlhas no blueprint capability), yet the API requires both on create with no server default. The provider now supplies the product values itself and rejects attempts to configure them, so a Terraform user cannot put a region into a state the UI would never produce.
pcd_cluster_blueprint.vnc_floating_ip(and on the data source): the floating IP through which VM VNC consoles are reached. Set""to clear. Works around a resmgr quirk wherePOST /v2/blueprintsilently discardsvncFloatingIpand onlyPUTpersists it — on create the provider follows the POST with a PUT when the value is set.
instance_shared_storagedocumentation now matches the UI toggle it maps to ("Enable if this path is mounted as shared storage (e.g. NFS) across all hosts") and referencesvm_storage, so the flag is discoverable next to the path it qualifies.
pcd_clusterfailed with "inconsistent result after apply" wheneverauto_resource_rebalancingwas set with arebalancing_strategy, and a partial block such as{ enabled = false }persisted""/0on the server. resmgr does no server-side normalization: it stores exactly what it is sent and applies its defaults only to absent keys, and the request marshalled Go zero values for every leaf the user did not configure. Unconfigured optional leaves are now omitted from the request so the server applies its own defaults, and read-back keeps configured values while adopting server values only for leaves the config left unset. All three shapes — partial, full, and omitted — now apply cleanly and plan with no changes, including after a forced refresh and an in-place update.
pcd_host_rolecould not assign a role at all. Create and Delete issuedPUT/DELETE /resmgr/v2/hosts/<id>/roles/<name>, but resmgr exposes no writable roles sub-resource on v2 and answers404 RoleNotFound; role assignment lives only on v1. Everypcd_host_roleapply failed against PCD 2026.4. The resource now uses a v1 client. Blueprints and host configs are unaffected — they exist only on v2 (/resmgr/v1/blueprintand/resmgr/v1/hostconfigsboth 404) and continue to use it.pcd_host_rolereported permanent drift, which would have persisted even once the write path was fixed. Read compared the configured role name againstGET /v2/hosts/<id>, whoserolesare mapped "uber-roles" (hypervisor) rather than the granularpf9-*names a role is assigned by, so the match never succeeded and Terraform removed the resource from state and recreated it on every plan. Read now uses v1, which reports granular names.pcd_cluster_blueprintshowed astorage_backends_jsondiff on every plan. resmgr echoes the blob with its own spacing and in insertion order, whilejsonencode()emits compact output with sorted keys — semantically identical, textually different, so Terraform reported an in-place update that never converged. The read-back is now canonicalised (compact, keys sorted) in both the resource and the data source.
pcd_host_cluster_role.wait_until_convergedcould return early during onboarding.role_statusaggregates only the roles assigned at that moment, so while several cluster roles were being assigned concurrently there was a window where it readokbefore the others landed — un-gating downstream resources (an image upload against a Glance that was not serving yet). The wait now also requires the cluster role's own granular marker (e.g.pf9-glance-roleforimage-library) to report applied.pcd_clustercreation failed on freshly deployed regions. resmgr answers500 Request FailedtoPOST /v2/clustersuntil the compute control plane is warm (the PCD UI health-checks Nova before offering the dialog); the identical request succeeds minutes later. Create now retries 500s for a bounded window so a single apply can bring up a region from nothing.
- New resource
pcd_host_cluster_role— assigns PCD cluster roles (hypervisor,image-library,persistent-storage,dns) via the resmgr v2 uber-role API, the same call the PCD UI onboards hosts with. The control plane expands a cluster role into its granularpf9-*roles and computes their settings from the cluster blueprint and the host's host configuration (persistent-storagetakes abackendslist naming entries in the blueprint'sstorage_backends_json;hypervisortakeshost_cluster, which PCD 2026.4 requires). An optionalwait_until_convergedblocks until the host reportsrole_status = ok— tolerating the transientfailedflaps normal onboarding produces — so a single configuration can onboard a hypervisor and boot instances on it in one apply. Assignment and removal retry through resmgr's transient409 RoleUpdateConflictwhile a host is converging. This closes the gap that made a fresh region impossible to bring up with Terraform alone:pcd_host_roleapplies granular roles with default settings, which wedges the host on settings-bearing roles (see its documentation for when it is still appropriate). - New resource
pcd_cluster— manages PCD clusters (host clusters / host groups), the unit hypervisors join and the scope for VM high-availability, auto-rebalancing, GPU, and CPU-model settings. Required bypcd_host_cluster_role'shypervisorrole, whosehost_clusternames it. Config.ResmgrV1Client()alongsideResmgrV2Client(). Anendpoint_overridesentry forresmgrnow names the service rather than one of its API versions: the required version is applied to it, replacing any version the override already carries, so a single override serves both clients.pcd_networking_networkgainssegments— provider-network attributes (admin only), mirroringopenstack_networking_network_v2. A single segment creates a physical network (network_typeflat/vlanon aphysical_networklabel, optionalsegmentation_id), sent as top-levelprovider:*attributes; multiple segments use Neutron's multi-providersegmentsform. Create-only and not refreshed from the API, matching the upstream provider's behavior. Without this, provider networks — including any external network — could not be created by Terraform at all.
- Drop the stale "pre-release, not yet published to the Terraform Registry" status note from
the README — the provider has been published at
platform9/pcdsince v0.1.0. Docs-only; no provider behavior change.
- Render the provider name as PCD (not
pcd) in generated documentation titles — the overview page heading is now "PCD Provider" and each resource/data-source page title reads "… - PCD". Done viatfplugindocs --rendered-provider-name PCD(wired intomake generateand the CI docs job). Resource/data-source type names (pcd_*) are unchanged, as is the provider's registry addressplatform9/pcd. Docs-only; no provider behavior change.
- Commit the generated registry documentation (
docs/) so the Terraform Registry renders the provider's resource, data-source, and guide pages. v0.1.0 shipped without a committeddocs/tree — the Registry builds documentation from the repository at the release tag, so its "Documentation" tab was empty. The docs are now generated withtfplugindocs(make generate) from the schema descriptions,examples/, andtemplates/, and committed. No provider behavior change.
Initial public release: a first-party Terraform provider for Platform9 Private Cloud
Director, covering the OpenStack services PCD exposes plus PCD-native cluster/host
management (resmgr) that has no OpenStack-provider equivalent.
-
Initial provider scaffold (
terraform-plugin-framework, protocol 6), modulegithub.com/platform9/terraform-provider-pcd, registry addressplatform9/pcd. -
Provider configuration schema with
OS_*environment fallbacks: password, token, and application-credential auth; self-signed TLS support (insecure,cacert_file,cert/key);region,endpoint_overrides, and Keystone v3 domain/project scoping. -
gophercloud v2 client wiring (
internal/clients) with a shared, authenticatedConfighanded to every resource and data source. -
Data source
pcd_identity_auth_scope(ported fromopenstack_identity_auth_scope_v3) — reports the current token's user, project, domain, and roles. -
Verified CE 2026.4 compatibility from Step 0 preflight.
-
Acceptance test harness (
internal/acctest: protocol-6 provider factory +PreCheck) and the first acceptance test forpcd_identity_auth_scope(passes against the CE lab). -
test.ymlCI: build, vet, gofmt, unit tests, andterraform fmton examples. -
Identity (Keystone v3) resources:
pcd_identity_project,pcd_identity_role,pcd_identity_user,pcd_identity_role_assignment,pcd_identity_application_credential. -
Identity data sources:
pcd_identity_project,pcd_identity_user,pcd_identity_role. -
Identity groups (close api-docs coverage):
pcd_identity_groupresource + data source, andpcd_identity_group_membership(one user↔group pair per resource, mapping toPUT/DELETE /groups/{id}/users/{user_id}). -
Images (Glance v2):
pcd_images_imageresource (local-file upload + web-download import, status waiter, checksum verify, unprotect-before-delete, settable custompropertiesmetadata) andpcd_images_image/pcd_images_image_idsdata sources. -
Networking (Neutron v2): resources
pcd_networking_network,_subnet,_secgroup,_secgroup_rule,_router,_router_interface; data sourcespcd_networking_network,_subnet,_secgroup. -
Networking extras:
pcd_networking_port(fixed IPs, security groups, allowed-address pairs, tags) andpcd_networking_floatingip(allocate from an external network bypoolname, associate/disassociate to a port); data sourcespcd_networking_port,_port_ids,_router,_subnet_ids, and_floatingip. Ports and floating IPs are code-complete with acceptance tests. -
Networking route and association resources:
pcd_networking_router_routeandpcd_networking_subnet_route(manage a single static/host route without disturbing others, serialized per parent to avoid clobbering);pcd_networking_port_secgroup_associate(attach security groups to an unmanaged port, shared or exclusive viaenforce); andpcd_networking_floatingip_associate(bind a pre-allocated floating IP to a port). -
Compute (Nova v2): resources
pcd_compute_keypair,pcd_compute_flavor,pcd_compute_servergroup,pcd_compute_instance(boot, in-place resize, import); data sourcespcd_compute_flavor,pcd_compute_keypair,pcd_compute_availability_zones. The instance resource reads back all server-computed fields (availability zone, security groups, network) after apply, and only pushes metadata when the user manages it. -
Compute follow-ups:
pcd_compute_flavorgains settableextra_specs(added/changed/ removed in place; the flavor's other attributes are now correctly immutable);pcd_compute_instancesupports in-place resize on a flavor change and booting byimage_name(resolved via Glance, alternative toimage_id); new resourcespcd_compute_interface_attach(attach a port/network to a server) andpcd_compute_volume_attach(attach a Cinder volume to a server). -
Block storage (Cinder v3):
pcd_blockstorage_volumeresource (create/extend/import; code-complete — acceptance is blocked on the CE lab having no storage backend) andpcd_blockstorage_volume/pcd_blockstorage_snapshotdata sources. -
Block storage gap resources (close api-docs coverage):
pcd_blockstorage_volume_type(name/description/is_public/extra_specs, with in-place spec add/change/remove via the extra-specs sub-API — needs no storage backend),pcd_blockstorage_snapshot(was data-source-only; now a managed resource with async wait-for-available), andpcd_blockstorage_volume_backup(backup/restore lifecycle, async waiter). -
Load balancing (Octavia v2) — Phase 3:
pcd_lb_loadbalancer,pcd_lb_listener,pcd_lb_pool,pcd_lb_member,pcd_lb_monitorresources and apcd_lb_loadbalancerdata source. Every child operation resolves the root load balancer and waits for itsprovisioning_statusto return toACTIVEbefore and after mutating (Octavia serializes changes per load balancer). PCD ships the OVN provider only, which is L4 (TCP/UDP/SCTP); use L4 listener protocols and OVN-supported pool algorithms. L7 policy/rule resources are omitted because the OVN provider does not support L7.pcd_lb_loadbalancerexposesloadbalancer_provider(defaults toovn) — required because Octavia's server-side default provider isamphora, which PCD does not enable. -
DNS (Designate v2) — Phase 3:
pcd_dns_zoneandpcd_dns_recordsetresources plus apcd_dns_zonedata source. Zone and recordset create/update/delete are asynchronous, so applies wait for the object to reachACTIVE(and to disappear after delete). -
Key management (Barbican v1) — Phase 3:
pcd_keymanager_secret(write-only, sensitivepayload) andpcd_keymanager_container(grouped secrets) resources plus apcd_keymanager_secretdata source (optionally fetches the payload). Barbican identifies objects by URL refs; the resources expose the full ref and use the bare UUID as the ID. -
Network QoS (Neutron
qosextension) — Phase 3:pcd_networking_qos_policyand its three rule types —pcd_networking_qos_bandwidth_limit_rule,pcd_networking_qos_dscp_marking_rule,pcd_networking_qos_minimum_bandwidth_rule— plus apcd_networking_qos_policydata source. Rules are nested under a policy and imported by a composite<qos_policy_id>/<rule_id>ID. -
Project quotas — Phase 3:
pcd_compute_quotaset(Nova),pcd_networking_quota(Neutron), andpcd_blockstorage_quotaset(Cinder). Each manages the per-project quota limits for its service; only the fields you set are managed (omitted fields keep their server value), and destroying the resource stops managing the quotas without resetting them to defaults (matching the upstream provider). Imported by a composite<project_id>/<region>ID (legacy bare<project_id>is also accepted). Cinder per-volume-type quotas (volume_type_quota) are not yet implemented. -
Cluster blueprint / host management (PCD
resmgrAPI — the first non-OpenStack, non-gophercloud service): a thinresmgrv2 REST client (clients.Config.ResmgrV2Client(), endpoint resolved from the Keystone catalog, token via the shared ProviderClient) pluspcd_host_config(interface ↔ traffic-type mapping and physical-network labels),pcd_host_role(assign a role such aspf9-ostackhost-neutronto a host),pcd_host_config_assignment(attach a host config to a host), and apcd_cluster_blueprintdata source (read a blueprint by name). -
pcd_cluster_blueprintresource — manage a cluster blueprint (networking, image library, VM storage, HA/rebalancing, and Cinder backends). PCD supports one blueprint per region, so the usual workflow is toterraform importthe existing blueprint and manage it in place (in-place update verified live). The write API requires the whole object, so all attributes areOptional+Computedand round-tripped;storage_backends_jsonis sensitive (driver credentials) and is read back so writes preserve the current backends unless you change it. -
Registry documentation generation wired via
tfplugindocs(make generate) — rendersdocs/for every resource and data source plus the provider index from schema descriptions. Generated docs are produced on demand / at release and are not committed. -
CI:
golangci-lint(v2) and a docs-generation smoke test added totest.yml. -
Documentation examples and guides: a self-contained Terraform
examples/snippet for every resource and data source (withimport.shfor importable resources), rendered as the "Example Usage" and "Import" sections;templates/add a subcategory to each page so the registry groups resources by service (Identity, Images, Networking, Compute, Block Storage); and two guides — Authentication and Migrating from terraform-provider-openstack (authored intemplates/guides/). -
Provider
cloud(clouds.yaml) support: whencloud(orOS_CLOUD) is set, auth defaults are sourced from aclouds.yamlentry (searched at$OS_CLIENT_CONFIG_FILE,./clouds.yaml,~/.config/openstack/clouds.yaml,/etc/openstack/clouds.yaml). Precedence is explicit config >OS_*env >clouds.yaml.
max_retries/ retry transport and per-resourceregionoverride are stubs pending Phase 1.