infra/stacks
Viktor Barzin 43b4e1d372 [payslip-ingest] Deploy stack + Grafana dashboard + Vault DB role
## Context

New service `payslip-ingest` (code lives in `/home/wizard/code/payslip-ingest/`)
needs in-cluster deployment, its own Postgres DB + rotating user, a Grafana
datasource, a dashboard, and a Claude agent definition for PDF extraction.

Cluster-internal only — webhook fires from Paperless-ngx in a sibling namespace.
No ingress, no TLS cert, no DNS record.

## What

### New stack `stacks/payslip-ingest/`
- `kubernetes_namespace` payslip-ingest, tier=aux.
- ExternalSecret (vault-kv) projects PAPERLESS_API_TOKEN, CLAUDE_AGENT_BEARER_TOKEN,
  WEBHOOK_BEARER_TOKEN into `payslip-ingest-secrets`.
- ExternalSecret (vault-database) reads rotating password from
  `static-creds/pg-payslip-ingest` and templates `DATABASE_URL` into
  `payslip-ingest-db-creds` with `reloader.stakater.com/match=true`.
- Deployment: single replica, Recreate strategy (matches single-worker queue
  design), `wait-for postgresql.dbaas:5432` annotation, init container runs
  `alembic upgrade head`, main container serves FastAPI on 8080, Kyverno
  dns_config lifecycle ignore.
- ClusterIP Service :8080.
- Grafana datasource ConfigMap in `monitoring` ns (label `grafana_datasource=1`,
  uid `payslips-pg`) reading password from the db-creds K8s Secret.

### Grafana dashboard `uk-payslip.json` (4 panels)
- Monthly gross/net/tax/NI (timeseries, currencyGBP).
- YTD tax-band progression with threshold lines at £12,570 / £50,270 / £125,140.
- Deductions breakdown (stacked bars).
- Effective rate + take-home % (timeseries, percent).

### Vault DB role `pg-payslip-ingest`
- Added to `allowed_roles` in `vault_database_secret_backend_connection.postgresql`.
- New `vault_database_secret_backend_static_role.pg_payslip_ingest`
  (username `payslip_ingest`, 7d rotation).

### DBaaS — DB + role creation
- New `null_resource.pg_payslip_ingest_db` mirrors `pg_terraform_state_db`:
  idempotent CREATE ROLE + CREATE DATABASE + GRANT ALL via `kubectl exec` into
  `pg-cluster-1`.

### Claude agent `.claude/agents/payslip-extractor.md`
- Haiku-backed agent invoked by `claude-agent-service`.
- Decodes base64 PDF from prompt, tries pdftotext → pypdf fallback, emits a single
  JSON object matching the schema to stdout. No network, no file writes outside /tmp,
  no markdown fences.

## Trade-offs / decisions

- Own DB per service (convention), NOT a schema in a shared `app` DB as the plan
  initially described. The Alembic migration still creates a `payslip_ingest`
  schema inside the `payslip_ingest` DB for table organisation.
- Paperless URL uses port 80 (the Service port), not 8000 (the pod target port).
- Grafana datasource uses the primary RW user — separate `_ro` role is aspirational
  and not yet a pattern in this repo.
- No ingress — webhook is cluster-internal; external exposure is unnecessary attack
  surface.
- No Uptime Kuma monitor yet: the internal-monitor list is a static block in
  `stacks/uptime-kuma/`; will add in a follow-up tied to code-z29 (internal monitor
  auto-creator).

## Test Plan

### Automated
```
terraform init -backend=false && terraform validate
Success! The configuration is valid.

terraform fmt -check -recursive
(exit 0)

python3 -c "import json; json.load(open('uk-payslip.json'))"
(exit 0)
```

### Manual Verification (post-merge)

Prerequisites:
1. Seed Vault: `vault kv put secret/payslip-ingest webhook_bearer_token=$(openssl rand -hex 32)`.
2. Seed Vault: `vault kv patch secret/paperless-ngx api_token=<paperless token>`.

Apply:
3. `scripts/tg apply vault` → creates pg-payslip-ingest static role.
4. `scripts/tg apply dbaas` → creates payslip_ingest DB + role.
5. `cd stacks/payslip-ingest && ../../scripts/tg apply -target=kubernetes_manifest.db_external_secret`
   (first-apply ESO bootstrap).
6. `scripts/tg apply payslip-ingest` (full).
7. `kubectl -n payslip-ingest get pods` → Running 1/1.
8. `kubectl -n payslip-ingest port-forward svc/payslip-ingest 8080:8080 && curl localhost:8080/healthz` → 200.

End-to-end:
9. Configure Paperless workflow (README in code repo has steps).
10. Upload sample payslip tagged `payslip` → row in `payslip_ingest.payslip` within 60s.
11. Grafana → Dashboards → UK Payslip → 4 panels render.

