This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Terrifi is a Terraform provider for managing Ubiquiti UniFi network infrastructure, built from scratch using the HashiCorp Terraform Plugin Framework (not the legacy SDK). It uses the go-unifi SDK under the hood.
This project uses Task (not Make) as the build runner. Read Taskfile.yml for all available tasks.
Run a single test:
task test:unit -- -run TestDNSRecordModelToAPI
task test:acc -- -run TestAccDNSRecord_basicThe -- -run <pattern> syntax passes -run through to go test via {{.CLI_ARGS}}.
All provider code lives in internal/provider/. Each resource follows the Terraform Plugin Framework pattern:
- Model struct (e.g.,
dnsRecordModel) — Go struct withtfsdk:tags mapping HCL attributes - CRUD methods —
Create,Read,Update,Delete,ImportState - Model-to-API conversion — Functions converting between Terraform model types (
types.String,types.Bool) and go-unifi API structs - Schema — Declares HCL attributes with validators, defaults, and plan modifiers
- Null-aware field handling: Terraform wrapper types (
types.String,types.Bool,types.Int64) distinguish null/unknown/set. Optional fields use pointer types in go-unifi structs. Zero values are treated as null to avoid spurious diffs. - Full object updates via
applyPlanToState(): The UniFi API requires sending complete objects on PUT. Each resource has anapplyPlanToState()method that merges the user's planned changes into the current state before sending, preventing accidental clearing of API-set fields. - Site fallback: Resources have an optional
siteattribute that falls back to the provider's default site viaClient.SiteOrDefault(). - Configuration cascading: HCL attributes → environment variables (
UNIFI_API,UNIFI_USERNAME,UNIFI_PASSWORD,UNIFI_API_KEY,UNIFI_INSECURE,UNIFI_SITE) → defaults. - Compile-time interface checks:
var _ resource.Resource = &dnsRecordResource{}pattern at the top of each resource file.
Tests are in the same package (internal/provider/), controlled by TestMain:
- Unit tests (no
TF_ACC): Test model-to-API conversions and field mappings. Usetestify/assertandtestify/require. - Acceptance tests (
TF_ACC=1): Full Terraform lifecycle tests usinghelper/resource.Test(). Prefixed withTestAcc. Two modes:TERRIFI_ACC_TARGET=docker(default): Spins up UniFi controller via Docker Compose with testcontainers-goTERRIFI_ACC_TARGET=hardware: Uses real hardware configured via.envrc.local
- Test helpers in
provider_test.go:preCheck()validates env vars,randomSuffix()generates unique resource names to avoid conflicts from leftover resources.
Each new feature should include extensive acceptance testing. Think of interesting permutations of settings and sequences of changes. Too much testing is better than too little - don't hold back.
The go-unifi SDK has several bugs that require workarounds in this provider. All workarounds are tagged with TODO(go-unifi) comments so they can be found and removed when the SDK is fixed.
Conventions for SDK workarounds:
- Tag every workaround site with a
// TODO(go-unifi):comment explaining the upstream bug, the symptom, and what SDK fix would allow removing the workaround. - When possible, isolate workarounds into named helper functions (e.g.,
applySDKSettingPreferenceWorkaround) so they can be deleted as a unit. - When the SDK lacks working methods for an API (e.g., v2 firewall zones), put all custom HTTP logic in a dedicated
*_api.gofile (e.g.,firewall_zone_api.go) with a file-level TODO explaining which SDK methods it replaces. - Never patch the SDK in the module cache. All workarounds live in this repo.
Current workarounds (search TODO(go-unifi) for details):
firewall_zone_api.go— Bypasses SDK's firewall zone CRUD entirely due to three bugs:default_zoneserialization (400), missing_idin PUT body (500), and DELETE returning 204 treated as error.firewall_policy_api.go— Custom HTTP methods for v2 firewall policy endpoints not supported by the SDK.client_device_api.go— Custom HTTP methods for v2 client device endpoints not supported by the SDK.network_resource.go—applySDKSettingPreferenceWorkaround()forcessetting_preference=manualbecause the SDK defaults toauto, which causes the controller to auto-override settings like DHCP enable.network_resource.go—IsUnknown()guards on DHCP fields inmodelToAPIprevent passing empty strings to the SDK, which would crash the controller viamarshalCorporate()'svalueOrDefault()defaults.
The cmd/terrifi/ directory contains a Cobra-based CLI. Its main command is generate-imports, which connects to a live UniFi controller and outputs Terraform import {} + resource {} blocks to stdout.
The internal/generate/ package provides the conversion logic: each resource type has a <Name>Blocks() function (e.g., DNSRecordBlocks()) that converts go-unifi API structs into ResourceBlock objects, which are then rendered as HCL. Shared helpers like ToTerraformName(), DeduplicateNames(), and HCL formatting functions (HCLString, HCLBool, etc.) live in generate.go.
internal/provider/client.go defines the Client struct wrapping go-unifi's ApiClient. Key details:
- Uses
go-retryablehttpfor automatic retries with TLS configuration. - On initialization, probes the controller to discover the API path (
/proxy/networkfor UniFi OS vs. empty for legacy controllers). ClientConfigFromEnv()readsUNIFI_*env vars, shared between the provider and CLI.
- Create
internal/provider/<name>_resource.gowith model struct, CRUD methods, and schema - Create
internal/provider/<name>_resource_test.gowith unit tests for model conversion and acceptance tests for CRUD lifecycle - Register the resource in
provider.go→Resources()method - Create
internal/generate/<name>.gowith a<Name>Blocks()function for import generation - Add the resource type to
cmd/terrifi/generate_imports.go(validResourceTypesslice and switch statement) - Add docs in
docs/resources/and examples inexamples/