prole/docs/completed/k3d-knoe-auth-dev-loop.md
chrisfu 73dce037f3 docs: reorganise briefs into docs/completed/; add conf/service/knoe.cfg
docs/completed/ — new directory; 9 shipped Junie briefs moved from
docs/plans/junie/ (02-k3s-prole-rename, 03-image-rename, 06-patch-garage,
07-init-cnpg-gke-sa-wiring, 13-podmonitor, 15-remove-dead-dashboard,
k3d-knoe-auth-dev-loop, phase2-oidc-gke-deploy, todo-1-cfg-save-path-bug).
docs/completed/README.md summarises all shipped work with dates/refs.

docs/plans/junie/README.md — updated to 2026-05-23; active/pending tables
reflect current state; shipped section now points to docs/completed/.

conf/service/knoe.cfg — new unattended deploy config for the service/k3s
environment (used by ./deploy.sh).

Co-authored-by: Junie <junie@jetbrains.com>
2026-05-23 21:32:01 -07:00

320 lines
13 KiB
Markdown

# Junie brief — Phase 1 of k3d-mirror-of-GKE: laptop dev loop for knoe-auth
> **Self-contained brief.** Reference: parent architectural plan at
> [`../k3d-gke-mirror.md`](../k3d-gke-mirror.md). Read that for the
> "why" and the multi-phase shape. This brief delivers Phase 1 only.
---
## 1. Why
knoe-auth Phase 2 OIDC code is on `main` but no fast inner-loop
exists for iterating on it. Hitting the GKE cluster on every change
is slow. Running unit tests with testcontainers covers a lot but
doesn't exercise the real DB schema, Kerberos KDC, or the actual HTTP
surface against a Postgres+JDBC stack.
This brief wires up a **laptop-resident dev loop**: bring up CNPG +
KDC in k3d, port-forward 5432/88/464 to localhost, and let the
engineer run knoe-auth from their IDE/`mvn spring-boot:run` against
those local ports. Edit Java → re-run → see the change at
`http://localhost:8080`, with a real PostgreSQL and a real Kerberos
backing it.
knoe-auth itself stays on the host. Image-build-and-load into k3d is
a separate later phase (and is in fact mostly orthogonal to this
work — Phase 1 finishes when the host loop is solid).
## 2. What you produce
Concrete deliverables, all in one MR:
### 2.1 — KDC manifest set for k3d
New files (mirroring the auth-side filename pattern under `k8s/knoe/`):
- `k8s/knoe/knoe-kdc-deployment.yaml` — single-replica `Deployment`
running `krb5kdc` + `kadmind`. Realm `KNOE.LOCAL`. Use a
Debian-derived MIT Kerberos image; whatever the GKE sidecar image
uses is fine to reuse. **Set `realm = KNOE.LOCAL`** explicitly —
do NOT inherit from the existing `KNOE.DEV` GKE config.
- `k8s/knoe/knoe-kdc-service.yaml` — ClusterIP service exposing 88
(TCP+UDP) and 464 (TCP+UDP — kpasswd).
- `k8s/knoe/knoe-kdc-configmap.yaml``krb5.conf` + `kdc.conf` with
realm `KNOE.LOCAL` and a `[realms] KNOE.LOCAL = { kdc = knoe-kdc:88 ... }`
block.
- `k8s/knoe/knoe-kdc-pvc.yaml` — small PVC (1 Gi) for the KDC
database `/var/lib/krb5kdc/principal`. Local-path provisioner
(k3d default).
- `k8s/knoe/knoe-kdc-init-job.yaml` — one-shot `Job` that runs
`kdb5_util create -s -P <stash>` (database init) and then
`kadmin.local addprinc -randkey admin/admin@KNOE.LOCAL` plus a
developer principal. Idempotent (skip create if DB exists). Runs
once on first apply; subsequent apply is a no-op via a guard
inside the script.
Reference existing GKE `kdc.conf` and `krb5.conf` shapes from
[`deploy/gcp/gke/knoe-kdc-configmap.yaml`](../../deploy/gcp/gke/knoe-kdc-configmap.yaml).
Substitute the realm, drop GCE-specific bits.
### 2.2 — Init-script entry point for k3d
Either:
- **(preferred)** Extend `etc/init_knoe_auth.sh` with a `--mode k3d`
flag that swaps the `APP_CTX` default to `k3d-knoe` (or whatever
`kubectl config current-context` returns from the k3d-up flow),
swaps the realm to `KNOE.LOCAL`, and skips the GCP/Workload
Identity steps (no GCP project on a laptop). Same `schema` /
`invite` / `status` subcommands work.
- **(or)** Write a thin `etc/init_knoe_auth_k3d.sh` that wraps the
existing one with the right env vars set.
Pick whichever results in less duplication. The existing script's
`schema` subcommand already runs the SQL via `kubectl exec` on the
CNPG primary — that path works in k3d unchanged.
### 2.3 — Make targets
Three new targets in the top-level `Makefile`:
- **`make k3d-knoe-up`** — provisions the local k3d cluster (if not
present), installs the CNPG operator, applies
`k8s/knoe/knoe-db.yaml` (single-replica per the architectural
plan §3), waits for `Cluster in healthy state`, runs the
knoe-kdc init job, applies the KDC Deployment + Service, runs
`etc/init_knoe_auth.sh schema` to seed `knoe.*` tables.
- **`make k3d-knoe-pf`** — opens three port-forwards in the
foreground:
- `5432``service/knoe-db-rw -n knoe-db-0`
- `88`/`464` → `service/knoe-kdc -n knoe-system` (TCP and UDP both)
- Prints the JDBC URL and the env vars the engineer should
`export` (e.g. `KRB5_CONFIG=$PWD/etc/krb5.local.conf`,
`KNOE_DB_PASSWORD=...`).
- On ^C, cleans up all three forwards.
- **`make k3d-knoe-down`** — tears down the k3d cluster (or just
the namespaces if you want incremental cleanup).
The cluster name should be `k3d-knoe` (so the kubeconfig context is
`k3d-k3d-knoe`, matching k3d's prefix convention). Reuse anything in
`scripts/` that already drives k3d if it's there.
### 2.4 — Engineer-side krb5 config
A new file `etc/krb5.local.conf` checked into the repo:
```ini
[libdefaults]
default_realm = KNOE.LOCAL
rdns = false
forwardable = true
udp_preference_limit = 1 # workaround for kubectl port-forward UDP flakiness; force TCP
[realms]
KNOE.LOCAL = {
kdc = localhost:88
admin_server = localhost:749
default_domain = local
}
[domain_realm]
.local = KNOE.LOCAL
localhost = KNOE.LOCAL
```
Engineer's runtime: `export KRB5_CONFIG=$PWD/etc/krb5.local.conf` and
the JVM picks it up automatically.
### 2.5 — Engineer-facing doc
New `docs/local-dev-knoe-auth.md`. Sections:
1. **Prerequisites** — k3d, kubectl, Docker Desktop running, JDK 21,
Maven. Brew one-liner.
2. **One-time setup**`make k3d-knoe-up`. Wait ~3 minutes. Verify
`kubectl --context k3d-k3d-knoe -n knoe-db-0 get cluster knoe-db`
shows `Healthy`.
3. **Daily loop** — open two terminals.
- Terminal A: `make k3d-knoe-pf` (leave running).
- Terminal B: `export KRB5_CONFIG=$PWD/etc/krb5.local.conf` then
`mvn -pl authority spring-boot:run`.
4. **Verify the loop**`curl localhost:8080/health` returns `ok`;
`curl localhost:8080/.well-known/openid-configuration` returns
the OIDC discovery JSON; `psql
"postgresql://postgres:$(make show-db-password)@localhost:5432/knoe-db?sslmode=require"`
connects.
5. **`kinit` for SPNEGO testing** — `kinit -k -t <keytab>
admin/admin@KNOE.LOCAL` (full SPNEGO E2E is Phase 2; document
the expected limitations: `curl --negotiate -u : http://localhost:8080/`
may or may not fully round-trip on macOS depending on
`udp_preference_limit`).
6. **IntelliJ run configuration** — paste-ready text or
screenshot. Module `authority`, main class
`dev.knoe.auth.KnoeAuthApplication`, env vars
`KRB5_CONFIG=…/etc/krb5.local.conf`,
`KNOE_DB_PASSWORD=…`.
7. **Reset**`make k3d-knoe-down && make k3d-knoe-up`. Time: ~3 min.
### 2.6 — Smoke-test script
`scripts/k3d-knoe-smoke.sh` — bash, runs after `make k3d-knoe-up`
to confirm the loop is intact:
```bash
#!/usr/bin/env bash
set -euo pipefail
ctx=k3d-k3d-knoe
ns_db=knoe-db-0
ns_sys=knoe-system
# 1. CNPG cluster healthy
phase=$(kubectl --context=$ctx -n $ns_db get cluster knoe-db -o jsonpath='{.status.phase}')
[[ "$phase" == "Cluster in healthy state" ]] || { echo "CNPG: $phase"; exit 1; }
# 2. knoe.* schema present
tables=$(kubectl --context=$ctx -n $ns_db exec sts/knoe-db-1 -c postgres -- \
psql -U postgres -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema='knoe'")
[[ "$tables" -ge 6 ]] || { echo "knoe.* tables: $tables (expected ≥6)"; exit 1; }
# 3. KDC has the seed admin principal
kubectl --context=$ctx -n $ns_sys exec deploy/knoe-kdc -- \
kadmin.local listprincs | grep -q 'admin/admin@KNOE.LOCAL' \
|| { echo "missing admin principal"; exit 1; }
echo "k3d-knoe-smoke: PASS"
```
CI integration is out of scope for Phase 1 — this is engineer-run.
## 3. Don't break
- The existing GKE knoe-auth deployment must continue to work
unchanged. If you touch `etc/init_knoe_auth.sh`, the GKE
invocation must remain the default behavior (no `--mode` flag =
current GKE behavior).
- The k3s manifests under `deploy/opentofu/k3s/manifests/knoe/`
must NOT be modified. The k3s deploy mode targets a different
use case (on-prem VM); leave it alone.
- The CNPG cluster manifest at `k8s/knoe/knoe-db.yaml` must stay
the canonical k3d/min Postgres definition. If you need it to
behave differently for k3d (e.g. single-replica), make the change
configurable, not destructive — `_strip_k3d_synology_blocks` in
`knoe/core/ops/cloudnative_pg.py` is the existing precedent for
k3d-specific manifest manipulation.
## 4. Verification (Definition of done)
A new engineer, freshly cloning the repo, completes this in ≤ 8
minutes on a MacBook Air:
```bash
git clone <repo> && cd knoe-db
make k3d-knoe-up # 3-5 min
make k3d-knoe-pf & # foreground; backgrounded for this script
sleep 3
scripts/k3d-knoe-smoke.sh # PASS
export KRB5_CONFIG=$PWD/etc/krb5.local.conf
mvn -pl authority spring-boot:run & # backgrounded for the verify
sleep 30
curl -fsS http://localhost:8080/health # ok
curl -fsS http://localhost:8080/.well-known/openid-configuration | jq .issuer
# → "http://localhost:8080"
```
Tick all of:
- [ ] `make k3d-knoe-up` exits 0 on a fresh laptop with k3d/kubectl
installed.
- [ ] `kubectl --context=k3d-k3d-knoe -n knoe-db-0 get cluster knoe-db`
reports `Healthy`.
- [ ] `kubectl --context=k3d-k3d-knoe -n knoe-system get deploy knoe-kdc`
reports `1/1 ready`.
- [ ] `scripts/k3d-knoe-smoke.sh` reports `PASS`.
- [ ] Host-side `psql` connects via `localhost:5432` after
`make k3d-knoe-pf` is running.
- [ ] `kinit admin/admin@KNOE.LOCAL` from the host succeeds.
- [ ] `mvn -pl authority spring-boot:run` starts cleanly with
`KRB5_CONFIG=etc/krb5.local.conf` set; `/health` returns `ok`;
`/.well-known/openid-configuration` returns valid JSON.
- [ ] `make k3d-knoe-down && make k3d-knoe-up` is idempotent.
- [ ] `docs/local-dev-knoe-auth.md` exists and is accurate.
- [ ] GKE deploy mode unchanged: `git diff main -- deploy/gcp/gke/`
shows no changes; the existing init scripts behave identically
when invoked the same way they were before.
## 5. Out of scope
Captured in the parent plan §6, but call out the highest-friction
deferrals here so reviewer expectations are right:
- **SPNEGO E2E from a host browser** — requires a service principal
for `HTTP/localhost@KNOE.LOCAL` and a keytab the host knoe-auth
process can read. Phase 1 stops at "KDC reachable, principals
exist, `kinit` works." Phase 2 makes browser SPNEGO actually
authenticate.
- **knoe-auth as a pod inside k3d** — image-build-and-load. Phase 3.
Don't take this on now.
- **Supabase / Studio / Kong / oauth2-proxy on k3d** — all explicit
Phase 4+.
- **OIDC code persistence in DB** — `OidcCodeService` uses an
in-memory `ConcurrentHashMap` today. That's a real Phase 2.5 gap
but it's the same gap on GKE; not k3d-specific. Don't fold it
into this brief.
## 6. Commit shape
Single commit unless the deliverables genuinely separate into two
clean halves (e.g. infra + docs). Suggested message:
```
feat(k3d): laptop dev loop for knoe-auth — CNPG + KDC + port-forward
Brings up the smallest k3d-resident stack that lets a host-side
knoe-auth (run via mvn spring-boot:run or IntelliJ) iterate against
real Postgres + Kerberos. Closes Phase 1 of the k3d-gke-mirror plan.
Scope:
- k8s/knoe/knoe-kdc-{deployment,service,configmap,pvc,init-job}.yaml
NEW; standalone KDC, realm KNOE.LOCAL (distinct from KNOE.DEV).
- etc/init_knoe_auth.sh: --mode k3d flag, swaps realm + skips
GCP-specific steps. GKE behavior unchanged when flag absent.
- Makefile: k3d-knoe-up, k3d-knoe-pf, k3d-knoe-down.
- etc/krb5.local.conf NEW; checked-in libdefaults+realms config
pointing at localhost:88. udp_preference_limit=1 to dodge
kubectl port-forward UDP flakiness.
- docs/local-dev-knoe-auth.md NEW; one-time setup + daily loop.
- scripts/k3d-knoe-smoke.sh NEW; bash sanity script.
Verified by following docs/local-dev-knoe-auth.md from a fresh
clone: end-to-end in <8 min, knoe-auth at localhost:8080 hits
real DB + KDC; OIDC discovery returns valid JSON.
Out of scope (parent plan docs/plans/k3d-gke-mirror.md §6):
- SPNEGO from host browsers
- knoe-auth-as-pod (image build/load)
- Supabase stack
- OidcCodeService DB persistence
Closes Phase 1; Phase 2+ briefs filed as needed.
```
## 7. Notes for reading
- The user is happy to **decide between two paths if you surface
them clearly** — e.g. "extend `init_knoe_auth.sh` with `--mode
k3d` vs write a sibling `init_knoe_auth_k3d.sh`". Pick the one
that minimizes drift and explain why in the commit.
- The user runs macOS. UDP port-forward through kubectl on Mac has
historically been finicky; setting `udp_preference_limit = 1` in
`krb5.local.conf` (force TCP) is the recommended workaround and is
why it's in the example above.
- If you run into resource ceiling issues on the laptop (Docker
Desktop OOM, k3d node not ready), document the workaround in the
engineer-side doc rather than working around it in the manifests.
- If the existing Python orchestrator
(`knoe.core.ops.cloudnative_pg`) already handles the
k3d-CNPG-bring-up cleanly, leverage it rather than writing a
shell wrapper from scratch. The Make target can call into it via
`python -m knoe.core.ops.cloudnative_pg ...` if the entrypoints
exist; if they don't, a bash wrapper is fine.