Closes: code-do7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:07:05 +00:00
..
_template [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
actualbudget [actualbudget] Upgrade 26.3.0 → 26.4.0 for native Sankey report 2026-04-18 13:19:27 +00:00
affine [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
authentik [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
beads-server [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
blog [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
broker-sync broker-sync: add Fidelity PlanViewer CronJob (suspended) 2026-04-18 18:51:20 +00:00
changedetection [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
city-guesser [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
claude-agent-service [infra/claude-agent-service] Seed beads metadata + scratch dir at runtime 2026-04-18 14:23:19 +00:00
claude-memory [infra] Document intended ignore_changes drift-workarounds [ci skip] 2026-04-18 14:08:10 +00:00
cloudflared [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
cnpg [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
coturn [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
crowdsec [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
cyberchef [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
dashy [cleanup] Remove ollama from dashy + docs + nfs_directories 2026-04-18 11:17:59 +00:00
dawarich [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
dbaas [payslip-ingest] Deploy stack + Grafana dashboard + Vault DB role 2026-04-18 19:07:05 +00:00
descheduler [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
diun [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
ebook2audiobook [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
ebooks [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
echo [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
excalidraw [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
external-secrets [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
f1-stream [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
foolery Add broker-sync Terraform stack (#7) 2026-04-17 21:17:45 +01:00
forgejo [forgejo] Probe /api/healthz for external monitor 2026-04-17 22:06:23 +00:00
freedify [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
freshrss [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
frigate [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
grampsweb [grampsweb] Align PVC resource to encrypted storage; imported state 2026-04-18 11:37:45 +00:00
hackmd [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
headscale [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
health [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
hermes-agent [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
homepage [cleanup] Remove ollama from dashy + docs + nfs_directories 2026-04-18 11:17:59 +00:00
immich [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
infra [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
infra-maintenance [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
insta2spotify [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
isponsorblocktv [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
jsoncrack [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
k8s-dashboard [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
k8s-portal [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
kms [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
kyverno kyverno: strip resources.limits.cpu cluster-wide via ClusterPolicy 2026-04-18 11:34:39 +00:00
linkwarden [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
mailserver [infra] Document intended ignore_changes drift-workarounds [ci skip] 2026-04-18 14:08:10 +00:00
matrix [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
meshcentral [meshcentral] Remove accidentally-committed Terragrunt-generated files 2026-04-18 12:35:44 +00:00
metallb [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
metrics-server [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
monitoring [payslip-ingest] Deploy stack + Grafana dashboard + Vault DB role 2026-04-18 19:07:05 +00:00
n8n [n8n] Fix broken DIUN auto-upgrade pipeline — missing auth token to claude-agent-service 2026-04-18 10:41:09 +00:00
navidrome [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
netbox [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
networking-toolbox [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
nextcloud [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
nfs-csi [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
novelapp [infra] Document intended ignore_changes drift-workarounds [ci skip] 2026-04-18 14:08:10 +00:00
ntfy [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
nvidia [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
onlyoffice [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
openclaw [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
osm_routing [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
owntracks [infra] Document intended ignore_changes drift-workarounds [ci skip] 2026-04-18 14:08:10 +00:00
paperless-ngx [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
payslip-ingest [payslip-ingest] Deploy stack + Grafana dashboard + Vault DB role 2026-04-18 19:07:05 +00:00
phpipam [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
platform [infra] Add Cloudflare provider to all stack lock files and generated providers 2026-04-16 16:31:36 +00:00
plotting-book [infra] Document intended ignore_changes drift-workarounds [ci skip] 2026-04-18 14:08:10 +00:00
poison-fountain [infra] Scale down unused services + remove DoH ingress 2026-04-17 18:55:52 +00:00
priority-pass [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
privatebin [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
proxmox-csi feat(storage): migrate all sensitive services to proxmox-lvm-encrypted 2026-04-15 20:15:30 +00:00
pvc-autoresizer fix: disable cert-manager webhook for pvc-autoresizer, use self-signed cert [ci skip] 2026-04-03 23:44:49 +03:00
rbac [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
real-estate-crawler [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
redis [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
reloader [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
resume [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
reverse-proxy [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
rybbit [rybbit] Narrow CF Worker routes to SITE_IDS hosts — fix free-tier quota breach 2026-04-18 13:23:15 +00:00
sealed-secrets [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
send [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
servarr [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
shadowsocks [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
speedtest [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
status-page [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
stirling-pdf [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
tandoor [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
technitium [infra] Scale down unused services + remove DoH ingress 2026-04-17 18:55:52 +00:00
terminal Add broker-sync Terraform stack (#7) 2026-04-17 21:17:45 +01:00
tor-proxy [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
trading-bot [infra] Document intended ignore_changes drift-workarounds [ci skip] 2026-04-18 14:08:10 +00:00
traefik [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
travel_blog [infra] Scale down unused services + remove DoH ingress 2026-04-17 18:55:52 +00:00
tuya-bridge [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
uptime-kuma [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
url [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
vault [payslip-ingest] Deploy stack + Grafana dashboard + Vault DB role 2026-04-18 19:07:05 +00:00
vaultwarden [traefik] Remove broken rewrite-body plugin and all rybbit/anti-AI injection 2026-04-17 12:41:17 +00:00
vpa [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
wealthfolio [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
webhook_handler [infra] Establish KYVERNO_LIFECYCLE_V1 drift-suppression convention [ci skip] 2026-04-18 14:15:51 +00:00
whisper [whisper] Remove ollama_tcp IngressRouteTCP (ollama decom) 2026-04-18 11:11:21 +00:00
wireguard [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
woodpecker [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
xray [infra] Migrate Terraform state from local SOPS to PostgreSQL backend 2026-04-16 19:33:12 +00:00
ytdlp [ytdlp] Remove ollama_host variable and fallback env vars 2026-04-18 11:13:42 +00:00