No description
  • Go 89.6%
  • Shell 6.7%
  • Makefile 3.7%
Find a file
riley snyder b72a500d8c
All checks were successful
test / build (push) Successful in 55s
release / release (push) Successful in 16m25s
Merge pull request 'fix: force gitea token in goreleaser' (#2) from fix/goreleaser-token into main
Reviewed-on: #2
2026-07-15 21:44:47 -05:00
.forgejo/workflows feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
internal/provider feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
.gitignore feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
.goreleaser.yaml fix: force gitea token in goreleaser 2026-07-15 21:42:47 -05:00
go.mod feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
go.sum feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
install.sh feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
main.go feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
Makefile feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
README.md feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00
spec.md feat: unifi-backed unused-IP allocation provider 2026-07-15 18:47:37 -05:00

terraform-provider-local-dhcp

Allocates unused IP addresses on the local network so Terraform/OpenTofu can create machines with static IPs in a single apply — no more hardcoding free IPs by hand. Built for the talos-faktory Proxmox/Talos module, but generic to any UniFi-backed network.

How it picks an IP

An address is only allocated when both checks say it's free:

  1. UniFi controller inventory (authoritative): active clients (stat/sta) plus every known client's last lease and fixed-IP reservation (rest/user) — this covers devices that are offline right now but still own an address. Read-only: the provider never writes to the controller.
  2. Live probe (belt and braces): one ICMP echo, then a kernel ARP-table check — catching statically-configured hosts the controller has never seen. Hosts that drop ICMP still answer the ARP request the ping triggers. Disable with verify = false when Terraform doesn't run on the same L2 segment as the range.

Allocation is a resource, not a data source: the pick happens once at create time and is then pinned in state, so later plans never churn node IPs as the network changes. Concurrent allocations in one apply are serialized and never collide. Destroy is a no-op — the allocation only ever lives in Terraform state.

Installation

No provider registry is involved. Releases on this Forgejo repo are installed into the local filesystem mirror (~/.terraform.d/plugins), which terraform and tofu consult before ever contacting a registry:

curl -fsSL https://git.ttdsm.org/rssnyder/terraform-provider-localdhcp/raw/branch/main/install.sh | sh -s 0.1.0

(or clone and ./install.sh 0.1.0). Then reference it normally:

terraform {
  required_providers {
    localdhcp = {
      source  = "rssnyder/localdhcp"
      version = "0.1.0"
    }
  }
}

tofu init resolves the provider from the mirror; the lock file records the local build's hash. (The script installs under both the registry.opentofu.org and registry.terraform.io hostnames, since tofu and terraform expand rssnyder/localdhcp against different default registries.) Re-run install.sh with the new version when upgrading. Machines that run applies (laptop, CI runners) each need the install once per version.

Usage

# unifi_url / unifi_api_key / allow_insecure fall back to the
# UNIFI_API / UNIFI_API_KEY / UNIFI_INSECURE environment variables.
provider "localdhcp" {}

resource "localdhcp_ip" "control_plane" {
  count = 1

  # keep picks in the static zone, outside the DHCP pool
  range_start = "192.168.2.32"
  range_end   = "192.168.2.127"
  exclude     = ["192.168.2.69"]
}

output "control_plane_ips" {
  value = localdhcp_ip.control_plane[*].ip
}

localdhcp_free_ips is a companion data source that lists all controller-free addresses in a range — for inspection only (it recomputes every plan).

Provider configuration

Attribute Env fallback Default
unifi_url UNIFI_API Controller base URL, e.g. https://192.168.2.1
unifi_api_key UNIFI_API_KEY API key (UniFi OS: Settings → Control Plane → Integrations)
unifi_site default Site name
allow_insecure UNIFI_INSECURE false Skip TLS verification
verify true Live ICMP/ARP probe before allocating

Works against UniFi OS consoles (/proxy/network/...) and self-hosted controllers (/api/...) — the client tries both paths.

Releasing

Push a tag and the release Forgejo action builds zips for linux/darwin × amd64/arm64 with GoReleaser and attaches them (plus a SHA256SUMS file) to a Forgejo release:

git tag v0.1.0 && git push origin v0.1.0

Why no registry?

The public registries (registry.terraform.io / registry.opentofu.org) only ingest providers from GitHub repos and require GPG-signed releases, and Forgejo doesn't implement the provider-registry protocol. None of that is needed: the filesystem mirror above gives full init/lockfile support from plain release assets. If this ever needs to be consumed by machines where pre-installing is annoying, the next step up is a network mirror — static JSON hosted anywhere (e.g. the MinIO/Garage S3) pointing at the Forgejo release zips.

Development

make build     # binary in bin/
make test
make install VERSION=0.1.0   # put the local build in the filesystem mirror

Or point at the working tree with a dev override (no version pinning, no init needed):

# dev.tfrc
provider_installation {
  dev_overrides {
    "rssnyder/localdhcp" = "/home/riley/terraform-provider-local-dhcp/bin"
  }
  direct {}
}
TF_CLI_CONFIG_FILE=$PWD/dev.tfrc tofu apply