Scaffold terraform-provider-tomswallapi #1
@@ -0,0 +1,4 @@
|
||||
terraform-provider-tomswallapi
|
||||
*.zip
|
||||
.terraform*
|
||||
*.tfstate*
|
||||
@@ -0,0 +1,24 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: gofmt
|
||||
name: gofmt
|
||||
entry: gofmt -l -d
|
||||
language: system
|
||||
types: [go]
|
||||
pass_filenames: true
|
||||
- id: go-vet
|
||||
name: go vet
|
||||
entry: go vet ./...
|
||||
language: system
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
@@ -0,0 +1,18 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:1.25
|
||||
commands:
|
||||
- make build
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
@@ -0,0 +1,18 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: pre-commit
|
||||
image: git.unkin.net/unkin/almalinux9-gobuilder:20260606
|
||||
commands:
|
||||
- uvx pre-commit run --all-files
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
@@ -0,0 +1,40 @@
|
||||
when:
|
||||
- event: tag
|
||||
|
||||
steps:
|
||||
- name: package
|
||||
image: git.unkin.net/unkin/almalinux9-gobuilder:20260606
|
||||
commands:
|
||||
- make package VERSION=${CI_COMMIT_TAG}
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
- name: upload
|
||||
image: git.unkin.net/unkin/almalinux9-base:20260606
|
||||
commands:
|
||||
- |
|
||||
VERSION=$$(echo ${CI_COMMIT_TAG} | sed 's/^v//')
|
||||
FILE="terraform-provider-tomswallapi_$${VERSION}_linux_amd64.zip"
|
||||
curl -f -X PUT \
|
||||
"https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/terraform-unkin/files/unkin/tomswallapi/$${FILE}" \
|
||||
-H "Content-Type: application/zip" \
|
||||
--data-binary @"$${FILE}"
|
||||
depends_on: [package]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 128Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
@@ -0,0 +1,19 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.25
|
||||
commands:
|
||||
- make lint
|
||||
- make test
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
@@ -0,0 +1,61 @@
|
||||
.PHONY: build install test lint fmt clean tidy package patch minor major
|
||||
|
||||
BINARY := terraform-provider-tomswallapi
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-dev")
|
||||
OS_ARCH := linux_amd64
|
||||
INSTALL_VERSION := $(shell echo $(VERSION) | sed 's/^v//')
|
||||
INSTALL_DIR := ~/.terraform.d/plugins/git.unkin.net/unkin/tomswallapi/$(INSTALL_VERSION)/$(OS_ARCH)
|
||||
ZIP := $(BINARY)_$(INSTALL_VERSION)_$(OS_ARCH).zip
|
||||
|
||||
GO_VERSION_REQUIRED := 1.25
|
||||
GO_VERSION_ACTUAL := $(shell go version | sed 's/go version go\([0-9]*\.[0-9]*\).*/\1/')
|
||||
|
||||
check-go:
|
||||
@if [ "$$(printf '%s\n%s' "$(GO_VERSION_REQUIRED)" "$(GO_VERSION_ACTUAL)" | sort -V | head -1)" != "$(GO_VERSION_REQUIRED)" ]; then \
|
||||
echo "ERROR: Go >= $(GO_VERSION_REQUIRED) required, found $(GO_VERSION_ACTUAL)"; exit 1; \
|
||||
fi
|
||||
|
||||
build: check-go tidy
|
||||
go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)
|
||||
|
||||
install: build
|
||||
mkdir -p $(INSTALL_DIR)
|
||||
cp $(BINARY) $(INSTALL_DIR)/
|
||||
|
||||
test: check-go
|
||||
go test -race -count=1 ./...
|
||||
|
||||
lint: check-go
|
||||
go vet ./...
|
||||
|
||||
fmt: check-go
|
||||
gofmt -w .
|
||||
|
||||
package: build
|
||||
cp $(BINARY) $(BINARY)_v$(INSTALL_VERSION)
|
||||
python3 -c "import zipfile,sys; z=zipfile.ZipFile(sys.argv[1],'w',zipfile.ZIP_DEFLATED); z.write(sys.argv[2]); z.close()" $(ZIP) $(BINARY)_v$(INSTALL_VERSION)
|
||||
rm $(BINARY)_v$(INSTALL_VERSION)
|
||||
|
||||
clean:
|
||||
rm -f $(BINARY) *.zip
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
|
||||
_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0)
|
||||
_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1)
|
||||
_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2)
|
||||
_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3)
|
||||
|
||||
patch:
|
||||
@NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
|
||||
minor:
|
||||
@NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
|
||||
major:
|
||||
@NEW=v$(shell expr $(_MAJ) + 1).0.0; \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
@@ -1,3 +1,75 @@
|
||||
# terraform-provider-tomswallapi
|
||||
|
||||
Terraform provider for tomswallapi: manage fleet-global firewall policy (zones, address groups, portgroups, rules, policies, fabrics) and per-device bindings.
|
||||
Terraform/OpenTofu provider for [tomswallapi](https://git.unkin.net/unkin/tomswallapi),
|
||||
the fleet control plane for tomswall. Declare the fleet-global firewall model —
|
||||
zones, address groups, portgroups, rules, and fabrics — plus per-device
|
||||
zone→interface bindings, as HCL.
|
||||
|
||||
## Provider configuration
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
tomswallapi = {
|
||||
source = "git.unkin.net/unkin/tomswallapi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "tomswallapi" {
|
||||
endpoint = "https://tomswallapi.k8s.syd1.au.unkin.net"
|
||||
# token defaults to the TOMSWALLAPI_WRITE_TOKEN environment variable
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
| resource | key | notes |
|
||||
|---|---|---|
|
||||
| `tomswallapi_zone` | name | fleet-global segment; `subnets` may be empty for edge zones |
|
||||
| `tomswallapi_address_group` | name | `type` = static/dns/asn; asn `resolved`/`resolved_at` are computed |
|
||||
| `tomswallapi_portgroup` | name | reusable proto+ports |
|
||||
| `tomswallapi_fabric` | name | `enforce_on_routers` toggles defense-in-depth |
|
||||
| `tomswallapi_device` | name | `class` = router/firewall, `fabric`, `resolver`, `settings` |
|
||||
| `tomswallapi_binding` | device:zone | zone→interface map (import as `device:zone`) |
|
||||
| `tomswallapi_rule` | id | shorewall-style `source`/`dest` element lists |
|
||||
|
||||
## Example
|
||||
|
||||
See [`examples/`](examples/). A minimal A→cloudflare rule:
|
||||
|
||||
```hcl
|
||||
resource "tomswallapi_zone" "loc" { name = "loc" subnets = ["10.1.0.0/24"] }
|
||||
resource "tomswallapi_zone" "net" { name = "net" } # edge zone, no subnets
|
||||
|
||||
resource "tomswallapi_address_group" "cloudflare" {
|
||||
name = "cloudflare"
|
||||
type = "asn"
|
||||
members = ["13335"]
|
||||
refresh = "24h"
|
||||
}
|
||||
|
||||
resource "tomswallapi_rule" "cf_https" {
|
||||
action = "accept"
|
||||
source = ["loc"]
|
||||
dest = ["net:+asn_cloudflare"]
|
||||
proto = "tcp"
|
||||
ports = ["443"]
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
make build # build the provider binary
|
||||
make install # install into ~/.terraform.d/plugins for local testing
|
||||
make test # go test
|
||||
make lint # go vet
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
`make patch|minor|major` tags and pushes the next `v*`, which triggers the
|
||||
Woodpecker release pipeline to package the provider and publish it to the
|
||||
artifactapi terraform registry (`terraform-unkin`), installable via the bare
|
||||
`source = "git.unkin.net/unkin/tomswallapi"` address.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
tomswallapi = {
|
||||
source = "git.unkin.net/unkin/tomswallapi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "tomswallapi" {
|
||||
endpoint = "https://tomswallapi.k8s.syd1.au.unkin.net"
|
||||
# token from TOMSWALLAPI_WRITE_TOKEN
|
||||
}
|
||||
|
||||
# --- Fabric + zones ---------------------------------------------------------
|
||||
|
||||
resource "tomswallapi_fabric" "core" {
|
||||
name = "core"
|
||||
enforce_on_routers = true
|
||||
}
|
||||
|
||||
resource "tomswallapi_zone" "zone_a" {
|
||||
name = "zone-a"
|
||||
subnets = ["10.1.0.0/24"]
|
||||
}
|
||||
|
||||
resource "tomswallapi_zone" "zone_b" {
|
||||
name = "zone-b"
|
||||
subnets = ["10.4.0.0/24"]
|
||||
}
|
||||
|
||||
# Internet-facing zone: no subnets, so rules must pair it with a selector.
|
||||
resource "tomswallapi_zone" "net" {
|
||||
name = "net"
|
||||
}
|
||||
|
||||
# --- Address group (ASN) ----------------------------------------------------
|
||||
|
||||
resource "tomswallapi_address_group" "cloudflare" {
|
||||
name = "cloudflare"
|
||||
type = "asn"
|
||||
members = ["13335", "209242"]
|
||||
refresh = "24h"
|
||||
}
|
||||
|
||||
# --- Portgroup --------------------------------------------------------------
|
||||
|
||||
resource "tomswallapi_portgroup" "https" {
|
||||
name = "https"
|
||||
proto = "tcp"
|
||||
ports = ["443"]
|
||||
}
|
||||
|
||||
# --- Devices + bindings -----------------------------------------------------
|
||||
|
||||
resource "tomswallapi_device" "fw_a" {
|
||||
name = "fw-a"
|
||||
class = "firewall"
|
||||
fabric = tomswallapi_fabric.core.name
|
||||
}
|
||||
|
||||
resource "tomswallapi_device" "rt1" {
|
||||
name = "rt1"
|
||||
class = "router"
|
||||
fabric = tomswallapi_fabric.core.name
|
||||
}
|
||||
|
||||
resource "tomswallapi_binding" "fw_a_zone_a" {
|
||||
device = tomswallapi_device.fw_a.name
|
||||
zone = tomswallapi_zone.zone_a.name
|
||||
interfaces = ["eth1"]
|
||||
}
|
||||
|
||||
# --- Rules (declared once, compiled to every device on the path) ------------
|
||||
|
||||
# zone-a can reach zone-b on ssh, through every hop.
|
||||
resource "tomswallapi_rule" "ssh_a_to_b" {
|
||||
action = "accept"
|
||||
source = [tomswallapi_zone.zone_a.name]
|
||||
dest = [tomswallapi_zone.zone_b.name]
|
||||
proto = "tcp"
|
||||
ports = ["22"]
|
||||
}
|
||||
|
||||
# zone-a to cloudflare's prefixes on https (edge zone gated by the asn set).
|
||||
resource "tomswallapi_rule" "a_to_cloudflare" {
|
||||
action = "accept"
|
||||
source = [tomswallapi_zone.zone_a.name]
|
||||
dest = ["net:+asn_cloudflare"]
|
||||
portgroup = tomswallapi_portgroup.https.name
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
module git.unkin.net/unkin/terraform-provider-tomswallapi
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/hashicorp/terraform-plugin-framework v1.15.0
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.3 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/terraform-plugin-go v0.27.0 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
||||
github.com/hashicorp/terraform-registry-address v0.2.5 // indirect
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.0.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg=
|
||||
github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.15.0 h1:LQ2rsOfmDLxcn5EeIwdXFtr03FVsNktbbBci8cOKdb4=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.15.0/go.mod h1:hxrNI/GY32KPISpWqlCoTLM9JZsGH3CyYlir09bD/fI=
|
||||
github.com/hashicorp/terraform-plugin-go v0.27.0 h1:ujykws/fWIdsi6oTUT5Or4ukvEan4aN9lY+LOxVP8EE=
|
||||
github.com/hashicorp/terraform-plugin-go v0.27.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,90 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type apiClient struct {
|
||||
baseURL string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func newAPIClient(baseURL, token string) *apiClient {
|
||||
return &apiClient{baseURL: baseURL, token: token, httpClient: &http.Client{}}
|
||||
}
|
||||
|
||||
func (c *apiClient) get(ctx context.Context, path string, out any) error {
|
||||
return c.do(ctx, http.MethodGet, path, nil, out)
|
||||
}
|
||||
|
||||
func (c *apiClient) put(ctx context.Context, path string, body, out any) error {
|
||||
return c.do(ctx, http.MethodPut, path, body, out)
|
||||
}
|
||||
|
||||
func (c *apiClient) post(ctx context.Context, path string, body, out any) error {
|
||||
return c.do(ctx, http.MethodPost, path, body, out)
|
||||
}
|
||||
|
||||
func (c *apiClient) del(ctx context.Context, path string) error {
|
||||
return c.do(ctx, http.MethodDelete, path, nil, nil)
|
||||
}
|
||||
|
||||
func (c *apiClient) do(ctx context.Context, method, path string, body, out any) error {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(b)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if c.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return ¬FoundError{path: path}
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("api error %d: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
if out != nil && resp.StatusCode != http.StatusNoContent {
|
||||
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
||||
return fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathEscape(s string) string { return url.PathEscape(s) }
|
||||
|
||||
type notFoundError struct{ path string }
|
||||
|
||||
func (e *notFoundError) Error() string { return fmt.Sprintf("not found: %s", e.path) }
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
_, ok := err.(*notFoundError)
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// configureClient extracts the shared *apiClient from provider data, used by
|
||||
// every resource's Configure.
|
||||
func configureClient(req resource.ConfigureRequest, resp *resource.ConfigureResponse) *apiClient {
|
||||
if req.ProviderData == nil {
|
||||
return nil
|
||||
}
|
||||
client, ok := req.ProviderData.(*apiClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
|
||||
return nil
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// listToStrings converts a Terraform list into a []string. A null/unknown list
|
||||
// yields nil.
|
||||
func listToStrings(ctx context.Context, l types.List, diags *diag.Diagnostics) []string {
|
||||
if l.IsNull() || l.IsUnknown() {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
diags.Append(l.ElementsAs(ctx, &out, false)...)
|
||||
return out
|
||||
}
|
||||
|
||||
// stringsToList converts a []string into a Terraform list of strings, rendering
|
||||
// nil as an empty list so state stays consistent.
|
||||
func stringsToList(ctx context.Context, s []string, diags *diag.Diagnostics) types.List {
|
||||
if s == nil {
|
||||
s = []string{}
|
||||
}
|
||||
l, d := types.ListValueFrom(ctx, types.StringType, s)
|
||||
diags.Append(d...)
|
||||
return l
|
||||
}
|
||||
|
||||
// mapToStrings converts a Terraform map into a map[string]string.
|
||||
func mapToStrings(ctx context.Context, m types.Map, diags *diag.Diagnostics) map[string]string {
|
||||
if m.IsNull() || m.IsUnknown() {
|
||||
return nil
|
||||
}
|
||||
out := map[string]string{}
|
||||
diags.Append(m.ElementsAs(ctx, &out, false)...)
|
||||
return out
|
||||
}
|
||||
|
||||
// stringsToMap converts a map[string]string into a Terraform map.
|
||||
func stringsToMap(ctx context.Context, m map[string]string, diags *diag.Diagnostics) types.Map {
|
||||
if m == nil {
|
||||
m = map[string]string{}
|
||||
}
|
||||
v, d := types.MapValueFrom(ctx, types.StringType, m)
|
||||
diags.Append(d...)
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var _ provider.Provider = &tomswallProvider{}
|
||||
|
||||
type tomswallProvider struct {
|
||||
version string
|
||||
}
|
||||
|
||||
type tomswallProviderModel struct {
|
||||
Endpoint types.String `tfsdk:"endpoint"`
|
||||
Token types.String `tfsdk:"token"`
|
||||
}
|
||||
|
||||
// New returns the provider constructor.
|
||||
func New(version string) func() provider.Provider {
|
||||
return func() provider.Provider { return &tomswallProvider{version: version} }
|
||||
}
|
||||
|
||||
func (p *tomswallProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||||
resp.TypeName = "tomswallapi"
|
||||
resp.Version = p.version
|
||||
}
|
||||
|
||||
func (p *tomswallProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manage the tomswall fleet control plane: fleet-global zones, address groups, portgroups, rules, policies, and fabrics, plus per-device zone->interface bindings.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"endpoint": schema.StringAttribute{
|
||||
Description: "The tomswallapi base URL (e.g. https://tomswallapi.k8s.syd1.au.unkin.net).",
|
||||
Required: true,
|
||||
},
|
||||
"token": schema.StringAttribute{
|
||||
Description: "Write token (bearer) for mutating operations. Defaults to the TOMSWALLAPI_WRITE_TOKEN environment variable.",
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *tomswallProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||
var config tomswallProviderModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
token := config.Token.ValueString()
|
||||
if token == "" {
|
||||
token = os.Getenv("TOMSWALLAPI_WRITE_TOKEN")
|
||||
}
|
||||
|
||||
client := newAPIClient(config.Endpoint.ValueString(), token)
|
||||
resp.DataSourceData = client
|
||||
resp.ResourceData = client
|
||||
}
|
||||
|
||||
func (p *tomswallProvider) Resources(_ context.Context) []func() resource.Resource {
|
||||
return []func() resource.Resource{
|
||||
NewZoneResource,
|
||||
NewFabricResource,
|
||||
NewPortGroupResource,
|
||||
NewAddressGroupResource,
|
||||
NewDeviceResource,
|
||||
NewBindingResource,
|
||||
NewRuleResource,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *tomswallProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &addressGroupResource{}
|
||||
_ resource.ResourceWithImportState = &addressGroupResource{}
|
||||
)
|
||||
|
||||
type addressGroupResource struct{ client *apiClient }
|
||||
|
||||
type addressGroupModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
Members types.List `tfsdk:"members"`
|
||||
Refresh types.String `tfsdk:"refresh"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
Resolved types.List `tfsdk:"resolved"`
|
||||
ResolvedAt types.String `tfsdk:"resolved_at"`
|
||||
}
|
||||
|
||||
type addressGroupAPI struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Members []string `json:"members"`
|
||||
Refresh string `json:"refresh,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Resolved []string `json:"resolved,omitempty"`
|
||||
ResolvedAt string `json:"resolved_at,omitempty"`
|
||||
}
|
||||
|
||||
func NewAddressGroupResource() resource.Resource { return &addressGroupResource{} }
|
||||
|
||||
func (r *addressGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_address_group"
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "An address group, materialized as an nftables named set. Population source is static (CIDRs), dns (FQDNs resolved on-device), or asn (ASNs expanded centrally).",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Group name (globally unique). ASN groups conventionally use an asn_ prefix.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "Population source: static, dns, or asn.",
|
||||
Required: true,
|
||||
},
|
||||
"members": schema.ListAttribute{
|
||||
Description: "For static: CIDRs/IPs. For dns: FQDNs. For asn: ASN numbers.",
|
||||
Required: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"refresh": schema.StringAttribute{
|
||||
Description: "asn: prefix cache TTL (e.g. 24h). dns: honor_ttl.",
|
||||
Optional: true,
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
"resolved": schema.ListAttribute{
|
||||
Description: "Server-managed: concrete prefixes the ASN expander last produced.",
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"resolved_at": schema.StringAttribute{
|
||||
Description: "Server-managed: timestamp of the last ASN expansion.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.client = configureClient(req, resp)
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan addressGroupModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan addressGroupModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) upsert(ctx context.Context, plan addressGroupModel, diags *diag.Diagnostics, state *tfsdk.State) {
|
||||
body := addressGroupAPI{
|
||||
Name: plan.Name.ValueString(),
|
||||
Type: plan.Type.ValueString(),
|
||||
Members: listToStrings(ctx, plan.Members, diags),
|
||||
Refresh: plan.Refresh.ValueString(),
|
||||
Description: plan.Description.ValueString(),
|
||||
}
|
||||
if diags.HasError() {
|
||||
return
|
||||
}
|
||||
var out addressGroupAPI
|
||||
if err := r.client.put(ctx, "/api/v1/address-groups/"+pathEscape(body.Name), body, &out); err != nil {
|
||||
diags.AddError("upsert address_group failed", err.Error())
|
||||
return
|
||||
}
|
||||
diags.Append(state.Set(ctx, r.toModel(ctx, out, plan, diags))...)
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state addressGroupModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out addressGroupAPI
|
||||
if err := r.client.get(ctx, "/api/v1/address-groups/"+pathEscape(state.Name.ValueString()), &out); err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("read address_group failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state addressGroupModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if err := r.client.del(ctx, "/api/v1/address-groups/"+pathEscape(state.Name.ValueString())); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete address_group failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *addressGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
// toModel maps the API response back to state. Optional string fields fall back
|
||||
// to the prior plan/state value so that omitempty responses don't churn state.
|
||||
func (r *addressGroupResource) toModel(ctx context.Context, api addressGroupAPI, prior addressGroupModel, diags *diag.Diagnostics) addressGroupModel {
|
||||
m := addressGroupModel{
|
||||
Name: types.StringValue(api.Name),
|
||||
Type: types.StringValue(api.Type),
|
||||
Members: stringsToList(ctx, api.Members, diags),
|
||||
Refresh: optionalString(api.Refresh, prior.Refresh),
|
||||
Description: optionalString(api.Description, prior.Description),
|
||||
Resolved: stringsToList(ctx, api.Resolved, diags),
|
||||
ResolvedAt: types.StringValue(api.ResolvedAt),
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// optionalString returns a value for an optional attribute: the API value when
|
||||
// present, else the prior planned value (preserving null when both are empty).
|
||||
func optionalString(apiVal string, prior types.String) types.String {
|
||||
if apiVal != "" {
|
||||
return types.StringValue(apiVal)
|
||||
}
|
||||
return prior
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &bindingResource{}
|
||||
_ resource.ResourceWithImportState = &bindingResource{}
|
||||
)
|
||||
|
||||
type bindingResource struct{ client *apiClient }
|
||||
|
||||
type bindingModel struct {
|
||||
Device types.String `tfsdk:"device"`
|
||||
Zone types.String `tfsdk:"zone"`
|
||||
Interfaces types.List `tfsdk:"interfaces"`
|
||||
}
|
||||
|
||||
type bindingAPI struct {
|
||||
Device string `json:"device"`
|
||||
Zone string `json:"zone"`
|
||||
Interfaces []string `json:"interfaces"`
|
||||
}
|
||||
|
||||
func NewBindingResource() resource.Resource { return &bindingResource{} }
|
||||
|
||||
func (r *bindingResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_binding"
|
||||
}
|
||||
|
||||
func (r *bindingResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Binds a global zone to a device's local interface(s) — the only legitimately host-specific object in the model.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"device": schema.StringAttribute{
|
||||
Description: "Device name.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||
},
|
||||
"zone": schema.StringAttribute{
|
||||
Description: "Global zone name.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||
},
|
||||
"interfaces": schema.ListAttribute{
|
||||
Description: "Local interface names this zone attaches to on the device, e.g. [\"eth1\"].",
|
||||
Required: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *bindingResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.client = configureClient(req, resp)
|
||||
}
|
||||
|
||||
func (r *bindingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan bindingModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *bindingResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan bindingModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *bindingResource) bindingPath(device, zone string) string {
|
||||
return "/api/v1/devices/" + pathEscape(device) + "/bindings/" + pathEscape(zone)
|
||||
}
|
||||
|
||||
func (r *bindingResource) upsert(ctx context.Context, plan bindingModel, diags *diag.Diagnostics, state *tfsdk.State) {
|
||||
body := bindingAPI{
|
||||
Device: plan.Device.ValueString(),
|
||||
Zone: plan.Zone.ValueString(),
|
||||
Interfaces: listToStrings(ctx, plan.Interfaces, diags),
|
||||
}
|
||||
if diags.HasError() {
|
||||
return
|
||||
}
|
||||
var out bindingAPI
|
||||
if err := r.client.put(ctx, r.bindingPath(body.Device, body.Zone), body, &out); err != nil {
|
||||
diags.AddError("upsert binding failed", err.Error())
|
||||
return
|
||||
}
|
||||
diags.Append(state.Set(ctx, r.toModel(ctx, out, diags))...)
|
||||
}
|
||||
|
||||
func (r *bindingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state bindingModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out bindingAPI
|
||||
if err := r.client.get(ctx, r.bindingPath(state.Device.ValueString(), state.Zone.ValueString()), &out); err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("read binding failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
func (r *bindingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state bindingModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if err := r.client.del(ctx, r.bindingPath(state.Device.ValueString(), state.Zone.ValueString())); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete binding failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ImportState accepts "device:zone".
|
||||
func (r *bindingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
device, zone, ok := strings.Cut(req.ID, ":")
|
||||
if !ok || device == "" || zone == "" {
|
||||
resp.Diagnostics.AddError("invalid import ID", fmt.Sprintf("expected device:zone, got %q", req.ID))
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("device"), device)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("zone"), zone)...)
|
||||
}
|
||||
|
||||
func (r *bindingResource) toModel(ctx context.Context, api bindingAPI, diags *diag.Diagnostics) bindingModel {
|
||||
return bindingModel{
|
||||
Device: types.StringValue(api.Device),
|
||||
Zone: types.StringValue(api.Zone),
|
||||
Interfaces: stringsToList(ctx, api.Interfaces, diags),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &deviceResource{}
|
||||
_ resource.ResourceWithImportState = &deviceResource{}
|
||||
)
|
||||
|
||||
type deviceResource struct{ client *apiClient }
|
||||
|
||||
type deviceModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Class types.String `tfsdk:"class"`
|
||||
Fabric types.String `tfsdk:"fabric"`
|
||||
Resolver types.List `tfsdk:"resolver"`
|
||||
Settings types.Map `tfsdk:"settings"`
|
||||
}
|
||||
|
||||
type deviceAPI struct {
|
||||
Name string `json:"name"`
|
||||
Class string `json:"class"`
|
||||
Fabric string `json:"fabric,omitempty"`
|
||||
Resolver []string `json:"resolver,omitempty"`
|
||||
Settings map[string]string `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
func NewDeviceResource() resource.Resource { return &deviceResource{} }
|
||||
|
||||
func (r *deviceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_device"
|
||||
}
|
||||
|
||||
func (r *deviceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "A fleet member. Its class (router/firewall) and fabric determine how it enforces intents; bindings map global zones to its local interfaces.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Device name (globally unique).",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||
},
|
||||
"class": schema.StringAttribute{
|
||||
Description: "Device class: router or firewall.",
|
||||
Required: true,
|
||||
},
|
||||
"fabric": schema.StringAttribute{
|
||||
Description: "Routing domain (fabric) this device belongs to.",
|
||||
Optional: true,
|
||||
},
|
||||
"resolver": schema.ListAttribute{
|
||||
Description: "Per-device DNS resolver override for dns address groups. Falls back to the fleet default, then system resolvers.",
|
||||
Optional: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"settings": schema.MapAttribute{
|
||||
Description: "Per-device settings overrides (e.g. table_name, log_level, address_family).",
|
||||
Optional: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *deviceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.client = configureClient(req, resp)
|
||||
}
|
||||
|
||||
func (r *deviceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan deviceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *deviceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan deviceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *deviceResource) upsert(ctx context.Context, plan deviceModel, diags *diag.Diagnostics, state *tfsdk.State) {
|
||||
body := deviceAPI{
|
||||
Name: plan.Name.ValueString(),
|
||||
Class: plan.Class.ValueString(),
|
||||
Fabric: plan.Fabric.ValueString(),
|
||||
Resolver: listToStrings(ctx, plan.Resolver, diags),
|
||||
Settings: mapToStrings(ctx, plan.Settings, diags),
|
||||
}
|
||||
if diags.HasError() {
|
||||
return
|
||||
}
|
||||
var out deviceAPI
|
||||
if err := r.client.put(ctx, "/api/v1/devices/"+pathEscape(body.Name), body, &out); err != nil {
|
||||
diags.AddError("upsert device failed", err.Error())
|
||||
return
|
||||
}
|
||||
diags.Append(state.Set(ctx, r.toModel(ctx, out, plan, diags))...)
|
||||
}
|
||||
|
||||
func (r *deviceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state deviceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out deviceAPI
|
||||
if err := r.client.get(ctx, "/api/v1/devices/"+pathEscape(state.Name.ValueString()), &out); err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("read device failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
func (r *deviceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state deviceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if err := r.client.del(ctx, "/api/v1/devices/"+pathEscape(state.Name.ValueString())); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete device failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *deviceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
func (r *deviceResource) toModel(ctx context.Context, api deviceAPI, prior deviceModel, diags *diag.Diagnostics) deviceModel {
|
||||
m := deviceModel{
|
||||
Name: types.StringValue(api.Name),
|
||||
Class: types.StringValue(api.Class),
|
||||
Fabric: optionalString(api.Fabric, prior.Fabric),
|
||||
}
|
||||
// Preserve null for optional collections the user left unset.
|
||||
if prior.Resolver.IsNull() && len(api.Resolver) == 0 {
|
||||
m.Resolver = types.ListNull(types.StringType)
|
||||
} else {
|
||||
m.Resolver = stringsToList(ctx, api.Resolver, diags)
|
||||
}
|
||||
if prior.Settings.IsNull() && len(api.Settings) == 0 {
|
||||
m.Settings = types.MapNull(types.StringType)
|
||||
} else {
|
||||
m.Settings = stringsToMap(ctx, api.Settings, diags)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &fabricResource{}
|
||||
_ resource.ResourceWithImportState = &fabricResource{}
|
||||
)
|
||||
|
||||
type fabricResource struct{ client *apiClient }
|
||||
|
||||
type fabricModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
EnforceOnRouters types.Bool `tfsdk:"enforce_on_routers"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
}
|
||||
|
||||
type fabricAPI struct {
|
||||
Name string `json:"name"`
|
||||
EnforceOnRouters bool `json:"enforce_on_routers"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func NewFabricResource() resource.Resource { return &fabricResource{} }
|
||||
|
||||
func (r *fabricResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_fabric"
|
||||
}
|
||||
|
||||
func (r *fabricResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "A routing domain. enforce_on_routers toggles defense-in-depth (every router carries the intent) versus transparent transit (only boundary firewalls enforce).",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Fabric name (globally unique).",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||
},
|
||||
"enforce_on_routers": schema.BoolAttribute{
|
||||
Description: "When true, routers in this fabric enforce intents too (defense-in-depth).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: stringdefault.StaticString(""),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *fabricResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.client = configureClient(req, resp)
|
||||
}
|
||||
|
||||
func (r *fabricResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan fabricModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *fabricResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan fabricModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *fabricResource) upsert(ctx context.Context, plan fabricModel, diags *diag.Diagnostics, state *tfsdk.State) {
|
||||
body := fabricAPI{
|
||||
Name: plan.Name.ValueString(),
|
||||
EnforceOnRouters: plan.EnforceOnRouters.ValueBool(),
|
||||
Description: plan.Description.ValueString(),
|
||||
}
|
||||
var out fabricAPI
|
||||
if err := r.client.put(ctx, "/api/v1/fabrics/"+pathEscape(body.Name), body, &out); err != nil {
|
||||
diags.AddError("upsert fabric failed", err.Error())
|
||||
return
|
||||
}
|
||||
diags.Append(state.Set(ctx, fabricToModel(out))...)
|
||||
}
|
||||
|
||||
func (r *fabricResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state fabricModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out fabricAPI
|
||||
if err := r.client.get(ctx, "/api/v1/fabrics/"+pathEscape(state.Name.ValueString()), &out); err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("read fabric failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, fabricToModel(out))...)
|
||||
}
|
||||
|
||||
func (r *fabricResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state fabricModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if err := r.client.del(ctx, "/api/v1/fabrics/"+pathEscape(state.Name.ValueString())); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete fabric failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *fabricResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
func fabricToModel(api fabricAPI) fabricModel {
|
||||
return fabricModel{
|
||||
Name: types.StringValue(api.Name),
|
||||
EnforceOnRouters: types.BoolValue(api.EnforceOnRouters),
|
||||
Description: types.StringValue(api.Description),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &portGroupResource{}
|
||||
_ resource.ResourceWithImportState = &portGroupResource{}
|
||||
)
|
||||
|
||||
type portGroupResource struct{ client *apiClient }
|
||||
|
||||
type portGroupModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Proto types.String `tfsdk:"proto"`
|
||||
Ports types.List `tfsdk:"ports"`
|
||||
}
|
||||
|
||||
type portGroupAPI struct {
|
||||
Name string `json:"name"`
|
||||
Proto string `json:"proto"`
|
||||
Ports []string `json:"ports"`
|
||||
}
|
||||
|
||||
func NewPortGroupResource() resource.Resource { return &portGroupResource{} }
|
||||
|
||||
func (r *portGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_portgroup"
|
||||
}
|
||||
|
||||
func (r *portGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "A reusable proto+ports combination referenced by rules.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Portgroup name (globally unique).",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||
},
|
||||
"proto": schema.StringAttribute{
|
||||
Description: "Protocol, e.g. tcp or udp.",
|
||||
Required: true,
|
||||
},
|
||||
"ports": schema.ListAttribute{
|
||||
Description: "Ports or port ranges, e.g. [\"80\", \"443\", \"1024-65535\"].",
|
||||
Required: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *portGroupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.client = configureClient(req, resp)
|
||||
}
|
||||
|
||||
func (r *portGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan portGroupModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *portGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan portGroupModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *portGroupResource) upsert(ctx context.Context, plan portGroupModel, diags *diag.Diagnostics, state *tfsdk.State) {
|
||||
body := portGroupAPI{
|
||||
Name: plan.Name.ValueString(),
|
||||
Proto: plan.Proto.ValueString(),
|
||||
Ports: listToStrings(ctx, plan.Ports, diags),
|
||||
}
|
||||
if diags.HasError() {
|
||||
return
|
||||
}
|
||||
var out portGroupAPI
|
||||
if err := r.client.put(ctx, "/api/v1/portgroups/"+pathEscape(body.Name), body, &out); err != nil {
|
||||
diags.AddError("upsert portgroup failed", err.Error())
|
||||
return
|
||||
}
|
||||
diags.Append(state.Set(ctx, r.toModel(ctx, out, diags))...)
|
||||
}
|
||||
|
||||
func (r *portGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state portGroupModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out portGroupAPI
|
||||
if err := r.client.get(ctx, "/api/v1/portgroups/"+pathEscape(state.Name.ValueString()), &out); err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("read portgroup failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
func (r *portGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state portGroupModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if err := r.client.del(ctx, "/api/v1/portgroups/"+pathEscape(state.Name.ValueString())); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete portgroup failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *portGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
func (r *portGroupResource) toModel(ctx context.Context, api portGroupAPI, diags *diag.Diagnostics) portGroupModel {
|
||||
return portGroupModel{
|
||||
Name: types.StringValue(api.Name),
|
||||
Proto: types.StringValue(api.Proto),
|
||||
Ports: stringsToList(ctx, api.Ports, diags),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &ruleResource{}
|
||||
_ resource.ResourceWithImportState = &ruleResource{}
|
||||
)
|
||||
|
||||
type ruleResource struct{ client *apiClient }
|
||||
|
||||
type ruleModel struct {
|
||||
ID types.Int64 `tfsdk:"id"`
|
||||
Priority types.Int64 `tfsdk:"priority"`
|
||||
Action types.String `tfsdk:"action"`
|
||||
Source types.List `tfsdk:"source"`
|
||||
Dest types.List `tfsdk:"dest"`
|
||||
Proto types.String `tfsdk:"proto"`
|
||||
PortGroup types.String `tfsdk:"portgroup"`
|
||||
Ports types.List `tfsdk:"ports"`
|
||||
Log types.String `tfsdk:"log"`
|
||||
Comment types.String `tfsdk:"comment"`
|
||||
}
|
||||
|
||||
type ruleAPI struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
Action string `json:"action"`
|
||||
Source []string `json:"source"`
|
||||
Dest []string `json:"dest"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
PortGroup string `json:"portgroup,omitempty"`
|
||||
Ports []string `json:"ports,omitempty"`
|
||||
Log string `json:"log,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
func NewRuleResource() resource.Resource { return &ruleResource{} }
|
||||
|
||||
func (r *ruleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_rule"
|
||||
}
|
||||
|
||||
func (r *ruleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "A fleet-global firewall intent. Source and dest are shorewall-style element lists (bare zone, or zone:+ipset / zone:&fqdn); a selector must always be paired with a zone.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.Int64Attribute{
|
||||
Description: "Server-assigned rule id.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()},
|
||||
},
|
||||
"priority": schema.Int64Attribute{
|
||||
Description: "Evaluation priority; lower is evaluated first.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: int64default.StaticInt64(0),
|
||||
},
|
||||
"action": schema.StringAttribute{
|
||||
Description: "accept, drop, reject, etc.",
|
||||
Required: true,
|
||||
},
|
||||
"source": schema.ListAttribute{
|
||||
Description: "Source element list, e.g. [\"loc\", \"net:+asn_cloudflare\"].",
|
||||
Required: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"dest": schema.ListAttribute{
|
||||
Description: "Dest element list.",
|
||||
Required: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"proto": schema.StringAttribute{
|
||||
Description: "Protocol (used when portgroup is not set).",
|
||||
Optional: true,
|
||||
},
|
||||
"portgroup": schema.StringAttribute{
|
||||
Description: "Named portgroup supplying proto+ports.",
|
||||
Optional: true,
|
||||
},
|
||||
"ports": schema.ListAttribute{
|
||||
Description: "Explicit ports (used when portgroup is not set).",
|
||||
Optional: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"log": schema.StringAttribute{Optional: true},
|
||||
"comment": schema.StringAttribute{Optional: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ruleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.client = configureClient(req, resp)
|
||||
}
|
||||
|
||||
func (r *ruleResource) body(ctx context.Context, plan ruleModel, diags *diag.Diagnostics) ruleAPI {
|
||||
return ruleAPI{
|
||||
Priority: int(plan.Priority.ValueInt64()),
|
||||
Action: plan.Action.ValueString(),
|
||||
Source: listToStrings(ctx, plan.Source, diags),
|
||||
Dest: listToStrings(ctx, plan.Dest, diags),
|
||||
Proto: plan.Proto.ValueString(),
|
||||
PortGroup: plan.PortGroup.ValueString(),
|
||||
Ports: listToStrings(ctx, plan.Ports, diags),
|
||||
Log: plan.Log.ValueString(),
|
||||
Comment: plan.Comment.ValueString(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ruleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan ruleModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
body := r.body(ctx, plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out ruleAPI
|
||||
if err := r.client.post(ctx, "/api/v1/rules", body, &out); err != nil {
|
||||
resp.Diagnostics.AddError("create rule failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
// Update: the rules API is create/delete only (no in-place PUT), so replace by
|
||||
// deleting the old id and creating the new definition, keeping it in one apply.
|
||||
func (r *ruleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan, state ruleModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
body := r.body(ctx, plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out ruleAPI
|
||||
if err := r.client.post(ctx, "/api/v1/rules", body, &out); err != nil {
|
||||
resp.Diagnostics.AddError("recreate rule failed", err.Error())
|
||||
return
|
||||
}
|
||||
if id := state.ID.ValueInt64(); id != 0 {
|
||||
if err := r.client.del(ctx, "/api/v1/rules/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete old rule failed", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
func (r *ruleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state ruleModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out ruleAPI
|
||||
if err := r.client.get(ctx, "/api/v1/rules/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("read rule failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
func (r *ruleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state ruleModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if err := r.client.del(ctx, "/api/v1/rules/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete rule failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ruleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
id, err := strconv.ParseInt(req.ID, 10, 64)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("invalid import ID", "rule id must be an integer")
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
|
||||
}
|
||||
|
||||
func (r *ruleResource) toModel(ctx context.Context, api ruleAPI, prior ruleModel, diags *diag.Diagnostics) ruleModel {
|
||||
m := ruleModel{
|
||||
ID: types.Int64Value(api.ID),
|
||||
Priority: types.Int64Value(int64(api.Priority)),
|
||||
Action: types.StringValue(api.Action),
|
||||
Source: stringsToList(ctx, api.Source, diags),
|
||||
Dest: stringsToList(ctx, api.Dest, diags),
|
||||
Proto: optionalString(api.Proto, prior.Proto),
|
||||
PortGroup: optionalString(api.PortGroup, prior.PortGroup),
|
||||
Log: optionalString(api.Log, prior.Log),
|
||||
Comment: optionalString(api.Comment, prior.Comment),
|
||||
}
|
||||
// ports is optional; preserve null when unset and the API returns none.
|
||||
if prior.Ports.IsNull() && len(api.Ports) == 0 {
|
||||
m.Ports = types.ListNull(types.StringType)
|
||||
} else {
|
||||
m.Ports = stringsToList(ctx, api.Ports, diags)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &zoneResource{}
|
||||
_ resource.ResourceWithImportState = &zoneResource{}
|
||||
)
|
||||
|
||||
type zoneResource struct{ client *apiClient }
|
||||
|
||||
type zoneModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
Subnets types.List `tfsdk:"subnets"`
|
||||
Parent types.String `tfsdk:"parent"`
|
||||
}
|
||||
|
||||
type zoneAPI struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Subnets []string `json:"subnets"`
|
||||
Parent string `json:"parent,omitempty"`
|
||||
}
|
||||
|
||||
func NewZoneResource() resource.Resource { return &zoneResource{} }
|
||||
|
||||
func (r *zoneResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_zone"
|
||||
}
|
||||
|
||||
func (r *zoneResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "A fleet-global network segment (a named set of subnets). Zones are the topological anchors of the firewall model.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Zone name (globally unique).",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "Zone type: ip, ip6, or firewall.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: stringdefault.StaticString("ip"),
|
||||
},
|
||||
"subnets": schema.ListAttribute{
|
||||
Description: "CIDRs belonging to this zone. May be empty for a no-subnet zone such as an internet-facing 'net'.",
|
||||
Optional: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"parent": schema.StringAttribute{
|
||||
Description: "Parent zone name for subzone nesting; the child's subnets must be within the parent's.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *zoneResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.client = configureClient(req, resp)
|
||||
}
|
||||
|
||||
func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan zoneModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan zoneModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
r.upsert(ctx, plan, &resp.Diagnostics, &resp.State)
|
||||
}
|
||||
|
||||
func (r *zoneResource) upsert(ctx context.Context, plan zoneModel, diags *diag.Diagnostics, state *tfsdk.State) {
|
||||
body := zoneAPI{
|
||||
Name: plan.Name.ValueString(),
|
||||
Type: plan.Type.ValueString(),
|
||||
Subnets: listToStrings(ctx, plan.Subnets, diags),
|
||||
Parent: plan.Parent.ValueString(),
|
||||
}
|
||||
if diags.HasError() {
|
||||
return
|
||||
}
|
||||
var out zoneAPI
|
||||
if err := r.client.put(ctx, "/api/v1/zones/"+pathEscape(body.Name), body, &out); err != nil {
|
||||
diags.AddError("upsert zone failed", err.Error())
|
||||
return
|
||||
}
|
||||
diags.Append(state.Set(ctx, r.toModel(ctx, out, diags))...)
|
||||
}
|
||||
|
||||
func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state zoneModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
var out zoneAPI
|
||||
if err := r.client.get(ctx, "/api/v1/zones/"+pathEscape(state.Name.ValueString()), &out); err != nil {
|
||||
if isNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("read zone failed", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, &resp.Diagnostics))...)
|
||||
}
|
||||
|
||||
func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state zoneModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if err := r.client.del(ctx, "/api/v1/zones/"+pathEscape(state.Name.ValueString())); err != nil && !isNotFound(err) {
|
||||
resp.Diagnostics.AddError("delete zone failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *zoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
func (r *zoneResource) toModel(ctx context.Context, api zoneAPI, diags *diag.Diagnostics) zoneModel {
|
||||
m := zoneModel{
|
||||
Name: types.StringValue(api.Name),
|
||||
Type: types.StringValue(api.Type),
|
||||
Subnets: stringsToList(ctx, api.Subnets, diags),
|
||||
}
|
||||
if api.Parent == "" {
|
||||
m.Parent = types.StringNull()
|
||||
} else {
|
||||
m.Parent = types.StringValue(api.Parent)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Command terraform-provider-tomswallapi is the Terraform/OpenTofu provider for
|
||||
// tomswallapi: it manages the fleet-global firewall model (zones, address groups,
|
||||
// portgroups, rules, policies, fabrics) and per-device zone->interface bindings.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
||||
|
||||
"git.unkin.net/unkin/terraform-provider-tomswallapi/internal/provider"
|
||||
)
|
||||
|
||||
var version = "0.0.1"
|
||||
|
||||
func main() {
|
||||
var debug bool
|
||||
flag.BoolVar(&debug, "debug", false, "enable debug mode")
|
||||
flag.Parse()
|
||||
|
||||
opts := providerserver.ServeOpts{
|
||||
Address: "git.unkin.net/unkin/tomswallapi",
|
||||
Debug: debug,
|
||||
}
|
||||
|
||||
if err := providerserver.Serve(context.Background(), provider.New(version), opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user