From e18e1de88acbcfb4807e1840344209578862e847 Mon Sep 17 00:00:00 2001 From: vthwang Date: Thu, 30 Jul 2026 22:01:06 -0700 Subject: [PATCH] feat: shared PostgreSQL for the dev environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One PostgreSQL in the dev cluster, reached by every developer through kubectl port-forward, replacing the per-developer docker-compose database. The dev cluster was already shared; the database was not, so session rows lived on one laptop while the namespaces they describe lived where everyone could see them. - k8s/dev-postgres/: Secret, PVC, Deployment, Service in default - make deploy-db / forward-db / forward-vault; docker-compose targets removed - ORCHESTRATOR_RESUME gates both startup resumes (default true, so production keeps crash recovery) — otherwise every developer's API resumes the same rows - Pin postgres:18.4-alpine in both dev and the chart, enforced by make check-pg-image in make test - docs/shared-dev-database.md Signed-off-by: vthwang --- .env.example | 10 +- .gitignore | 4 +- CLAUDE.md | 20 +++- Makefile | 75 ++++++++++--- README.md | 43 +++++--- docker-compose.yml | 17 --- docs/shared-dev-database.md | 183 +++++++++++++++++++++++++++++++ helm/vtafarm-api/values.yaml | 5 +- internal/config/config.go | 24 ++-- k8s/dev-postgres/deployment.yaml | 61 +++++++++++ k8s/dev-postgres/pvc.yaml | 16 +++ k8s/dev-postgres/secret.yaml | 12 ++ k8s/dev-postgres/service.yaml | 14 +++ main.go | 15 ++- 14 files changed, 430 insertions(+), 69 deletions(-) delete mode 100644 docker-compose.yml create mode 100644 docs/shared-dev-database.md create mode 100644 k8s/dev-postgres/deployment.yaml create mode 100644 k8s/dev-postgres/pvc.yaml create mode 100644 k8s/dev-postgres/secret.yaml create mode 100644 k8s/dev-postgres/service.yaml diff --git a/.env.example b/.env.example index 728a915..225a08d 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,13 @@ APP_PORT=8080 APP_ENV=development JWT_SECRET=change-me-in-production -# Database -# Local dev: DB_HOST=localhost -# Docker Compose: DB_HOST is overridden to "db" in docker-compose.yml +# Re-attach interrupted sessions/upgrades at startup. Production keeps the +# default (true). False locally — every dev's API shares one database and would +# resume the same sessions. See docs/shared-dev-database.md. +ORCHESTRATOR_RESUME=false + +# Database — the shared PostgreSQL in the dev cluster, via `make forward-db`. +# DB_* and JWT_SECRET must be identical across the team. DB_HOST=localhost DB_PORT=5432 DB_USER=postgres diff --git a/.gitignore b/.gitignore index a867b24..fd55e10 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,9 @@ tmp/ # Go vendor/ -# Local PostgreSQL data (bind mount from docker-compose) +# Leftover data from the removed docker-compose PostgreSQL. Kept ignored only so +# nobody commits it by accident — delete both `data/` and this rule once the +# shared dev database (docs/shared-dev-database.md) has proved itself. data/ # Air logs diff --git a/CLAUDE.md b/CLAUDE.md index d54c681..103ecd3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,14 +12,21 @@ Go REST API backend for managing VTA setup sessions with per-user namespace isol | Database | PostgreSQL 18 | | K8s client | `k8s.io/client-go` v0.36 | | Hot reload | Air (`github.com/air-verse/air`) | -| Container | Docker Compose (dev) + multi-stage Dockerfile (prod) | +| Container | Multi-stage Dockerfile + Helm (prod) | ## Quick Start +The database is **shared**: one PostgreSQL in the dev cluster that every +developer tunnels to. There is no local database. Read +`docs/shared-dev-database.md` once — it changes how migrations, accounts and +`ORCHESTRATOR_RESUME` behave. + ```bash cp .env.example .env -make dev # start DB (Docker) + API with Air hot-reload; migrations run automatically -make enroll # create first admin + print 24h enrollment token (run in a separate terminal) +make forward-db # tunnel the shared dev database (own terminal, keep open) +make forward-vault # tunnel Vault — required for setup work (own terminal) +make dev # API with Air hot-reload; migrations run automatically +make enroll # first admin + 24h enrollment token — ONCE for the whole team ``` API: `http://localhost:8080` @@ -33,9 +40,10 @@ See `.env.example` for all options. Key ones: | --- | --- | --- | | `APP_PORT` | `8080` | HTTP listen port | | `APP_ENV` | `development` | Set to `production` to disable `/docs` | -| `DB_HOST` | `localhost` | Overridden to `db` in docker-compose | +| `DB_HOST` | `localhost` | The `make forward-db` tunnel to the shared dev database | | `DB_NAME` | `vtafarm` | | -| `JWT_SECRET` | `change-me-in-production` | HS256 signing secret | +| `JWT_SECRET` | `change-me-in-production` | HS256 signing secret — identical across the team, since they share one set of accounts | +| `ORCHESTRATOR_RESUME` | `true` | Re-attach interrupted sessions/upgrades at startup. Crash recovery, so production keeps the default; local `.env` sets `false` so that N developers' APIs don't all resume the same rows | | `KUBECONFIG` | `~/.kube/config` | Leave empty; auto-detected | | `K8S_NAMESPACE_PREFIX` | `vtafarm-user` | Per-user namespace prefix | | `DID_HOSTING_DID` / `DID_HOSTING_PRIVATE_KEY` | — | vtafarm-api's **own** keypair (`make gen-keypair`) for the DID-hosting control API, enrolled in a daemon's ACL with `role=admin`. Not anything a daemon issued, so one keypair serves every daemon it is enrolled in. There are deliberately no DID-hosting **URLs** here — see "Shared infrastructure comes from the platform stack" below | @@ -55,10 +63,12 @@ See `.env.example` for all options. Key ones: ├── migrations/ │ ├── 000001_init.up.sql │ └── 000001_init.down.sql +├── k8s/dev-postgres/ # The shared dev database (Secret / PVC / Deployment / Service) ├── docs/ │ ├── vta-setup-design.md # API design for VTA setup automation (Mode A + shared shape) │ ├── full-stack-setup-design.md # Authoritative design for the full_stack mode (all 4 components) │ ├── custom-domain-design.md # Custom + platform domains, the dev- prefix (§17 = what has shipped) +│ ├── shared-dev-database.md # One PostgreSQL for the team + what it changes │ └── vault-transit-upgrade.md # Vault / transit upgrade + restore runbook └── internal/ ├── apidocs/ diff --git a/Makefile b/Makefile index 990a278..d47a933 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,16 @@ NAMESPACE ?= default DEPLOY_ENV ?= production INGRESS_HOST ?= -.PHONY: build test gen-keypair tidy dev \ +# ─── Dev cluster ────────────────────────────────────────────────────────────── +# The database is shared and lives here — see docs/shared-dev-database.md. +DEV_CONTEXT ?= k8s-fpp-dev +DEV_DB ?= vtafarm-dev-postgres +DB_PORT ?= 5432 +VAULT_PORT ?= 8200 + +.PHONY: build test check-pg-image gen-keypair tidy dev \ migrate migrate-down migrate-new enroll enroll-prod \ - up down reset \ + deploy-db forward-db forward-vault \ image-build image-push \ deploy @@ -18,19 +25,42 @@ build: go build -o bin/api ./main.go # Same checks CI runs (.github/workflows) -test: +test: check-pg-image go vet ./... go test ./... +# Dev and production must run the identical PostgreSQL image — a version that +# only differs locally turns "works on dev" into a guess. Enforced here rather +# than by convention, because the two files are edited months apart. +check-pg-image: + @dev=$$(grep -o 'postgres:[0-9a-z.-]*' k8s/dev-postgres/deployment.yaml); \ + prod=$$(grep -o 'postgres:[0-9a-z.-]*' helm/vtafarm-api/values.yaml); \ + if [ "$$dev" != "$$prod" ]; then \ + echo "PostgreSQL image mismatch:"; \ + echo " k8s/dev-postgres/deployment.yaml : $$dev"; \ + echo " helm/vtafarm-api/values.yaml : $$prod"; \ + exit 1; \ + fi; \ + echo "PostgreSQL image matches in dev and production: $$dev" + gen-keypair: go run ./cmd/gen-keypair tidy: go mod tidy -# Start DB + API with Air hot-reload +# Start the API with Air hot-reload. The database is the shared one in the dev +# cluster, so `make forward-db` must already be running in another terminal — +# checked here because otherwise the failure is a bare "connection refused". dev: - $(MAKE) up + @nc -z localhost $(DB_PORT) 2>/dev/null || { \ + echo "Nothing listening on localhost:$(DB_PORT)."; \ + echo "Start the tunnel to the shared dev database first, in another terminal:"; \ + echo ""; \ + echo " make forward-db"; \ + echo ""; \ + exit 1; \ + } air # ─── Migrations (run locally against DB_HOST=localhost) ─────────────────────── @@ -54,16 +84,31 @@ enroll: enroll-prod: kubectl exec -n $(NAMESPACE) deploy/$(NAME) -- ./enroll -# ─── Docker Compose (DB only) ───────────────────────────────────────────────── -up: - docker compose up -d - -down: - docker compose down - -reset: - docker compose down -v - docker compose up -d +# ─── Dev cluster ────────────────────────────────────────────────────────────── +# Deploy / update the shared database. Applies only — the PVC is never deleted +# here, so team data survives every redeploy. The context is explicit so this +# can't land in docker-desktop by accident. +deploy-db: + kubectl --context $(DEV_CONTEXT) apply -f k8s/dev-postgres/ + +# Tunnels. Keep each running in its own terminal while developing. The loops are +# not cosmetic: kubectl port-forward dies on a dropped connection or a pod +# restart and never comes back on its own. +forward-db: + @echo "Forwarding $(DEV_CONTEXT) svc/$(DEV_DB) → localhost:$(DB_PORT) (Ctrl-C to stop)" + @trap 'exit 0' INT; while true; do \ + kubectl --context $(DEV_CONTEXT) port-forward svc/$(DEV_DB) $(DB_PORT):5432 || true; \ + echo "port-forward dropped — reconnecting in 2s"; \ + sleep 2; \ + done + +forward-vault: + @echo "Forwarding $(DEV_CONTEXT) vault/svc/vault → localhost:$(VAULT_PORT) (Ctrl-C to stop)" + @trap 'exit 0' INT; while true; do \ + kubectl --context $(DEV_CONTEXT) port-forward -n vault svc/vault $(VAULT_PORT):8200 || true; \ + echo "port-forward dropped — reconnecting in 2s"; \ + sleep 2; \ + done # ─── Docker Hub ─────────────────────────────────────────────────────────────── image-build: diff --git a/README.md b/README.md index dc0d4d7..7579263 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,25 @@ Go REST API backend for managing VTA setup sessions with per-user namespace isol | Database | PostgreSQL 18 | | K8s client | client-go v0.36 | | Hot reload | Air | -| Container | Docker Compose (dev) / Helm (prod) | +| Container | Helm (prod) | --- ## Local Development -The API runs directly on your machine while only the database runs in Docker. -This gives the API direct access to your local `~/.kube/config` without any -networking workarounds. +The API runs directly on your machine, against the **shared PostgreSQL in the dev +cluster** — there is no local database. Running the API locally gives it direct +access to your `~/.kube/config` without any networking workarounds. + +The database being shared has consequences worth reading once: +[`docs/shared-dev-database.md`](docs/shared-dev-database.md). It already exists — +`make deploy-db` (re)deploys it and is not something you need for daily work. ### Prerequisites - Go 1.26+ -- Docker & Docker Compose - [Air](https://github.com/air-verse/air) — `go install github.com/air-verse/air@latest` -- `kubectl` configured with access to a cluster (for K8s features) +- `kubectl` with access to the dev cluster (context `k8s-fpp-dev`) ### Setup @@ -37,23 +40,30 @@ networking workarounds. cp .env.example .env ``` -2. Start the DB + API (migrations run automatically on startup): + `DB_*` and `JWT_SECRET` must match the rest of the team — one database means + one set of accounts, and a token signed with a different secret is rejected. + +2. Open the two tunnels into the dev cluster. Each needs its own terminal and + stays open while you develop — both reconnect on their own, since + `kubectl port-forward` drops on pod restarts. ```bash - make dev + make forward-db # localhost:5432 → svc/vtafarm-dev-postgres + make forward-vault # localhost:8200 → vault/svc/vault ``` - The API is now available at `http://localhost:8080`. - API docs: `http://localhost:8080/docs` + Vault is required for VTA setup — the API provisions per-user Vault + policies/roles. -3. Port-forward Vault so the locally-running API can reach it — required for - VTA setup (the API provisions per-user Vault policies/roles). Run in a - separate terminal and keep it open: +3. Start the API (migrations run automatically on startup): ```bash - kubectl port-forward -n vault svc/vault 8200:8200 + make dev ``` + The API is now available at `http://localhost:8080`. + API docs: `http://localhost:8080/docs` + 4. (Optional) Generate a DID hosting keypair (required only if DID hosting is enabled): ```bash @@ -90,9 +100,10 @@ Copy `.env.example` and adjust as needed: | --- | --- | --- | | `APP_PORT` | `8080` | HTTP listen port | | `APP_ENV` | `development` | Set to `production` to disable `/docs` | -| `DB_HOST` | `localhost` | Points to the Docker-managed PostgreSQL | +| `DB_HOST` | `localhost` | The `make forward-db` tunnel to the shared dev database | | `DB_NAME` | `vtafarm` | | -| `JWT_SECRET` | _(required)_ | HS256 signing secret — see below | +| `JWT_SECRET` | _(required)_ | HS256 signing secret — must match the team, see below | +| `ORCHESTRATOR_RESUME` | `true` | Re-attach interrupted sessions at startup. Set `false` locally — see [`docs/shared-dev-database.md`](docs/shared-dev-database.md) | | `CLUSTER_INGRESS_IP` | _(required)_ | External IP of the cluster's Ingress-NGINX LoadBalancer | | `CLOUDFLARE_API_TOKEN` | _(optional)_ | Required for VTA setup wizard | | `CLOUDFLARE_ZONE_ID` | _(optional)_ | Required for VTA setup wizard | diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3d71349..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -services: - db: - image: postgres:18.4 - restart: unless-stopped - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: vtafarm - ports: - - "5432:5432" - volumes: - - ./data/postgres:/var/lib/postgresql - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d vtafarm"] - interval: 5s - timeout: 5s - retries: 10 diff --git a/docs/shared-dev-database.md b/docs/shared-dev-database.md new file mode 100644 index 0000000..2539cc6 --- /dev/null +++ b/docs/shared-dev-database.md @@ -0,0 +1,183 @@ +# The shared development database + +One PostgreSQL for the whole team, running in the dev cluster, reached by every +developer through `kubectl port-forward`. It replaces the per-developer +`docker-compose` database. + +## Why + +The dev **cluster** was already shared; the database was not. So `setup_sessions` +rows lived on one laptop while the namespaces, PVCs and Jobs they describe lived +in `k8s-fpp-dev` where everyone could see them. Keeping the two in agreement +meant passing dumps around by hand, and every restore silently reintroduced +whatever the sender's laptop happened to hold. + +Moving the database next to the cluster it describes removes the sync step +rather than automating it. The cost is real and is the subject of most of this +document: the database is now shared mutable state, and several of this API's +behaviours quietly assumed it was not. + +## What is deployed + +`k8s/dev-postgres/` — four manifests in the `default` namespace of the dev +cluster (`k8s-fpp-dev`): + +| Object | Notes | +| --- | --- | +| Secret `vtafarm-dev-postgres` | password, committed on purpose — see below | +| PVC `vtafarm-dev-postgres` | Longhorn, 5Gi, `ReadWriteOnce` | +| Deployment `vtafarm-dev-postgres` | `postgres:18.4-alpine`, single replica, `Recreate` | +| Service `vtafarm-dev-postgres` | ClusterIP, 5432 | + +Deployed with `make deploy-db`, which pins `--context k8s-fpp-dev` so it cannot +land in `docker-desktop` by accident. + +Some deliberate choices: + +- **The same image as production, pinned to the patch.** Both + `k8s/dev-postgres/deployment.yaml` and `helm/vtafarm-api/values.yaml` say + `postgres:18.4-alpine`, and `make check-pg-image` (part of `make test`, so CI + runs it) fails the build if they ever differ. Production used to track the + floating `18-alpine`, which meant a pod restart could move it a patch without + anyone choosing to — and "works against dev" would quietly stop meaning + "works against production". Bumping the version is a two-file edit, on + purpose. +- **`default`, not a dedicated namespace.** It must never live under + `fpp-user-*`: those namespaces are created and *deleted* by this API as + sessions come and go, and the database would go with them. +- **Named `vtafarm-dev-postgres`, not `vtafarm-api-postgresql`.** The Helm chart + (`helm/vtafarm-api/templates/postgresql/`) uses the latter, so if anyone ever + installs the full chart into this cluster's `default` namespace, the two sets + of objects don't collide. +- **The password is in git.** It is `postgres`, the same throwaway value + `.env.example` has always carried, and nothing reaches port 5432 without a + kubeconfig for the cluster. Committing it is what makes setup a single + command. The corollary is a rule, not a hope: **nothing that matters may live + in this database.** No production data, no real user records, no secret worth + having. Master seeds are in Vault and stay there. +- **Not exposed.** No NodePort, no LoadBalancer, no Ingress. `port-forward` is + the only path in, so access is authorised by the cluster's RBAC. +- **No backup job.** Decided deliberately: this is scratch data. Longhorn's + reclaim policy is `Delete`, so removing the PVC destroys the team's data with + no way back. `make deploy-db` only ever applies, never deletes, so a redeploy + is safe; a `kubectl delete pvc` is not. + +## Daily use + +Three terminals, all left running: + +```bash +make forward-db # localhost:5432 → svc/vtafarm-dev-postgres +make forward-vault # localhost:8200 → vault/svc/vault (needed for setup work) +make dev # air, against those tunnels +``` + +`make dev` refuses to start when nothing is listening on 5432 — without the +check the symptom is a bare `connection refused` from GORM, which reads like a +broken database rather than a missing tunnel. + +Both `forward-*` targets loop on purpose. `kubectl port-forward` dies on a +dropped connection or whenever the pod restarts, and never returns on its own; +the loop reconnects every 2s. Ctrl-C stops it. + +## Things that changed because the database is shared + +### Startup no longer resumes interrupted work + +`Orchestrator.Resume` picks up every session in `vta_setup_running` or +`provisioning` at startup, and `upgrade.Runner.Resume` does the same for image +upgrades. That is crash recovery, and it is correct when one API owns the +database. Against a shared one, every developer who starts their API resumes +**everyone's** in-flight sessions: several orchestrators creating Jobs for the +same session and writing the same status column. + +So `ORCHESTRATOR_RESUME` gates both (they are one hazard; gating either alone +would achieve nothing): + +- **Defaults to `true`.** Production must never lose crash recovery because + someone forgot a Helm value, so the flag is opt-out and the chart needs no + change. +- **`.env.example` sets it to `false`.** Local APIs are observers by default. +- **Turn it on in exactly one API** when you actually need to drive a `full_stack` + pipeline, and coordinate that with the team. + +Creating a session through the API still runs the orchestrator in that same +process — the flag only governs what happens at *startup*. Two people running +`POST /setup` at once is fine; they are different sessions. + +### Migrations are a shared resource + +Migrations run automatically on every API start, against everyone's database. + +- Starting on an **older** branch is harmless: golang-migrate finds no file past + the recorded version and returns `ErrNoChange`. Your schema simply has columns + your code doesn't know about. +- **Destructive migrations are not harmless.** A `DROP COLUMN` merged by one + person breaks everyone still on a branch whose code selects it. +- **`make migrate-down` hits everybody.** Don't run it against the shared + database to test a rollback; do that against a throwaway local container. +- **A failed migration blocks the whole team.** golang-migrate marks the schema + `dirty` and every subsequent start fails until someone repairs the version + manually. + +Working rules that follow: + +1. Iterate on a new migration locally (a disposable `docker run postgres:18.4-alpine`) + until it applies cleanly. The shared database sees it once it's settled. +2. Prefer additive migrations. Split a rename into add → backfill → drop across + separate merges, so nobody's branch is broken between them. +3. Say something in the team channel before anything destructive lands. + +### Accounts are shared, passkeys are not + +`make enroll` creates the first admin. **Run it once for the team**, not once per +person. Everyone else gets their own account from an authenticated admin: + +``` +POST /api/v1/admin/admins → enrollment token → register your own passkey +``` + +Passkeys are bound to the device that created them, so each person registers +their own even though the account rows are shared. `WEBAUTHN_RP_ID=localhost` is +the same for everyone, so a credential registered against one developer's +`localhost` works with their own API only — which is the intent. + +`JWT_SECRET` **must be identical across the team**. One database means one set of +accounts, but a token signed by one API is rejected by another that signs with a +different secret, and the failure looks like a broken login rather than a config +mismatch. + +### The database and the cluster are a pair + +Rows in `setup_sessions` describe objects in `k8s-fpp-dev`. Anyone connected to +the shared database must also be pointed at that cluster and configured the same +way — `KUBECONFIG` context, `K8S_NAMESPACE_PREFIX=fpp-user`, `CLUSTER_DOMAIN`, +the Cloudflare token, Vault. + +Run against `docker-desktop` by mistake and you leave rows behind that describe +namespaces nobody has: a session the team can see, can't use, and can only clear +by hand. + +## Resetting it + +There is no `make db-reset`, on purpose — the old `make reset` destroyed only +your own data, and a same-named target here would destroy everyone's. + +To wipe and start over, deliberately: + +```bash +kubectl --context k8s-fpp-dev delete deployment vtafarm-dev-postgres +kubectl --context k8s-fpp-dev delete pvc vtafarm-dev-postgres +make deploy-db +``` + +The next API start recreates the schema from the migrations, and the team needs +a fresh `make enroll`. + +## Working offline + +Nothing stops you running your own PostgreSQL — point `DB_HOST`/`DB_PORT` at it +in `.env` and skip `make forward-db`. It's the right move for testing a +destructive migration or a schema experiment. Just remember the cluster is still +shared: a local database plus the dev cluster is exactly the split this setup +exists to remove, so it's a temporary mode, not a way of working. diff --git a/helm/vtafarm-api/values.yaml b/helm/vtafarm-api/values.yaml index 2a672fe..ffb04a9 100644 --- a/helm/vtafarm-api/values.yaml +++ b/helm/vtafarm-api/values.yaml @@ -26,7 +26,10 @@ db: postgresql: enabled: true - image: postgres:18-alpine + # Exact patch, and identical to k8s/dev-postgres/deployment.yaml — `make test` + # fails if the two drift. A floating 18-alpine would let production and the + # shared dev database land on different patches without anyone choosing to. + image: postgres:18.4-alpine # Pre-create with: kubectl create secret generic vtafarm-api-postgresql --from-literal=postgres-password='...' # The secret must contain key: postgres-password existingSecret: "vtafarm-api-postgresql" diff --git a/internal/config/config.go b/internal/config/config.go index 71c4803..133b2f9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,14 +17,19 @@ type Config struct { // domains' certificates. The same one in every environment — see // DefaultACMEIssuer for why there is no staging variant to pick between. ACMEClusterIssuer string - DB DBConfig - K8s K8sConfig - Cloudflare CloudflareConfig - GHCR GHCRConfig - DidHosting DidHostingConfig - WebAuthn WebAuthnConfig - Vault VaultConfig - Monitor MonitorConfig + // OrchestratorResume re-attaches interrupted sessions and upgrades at startup. + // Crash recovery, so it defaults to true; false only against the shared dev + // database, where every API would resume the same rows. + // See docs/shared-dev-database.md. + OrchestratorResume bool + DB DBConfig + K8s K8sConfig + Cloudflare CloudflareConfig + GHCR GHCRConfig + DidHosting DidHostingConfig + WebAuthn WebAuthnConfig + Vault VaultConfig + Monitor MonitorConfig } // MonitorConfig configures the token-gated /api/v1/monitor/* endpoints polled @@ -157,7 +162,8 @@ func Load() *Config { ClusterIngressIP: getEnv("CLUSTER_INGRESS_IP", ""), ClusterDomain: getEnv("CLUSTER_DOMAIN", ""), - ACMEClusterIssuer: getEnv("ACME_CLUSTER_ISSUER", DefaultACMEIssuer), + ACMEClusterIssuer: getEnv("ACME_CLUSTER_ISSUER", DefaultACMEIssuer), + OrchestratorResume: getEnvBool("ORCHESTRATOR_RESUME", true), DB: DBConfig{ Host: getEnv("DB_HOST", "localhost"), Port: getEnv("DB_PORT", "5432"), diff --git a/k8s/dev-postgres/deployment.yaml b/k8s/dev-postgres/deployment.yaml new file mode 100644 index 0000000..030d23b --- /dev/null +++ b/k8s/dev-postgres/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vtafarm-dev-postgres + namespace: default + labels: + app: vtafarm-dev-postgres +spec: + replicas: 1 + # Recreate: single replica on a ReadWriteOnce PVC — a rolling update could + # deadlock with the new pod unable to attach the volume the old pod holds. + strategy: + type: Recreate + selector: + matchLabels: + app: vtafarm-dev-postgres + template: + metadata: + labels: + app: vtafarm-dev-postgres + spec: + containers: + - name: postgresql + # Exact patch, not a floating tag: a pod restart must not move the + # whole team's server a version. + image: postgres:18.4-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_DB + value: vtafarm + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: vtafarm-dev-postgres + key: postgres-password + # Subdirectory, not the mount root: initdb refuses Longhorn's lost+found. + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + readinessProbe: + exec: + command: ["pg_isready", "-U", "postgres", "-d", "vtafarm"] + initialDelaySeconds: 5 + periodSeconds: 5 + # No CPU limit — requests give fair-share; memory is incompressible so + # it keeps a hard limit. Same shape as the production chart. + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 512Mi + volumes: + - name: data + persistentVolumeClaim: + claimName: vtafarm-dev-postgres diff --git a/k8s/dev-postgres/pvc.yaml b/k8s/dev-postgres/pvc.yaml new file mode 100644 index 0000000..dbb2adf --- /dev/null +++ b/k8s/dev-postgres/pvc.yaml @@ -0,0 +1,16 @@ +# Longhorn reclaims on delete and there is no backup: deleting this PVC destroys +# the team's dev data. `make deploy-db` only applies, so redeploys are safe. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: vtafarm-dev-postgres + namespace: default + labels: + app: vtafarm-dev-postgres +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 5Gi diff --git a/k8s/dev-postgres/secret.yaml b/k8s/dev-postgres/secret.yaml new file mode 100644 index 0000000..8a531b6 --- /dev/null +++ b/k8s/dev-postgres/secret.yaml @@ -0,0 +1,12 @@ +# Committed on purpose: throwaway value, and the DB is only reachable through +# port-forward. Nothing that matters lives here — docs/shared-dev-database.md. +apiVersion: v1 +kind: Secret +metadata: + name: vtafarm-dev-postgres + namespace: default + labels: + app: vtafarm-dev-postgres +type: Opaque +stringData: + postgres-password: postgres diff --git a/k8s/dev-postgres/service.yaml b/k8s/dev-postgres/service.yaml new file mode 100644 index 0000000..7a5edfc --- /dev/null +++ b/k8s/dev-postgres/service.yaml @@ -0,0 +1,14 @@ +# ClusterIP only — reached with `make forward-db`, so access is gated by kubeconfig. +apiVersion: v1 +kind: Service +metadata: + name: vtafarm-dev-postgres + namespace: default + labels: + app: vtafarm-dev-postgres +spec: + selector: + app: vtafarm-dev-postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/main.go b/main.go index 279f7ea..56965e4 100644 --- a/main.go +++ b/main.go @@ -88,11 +88,20 @@ func main() { log.Printf("warn: VAULT_ADDR not set — vta setup disabled") } + // Resuming assumes this process is the only one reading these rows — untrue + // against the shared dev database. Both resumes gate together; they are the + // same hazard. + if !cfg.OrchestratorResume { + log.Printf("ORCHESTRATOR_RESUME=false — interrupted sessions and upgrades will not be re-attached") + } + var orch *setup.Orchestrator if k8sClient != nil { orch = setup.NewOrchestrator(db, k8sClient, vaultClient, cfg.Vault.VTAAddr, dhFactory, cfg.ClusterIngressIP, cfg.ACMEClusterIssuer) - orch.Resume(context.Background()) + if cfg.OrchestratorResume { + orch.Resume(context.Background()) + } } // Upgrade runner processes admin image-upgrade batches in the background; @@ -100,7 +109,9 @@ func main() { var upgradeRunner *upgrade.Runner if k8sClient != nil { upgradeRunner = upgrade.NewRunner(db, k8sClient) - upgradeRunner.Resume() + if cfg.OrchestratorResume { + upgradeRunner.Resume() + } } // GHCR clients for listing available image tags (optional, one per component).