unifi_device_ports
What: build unifi_device_ports in the provider fork, release 0.56.0-fzymgc.2, then adopt
switch-main’s ports in tf/unifi as an import with 0 changes (#2118).
Design: docs/engineering/specs/2026-09-19-unifi-device-ports-design.md. Decisions D1 to D10
there are agreed and are not reopened here.
Riskiest assumption: that the demo-mode controller in the fork’s acceptance suite has a
simulated switch that accepts port_overrides writes. Without one, the write path is proven only
by unit tests until the first real port change.
1. Decisions you’ll probably want to tweak
Section titled “1. Decisions you’ll probably want to tweak”| # | Decision | Alternative | Cost of changing later |
|---|---|---|---|
| P1 | The go-unifi fork starts at ead99009383e, the commit the provider fork already pins. The only change is one exported raw-request method |
Base it on go-unifi v1.34.1 |
High now: the provider fork does not build on v1.34.x (DeviceRadioTable.AssistedRoaming*; memory 1ky2wjft08). Moving to v1.34.x is separate fork maintenance, and the raw method rebases onto it trivially |
| P2 | Test fixtures are synthetic, shaped like switch-main’s 22 entries (same key set, LAGs, and the trunk pattern) but with invented names and IDs | Commit the real dump with x_ keys stripped |
The fork repo is public, and the real dump names hosts and network IDs. A synthetic fixture tests the same key-preservation property |
| P3 | Release -fzymgc.2 by hand, following docs/operations/terraform-provider-fork.md |
Build the #2119 release automation first | #2119 stays independent. A manual release is about a 15-minute runbook |
| P4 | Adoption scope is switch-main only | Adopt every switch at once | The other switches use port profiles (AllMainDefault), and profile policy (D8) is not decided yet |
2. Known unknowns
Section titled “2. Known unknowns”| Unknown | Default | Pivot signal |
|---|---|---|
| Does the demo controller simulate a switch whose ports can be written? | Proceed. The existing unifi_device acceptance tests use simulated device 00:27:22:00:00:02 |
No port table, or the PUT is rejected. Then cover the write path with httptest against the synthetic fixture, and state in the PR that the controller write is not proven |
What does the controller do with forward (spec §5)? |
The acceptance test decides | Whatever it shows goes into mergePortOverrides |
Does the controller accept {"port_overrides": []} from Delete? |
Yes for switches. #438 only saw gateways reject null |
400 from the controller. Then Delete writes each entry reset to defaults instead |
Does UseStateForUnknown hold per map element? |
Yes, because nested plan modifiers resolve the prior state by path | An import plan shows “known after apply” on keys left out of config |
Can the agent-pool run install -fzymgc.2? |
Yes, as it did for -fzymgc.1 |
init fails. Check the registry upload and the platform hashes |
3. Work
Section titled “3. Work”Each phase is one PR in its repository. Phases run in order.
Phase 0: go-ahead (Sean)
Section titled “Phase 0: go-ahead (Sean)”- Create the GitHub fork
fzymgc-house/go-unififromubiquiti-community/go-unifi, with branchfzymgcatead99009383e. This is an external action and needs Sean’s go-ahead, as the provider fork did.
Phase 1: go-unifi fork
Section titled “Phase 1: go-unifi fork”unifi/unifi.go: add an exportedDo(ctx, method, apiPath string, reqBody, respBody any) erroras a thin wrapper overdo. Session, CSRF and the cloud-connector path handling stay indo.- Test: an
httptestround-trip of GET and PUT throughDo, using a response with keys the structs do not model. The test asserts that the raw bytes survive. - Keep the module path unchanged. The provider consumes the fork through
replace.
Phase 2: provider fork (fzymgc-house/terraform-provider-unifi, branch fzymgc)
Section titled “Phase 2: provider fork (fzymgc-house/terraform-provider-unifi, branch fzymgc)”-
go.mod:replace github.com/ubiquiti-community/go-unifi => github.com/fzymgc-house/go-unifi <pseudo-version>.go build ./...and the existing unit suite pass unchanged. -
unifi/device_ports_raw.go:getDeviceRaw(site, mac)returns the device_idand the rawport_overridesas[]map[string]any.putPortOverrides(site, id, entries)sends{"port_overrides": entries}and nothing else. -
unifi/device_ports_merge.go: a pure functionmergePortOverrides(live, declared).- It keeps every key not declared.
- It sets declared keys.
- It drops entries whose index is not declared.
- It applies the §5
forwardrule.
Unit tests use the synthetic fixture (P2), which contains unmodelled keys (
lag_idx,stp_edge_state, and an inventedx_future_key). They assert:- Updating one key on port 12 changes only that key, and the other 21 entries are byte-equal.
- Leaving a port out drops it.
- A new port starts from
{}. - LAG members never get their own entry.
-
unifi/device_ports_resource.go: implements the schema from spec §3 and the lifecycle from spec §4.- The schema is a
MapNestedAttribute. Every per-port attribute is Optional and Computed withUseStateForUnknown, and enums are validated. - Create refuses a device that already has entries.
- Import is by MAC.
- Register the resource in
provider.go, and add a docs template and an example.
- The schema is a
-
Acceptance tests (
TF_ACC=1, demo controller):- create, then update one key, then import with an empty plan, then delete;
- the
forwardreconciliation from spec §5; - unknown-key survival: seed a raw key with
Do, update another key, and read back; - Delete writes
[].
-
unifi/device_resource.go: markport_overridewith aDeprecationMessagepointing tounifi_device_ports. In a separate commit, route the inline block’s update path throughmergePortOverrides, so the block stops dropping unmodelled keys (spec §6.3). -
Update
CHANGELOG.md, then releasev0.56.0-fzymgc.2forlinux_arm64anddarwin_arm64, signed with the Vault key. Publish withscripts/publish-hcp-registry.sh(P3).
Phase 3: this repository
Section titled “Phase 3: this repository”tf/unifi/versions.tf: pin0.56.0-fzymgc.2. Refresh.terraform.lock.hclfor both platforms.- Read switch-main’s live entries with the View Only credential again. The probe script from
quick task
260919-jrmis the starting point. Writetf/unifi/ports.tfwithunifi_device_ports.switch_main:- all 22 entries from that read, with networks referenced as
unifi_network.*.id; prevent_destroy;- an import block keyed by MAC in
main.tf.
- all 22 entries from that read, with networks referenced as
- Acceptance: a speculative plan prints exactly
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.There is noignore_changes. - Docs:
docs/operations/unifi.md: take ports out of “Not yet managed”, and add “Changing a port” (a PR toports.tf, reviewed with the plan).docs/operations/terraform-provider-fork.md: add the go-unifi fork and itsreplace.
- Open the PR (Sean merges). After the merge, the workspace auto-applies the import-only run. Close #2118.
Phase 4: upstream (each post on Sean’s go-ahead)
Section titled “Phase 4: upstream (each post on Sean’s go-ahead)”- go-unifi: the
Domethod. - Provider:
unifi_device_portsplus the deprecation, and separately the inline-block field fix. Link the probe evidence, and explain why this resource is authoritative on purpose while #266 fixed an accidental whole-list replace.