Files
fancy-dns/Makefile
T
matsandClaude Sonnet 5.5 1a536e7d0e Split the console from the resolver: fancy-dns-console writes the cluster
fancy-dns-console is a second binary holding the web UI, authentication and
every edit. It writes ClientGroup, BlockList and DNSZone objects through the new
kubeedit package and forwards all other /api routes to a running resolver
with its own key, dropping the browser's credentials, so it can run in the
cluster or on a workstation with a kubeconfig and nothing about the resolver's
placement matters. The resolver never imports kubeedit and the console is not on
the DNS path.

kubeedit validates every spec with the runtime's own converters
(kubesource.CheckGroup/CheckList/CheckZone) before writing, so the console
cannot store an object the resolver would reject. Zone edits keep the
file-based writer's behaviour: duplicate and below-a-delegation records are
refused, a delegation in a new zone also gives the zone its servers, and a
zone with no apex NS is never stored. Each object records the editing user in
an annotation; updates retry on conflict.

The resolver reports groups from ClientGroup objects as source "kubernetes" and
counter-only rows (the "(default)" bucket, or a group removed since) as
"history", and lists DNSZone zones as the editable ones. The console uses that
to leave the YAML's groups alone and to drop history rows, which the UI would
otherwise send back on save. Its GET /api/clients overlays the cluster's
current objects on the resolver's list, because the resolver only sees an edit
after its watch fires and a save made just after another would delete the
first.

An end-to-end test runs both halves against one fake apiserver: a group saved
through the console refuses queries, a zone built record by record answers
authoritatively, and a request with the resolver's own key is rejected by the
console.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 08:43:41 +02:00

177 lines
6.0 KiB
Makefile

