mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 19:21:29 +00:00
bee47f0318
Closes the production-readiness loop on the ACME surface. After this
commit, certctl ships per-account rate limits + a GC sweeper for
expired ACME state + a kind-driven cert-manager 1.15 integration test
+ a lego-driven RFC conformance harness + a k6 loadtest scenario for
the unauthenticated ACME path.
Architecture:
- Rate limits live in-memory + per-replica. Restart wipes the
counters; orders/hour caps are eventual-consistency anyway. A
3-replica certctl-server fleet behind an LB effectively has 3x
the configured throughput per account; persistent rate limiting
is a follow-up if production telemetry shows abuse patterns we
can't catch in a single restart cycle. Per-key + per-action
isolation: ActionNewOrder/acc-1, ActionKeyChange/acc-1, and
ActionChallengeRespond/<challenge-id> are independent buckets.
- GC loop follows the existing scheduler-loop pattern (atomic.Bool
+ sync.WaitGroup; see crlGenerationLoop for shape). Three
independent SQL sweeps per tick (DELETE expired nonces; UPDATE
pending authzs whose expires_at < now() to expired; UPDATE
pending/ready/processing orders whose expires_at < now() to
invalid). Each sweep is a single statement; failures are logged-
and-continued so a failing nonces sweep doesn't block authzs.
Per-sweep 1m timeout bounds a stuck Postgres.
- cert-manager integration test is gated on KIND_AVAILABLE so CI
skips it cleanly (kind is too heavy for per-PR). Operators run
locally via 'make acme-cert-manager-test'; the harness brings up
a fresh cluster each run + tears it down on Cleanup.
- lego conformance harness drives a real ACME client through
register → run → cert-PEM-landed against a hermetic certctl
stack. Catches RFC-shape regressions third-party clients would
hit before they ship.
- k6 ACME-flow scenario hammers the unauthenticated surface
(directory + new-nonce + ARI synthetic-id) at 100 VUs × 5m. JWS-
signed flows are out of scope for k6 (no JWS support); they're
covered by the lego harness above.
What ships:
- internal/api/acme/ratelimit.go (+ ratelimit_test.go: 7 cases —
disable-when-perHour-zero, capacity, per-key isolation, per-
action isolation, refill-over-time, RetryAfter, concurrent-access
with -race + 200 goroutines × 200 calls).
- internal/repository/postgres/acme.go: 4 new methods —
CountActiveOrdersByAccount + GCExpiredNonces + GCExpireAuthorizations
+ GCInvalidateExpiredOrders. Each a single SQL statement.
- internal/service/acme.go: SetRateLimiter + GarbageCollect +
rate-limit gates at 3 entry points (CreateOrder + RotateAccountKey
+ RespondToChallenge) + concurrent-orders gate at CreateOrder.
2 new sentinels (ErrACMERateLimited, ErrACMEConcurrentOrdersExceeded);
5 new GC metrics (gc_runs / gc_run_failures / gc_nonces_reaped /
gc_authzs_expired / gc_orders_invalidated).
- internal/scheduler/scheduler.go: ACMEGarbageCollector interface +
acmeGCRunning atomic.Bool + acmeGCInterval + 2 setters (SetACME-
GarbageCollector + SetACMEGCInterval) + acmeGCLoop following the
crlGenerationLoop shape.
- internal/api/handler/acme.go: writeServiceError gains rateLimited
(429 + RFC 8555 §6.7) + concurrent-orders-exceeded mappings.
- internal/config/config.go: 5 new env vars
(CERTCTL_ACME_SERVER_RATE_LIMIT_ORDERS_PER_HOUR=100,
CERTCTL_ACME_SERVER_RATE_LIMIT_CONCURRENT_ORDERS=5,
CERTCTL_ACME_SERVER_RATE_LIMIT_KEY_CHANGE_PER_HOUR=5,
CERTCTL_ACME_SERVER_RATE_LIMIT_CHALLENGE_RESPONDS_PER_HOUR=60,
CERTCTL_ACME_SERVER_GC_INTERVAL=1m).
- cmd/server/main.go: NewRateLimiter() + SetRateLimiter() at
startup; conditional SetACMEGarbageCollector(acmeService) +
SetACMEGCInterval(cfg.ACMEServer.GCInterval) when Enabled+
GCInterval > 0.
- deploy/test/acme-integration/: kind-config.yaml + cert-manager-
install.sh + clusterissuer-trust-authenticated.yaml +
clusterissuer-challenge.yaml + certificate-test.yaml + conformance-
lego.sh + certmanager_test.go (//go:build integration + KIND_AVAILABLE
gate).
- deploy/test/loadtest/k6/acme_flow.js + README ACME-flows section.
- Makefile: 2 new PHONY targets (acme-cert-manager-test +
acme-rfc-conformance-test).
- docs/acme-server.md: status flipped to Phase 5; Configuration
table grows 5 rows; new 'Phase 5 — operational guidance' section
explaining rate-limit math + GC sweeper semantics + cert-manager
integration + lego conformance + k6 baseline.
Tests:
- 'go vet ./...' clean across the repo.
- 'go test -short -count=1 ./internal/...' green across every
affected package (service / acme / handler / scheduler / repo /
config).
- 'go vet -tags=integration ./deploy/test/acme-integration/' clean
(the integration test compiles cleanly with the build tag).
- The kind/cert-manager harness is gated behind KIND_AVAILABLE so
CI skips by default; operators run locally via 'make acme-cert-
manager-test'.
Engineering history: cowork/WORKSPACE-CHANGELOG.md 'ACME-Server-5'.
302 lines
13 KiB
Makefile
302 lines
13 KiB
Makefile
.PHONY: help build run test lint verify verify-docs verify-deploy loadtest acme-cert-manager-test acme-rfc-conformance-test clean docker-up docker-down migrate-up migrate-down generate test-cover frontend-build qa-stats
|
|
|
|
# Default target - show help
|
|
help:
|
|
@echo "Certctl Development Commands"
|
|
@echo "============================="
|
|
@echo ""
|
|
@echo "Build & Run:"
|
|
@echo " make build Build server and agent binaries"
|
|
@echo " make run Run server locally (requires DB)"
|
|
@echo " make run-agent Run agent locally"
|
|
@echo ""
|
|
@echo "Testing & Quality:"
|
|
@echo " make test Run all tests"
|
|
@echo " make test-verbose Run tests with verbose output"
|
|
@echo " make lint Run linter (golangci-lint)"
|
|
@echo " make fmt Format code with gofmt"
|
|
@echo " make verify Pre-commit gate: fmt + vet + lint + test (CI-parity)"
|
|
@echo " make verify-docs Pre-tag gate: QA-doc drift checks (operator-facing docs)"
|
|
@echo " make verify-deploy Pre-push gate: digest validity + OpenAPI parity + docker build smoke"
|
|
@echo " make loadtest k6 throughput run against postgres + certctl (NOT in verify; manual + cron only)"
|
|
@echo ""
|
|
@echo "Database:"
|
|
@echo " make migrate-up Run migrations (requires DB_URL)"
|
|
@echo " make migrate-down Rollback last migration"
|
|
@echo " make db-seed Seed database with test data"
|
|
@echo ""
|
|
@echo "Docker:"
|
|
@echo " make docker-build Build Docker images"
|
|
@echo " make docker-up Start Docker Compose stack"
|
|
@echo " make docker-down Stop Docker Compose stack"
|
|
@echo " make docker-logs View Docker logs"
|
|
@echo " make docker-clean Remove Docker resources"
|
|
@echo ""
|
|
@echo "Code Generation:"
|
|
@echo " make generate Run go generate"
|
|
@echo " make clean Clean build artifacts"
|
|
@echo ""
|
|
|
|
# Build targets
|
|
build:
|
|
@echo "Building server and agent..."
|
|
mkdir -p bin
|
|
CGO_ENABLED=0 go build -o bin/server ./cmd/server
|
|
CGO_ENABLED=0 go build -o bin/agent ./cmd/agent
|
|
@echo "Build complete: bin/server, bin/agent"
|
|
|
|
build-server:
|
|
@echo "Building server..."
|
|
mkdir -p bin
|
|
CGO_ENABLED=0 go build -o bin/server ./cmd/server
|
|
@echo "Server build complete"
|
|
|
|
build-agent:
|
|
@echo "Building agent..."
|
|
mkdir -p bin
|
|
CGO_ENABLED=0 go build -o bin/agent ./cmd/agent
|
|
@echo "Agent build complete"
|
|
|
|
# Run targets
|
|
run: build-server
|
|
@echo "Starting server (requires DATABASE_URL or DB_* env vars)..."
|
|
./bin/server
|
|
|
|
run-agent: build-agent
|
|
@echo "Starting agent (requires SERVER_URL and API_KEY env vars)..."
|
|
./bin/agent
|
|
|
|
# Testing targets
|
|
test:
|
|
@echo "Running tests..."
|
|
go test ./...
|
|
|
|
test-verbose:
|
|
@echo "Running tests with verbose output..."
|
|
go test -v ./...
|
|
|
|
test-coverage:
|
|
@echo "Running tests with coverage..."
|
|
go test -coverprofile=coverage.out ./...
|
|
go tool cover -html=coverage.out -o coverage.html
|
|
@echo "Coverage report: coverage.html"
|
|
|
|
test-cover:
|
|
@echo "Running tests with coverage..."
|
|
go test ./internal/service/... ./internal/api/handler/... ./internal/integration/... -count=1 -cover -coverprofile=coverage.out
|
|
@echo "Coverage report: coverage.out"
|
|
|
|
# Linting targets
|
|
lint:
|
|
@echo "Running golangci-lint..."
|
|
@which golangci-lint > /dev/null || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)
|
|
golangci-lint run ./...
|
|
|
|
fmt:
|
|
@echo "Formatting code..."
|
|
go fmt ./...
|
|
@echo "Code formatted"
|
|
|
|
vet:
|
|
@echo "Running go vet..."
|
|
go vet ./...
|
|
|
|
# verify: aggregate pre-commit gate. Mirrors what CI enforces, so
|
|
# running `make verify` locally before committing prevents the
|
|
# class of breakages that ship green-locally / red-on-CI (e.g.
|
|
# Bundle-9's ST1018 invisible-Unicode-literal hits, which `go vet`
|
|
# alone cannot catch — staticcheck under golangci-lint does).
|
|
verify:
|
|
@echo "==> fmt"
|
|
@go fmt ./... | { ! grep -q '.'; } || (echo "gofmt produced changes — commit them" && exit 1)
|
|
@echo "==> go vet ./..."
|
|
@go vet ./...
|
|
@echo "==> golangci-lint run ./... (incl. staticcheck ST*)"
|
|
@which golangci-lint > /dev/null || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)
|
|
@golangci-lint run ./... --timeout 5m
|
|
@echo "==> go test -short ./..."
|
|
@go test -short -count=1 ./...
|
|
@echo ""
|
|
@echo "verify: PASS — safe to commit"
|
|
|
|
# verify-docs: pre-tag gate. Runs the QA-doc Part-count + seed-count
|
|
# drift guards that ci-pipeline-cleanup Phase 11 / frozen decision 0.13
|
|
# moved out of CI (was per-push blocking; now operator-runs pre-tag).
|
|
# These guards protect docs/qa-test-guide.md headlines from drifting
|
|
# vs the underlying source-of-truth (testing-guide Part count, seed
|
|
# row count). Operator-facing docs only — not product-affecting.
|
|
verify-docs:
|
|
@echo "==> QA-doc Part-count drift"
|
|
@bash scripts/qa-doc-part-count.sh
|
|
@echo "==> QA-doc seed-count drift"
|
|
@bash scripts/qa-doc-seed-count.sh
|
|
@echo ""
|
|
@echo "verify-docs: PASS — safe to tag"
|
|
|
|
# verify-deploy: optional pre-push gate. Runs the digest-validity check,
|
|
# the OpenAPI ↔ handler parity check, and a Docker build smoke for the
|
|
# production images (server + agent only — fast subset for local; CI
|
|
# builds all 4 Dockerfiles per ci-pipeline-cleanup Phase 8 / frozen
|
|
# decision 0.10).
|
|
#
|
|
# Per ci-pipeline-cleanup bundle Phase 11 / frozen decision 0.13.
|
|
verify-deploy:
|
|
@echo "==> Digest validity"
|
|
@bash scripts/ci-guards/digest-validity.sh
|
|
@echo "==> OpenAPI ↔ handler parity"
|
|
@bash scripts/ci-guards/openapi-handler-parity.sh
|
|
@echo "==> Docker build smoke (server + agent — fast subset)"
|
|
@docker build -f Dockerfile -t certctl:verify .
|
|
@docker build -f Dockerfile.agent -t certctl-agent:verify .
|
|
@echo ""
|
|
@echo "verify-deploy: PASS — safe to push"
|
|
|
|
# Load-test harness — closes the #8 acquisition-readiness blocker from
|
|
# the 2026-05-01 issuer coverage audit. Boots a minimal certctl stack
|
|
# (postgres + tls-init + certctl-server) and runs k6 against the API
|
|
# tier for ~5 minutes. Exits non-zero on any threshold breach.
|
|
#
|
|
# NOT in `make verify` — load tests take minutes, not seconds, and
|
|
# don't gate per-PR signal. CI gates this behind workflow_dispatch +
|
|
# weekly cron in .github/workflows/loadtest.yml. See
|
|
# deploy/test/loadtest/README.md for thresholds, baseline, and how to
|
|
# interpret a regression.
|
|
loadtest:
|
|
@echo "==> spinning up postgres + certctl + k6 driver (this takes ~7m)"
|
|
@cd deploy/test/loadtest && docker compose up --build --abort-on-container-exit --exit-code-from k6
|
|
@echo ""
|
|
@echo "==> results landed in deploy/test/loadtest/results/"
|
|
@if [ -f deploy/test/loadtest/results/summary.txt ]; then cat deploy/test/loadtest/results/summary.txt; fi
|
|
|
|
# Phase 5 — kind-driven cert-manager integration test. Requires
|
|
# `kind`, `kubectl`, `helm`, and a local Docker daemon. Sets
|
|
# KIND_AVAILABLE=1 so the test runs (it skips cleanly when unset, which
|
|
# is the CI default — kind is too heavy for per-PR CI). The test
|
|
# brings up a fresh cluster, installs cert-manager 1.15, helm-installs
|
|
# certctl-test, applies a ClusterIssuer + Certificate, and asserts the
|
|
# Secret lands.
|
|
acme-cert-manager-test:
|
|
@echo "==> running cert-manager integration test (requires kind/kubectl/helm)"
|
|
@KIND_AVAILABLE=1 go test -tags=integration -count=1 -timeout=15m \
|
|
./deploy/test/acme-integration/...
|
|
|
|
# Phase 5 — RFC 8555 conformance against `lego` driving the certctl
|
|
# server. Hermetic: brings up a single certctl-server via docker
|
|
# compose, points lego at it, runs the conformance scenarios. Skips
|
|
# when the operator hasn't built the test image (`make docker-build`
|
|
# first).
|
|
acme-rfc-conformance-test:
|
|
@echo "==> running RFC 8555 conformance via lego"
|
|
@if ! command -v lego >/dev/null 2>&1; then \
|
|
echo "lego not installed — go install github.com/go-acme/lego/v4/cmd/lego@latest"; \
|
|
exit 1; \
|
|
fi
|
|
@cd deploy/test/loadtest && docker compose up -d certctl postgres
|
|
@sleep 8
|
|
@CERTCTL_ACME_DIR=https://localhost:8443/acme/profile/prof-test/directory \
|
|
bash deploy/test/acme-integration/conformance-lego.sh
|
|
@cd deploy/test/loadtest && docker compose down
|
|
|
|
# Database targets (requires migrate tool)
|
|
migrate-up:
|
|
@echo "Running migrations..."
|
|
@which migrate > /dev/null || (echo "Installing migrate CLI..." && go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest)
|
|
migrate -path migrations -database "${DB_URL:-postgres://certctl:certctl@localhost:5432/certctl?sslmode=disable}" up
|
|
|
|
migrate-down:
|
|
@echo "Rolling back last migration..."
|
|
@which migrate > /dev/null || (echo "Installing migrate CLI..." && go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest)
|
|
migrate -path migrations -database "${DB_URL:-postgres://certctl:certctl@localhost:5432/certctl?sslmode=disable}" down 1
|
|
|
|
migrate-status:
|
|
@echo "Checking migration status..."
|
|
@which migrate > /dev/null || (echo "Installing migrate CLI..." && go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest)
|
|
migrate -path migrations -database "${DB_URL:-postgres://certctl:certctl@localhost:5432/certctl?sslmode=disable}" version
|
|
|
|
db-seed:
|
|
@echo "Seeding database with test data..."
|
|
go run ./scripts/seed/main.go
|
|
|
|
# Docker targets
|
|
docker-build:
|
|
@echo "Building Docker images..."
|
|
docker-compose -f deploy/docker-compose.yml build
|
|
|
|
docker-up:
|
|
@echo "Starting Docker Compose stack..."
|
|
docker-compose -f deploy/docker-compose.yml up -d
|
|
@echo "Stack running. Access server at http://localhost:8443"
|
|
|
|
docker-up-dev:
|
|
@echo "Starting Docker Compose stack (dev mode)..."
|
|
docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml up -d
|
|
@echo "Stack running. PgAdmin at http://localhost:5050"
|
|
|
|
docker-down:
|
|
@echo "Stopping Docker Compose stack..."
|
|
docker-compose -f deploy/docker-compose.yml down
|
|
|
|
docker-logs:
|
|
docker-compose -f deploy/docker-compose.yml logs -f
|
|
|
|
docker-logs-server:
|
|
docker-compose -f deploy/docker-compose.yml logs -f certctl-server
|
|
|
|
docker-logs-agent:
|
|
docker-compose -f deploy/docker-compose.yml logs -f certctl-agent
|
|
|
|
docker-clean:
|
|
@echo "Removing Docker resources..."
|
|
docker-compose -f deploy/docker-compose.yml down -v
|
|
@echo "Cleaned up"
|
|
|
|
# Code generation
|
|
generate:
|
|
@echo "Running go generate..."
|
|
go generate ./...
|
|
@echo "Code generation complete"
|
|
|
|
# Frontend build
|
|
frontend-build:
|
|
@echo "Building frontend..."
|
|
cd web && npm ci && npx vite build
|
|
@echo "Frontend build complete"
|
|
|
|
# QA Suite Stats — Bundle P / Strengthening #8.
|
|
# Single source-of-truth for every count claim in docs/qa-test-guide.md +
|
|
# docs/testing-guide.md. The Strengthening #6 CI drift guards consume the
|
|
# same numbers, eliminating the doc-drift class structurally.
|
|
qa-stats:
|
|
@echo "=== certctl QA Suite Stats ==="
|
|
@echo "Date: $$(date +%Y-%m-%d)"
|
|
@echo "HEAD: $$(git rev-parse HEAD 2>/dev/null || echo 'not-a-git-repo')"
|
|
@echo ""
|
|
@echo "Backend test files: $$(find . -name '*_test.go' -not -path './web/*' 2>/dev/null | wc -l | tr -d ' ')"
|
|
@echo "Backend Test functions: $$(find . -name '*_test.go' -not -path './web/*' 2>/dev/null | xargs grep -c '^func Test' 2>/dev/null | awk -F: '{s+=$$2} END{print s+0}')"
|
|
@echo "Backend t.Run subtests: $$(find . -name '*_test.go' -not -path './web/*' 2>/dev/null | xargs grep -c 't\.Run(' 2>/dev/null | awk -F: '{s+=$$2} END{print s+0}')"
|
|
@echo "Frontend test files: $$(find web/src -name '*.test.ts' -o -name '*.test.tsx' 2>/dev/null | wc -l | tr -d ' ')"
|
|
@echo "Fuzz targets: $$(grep -rE 'func Fuzz[A-Z]' --include='*_test.go' . 2>/dev/null | wc -l | tr -d ' ')"
|
|
@echo "t.Skip sites: $$(grep -rE 't\.Skip(Now|f)?\(' --include='*_test.go' . 2>/dev/null | wc -l | tr -d ' ')"
|
|
@echo "qa_test.go Part_ subtests: $$(grep -cE 't\.Run\(\"Part[0-9]+_' deploy/test/qa_test.go 2>/dev/null || echo 0)"
|
|
@echo "testing-guide.md Parts: $$(grep -cE '^## Part [0-9]+:' docs/testing-guide.md 2>/dev/null || echo 0)"
|
|
@echo "Seed unique mc-* IDs: $$(grep -oE "mc-[a-z0-9_-]+" migrations/seed_demo.sql 2>/dev/null | sort -u | wc -l | tr -d ' ')"
|
|
@echo "Seed unique ag-* IDs: $$(grep -oE "ag-[a-z0-9_-]+" migrations/seed_demo.sql 2>/dev/null | sort -u | wc -l | tr -d ' ') (incl. agent_groups; agents-table count is 12)"
|
|
@echo "Seed unique iss-* IDs: $$(grep -oE "iss-[a-z0-9_-]+" migrations/seed_demo.sql 2>/dev/null | sort -u | wc -l | tr -d ' ') (issuers table count is 13)"
|
|
@echo "Seed unique tgt-* IDs: $$(grep -oE "tgt-[a-z0-9_-]+" migrations/seed_demo.sql 2>/dev/null | sort -u | wc -l | tr -d ' ')"
|
|
@echo "Seed unique nst-* IDs: $$(grep -oE "nst-[a-z0-9_-]+" migrations/seed_demo.sql 2>/dev/null | sort -u | wc -l | tr -d ' ')"
|
|
|
|
# Cleanup
|
|
clean:
|
|
@echo "Cleaning build artifacts..."
|
|
rm -rf bin/ dist/ coverage.out coverage.html
|
|
go clean -testcache
|
|
cd web && rm -rf node_modules dist
|
|
@echo "Cleanup complete"
|
|
|
|
install-tools:
|
|
@echo "Installing development tools..."
|
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
|
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
|
go install github.com/cosmtrek/air@latest
|
|
@echo "Tools installed"
|