docs(01-02): complete NetBox client plan summary

- SUMMARY.md for plan 01-02 with go-netbox v4 API notes and deviation log
This commit is contained in:
Mikkel Georgsen 2026-04-10 05:18:13 +00:00
parent 17a2eb6f9f
commit f15c0c7ea7

View file

@ -0,0 +1,113 @@
---
phase: 01-foundation
plan: "02"
subsystem: netbox
tags: [go, netbox, client, custom-fields, tdd]
dependency_graph:
requires: []
provides: [internal/netbox.Client, internal/netbox.ParseCustomFields, internal/netbox.BuildCustomFieldsPatch]
affects: [internal/quality, internal/hwid, internal/waq]
tech_stack:
added: [github.com/netbox-community/go-netbox/v4 v4.3.0, gopkg.in/validator.v2 v2.0.1]
patterns: [wrapper-client, typed-custom-fields, integration-skip-guard]
key_files:
created:
- internal/netbox/client.go
- internal/netbox/types.go
- internal/netbox/client_test.go
- internal/netbox/custom_fields.go
- internal/netbox/custom_fields_test.go
modified:
- go.mod
- go.sum
decisions:
- id: NB-CLIENT-01
summary: "Strip /api suffix from base URL in NewClient to avoid go-netbox double-appending /api"
- id: NB-CLIENT-02
summary: "PatchedWritableDeviceWithConfigContextRequest.SetCustomFields(map) is the correct go-netbox v4 PATCH pattern"
- id: NB-CF-01
summary: "BuildCustomFieldsPatch only includes non-empty fields — avoids clearing existing NetBox values on partial update"
metrics:
duration: "~15 minutes"
completed: "2026-04-10T05:17:22Z"
tasks_completed: 2
files_created: 5
files_modified: 2
---
# Phase 1 Plan 02: NetBox Client Package Summary
**One-liner:** go-netbox v4 typed client wrapper with safe custom field read/write helpers and integration-skip guards for placeholder tokens.
## What Was Built
### `internal/netbox/types.go`
Domain types that decouple the rest of HWLab from go-netbox generated types:
- `Device` — HWLab inventory item (ID, Name, AssetTag, CustomFields, Created, LastUpdated)
- `CustomFields` — typed struct over NetBox's `map[string]interface{}` response (HWID, CatalogStatus, ProductURL, FirmwareVersion, TestDate, TestData, AINotes, PhotoURLs)
### `internal/netbox/client.go`
`Client` struct wrapping `*nb.APIClient` with methods:
- `NewClient(url, token string) (*Client, error)` — validates inputs, strips trailing `/api` from URL before passing to `nb.NewAPIClientFor`
- `Ping(ctx)` — lightweight connectivity check via `DcimDevicesList(limit=1)`
- `ListDevices(ctx, limit)` — returns `[]Device` mapped via `deviceFromNetBox`
- `GetDevice(ctx, id)` — retrieves single device by NetBox internal ID
### `internal/netbox/custom_fields.go`
- `ParseCustomFields(raw map[string]interface{}) CustomFields` — safe type assertions for all 8 HWLab custom field keys; handles nil input; handles `[]interface{}` photo_urls
- `BuildCustomFieldsPatch(hwID, catalogStatus string, photoURLs []string) map[string]interface{}` — selective patch (only non-empty fields included)
- `BuildFullCustomFieldsPatch(cf CustomFields) map[string]interface{}` — full patch for initial record creation
- `(c *Client) PatchCustomFields(ctx, deviceID, patch)` — uses `nb.PatchedWritableDeviceWithConfigContextRequest.SetCustomFields()` then `.Execute()`
## Integration Test Status
Integration tests (TestPingLive, TestListDevicesLive, TestPatchCustomFieldsRoundTrip) **SKIPPED** because `HWLAB_NETBOX_TOKEN=homelab-netbox-api-token-2024` is a placeholder (22 chars, not 40).
Skip guard: `if len(token) != 40 { t.Skip(...) }` — correct behavior, no errors.
To run integration tests: set `HWLAB_NETBOX_TOKEN` to a real 40-char hex token from NetBox UI, and `HWLAB_TEST_DEVICE_ID` to an existing device ID for round-trip test.
## go-netbox v4 API Notes
- **Struct confirmed:** `nb.PatchedWritableDeviceWithConfigContextRequest` with `SetCustomFields(map[string]interface{})` method
- **PATCH pattern:** `c.api.DcimAPI.DcimDevicesPartialUpdate(ctx, int32(id)).PatchedWritableDeviceWithConfigContextRequest(req).Execute()`
- **URL handling:** `NewAPIClientFor` appends `/api` internally — must strip trailing `/api` from the URL in the config to avoid `http://host:8000/api/api/`
- **AssetTag type:** `NullableString` on `DeviceWithConfigContext``GetAssetTag()` returns empty string when null (safe)
- **CustomFields on read:** `map[string]interface{}` — requires type assertions per field
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] custom_fields.go written in Task 1 to unblock compilation**
- **Found during:** Task 1 GREEN phase
- **Issue:** `client.go` calls `ParseCustomFields` which was planned for Task 2; package would not compile without it
- **Fix:** Wrote `custom_fields.go` as part of Task 1 implementation to make `go test` work; Task 2 then added only `custom_fields_test.go`
- **Files modified:** internal/netbox/custom_fields.go (created during Task 1 pass)
- **Commit:** 9f3ed9f (Task 1 commit includes custom_fields.go)
**2. [Rule 2 - Missing] Added HWLAB_TEST_DEVICE_ID guard to round-trip integration test**
- **Found during:** Task 2 test design
- **Issue:** Round-trip test needs an existing device ID — hardcoding one would fail on fresh NetBox installs
- **Fix:** Added second `t.Skip()` guard when `HWLAB_TEST_DEVICE_ID` env var is absent
- **Files modified:** internal/netbox/custom_fields_test.go
## Self-Check
Files created:
- internal/netbox/client.go: FOUND
- internal/netbox/types.go: FOUND
- internal/netbox/client_test.go: FOUND
- internal/netbox/custom_fields.go: FOUND
- internal/netbox/custom_fields_test.go: FOUND
Commits:
- 9f3ed9f — Task 1 (NetBox client wrapper)
- 17a2eb6 — Task 2 (custom field wrappers)
`go build ./...`: PASS
`go test ./internal/netbox/... -run TestNewClientValidation`: PASS
`go test ./internal/netbox/... -run "TestParseCustomFields|TestBuildCustomFields"`: PASS (5 tests)
Integration tests: SKIP (placeholder token — correct behavior)
## Self-Check: PASSED