# fancy-dns
#
# `make` alone gives you a help screen. The two targets you want most are
# `make build` (binary with the console embedded) and `make check` (what CI
# runs).
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
BINARY := fancy-dns
PKG := git.k8s-git-k6n.net/mats/fancy-dns
CMD := ./cmd/fancy-dns
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -s -w -X $(PKG)/internal/app.Version=$(VERSION)
DIST := dist
IMAGE ?= fancydns/fancy-dns:$(VERSION)
GO ?= go
NPM ?= npm
GOFILES := $(shell find . -name '*.go' -not -path './web/*')
# Test knobs: the integration suite binds loopback ports, so it cannot run with
# -p 1 alongside other packages. Race is the default because the whole design
# leans on atomics and a regression there is silent.
TESTFLAGS ?= -race -count=1
.PHONY: help
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage: make <target>\n"} \
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-16s\033[0m %s\n", $$1, $$2 } \
/^## / { printf "\n%s\n", substr($$0, 4) }' $(MAKEFILE_LIST)
## Build
.PHONY: build
build: web ## Build the binary with the console embedded
@mkdir -p $(DIST)
CGO_ENABLED=0 $(GO) build -trimpath -ldflags '$(LDFLAGS)' -o $(DIST)/$(BINARY) $(CMD)
CGO_ENABLED=0 $(GO) build -trimpath -ldflags '$(LDFLAGS)' -o $(DIST)/$(BINARY)-console ./cmd/fancy-dns-console
@echo "built $(DIST)/$(BINARY) and $(DIST)/$(BINARY)-console ($(VERSION))"
.PHONY: go-build
go-build: ## Build without the console (fast; used by the Go test loop)
CGO_ENABLED=0 $(GO) build -trimpath -ldflags '$(LDFLAGS)' -o $(DIST)/$(BINARY)-noassets $(CMD)
CGO_ENABLED=0 $(GO) build -trimpath -ldflags '$(LDFLAGS)' -o $(DIST)/$(BINARY)-console-noassets ./cmd/fancy-dns-console
.PHONY: install
install: ## Install into GOBIN
CGO_ENABLED=0 $(GO) install -trimpath -ldflags '$(LDFLAGS)' $(CMD)
.PHONY: web
web: ## Build the web console into internal/web/dist
@if [ ! -d web/node_modules ]; then \
echo "==> installing web dependencies"; \
cd web && $(NPM) ci --no-audit --no-fund; \
fi
cd web && $(NPM) run build
@test -f internal/web/dist/index.html || { echo "console build produced no index.html"; exit 1; }
.PHONY: web-dev
web-dev: ## Run the Vite dev server (proxies /api to 127.0.0.1:8080)
cd web && $(NPM) run dev
## Verify
.PHONY: check
check: check-go test ## What CI runs (format, vet, compile, then the suite)
.PHONY: check-go
check-go: fmt-check vet go-build ## The tight inner loop: format, vet, compile. No npm, no tests
@echo "check-go: ok"
.PHONY: gofmt-w
gofmt-w: ## Format every Go file in the tree, including ones just created
@gofmt -w $(GOFILES)
@echo "gofmt: ok"
.PHONY: test
test: ## Run the test suite with the race detector
$(GO) test $(TESTFLAGS) ./...
.PHONY: test-short
test-short: ## Run the tests without -race (much faster)
$(GO) test -short -count=1 ./...
.PHONY: cover
cover: ## Test with a coverage report at coverage.html
$(GO) test -race -count=1 -coverprofile=coverage.out ./...
$(GO) tool cover -html=coverage.out -o coverage.html
@$(GO) tool cover -func=coverage.out | tail -1
.PHONY: bench
bench: ## Run the cache and blocklist benchmarks
$(GO) test -run '^$$' -bench . -benchmem ./internal/cache/... ./internal/blocklist/...
.PHONY: vet
vet: ## go vet
$(GO) vet ./...
.PHONY: fmt
fmt: ## Format the Go code
gofmt -w $(GOFILES)
.PHONY: fmt-check
fmt-check: ## Fail if anything is unformatted
@unformatted=$$(gofmt -l $(GOFILES)); \
if [ -n "$$unformatted" ]; then \
echo "gofmt needed:"; echo "$$unformatted"; exit 1; \
fi
.PHONY: lint
lint: ## go vet, with the name it has always had
$(GO) vet ./...
@echo "lint: ok"
.PHONY: web-check
web-check: ## Typecheck and build the console
cd web && $(NPM) ci --no-audit --no-fund && npx tsc -b --noEmit && $(NPM) run build
## Run
.PHONY: run
run: go-build ## Run with deploy/config.example.yaml (binds :53 — rewrite the ports first)
$(DIST)/$(BINARY)-noassets -config deploy/config.example.yaml -log-level debug
.PHONY: check-config
check-config: go-build ## Validate a config file without starting
$(DIST)/$(BINARY)-noassets -config $(or $(CONFIG),deploy/config.example.yaml) -check
## End-to-end
.PHONY: smoke
smoke: build ## Boot the real binary on unprivileged ports; check the API, metrics and the embedded console
@set -euo pipefail; \
dir=$$(mktemp -d); \
pid=""; \
trap '[ -n "$$pid" ] && kill "$$pid" 2>/dev/null || true; rm -rf "$$dir"' EXIT; \
sed \
-e 's#127.0.0.1:8080#127.0.0.1:18080#' \
-e 's#0.0.0.0:53#127.0.0.1:15353#g' \
-e 's#\[::\]:53#[::1]:15353#g' \
-e 's#trust_loopback: true#trust_loopback: false#' \
deploy/config.example.yaml > "$$dir/config.yaml"; \
$(DIST)/$(BINARY) -config "$$dir/config.yaml" -check; \
$(DIST)/$(BINARY) -config "$$dir/config.yaml" > "$$dir/log" 2>&1 & \
pid=$$!; \
for i in $$(seq 1 40); do \
curl -fsS http://127.0.0.1:18080/api/healthz >/dev/null 2>&1 && break; \
sleep 0.5; \
done; \
echo "==> health"; curl -fsS http://127.0.0.1:18080/api/healthz; echo; \
echo "==> console"; curl -fsS http://127.0.0.1:18080/ | grep -qi '<!doctype html' && echo "embedded"; \
echo "==> auth"; \
test "$$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:18080/api/whoami)" = 401 && echo "unauthenticated -> 401"; \
curl -fsS -H "X-API-Key: CHANGE-ME-long-random-string" http://127.0.0.1:18080/api/whoami | grep -q '"can_write":true' && echo "authenticated -> can_write"; \
echo "==> metrics"; curl -fsS -H "X-API-Key: CHANGE-ME-long-random-string" http://127.0.0.1:18080/api/metrics | grep -c '^fancydns_'
## Container
.PHONY: image
image: ## Build the container image
docker build --build-arg VERSION=$(VERSION) -t $(IMAGE) .
.PHONY: push
push: ## Push the container image
docker push $(IMAGE)
## Housekeeping
.PHONY: clean
clean: ## Remove build output
rm -rf $(DIST) coverage.out coverage.html
.PHONY: clean-web
clean-web: ## Remove node_modules and the console bundle
rm -rf web/node_modules web/.vite internal/web/dist/assets internal/web/dist/index.html