feat(nextcloud-todos): Phase 4 IaC — service stack, Vault role, DB bootstrap, OpenClaw plugin, monitoring

Phase 4 infrastructure-as-code for the nextcloud-todos service (watches the
Nextcloud Personal task list; classifies todos via local qwen3-8b and routes
research/mutating work through claude-agent-service). Clones the
recruiter-responder service pattern end-to-end. Written only — NOT applied.

- stacks/nextcloud-todos/{main.tf,terragrunt.hcl}: new aux stack cloning
  recruiter-responder — ns (tier aux, istio-injection disabled, keel enrolled),
  two ExternalSecrets (vault-kv app secrets + vault-database DSN), Recreate
  deployment with alembic-migrate init-container, ClusterIP svc, /cb-only
  HMAC-gated ingress (auth=none, proxied), and an idempotent webhook-register
  null_resource (OCS webhook_listeners API, both CalendarObject Created/Updated
  events -> internal svc URL, Bearer auth).
- stacks/vault/main.tf: pg_nextcloud_todos static role (nextcloud_todos, 7d
  rotation) + pg-nextcloud-todos in the postgresql allowed_roles array.
- stacks/dbaas/modules/dbaas/main.tf: pg_nextcloud_todos_db null_resource
  (clone of pg_tripit_db) — creates role+DB, pins role search_path, and
  creates schema nextcloud_todos AUTHORIZATION nextcloud_todos.
- stacks/openclaw/main.tf: install-nextcloud-todos-plugin init-container,
  nextcloud-todos-api in plugins.allow + the doctor-fix re-add + plugins
  enable, NEXTCLOUD_TODOS_URL/NEXTCLOUD_TODOS_TOKEN env, and the cross-path
  ESO key (secret/nextcloud-todos.webhook_bearer_token).
- stacks/uptime-kuma/modules/uptime-kuma/main.tf: internal /healthz HTTP
  monitor. Prometheus /metrics scrape via svc annotations in the new stack.
- .gitleaksignore: allowlist two curl-auth-user false positives (the OCS
  webhook curl uses a Vault-sourced shell var, not a literal credential).

KV seed (secret/nextcloud-todos) + applies are deferred to the apply runbook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-04 03:58:07 +00:00
parent c3c3d5e010
commit c958f6a589
7 changed files with 571 additions and 2 deletions

View file

@ -58,6 +58,19 @@ resource "kubernetes_manifest" "external_secret" {
key = "openclaw"
}
}]
# Cross-path key: the nextcloud-todos-api plugin authenticates to the
# nextcloud-todos service with ITS bearer token, which lives in
# secret/nextcloud-todos (not secret/openclaw). Pull just that one key
# into openclaw-secrets so the plugin's NEXTCLOUD_TODOS_TOKEN env can
# secret_key_ref it (same model as the recruiter plugin, whose token
# happens to already live under secret/openclaw).
data = [{
secretKey = "nextcloud_todos_bearer_token"
remoteRef = {
key = "nextcloud-todos"
property = "webhook_bearer_token"
}
}]
}
}
depends_on = [kubernetes_namespace.openclaw]
@ -202,7 +215,7 @@ resource "kubernetes_config_map" "openclaw_config" {
}
}
plugins = {
allow = ["memory-core", "recruiter-api"]
allow = ["memory-core", "recruiter-api", "nextcloud-todos-api"]
slots = { memory = "memory-core" }
load = {
# /app/extensions is the legacy bundled-plugins path; OpenClaw
@ -517,6 +530,39 @@ resource "kubernetes_deployment" "openclaw" {
}
}
# Init 3b: install the nextcloud-todos-api OpenClaw plugin from the
# nextcloud-todos image into NFS extensions/. Plugin lifecycle is
# coupled to the nextcloud-todos image tag bumping that tag
# re-installs the plugin on next openclaw pod restart. Same pattern as
# install-recruiter-plugin above.
init_container {
name = "install-nextcloud-todos-plugin"
image = "forgejo.viktorbarzin.me/viktor/nextcloud-todos:latest"
command = ["sh", "-c", <<-EOT
set -eu
mkdir -p /home/node/.openclaw/extensions/nextcloud-todos-api
cp -r /app/openclaw-plugin/. /home/node/.openclaw/extensions/nextcloud-todos-api/
chown -R 1000:1000 /home/node/.openclaw/extensions/nextcloud-todos-api
echo "nextcloud-todos-api plugin installed at /home/node/.openclaw/extensions/nextcloud-todos-api"
ls -la /home/node/.openclaw/extensions/nextcloud-todos-api
EOT
]
# /home/node/.openclaw is uid 1000 on NFS; nextcloud-todos image
# otherwise drops to uid 10001 which can't write or chown. Run as
# root so mkdir + chown succeed.
security_context {
run_as_user = 0
}
volume_mount {
name = "openclaw-home"
mount_path = "/home/node/.openclaw"
}
resources {
requests = { cpu = "50m", memory = "64Mi" }
limits = { memory = "128Mi" }
}
}
# Init 4: install host-tools bundle (ssh, vault, jq, ripgrep, tmux, )
# into /tools/host-tools/ so the in-pod agent reaches CLI parity
# with the dev VM. Upstream OpenClaw image is minimal Debian
@ -1126,9 +1172,10 @@ resource "kubernetes_deployment" "openclaw" {
# doctor --fix overwrites plugins.allow with its bundled-plugins
# list. Re-add our third-party plugin to the allow list via
# `config patch`, then enable it. (Same pattern as mcp set above.)
echo '{"plugins":{"allow":["memory-core","recruiter-api","telegram","openrouter","brave","openai","codex"]}}' \
echo '{"plugins":{"allow":["memory-core","recruiter-api","nextcloud-todos-api","telegram","openrouter","brave","openai","codex"]}}' \
| node openclaw.mjs config patch --stdin 2>/dev/null || true
node openclaw.mjs plugins enable recruiter-api 2>/dev/null || true
node openclaw.mjs plugins enable nextcloud-todos-api 2>/dev/null || true
# Reindex memory-core so the seeded devvm-fallback note (and
# anything else dropped under /workspace/memory/) is searchable
# on first boot; daily memory-sync CronJob also keeps it indexed.
@ -1242,7 +1289,25 @@ resource "kubernetes_deployment" "openclaw" {
}
}
}
# nextcloud-todos API consumed by the nextcloud-todos-api plugin
# (mounted into /home/node/.openclaw/extensions/nextcloud-todos-api/
# via the install-nextcloud-todos-plugin init container above).
env {
name = "NEXTCLOUD_TODOS_URL"
value = "http://nextcloud-todos.nextcloud-todos.svc.cluster.local:8080"
}
env {
name = "NEXTCLOUD_TODOS_TOKEN"
value_from {
secret_key_ref {
name = "openclaw-secrets"
key = "nextcloud_todos_bearer_token"
optional = true
}
}
}
# Telegram chat ID for the recruiter-api plugin's announcement loop.
# Also consumed by the nextcloud-todos-api plugin (shared chat).
env {
name = "VIKTOR_CHAT_ID"
value_from {