Create Pocket ID service on Milner #38

Merged
tepichord merged 13 commits from feat/34-create-pocket-id-service into main 2026-09-10 18:26:55 -07:00
Owner

Closes #34

Automated PR created by forgejo-done adapter.

Closes #34 Automated PR created by forgejo-done adapter.
- Add 10 GiB qcow2 volume for Pocket ID (milner-worker-1-pocket-id.qcow2)
- Attach as /dev/vdc (third virtio disk, after vault at vdb)
- Add second UserVolumeConfig for pocket-id with size-based selector
- Make vault selector unambiguous: disk.size > 15u * GiB vs < 15u * GiB

Refs: #34
New Terraform root mirroring vault/ pattern:
- main.tf: namespace (privileged PSA), helm_release (hobbit44 chart 2.4.0),
  ingress (tailscale class, ingress-proxies proxy-group)
- values.yaml: image v2.14.0, hostPath volume, /healthz probes, nodeSelector
- README.md: self-contained walkthrough with first-launch and OIDC setup
- tests/: mock-provider plan assertions for namespace, chart, ingress

Refs: #34
The .terraform directory contains downloaded providers and should not be
committed to the repository.
- worker/tests: add pocket-id volume tests, update user_volume_configs
  assertions for 3 patches and size-based selectors, update cdrom index
- tests/test_static.py: add pocket-id/ to terraform fmt and yamllint
- tests/test_terraform.py: add test_terraform_test_pocket_id

Refs: #34
- README.md: add Pocket ID to workload list
- ARCHITECTURE.md: add to pod topology, network diagram, and service table
- AGENTS.md: add pocket-id/ to repo map
- OPERATIONS.md: add §5 Pocket ID (deploy, first-launch, OIDC client,
  Vault auth method), renumber §5→§6, §6→§7
- vault/README.md: add OIDC integration section pointing to Pocket ID

Refs: #34
- Add Pocket ID to project overview line
- Add terraform -chdir=pocket-id test to Testing section

Refs: #34
The hobbit44 chart's ConfigMap template emits config values unquoted, so
trustProxy: "true" rendered as the YAML boolean TRUST_PROXY: true.
ConfigMap data only accepts strings, so the Kubernetes API would reject
the manifest and the helm_release apply would fail. Store '"true"' so the
rendered datum stays a string, and document the quirk in the README.

Refs: #34
tepichord added this to the MVP project 2026-09-10 15:39:28 -07:00
Seven files conflicted; resolutions:

- .gitignore, AGENTS.md, ARCHITECTURE.md, tests/test_static.py,
  tests/test_terraform.py: union of both sides. main contributed the
  data/storage/drills/cloudflare roots and the Woodpecker "deployed" status;
  this branch contributed the pocket-id entry and its fmt/test targets.

- worker/main.tf, worker/tests/plan.tftest.hcl: taken from main. This branch
  had added a dedicated 10 GiB pocket-id qcow2 as /dev/vdc plus a
  disk.size < 15GiB UserVolumeConfig selector; main had meanwhile moved the
  durable volumes into the data/ root and put the shared 100 GiB data disk on
  /dev/vdc. Keeping both would have collided on that device and left the size
  selectors ambiguous (the 10 GiB and the 20 GiB disks satisfy either band).
  Pocket ID persists through the storage/ StorageClasses instead — see the
  following commit.
Pocket ID's SQLite database and uploads move off a worker-owned qcow2 onto a
Terraform-owned PVC on the `persistent` StorageClass — the durable tier backed
by the worker's shared 100 GiB data disk, which the data/ root preserves across
a teardown. Its ENCRYPTION_KEY is generated once, kept in Vault at
secret/pocket-id/encryption-key, and synced to the `pocket-id-encryption`
Secret; the app refuses to start without it, and losing it would make an
existing database undecryptable, so it lives with the data it decrypts rather
than in Terraform state.

Chart 2.4.0 hardcodes its persistence mountPath to /app/backend/data, but the
image (v2.4.0 through v2.14.0) keeps its data in /app/data: the entrypoint runs
`cd /app; mkdir -p /app/data`, and DB_CONNECTION_STRING and UPLOAD_PATH are
relative to that CWD. Taking the chart's volumeClaimTemplate would therefore
mount the PVC where the app never writes and leave SQLite in the container's
writable layer, lost on every restart. Terraform owns the claim instead
(wait_until_bound = false: `persistent` is WaitForFirstConsumer and the only
consumer is the pod from the release that depends on the claim), the chart's
persistence is disabled, and the claim is mounted at /app/data through its
volumes/volumeMounts escape hatch. The chart's mount is unconditional, so the
volume it names has to be supplied — hence the explicit `volumes` entry, and
`fullnameOverride` is pinned because that name derives from it.

Also:

- Serve at id.faun-snapper.ts.net (Ingress tls.hosts = ["id"]).
- Tighten the namespace from privileged to baseline PSA: with hostPath gone,
  baseline still admits the root-running upstream image while rejecting a
  regression to host volumes at admission.
