Commit Graph
100 Commits
Author SHA1 Message Date
rock 0666715a5e Merge pull request 'feat(argocd): reconcile azhi-infra from its private GitHub repo' (#60) from feat/azhi-riotpiao-argocd into main 2026-10-12 12:54:43 +00:00
rock e150969070 Merge pull request #59 from fix/etcd-defrag-main 2026-10-12 12:27:28 +00:00
rock abfdda5f57 fix(etcd): add a working 15-day defrag CronJob under the backend quota
etcd's backend file only grows: the apiserver compacts every 5m, but compaction
marks superseded revisions free inside the file, and only defrag returns that
space to the filesystem. Nothing ran defrag, so on 2026-10-07 the file reached
2,148,843,520 B, crossed the 2 GiB quota on talos-cp-1 and talos-cp-3, and etcd
raised NOSPACE. From then on etcd rejected every write cluster-wide: kubelets
could not post node status (3 nodes NotReady), apiserver writes returned 500 and
the tunnel API path returned 502. Live data was 15,943 keys, 69 MB after
compaction - the rest was dead pages. Compaction had also stalled while the
alarm was up, so dbSizeInUse read 100% of the file; defrag alone would have
reclaimed nothing. Compact must come first.

An earlier revision of this CronJob never ran, for two independent reasons, both
fixed here:
- its image quay.io/coreos/etcd ships no /bin/sh, so command: [/bin/sh, -c]
  failed with exec: "/bin/sh": no such file or directory
- it existed only on a side branch while the ArgoCD application
  cluster-maintenance tracks main, so the cluster never had it

Contents:
- CronJob etcd-defrag (kube-system): daily at 03:00, acts only when the UTC
  epoch day is a multiple of 15. That is an exact 15-day interval; */15 in the
  day-of-month field fires on the 1st, 16th and 31st, putting two runs one day
  apart.
- Image bitnamilegacy/etcd:3.6.4 pinned by digest, ships bash and etcdctl 3.6.4
  against the 3.6.11 servers.
- Compaction uses the MINIMUM revision across members, so a lagging member is
  never compacted past its own revision. Measured 2026-10-07: talos-cp-2 sat
  ~1.2M entries behind the leader with a frozen raft term and needed a reboot
  to rejoin.
- Defrag skips any member whose applied index lags the leader by 100k+, and
  runs one member at a time since defrag pauses that member's apply loop.
- Secret etcd-defrag-creds (SOPS/ksops, kube-system) carries the etcd CA plus a
  client certificate signed by the cluster's etcd CA, valid to 2036-10.
2026-10-07 17:12:39 +08:00
rock 2d9e3abee8 fix(oidc): match /proc/<pid>/comm instead of cmdline when signalling the API server
Exit code 143 proved the previous job killed itself: the script's own cmdline contains the
string 'kube-apiserver', so it matched its own process. Matching comm (== 'kube-apiserver')
and skipping $$ removes that failure mode. Staggered 2 minutes apart.
2026-10-07 04:07:37 +00:00
rock 9fa8d9cf60 fix(oidc): restart the apiservers by signalling the process, not by deleting the mirror pod
Static pods survive mirror-pod deletion, so the first bootstrap job had no effect.
These jobs share the host PID namespace and SIGTERM the kube-apiserver process itself,
staggered by 100s so only one control plane restarts at a time.
2026-10-07 03:59:22 +00:00
rock 2e4fad3bf7 fix(oidc): use curlimages/curl for the bootstrap job (bitnami/kubectl tag did not exist)
The first job could not pull its image; this one deletes the apiserver pods via the
in-cluster API with the mounted service-account token instead of a kubectl image.
2026-10-07 03:52:10 +00:00
rock 5d22691c3e chore(oidc): one-shot bootstrap - restart apiservers to initialise the OIDC authenticator
k8s/argocd/apps/99-oidc-bootstrap.yaml
2026-10-07 03:46:01 +00:00
rock 4eee161e0f chore(oidc): one-shot bootstrap - restart apiservers to initialise the OIDC authenticator
k8s/infra/oidc-bootstrap/bootstrap.yaml
2026-10-07 03:45:56 +00:00
rock 719fa77521 Merge pull request 'fix(secrets): populate the empty sops placeholders and drop the unused one' (#55) from fix/secrets-placeholders into main 2026-10-06 04:48:51 +00:00
rock 64cd1f2c4e fix(minio): freeze the community image by digest and alert on pull failure (#57)
Co-authored-by: rock <[email protected]>
2026-10-06 03:43:12 +00:00
rock f780e31f9c Merge origin/main into fix/secrets-placeholders (rebuild ksops file list, fix #54 indent) 2026-10-06 10:05:45 +08:00
rock c93a24f786 fix(secrets): populate the empty sops placeholders and drop the unused one
(cherry picked from commit f6207ff2b0)
2026-10-06 10:03:04 +08:00
rock b6e317b34c feat(poimen): pi sandbox workspace volume, encrypted api keys, cluster dns rewrite (#54)
Adds the pi coding-agent sandbox's cluster side:
- `k8s/infra/poimen/pi-agent-workspace-pv.yaml` — static hostPath PV + PVC for the sandbox /workspace (Longhorn cannot attach new volumes in this cluster; this gives real persistence and survived a pod delete/recreate test: marker file + 38 KB session.jsonl intact, the session answered from its own history afterwards).
- `k8s/argocd/secrets/pi-sandbox-secrets.enc.yaml` — the sandbox's API-key Secret, SOPS-encrypted (recipient from .sops.yaml, payload only: `encrypted_regex ^(data|stringData)$`), registered in the ksops generator. Round-trip verified against the live Secret.
- `terraform/files/coredns/Corefile` — `rewrite name pi.riotpiao.com ingress-nginx-controller...` so the hostname resolves in-cluster.

Note: the keys currently in that Secret are the ones leaked earlier and must be rotated; re-encrypt with `sops -e -i` after rotating.Reviewed-on: #54
Co-authored-by: rock <[email protected]>
2026-10-06 01:49:12 +00:00
rock f3015522e3 fix(storage): pin minio tenant to a published image release (#53)
## Why

`minio/minio:RELEASE.2025-07-23T15-54-02Z` no longer exists. The Docker Hub repository
`minio/minio` returns **404 for every tag** (checked: the pinned tag and `latest`), and
MinIO has moved its images behind registry authentication (`quay.io/minio/minio` → 401,
`registry.min.io/minio/minio` → auth required, `dl.min.io` binaries → 410 Gone).

The tenant therefore sat in `ImagePullBackOff` — 44,229 attempts over 6d23h — and the chain
from there is:

```
minio tenant pod not ready (1/2)
  -> headless Service minio-cluster-hl has no endpoints
  -> CoreDNS: lookup minio-cluster-hl.storage.svc.cluster.local -> no such host
  -> Vault S3 storage init fails at startup
  -> vault-0 CrashLoopBackOff, 1943 restarts, ~7 days
```

## Control checks (so this is not a network story)

| check | result |
| --- | --- |
| `alpine:3.20` pulled on talos-cp-1 | Succeeded — docker.io works |
| `quay.io/prometheus/node-exporter:v1.8.2` on the same node | Succeeded — public quay works |
| `minio/minio` (docker.io) | HTTP 404, repo removed |
| old tag `RELEASE.2024-06-13T20-48-48Z` with `IfNotPresent` | not cached on the node |

## Change

Pin `spec.image` to `coollabsio/minio:RELEASE.2025-10-15T17-29-55Z` — a public mirror, and a
newer upstream release than the previous pin. Verified by pulling it on talos-cp-1 and
running it: the image carries `/usr/bin/docker-entrypoint.sh` (the layout the operator
expects, which runs `minio server … --console-address :9090`) and reports
`minio version RELEASE.2025-10-15T17-29-55Z`.

## Caveat

This is a community mirror, not MinIO's official image, because the official ones now
require credentials. If MinIO registry credentials are available, add an imagePullSecret
and switch back to `quay.io/minio/minio`. Supply-chain decision, flagged deliberately.

## After merge (ArgoCD, not kubectl)

The storage app tracks `k8s/infra/minio`, so the sync rolls the tenant; then:

```
kubectl get pods -n storage          # minio-cluster-az-a-0 -> 2/2
kubectl get pods -n iam | grep vault  # vault-0 -> Running (storage init succeeds)
```

Residual risk, unrelated to this fix: the tenant's 4 x 25Gi volumes are Longhorn-backed, and
Longhorn currently fails to create engines for new volume attaches cluster-wide. If the
restarted pod cannot attach, that is a second, separate fault.Reviewed-on: #53

Co-authored-by: rock <[email protected]>
2026-10-06 01:48:57 +00:00
rock d60b520968 Merge pull request 'fix(llm-serving): V1 prefill scheduling + admission observability (#51)' (#52) from harness/51-vllm-prefill-scheduling into main 2026-09-30 11:49:52 +00:00
rock c7d52b5971 fix(llm-serving): V1 prefill scheduling + admission-control observability (#51)
The reasoning engine starves every other request while one long prompt
prefills. Measured: 262 tok/s contended prefill, ~250s for a 65k prompt,
with Avg generation throughput 0.0-0.1 tok/s at Running: 2-4 reqs.

Root cause is NOT what the issue first diagnosed. The issue proposed
--max-num-partial-prefills / --max-long-partial-prefills, which are V0-only
knobs. Verified in the live container: vLLM 0.11.0 ships V1 only (vllm/core/
is absent, so there is no V0 to fall back to), and EngineArgs.
_is_v1_supported_oracle returns False whenever either flag differs from its
default, calling _raise_or_fallback("Concurrent Partial Prefill") ->
"Falling back to V0" -> AssertionError. With strategy Recreate that is an
outage, not a slow rollout. Both flags are therefore NOT set, and a comment
records why so they are not reintroduced.

V1 needs no such flag: it has no prefill/decode phase at all. Every step it
walks self.running and hands out tokens from one shared budget
(vllm/v1/core/sched/scheduler.py). The real defect is that the per-request
cap is disabled by default (long_prefill_token_threshold=0), so one long
prompt takes the WHOLE budget, token_budget hits 0, the `while ... and
token_budget > 0` loop exits, and every other request is skipped.

The cap is applied before the budget clamp, so threshold >= budget never
binds. Replaying the loop: 8192/8192 serves 1 request per step (still
starved); 8192/2048 serves 4, and two long prompts advance together, which
is the issue's actual acceptance criterion.

  --max-num-batched-tokens=8192      (was unset -> default 2048)
  --long-prefill-token-threshold=2048 (was unset -> 0, cap disabled)
  --max-num-seqs=5, --gpu-memory-utilization=0.95

The ~166k KV figure is marked as a projection; the live reading is
num_gpu_blocks=9527 x block_size=16 = 152,432 tokens and is authoritative
post-boot. A preemption alert covers the accept-and-watch risk.

Observability for the companion admission gate (homelab-frontend#64, now
deployed at 64f2bfc1):
  - four dashboard panels: parked, rejected by reason, engine state stale,
    and gateway-view vs engine-truth. Each carries a description, because
    an unexplained blank panel reads as a broken dashboard.
  - alerts on observer_degraded and engine_state_stale_total, both with an
    absent() guard: the gate fails OPEN, so a missing or renamed metric must
    alert rather than look healthy. Verified live that the guards do not
    false-fire (both series present, 3 each).
  - LLMPreemptions and LLMPrefillStarvation: the acceptance metric and the
    regression direction this change introduces.
  - max(vllm_requests_running), not sum(): one engine-global gauge is read
    by all three gateway replicas, so sum() read 3x and could never agree.

Also corrects comments that were factually wrong. An earlier revision of
this branch claimed llm_ttft_seconds/llm_itl_seconds/llm_tokens_total were
dead code; homelab-frontend#63 wired them, verified live as 3 series each.
request_duration_seconds is the genuinely dead one (0 samples, zero
non-test call sites) and is now marked as such instead of being listed as
working.

Closes #51
2026-09-30 19:49:05 +08:00
rock 3c3b243790 docs(monitoring): correct the gateway latency-metric claim
Previous comments described llm_ttft_seconds / llm_itl_seconds /
llm_tokens_total as "streaming-only". That was wrong, and optimistic.

They are dead code. internal/proxy/llm_metrics.go defines
LLMMetricsCapture, which is the only thing that calls RecordTTFT /
RecordITL / RecordTokens — but NewLLMMetricsCapture() has zero call sites
anywhere in the repo, verified by grep. The exporter skips keys with no
samples, so those three series render as HELP-only stubs and can never be
graphed, streaming or not.

No behaviour change here; only the comments and the dashboard generator's
header are corrected so the next person does not build on a false
assumption. The gateway latency panels are left in place so the gap stays
visible — they populate unchanged once the capture wrapper is wired into
the streaming proxy path.

Series that DO work from the gateway, confirmed live with 3 series (one
per replica): queue_full_requests_total, warmup_requests_delayed_total,
vllm_prefix_cache_hit_tokens_total.
2026-09-29 22:47:38 +08:00
rock 420b5d6da6 fix(monitoring): drop redundant relabels, resolve ServiceMonitor drift
Both new ServiceMonitors sat permanently OutOfSync. Cause: `action: replace`
is the API server default for relabeling rules, so the live object carried it
while the manifests omitted it.

Rather than write out the default to paper over drift, removed the rules that
were never needed:

- vllm.yaml: the namespace/pod/service relabelings were redundant —
  Kubernetes service discovery already supplies all three. Only `app` has to
  be derived (TEI panels aggregate by it and SD does not promote arbitrary
  pod labels). Kept that one, with an explicit action.
- api-gateway.yaml: the metricRelabelings block was a no-op — it mapped
  __address__ onto instance, which is already the default. Removed. The
  series inventory it was documenting moved up into the file header, which
  is where it belonged anyway.
2026-09-29 22:15:02 +08:00
rock 1f3439c9e0 feat(monitoring): LLM observability dashboard + fix dead CAG rule
New Grafana dashboard (llm-observability) with 27 panels across 5 rows:
TTFT, inter-token latency, batching/scheduling, KV cache, throughput.

Both metric sources are shown side by side because they answer different
questions and neither is complete alone:

  vLLM 0.11.0 (reasoning) — engine truth: kv_cache_usage_perc,
    num_requests_running/waiting, num_preemptions_total, prefill vs
    decode time, prefix cache, per-request histogram quantiles.
  TEI 1.8.2 (embeddings, reranker) — te_batch_next_size, te_queue_size,
    te_embed_duration, te_embed_queue_duration.
  api-gateway — the ONLY per-request latency source covering Ollama
    models (ornith:35b, qwen2.5:3b-instruct), which expose no metrics.
    It is derived from the SSE byte stream, so it is streaming-only and
    counts SSE chunks rather than true tokens. vLLM's own histograms are
    authoritative for reasoning.

All 44 panel queries were executed against live Prometheus before commit —
all return success. vLLM/TEI panels have live series; gateway panels show
no data until the netpol fix in homelab-frontend PR #62 merges.

Also fixes a dead alert. alerts/cag-kv-cache-rules.yaml derived
vllm:prefix_cache_hit_rate from vllm:cache_query_hit_total and
vllm:cache_query_miss_total. Those metric names do not exist in vLLM
0.11.0 — the real series are vllm:prefix_cache_hits_total and
vllm:prefix_cache_queries_total. The recording rule produced no data, so
VLLMPrefixCacheHitRateLow could never fire.

Not included: real GPU hardware metrics (SM utilisation, framebuffer,
power, temperature). Only nvidia-device-plugin is deployed, which exposes
none of them; DCGM exporter is required. vllm:kv_cache_usage_perc and
preemption rate are the only GPU-pressure proxies available today.
2026-09-29 22:11:28 +08:00
rock 6ae36981bb fix(monitoring): wire llm-serving + api-gateway metric scraping
Both metric sources existed but neither was scraped:

- servicemonitors/vllm.yaml was orphaned (absent from kustomization
  resources:) and would not have matched anyway — it used 'port: http'
  but KServe-generated Services in llm-serving have UNNAMED ports.
  Switched to targetPort: 8080 (prometheus-operator v0.94.1 supports it),
  which avoids editing the InferenceServices and therefore avoids a
  predictor restart / weights reload.

- api-gw exposes llm_ttft_seconds, llm_itl_seconds, llm_tokens_total,
  queue_full_requests_total, warmup_requests_delayed_total and
  vllm_prefix_cache_hit_tokens_total, but the Deployment's
  prometheus.io/scrape annotation is ignored by kube-prometheus-stack
  (no additionalScrapeConfigs). Added a ServiceMonitor for it. This is
  the only per-request latency source covering the two Ollama models,
  which expose no metrics endpoint at all.

Ollama services are excluded from the vllm ServiceMonitor selector to
avoid permanently-DOWN targets.

Note: alerts/cag-kv-cache-rules.yaml references vllm:prefix_cache_hit_rate
derived from vllm:cache_query_hit_total/miss, which were never scraped.
Those alerts were dead; verify the metric names after the first scrape.
2026-09-29 22:05:00 +08:00
rock e6fd6f2b62 fix(prometheus): cut TSDB volume 100Gi -> 25Gi
Actual TSDB data is ~2Gi. 100Gi over-provisioned Longhorn storage
contributed to the cluster-wide scheduling pressure; 25Gi is ample
with room for retention growth.
2026-09-29 21:59:55 +08:00
rock c83d456ecd fix(forgejo): disable self-registration
GET /user/sign_up returned 200 — anyone reaching forgejo.riotpiao.com
could create an account. Add [service] DISABLE_REGISTRATION: true.

Admin-created accounts (UI/API) are unaffected.
2026-09-29 21:13:18 +08:00
rock 8bd40c5152 feat: poimen-sandbox namespace + security hardening + RBAC
Phase 1: sandbox namespace with PSA restricted enforcement,
ResourceQuota (20 pods, 10/20 CPU, 20/40Gi memory), and
CiliumNetworkPolicy denying pi-session egress to kube-apiserver.

Phase 2: sandbox-observer Role + poimen-devs RoleBinding for
OIDC-authenticated kubectl access to sandbox pod status/logs.
2026-09-28 23:15:25 +08:00
rock c472a994ce fix(longhorn): enable worker-1 scheduling, 400Gi reserved
32 degraded + 3 faulted volumes from replica rebuild failures.
cp-1/cp-2 over-committed; worker-1 had 602Gi free but scheduling
disabled. Enable it with 400Gi reserved for LLM weights.

- Add worker-1 to longhorn-nodes.yaml (allowScheduling: true)
- Add worker-1 disk config to add-disks-job (storageReserved: 400Gi,
  evictionRequested: false)
2026-09-28 23:06:22 +08:00
rock bb24418a2e feat: remove ArgoCD Image Updater, replaced by Forgejo webhook
Image promotion now handled by api-gw webhook endpoint
(/v1/webhooks/forgejo) which patches ArgoCD Application CRs
on Forgejo package push events (homelab-frontend PR #55).

- Delete Image Updater Application and values
- Remove argocd-image-updater.argoproj.io annotations from all apps
- Registry secrets kept (used by webhook digest queries + imagePullSecrets)
2026-09-28 23:06:16 +08:00
rock 797e11ce11 feat(secrets): add forgejo-registry-pat for api namespace
Encrypted SOPS secret (age) — api-gateway image promoter needs
REGISTRY_PAT to query Forgejo v2 registry for image digests.
Key: pat  Value: bdf6a1d... (registry PAT from existing forgejo-registry-pat secret)
Namespace: api  Secret name: forgejo-registry-pat
2026-09-28 11:11:06 +08:00
rock 76a3281e80 fix(argocd): image-list comma-separated — updater v0.15.1 treats pipe block as single image name 2026-09-28 09:39:13 +08:00
rock c04ed6136b refactor(poimen): move frontend k8s manifests to poimen-frontend repo
- Remove Deployment/Service/NetworkPolicy from homelab (wrong place)
- Add k8s/apps/poimen-frontend/ with ingress only (edge concern, same as api-gw)
- Update 70-poimen.yaml: source 4 → poimen-frontend.git k8s/
                          source 5 → homelab k8s/apps/poimen-frontend/ (ingress)
2026-09-28 09:13:02 +08:00
rock fbe8a083bd fix(poimen-frontend): remove speculative COREPACK env workarounds
Root cause fixed in Dockerfile (corepack prepare at build time).
No env overrides needed in the deployment.
2026-09-28 09:10:03 +08:00
rock 066c12463c fix(poimen-frontend): correct secret key refs, remove unused NextAuth env
- LLM_OIDC_TOKEN_URL/CLIENT_ID/CLIENT_SECRET/SCOPE → all from poimen-secrets (correct keys)
- Remove NEXTAUTH_URL/NEXTAUTH_SECRET (app uses getServerToken(), not NextAuth)
- GOTIFY_TOKEN → poimen-secrets (key patched in-cluster)
2026-09-28 08:57:54 +08:00
rock 227839c27a feat(poimen): consolidate into single multi-source ArgoCD app
- Merge poimen-workflows + poimen-memory + db + frontend into one app
- Delete duplicate apps: poimen-workflows, poimen-memory-app (kubectl done)
- Delete poimen-memory ArgoCD app file (replaced by multi-source source 2)
- Delete poimen-memory-database (replaced by source 3 → k8s/infra/poimen/)
- Move memory-db CNPG cluster to k8s/infra/poimen/ (isolated from other tenants)
- Remove memory-db from k8s/infra/databases/kustomization.yaml
- Add frontend manifests (Deployment, Service, Ingress) to k8s/apps/poimen/
- Add frontend NetworkPolicy: egress to api-gw (api:8080), memory (poimen:8080),
  Gotify (notifications:80), Authentik (iam:9000/9443), DNS (kube-system:53)
- Sync-wave -1 on CNPG cluster ensures DB ready before app pods (wave 0)
- Image updater: workflows + memory + frontend tracked in one annotation block
- poimen-toolkit stays separate (CRD management, independent rollback)
2026-09-28 08:51:31 +08:00
rock 8ed60c8b55 fix(forgejo): allow webhook delivery to api.riotpiao.com
GITEA__WEBHOOK__ALLOWED_HOST_LIST=external,api.riotpiao.com
api.riotpiao.com resolves to cluster ingress ClusterIP (private range)
which is blocked by Forgejo's default SSRF protection. Safe to allowlist
as it is our own api-gw.
2026-09-27 22:45:13 +08:00
rock 3914c78bc5 feat(argocd): poimen-memory Application — Redis + NetworkPolicy auto-deploy
Points to riotpiao-poimen/poimen-memory.git k8s/app/ (kustomize).
ArgoCD auto-syncs Redis deployment, NetworkPolicies, ConfigMap, Service.
Image updater: digest strategy on :latest tag for rolling deployments.
Wave 7 — same wave as poimen workflow app.
2026-09-27 19:27:05 +08:00
rock 605bc706e0 feat(cag): PrometheusRule + Grafana panels for KV cache observability
- cag-kv-cache-rules.yaml: recording rule vllm:prefix_cache_hit_rate
  (from cache_query_hit/miss counters), 3 alert rules:
  KVWarmupFailureHigh, KVWarmupMissHigh, VLLMPrefixCacheHitRateLow
- api-latency-dashboard.yaml: 5 new panels (id 7-11):
  prefix cache hit rate, warmup hit/miss, warmup latency p50/p95,
  standing context tier hits, alert panel with fixed threshold 0.01/s
2026-09-27 18:37:03 +08:00
rock 920693df1b feat: poimen-toolkit ArgoCD app + sourceRepo
- k8s/argocd/apps/71-poimen-toolkit.yaml: Application wave-7, points to
  poimen-toolkit repo k8s/ path, ServerSideApply for CRD schema size,
  image-updater for controller:latest digest
- k8s/argocd/projects/homelab-project.yaml: add poimen-toolkit.git to sourceRepos
2026-09-27 18:03:42 +08:00
rock dfd5f8dd2d tmp 2026-09-24 21:24:30 +08:00
rock f59fb476b8 feat(ornith): bump context length 32K → 64K
V100-PCIE-32GB has ~12GB VRAM headroom after model weights.
ornith:35b Q4_K_M KV cache at 64K ≈ 11.8GB — fits within budget.
Model natively supports 256K but hardware caps safe limit at 64K.
2026-09-16 07:21:45 +09:00
rock e33a2228e1 feat(igotify): deploy igotify server + upgrade gotify to 3.1.0
- igotify server v1.6.0.1 (supports Gotify 3.x)
- gotify upgraded 2.9.1 → 3.1.0
- igotify PVC, Deployment, Service, Ingress in notifications ns
- CoreDNS rewrite for igotify.riotpiao.com → nginx
2026-09-16 06:56:10 +09:00
rock 27bce41bf6 fix(gotify): rollback to 2.9.1 — iGotify stable incompatible with 3.x 2026-09-15 22:34:53 +09:00
rock 1edf1ba7f1 feat(gotify): upgrade to 3.1.0 for /gotifyinfo endpoint (iGotify compat) 2026-09-15 22:22:55 +09:00
rock 44d5207622 fix(gotify): remove configuration-snippet annotation blocking nginx ingress
nginx-ingress v1.15.1 blocks configuration-snippet by default as a
security risk, silently dropping the entire Ingress resource. WebSocket
upgrade headers are handled natively with proxy-http-version 1.1.
2026-09-15 18:01:48 +09:00
rock 7a239b2469 feat: add gotify.riotpiao.com coredns rewrite for mobile app access 2026-09-15 17:23:21 +09:00
rock 795cef5a85 feat(prometheus): scale up storage to 100Gi and move to worker-1
- Increase PVC from 20Gi to 100Gi (talos-cp-1 disk full)
- Move from az-a (talos-cp-1) to worker-1 (648GB available)
- Increase retention from 15d to 30d (95GB limit)
- Fixes Prometheus pod restarts due to disk pressure
2026-09-15 08:16:01 +09:00
rock c1a2dbf52a fix(minio): add BROWSER_REDIRECT_URL to enable SSO button in Console
- Add MINIO_BROWSER_REDIRECT_URL pointing to public URL
- Add MINIO_BROWSER_LOGIN_ANIMATION=off
- Enables Console to show OIDC/Authentik SSO button on login page
2026-09-15 02:44:09 +09:00
rock 0f854f2bfa feat(minio): use groups claim for OIDC auth instead of policy
- Change MINIO_IDENTITY_OPENID_CLAIM_NAME from 'policy' to 'groups'
- Add MINIO_IDENTITY_OPENID_CLAIM_PREFIX (empty) to use raw group names
- Update scopes to 'openid,profile,email,groups'
- Enables Authentik group-based access (homelab-admins, minio-admins, etc)
- Users in homelab-admins group will get admin access to MinIO
2026-09-15 02:40:02 +09:00
rock 283d30b422 fix(paperless): use correct env var PAPERLESS_SOCIAL_AUTO_SIGNUP for auto-linking
The correct env var is PAPERLESS_SOCIAL_AUTO_SIGNUP not PAPERLESS_SOCIALACCOUNT_AUTO_SIGNUP
This enables automatic linking of Authentik social accounts to existing Paperless users
2026-09-15 02:30:12 +09:00
rock 2b2ceab2d1 fix(paperless): enable OIDC auto-linking to existing users by email
- Add SOCIALACCOUNT_AUTO_SIGNUP for automatic social account creation
- Enable ACCOUNT_ALLOW_SIGNUPS for OIDC users
- Add SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT to link existing users
- Fixes 'An account already exists with this email' error
- Allows Authentik login to auto-link to existing Paperless users
2026-09-15 02:00:36 +09:00
rock 6243ac40b7 feat(paperless-ai): enable intelligent timeline tagging with date extraction
- Add ENABLE_INTELLIGENT_TAGGING for advanced AI-powered tagging
- Add ENABLE_DATE_EXTRACTION for automatic date/timeline parsing
- Add ENABLE_TIMELINE_TAGGING to create temporal tags (YYYY, Q#-YYYY, Last-Month, etc)
- Set TAG_EXTRACTION_MODEL to reasoning for sophisticated analysis
- Configure timeline granularity (year, quarter, month)
- Add custom prompt for temporal context extraction
- Auto-create tags for historical and recent documents
2026-09-15 00:53:30 +09:00
rock d6fca68f33 chore(paperless-ai): switch LLM model to reasoning for document processing 2026-09-15 00:38:37 +09:00
rock e12327f963 fix(grafana): re-enable org role sync for OAuth groups
Re-enable skip_org_role_sync=false to sync Admin role from grafana-admins group.
User creation worked with skip_org_role_sync=true, now restore role sync.
2026-09-15 00:37:06 +09:00
rock b03098aa1c fix(grafana): skip_org_role_sync on first login
Set skip_org_role_sync=true to allow user creation on first OAuth signin.
When false, Grafana tries to sync org roles before user exists, causing creation to fail.
2026-09-15 00:32:41 +09:00
rock f61e8f1f68 fix(grafana): use preferred_username for OAuth login lookup
Reverts to standard OpenID 'preferred_username' claim which is guaranteed to be present in all userinfo responses. The 'email' claim requires explicit scope mapping in Authentik that may not be consistently returned.

Authentik user 'rock' has preferred_username='rock' which matches the existing Grafana user login.
2026-09-15 00:17:50 +09:00
rock 7016f764e6 infrastructure(paperless): add paperless-ai production ConfigMap to git
- Commit paperless-ai-config.yaml with production environment variables
- Configure Paperless API endpoint and token
- Enable AI processing with qwen2.5:3b model
- Set up auto-tagging, correspondent/document type extraction
- Configure 60s scan interval for document processing
- Add ConfigMap to kustomization.yaml for GitOps deployment
2026-09-15 00:14:42 +09:00
rock d826510a98 feat(paperless-ai): configure for production with Paperless API + LLM integration
- Add ConfigMap with production paperless-ai config
- Mount .env config file for app startup
- Enable auto-tagging, correspondent extraction, document type detection
- Set LLM API endpoint and token file location
- Configure 60s scan interval for document processing
2026-09-15 00:13:02 +09:00
rock 467b3441c9 fix(grafana): use email for OAuth login lookup instead of preferred_username
- Change login_attribute_path from preferred_username to email for stable user matching
- Enable allow_sign_up to permit OAuth user sync with existing local users
- Root cause: Authentik's 'rock' user matches existing Grafana 'rock' by email, not by preferred_username claim
2026-09-15 00:01:10 +09:00
rock 7e91262257 fix(paperless-ai): write token to /tmp as fallback (PVC mount perms) 2026-09-14 23:58:41 +09:00
rock 0edd6bc73a fix(paperless-ai): use /app/data mount path consistently for init + main container 2026-09-14 23:57:44 +09:00
rock 4ad0102260 fix(paperless-ai): fix sed token extraction to handle JSON spaces 2026-09-14 23:56:33 +09:00
rock e0c08a90b3 Grafana OAuth: disable allow_sign_up to debug user.sync failure 2026-09-14 23:56:04 +09:00
rock dfbe2cc920 fix(paperless-ai): use sed for JWT token extraction (jq not available) 2026-09-14 23:55:53 +09:00
rock ae93c7ca0d fix(paperless-ai): use jq for JWT token extraction instead of grep 2026-09-14 23:54:57 +09:00
rock 91ce48ffae fix(sops): add LLM_AUTH_CLIENT_SECRET for paperless-ai-agent Authentik auth 2026-09-14 23:19:43 +09:00
rock 39036e6ff3 fix(paperless-ai): move LLM auth client secret from plaintext to SOPS
- Remove hardcoded client secret from init container
- Reference secret from paperless-ai-config (SOPS-encrypted)
- Init container reads LLM_AUTH_CLIENT_SECRET env var
- Secret key must be added to k8s/argocd/secrets/paperless-ai-secrets.enc.yaml
  by someone with SOPS/age key access

This removes the plaintext secret from commit history and future
deployments will source it securely from the encrypted Secret.
2026-09-14 23:07:52 +09:00
rock 8fa5053aef fix: allow paperless direct LLM access, bypass gateway JWT
- Add paperless namespace to llm-serving NetworkPolicy ingress
- Point paperless-ai CUSTOM_BASE_URL to reasoning-predictor directly
- Gateway JWT auth not yet available for service accounts
- paperless-ai-agent has llm:inference role in Authentik
2026-09-14 08:09:26 +09:00
rock 41e894f79e fix: aggressive 2m cleanup for completed jobs/pods, add PAPERLESS_USERNAME to paperless-ai
- stale-job-cleanup: run every 2m, delete completed/failed jobs+pods after 2m
- paperless-ai: add PAPERLESS_USERNAME=admin (required to find own user ID)
2026-09-14 07:56:05 +09:00
rock 7a5d0a83d5 fix: vendor Tekton release.yaml for proper ArgoCD management
ROOT CAUSE:
- tektoncd/pipeline config/ dir uses ko:// image refs (build-time placeholders)
- ArgoCD synced the raw dev manifests → InvalidImageName on all pods
- tektoncd/operator requires its own CRDs and controller (too heavy)
- Tekton has no official Helm chart

FIX:
- Vendor the pre-built release.yaml (v0.68.0) into k8s/infra/tekton/
- Point ArgoCD Application at our own repo (forgejo)
- Release contains real container images (ghcr.io/tektoncd/pipeline/*)
- Remove external tektoncd repo from AppProject (not needed)

TO UPGRADE TEKTON:
  1. Download new release from github.com/tektoncd/pipeline/releases
  2. Replace k8s/infra/tekton/release.yaml
  3. Commit — ArgoCD syncs automatically
2026-09-13 15:10:39 +09:00
rock b06ee310b5 fix: use tektoncd/operator for proper K8s-native Tekton installation
ROOT CAUSE:
- Previous Application pointed to storage bucket (not valid ArgoCD source)
- ArgoCD couldn't sync manifests from non-git/non-helm source
- tektoncd/operator is the official way to install Tekton

SOLUTION:
- Switch to tektoncd/operator repository
- Use operator's config/install path (contains release manifests)
- Proper GitOps flow: ArgoCD watches operator repo → syncs manifests → K8s reconciles

BENEFITS:
✓ Official Tekton approach
✓ Proper K8s Operator pattern
✓ ArgoCD-compatible (git source)
✓ Automatic updates from upstream
✓ Full GitOps workflow
2026-09-13 15:10:39 +09:00
rock cda7153da2 fix: add PAPERLESS_USERNAME env var to paperless-ai (#45)
Required by paperless-ai to find its own user ID for scanning.
Without this, scanning aborts with "Failed to get own user ID".Reviewed-on: #45

Co-authored-by: rock <[email protected]>
2026-09-13 04:58:44 +00:00
rock 0aaa45edb0 fix(comfyui): disable k8s service links to fix COMFYUI_PORT env collision
Root cause: k8s Service named 'comfyui' auto-injects COMFYUI_PORT=tcp://...
into pod env, clobbering ai-dock's own COMFYUI_PORT variable which expects
a port number. This broke caddy's proxy config, leaving port 8188 dead.

Fix: enableServiceLinks: false, revert to port 8188 (ai-dock default).
Also bump startup probe failureThreshold 60->120 (20min budget for model loading).
2026-09-13 09:29:14 +09:00
rock e4f6f03a8c fix(comfyui): correct port 8188->18188, add TLS ingress at comfyui.riotpiao.com (#44)
## Problem
ComfyUI pod restarting 273+ times over 45h. Startup probe failed every time.

## Root Cause
ai-dock image runs ComfyUI on port 18188 (adds 10000 to configured port), not 8188. Caddy does NOT proxy 8188->18188.

## Fix
- deployment: containerPort, probes -> 18188
- service: targetPort -> 18188
- ingress: TLS at comfyui.riotpiao.com, WebSocket headers
- CoreDNS: rewrite comfyui.riotpiao.com

## Post-merge
CoreDNS rewrite needs terraform apply + make apply-cp.
Cloudflare DNS: add CNAME comfyui.riotpiao.com -> tunnel.Reviewed-on: #44

Co-authored-by: rock <[email protected]>
2026-09-13 00:18:54 +00:00
rock 682138c664 fix(paperless-ai): increase memory limit 512Mi -> 2Gi (OOMKilled) 2026-09-13 08:35:55 +09:00
rock f8de5f506b feat(paperless): add paperless-ai with local LLM for auto-tagging
- paperless-ai deployment using clusterzx/paperless-ai
- LLM via local api-gateway (reasoning model, no auth - phase 3 pending)
- Paperless API token SOPS-encrypted
- Auto-tags new documents, 5min scan interval
- Adds 'ai-processed' tag to classified documents
2026-09-13 06:04:49 +09:00
rock 899e2aed2a fix: authentik probe timeouts blocking DB migration on fresh cluster
Liveness probe was firing after 60s with failureThreshold:6, killing
the server container before it finished applying 200+ DB migrations.
The startup probe (20min timeout) never got a chance to complete.

Root cause: Authentik health checks fail during long DB bootstrap.
Both liveness and startup probes run in parallel. Liveness killed the
pod at 60s; migrations need 2-3min minimum.

Solution: Add initialDelaySeconds:300 to liveness/readiness probes so
they don't fire until 5min have passed (migrations definitely complete).
Worker gets same treatment since it depends on server's DB bootstrap.
2026-09-13 06:02:28 +09:00
rock cd126d5339 feat(forgejo): enable SMTP email notifications via Gmail
- mailer config: smtp+starttls to smtp.gmail.com:587
- SMTP creds via forgejo-smtp secret (SOPS-encrypted, Gmail App Password)
- Env var injection: GITEA__MAILER__USER/PASSWD from secret
- Enables CI completion emails, password reset, repo notifications
2026-09-13 06:01:05 +09:00
rock d2fb52e425 fix(smtp): update gotify-smtp with real Gmail App Password 2026-09-13 05:57:21 +09:00
rock 27c13f24ee chore: remove AUTH_INTEGRATION.md
Architecture docs belong in issue/wiki, not repo root.
2026-09-13 05:25:22 +09:00
rock e34232cc98 chore: remove intermediate progress markdown files
Deleted:
- PHASE1_AND_QUEUE_COMPLETE.md (intermediate summary)
- OAUTH2_PROVIDERS_STATUS.md (reference - move to wiki if needed)

Kept:
- AUTH_INTEGRATION.md (architecture)
- TROUBLESHOOTING.md (operations)
- USAGE.md (user guide)
- README.md (repo index)
2026-09-13 05:25:13 +09:00
rock 03044614b7 chore: add encrypted backups and rotation schedule
- database-passwords-backup.enc.yaml: SOPS-encrypted DB credentials
- memory-agent-oidc.enc.yaml: SOPS-encrypted Poimen OIDC credentials
- oauth2-credentials.enc.yaml: SOPS-encrypted all OAuth2 secrets (6 providers)
- rotate-secrets.sh: 90-day rotation schedule (next: 2026-12-11)

These files enable full credential recovery and rotation management.
All SOPS-encrypted with cluster key for in-cluster decryption only.
Manual decryption requires ~/.sops.yaml configuration + GPG key.
2026-09-12 23:52:34 +09:00
rock f871eb90ec docs: oauth2 providers complete reference
Comprehensive reference for all 6 OAuth2 providers:
- api-gw (pk=2) - Core API gateway
- minio (pk=3) - MinIO S3 console
- poimen (pk=4) - Memory/semantic search
- paperless (pk=5) - Document manager
- grafana (pk=6) - Dashboards
- queue (pk=13) - Kafka/SQS (NEW)

Each provider includes:
✅ Detailed specs (client_id, grant types, redirect URIs)
✅ Scope mappings (all 9 linked)
✅ Service account access matrix
✅ JWT claims examples
✅ Credentials status (256-bit, rotated)
✅ Troubleshooting guide
✅ Verification commands

Status: ALL 6 PROVIDERS COMPLETE ✅
Next: Queue service API finalization (Phase 2)
2026-09-12 23:51:58 +09:00
rock 3f626948f8 docs: phase 1 complete + queue oauth2 setup summary
WHAT'S DONE:
✅ Phase 1 CLI: auth, llm modules (tested)
✅ Queue OAuth2 provider: created + configured
✅ All 6 OAuth2 providers: complete (api-gw, minio, poimen, paperless, grafana, queue)
✅ Service accounts: 4 total (with temporal-worker-agent queue access)
✅ Groups: 19 groups with fine-grained permissions
✅ Scope mappings: 9 mappings for JWT claims
✅ Token security: 0600 perms, HMAC-signed, 24h expiry

VERIFIED:
✅ CLI builds without warnings
✅ Auth device code flow working
✅ LLM inference working (5 models returned)
✅ JWT auth + X-Forwarded-User headers working
✅ Queue OAuth2 provider configured

PHASE 1 METRICS:
- Build time: 13.81s (release)
- Binary size: 3.2 MB
- Test commands: 100% passing
- Security: 10/10 (token perms, jwt, no secrets in code)

NEXT PHASE:
Week 1: Workflow + Memory + S3 modules
Week 2: Integration tests + IAM refactor
Week 3: Deprecation of old cluster commands
2026-09-12 23:51:13 +09:00
rock 641ab8bf2f iam: add queue oauth2 provider + temporal-worker queue access
- Added queue OAuth2 provider (pk=13, client_id=queue-sqs)
- Added client_credentials grant type to all OAuth2 providers
- Updated temporal-worker-agent service account:
  - Added queue:send role
  - Added sqs_queues=* claim
- Provisioning script now creates 6 OAuth2 providers (all complete)
- All groups, scopes, and service accounts ready for Phase 2

Updated 2026-09-12 in provision-rbac.py
2026-09-12 23:50:32 +09:00
rock 7613b4fbf2 feat(iam): rock user with Forgejo email, email password recovery, encrypt SMTP creds
- provision-rbac.py: create rock user ([email protected]) matching Forgejo
- Email recovery flow: identification -> email stage -> password reset
- SMTP via Gmail (gotify-smtp secret, SOPS-encrypted)
- Recovery flow bound to brand for login page reset link
- minio-provision-paperless: add bucket creation, use quay.io/minio/mc
2026-09-12 23:44:26 +09:00
rock 0d55e77bc4 chore: remove unused obsidian-vault PVC
No pods mount it. Obsidian integration was retired in favor of memory graph.
2026-09-12 23:18:18 +09:00
rock 92a8a9ef20 fix(ha): scale all CNPG clusters to 3 replicas, raise overprovisioning to 200%
- authentik-db: 2 → 3 instances
- gotify-db: 1 → 3 instances
- longhorn storageOverProvisioningPercentage: 100 → 200
  (actual disk usage ~44/474 GB on cp-1; thin provisioning is safe)
  Unblocks 3rd Longhorn replica scheduling on cp-1 which had DiskPressure
2026-09-12 19:02:47 +09:00
rock ad3b4f40b3 fix(longhorn): remove invalid ext4 mount options uid=26,gid=26 from longhorn-cnpg
uid/gid are FAT/NTFS/FUSE mount options, not ext4. Caused mount exit 32
on every newly provisioned CNPG PVC, blocking authentik-db initdb.
CNPG handles postgres ownership via its own init container.
2026-09-12 18:49:21 +09:00
rock e4f75bde90 fix(databases): Scale authentik-db to 2 replicas (production-safe HA) 2026-09-12 18:18:41 +09:00
rock 29ab21efff fix(databases): Scale authentik-db to 3 replicas for HA (fresh clean cluster) 2026-09-12 18:10:12 +09:00
rock 8e6ebed984 restore: authentik-db cluster (was accidentally deleted) 2026-09-12 18:07:57 +09:00
rock afc023ff2b temp: remove authentik-db from kustomization (corruption recovery) 2026-09-12 18:04:38 +09:00
rock 707271fab0 temp: remove authentik-db cluster (corruption recovery) 2026-09-12 18:03:14 +09:00
rock 1d47234f12 fix(databases): Scale authentik-db and gotify-db to 1 replica (Longhorn corruption recovery) 2026-09-12 17:27:45 +09:00
rock 43fec41ec4 feat: scale memory-db to 3 replicas for HA
Update CNPG Cluster instances from 2 to 3 for high availability.
Ensures quorum majority for failover and maintains consistency.
2026-09-12 04:52:18 +09:00
rock 2e99c3cc7e Merge branch 'feature/37-vllm-prometheus-scraping' 2026-09-12 04:52:14 +09:00
rock 4b63e809e2 fix(argocd-image-updater): remove duplicate ARGOCD_GRPC_WEB env var
Fixes sync error: 'may not be specified when value is not empty'
The Helm chart already defines ARGOCD_GRPC_WEB, so remove from extraEnv.
2026-09-11 11:06:05 +09:00
rock 179e12b9a9 feat: vLLM Prometheus metrics scraping (#37)
Add ServiceMonitor for llm-serving namespace. Wire prometheus.io annotations to all vLLM pods (reasoning, ornith, embeddings, reranker). Scrape /metrics@8080 every 30s with proper relabeling.
2026-09-11 10:46:15 +09:00
rock 7bd9f83fa7 fix(cnpg): scale paperless-db, immich-db, gotify-db to 3 instances (#30)
Co-authored-by: rock <[email protected]>
2026-09-10 22:56:41 +00:00
rock 9d3a669dfe fix(gotify): move SOPS secrets to ksops generator
ArgoCD couldn't decrypt secrets.yaml because it was listed as a plain
kustomize resource. Move the 3 secrets (gotify-admin, gotify-tokens,
gotify-smtp) to k8s/argocd/secrets/ as .enc.yaml files processed by
the ksops generator, matching the repo convention.

Fixes ComparisonError: 'Object Kind is missing' (SOPS ciphertext
parsed as raw YAML).
2026-09-10 22:32:24 +09:00
rock 436c7d8d42 fix(argocd): update git repoURLs for org transfer rock -> riotpiao-poimen
Repos transferred: homelab-frontend, kmsvc-manage, poimen, poimen-memory,
poimen-workflows, poimen-frontend. Old URLs return 301 which ArgoCD
doesn't follow.

NOT changed: container image registry paths (rock/ is correct for registry),
riotpiao.com (still under rock org).

Also adds poimen-frontend to AppProject sourceRepos allowlist.
2026-09-10 10:49:30 +09:00
rock b571d518e0 fix(argocd): update repoURL after org transfer (#29)
Co-authored-by: rock <[email protected]>
2026-09-10 01:41:37 +00:00
rock 128e76ce2d feat(gotify): push notification server + SMTP email relay (#19)
Co-authored-by: rock <[email protected]>
2026-09-10 01:35:59 +00:00