- Add pocket-id/.gitignore — the root reads the control-plane kubeconfig
  through terraform_remote_state, so its state holds the cluster client key.
- Guard the wiring in tests/test_static.py: the mount path, the durable class,
  wait_until_bound, the Secret, and appUrl agreeing with tls.hosts (the
  operator derives the MagicDNS name from the Ingress while the OIDC issuer
  comes from appUrl, and nothing in the chart ties them together).
- OPERATIONS.md §5: new §5a creates the namespace and the Vault-held
  ENCRYPTION_KEY (the generate + Vault->Secret sync recipe runs with the token
  and the value on stdin — never argv — and the doc notes
  --from-file=/dev/stdin exists precisely to avoid that). Deploy becomes §5b
  and now records why the claim is Terraform's rather than the chart's; the
  first-launch, OIDC-client, OIDC-auth and verify sections shift to §5c-§5f.
- pocket-id/README.md: prerequisites, the from-scratch deploy order, the data
  path and its retention semantics, and both traps (the chart's mountPath and
  the mandatory key).
- vault/README.md: OIDC pointer follows the renumbering (§5e).
OPERATIONS.md §5e claimed Vault could not reach Pocket ID's issuer without a
"reachability topology (in-cluster TLS + private CA or Tailscale egress)"
deferred to a follow-on commit. That was wrong on both halves, and the live
configuration writes cleanly. What actually holds:

- Pocket ID's internalAppUrl keeps `issuer` on the public hostname while
  rewriting token/jwks/userinfo/introspection to the in-cluster Service for
  every caller. Vault's go-oidc requires the issuer to be byte-identical to
  the URL discovery was fetched from, so `oidc_discovery_url` must be the
  public name — pointing it at the Service fails with `issuer did not match`.
- No private CA is involved: the libvirt host is itself a tailnet node
  (100.64.0.0/10 via tailscale0) and control-plane-net forwards with
  mode = "nat", so pod traffic is masqueraded out through the host's
  tailscaled. The dependency is the host's daemon, not the cluster's.

Also from running it end to end:

- §5d: saving a client yields no secret — it is generated on the Secrets tab —
  and Allowed User Groups is an allowlist that denies every login while empty.
  Both callback URLs are required and fail independently.
- §5e: OIDC_CLIENT_ID/SECRET now come from .env with the secret on stdin, and
  the section records that token_policies="default" names Vault's built-in
  policy rather than the role of the same word. Adds the static KV read/write
  policy the cluster uses, with its blast radius.
- §5e: verbose_oidc_logging emits at debug, so it is invisible at the default
  log_level=info — which is why a failed login logs nothing useful.
- §5f: the in-cluster health check shelled out to curl, which the pod does not
  ship, and printing the body of a 204 shows nothing. Replaced, and extended
  with an authorize-URL probe that checks client registration without a
  passkey.
- The bring-up order never mentioned Pocket ID at all; steps 13-14 now cover
  it and the OIDC configuration that consumes it.

vault/README.md and pocket-id/README.md lose their "deferred" hedges and carry
the two operational consequences instead. .env.example gains the two keys §5e
reads.

Verified: both live logins produced tokens (the second carrying
[default vault-users]), the drill KV predicates are untouched, and
`python -m pytest tests/` is green (26 passed, 1 skipped).
The pocket-id branch's own doc commit (7e4fc7c) covered AGENTS.md,
ARCHITECTURE.md, OPERATIONS.md, README.md and vault/README.md, but never
touched PLAN.md — which still listed a current state without Pocket ID and a
roadmap without it either. Add it as Phase 5 and to the current state, and
record that Vault authenticates against it.

While in there, correct two status claims that the live cluster contradicts:

- PLAN.md Phase 3 and README's service list both described Woodpecker as
  unfinished ("values skeleton", "coming soon"). It has been a Terraform root
  with a running server + agent, a Tailscale ingress and a public Cloudflare
  webhook endpoint since 10304bf. Both now say so.

Verified against the cluster: vault 1/1, pocket-id 1/1, woodpecker 2/2,
cloudflare 2/2, local-path-storage 1/1, tailscale 2/2.
PLAN.md and AGENTS.md both called the host "macOS arm64". It isn't macOS: it is
Fedora Linux Asahi Remix 42 on aarch64, which is what README.md already says
("On the libvirt host itself (Fedora Asahi, aarch64)"). The hardware is Apple
silicon, so "arm64" was never the wrong half — the OS was — and the distinction
matters here because the repo leans on Fedora-side specifics (dnf-installed
edk2-aarch64/libvirt/qemu-kvm, the `tailscale set --operator=naina` step).

Also adds Pocket ID to PLAN.md's stack line, which listed Vault and Woodpecker
but not the OIDC provider added by this branch.
tepichord deleted branch feat/34-create-pocket-id-service 2026-09-10 18:26:55 -07:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
tepichord/milner!38
No description provided.