From c958f6a5899b99e81eccab9722bf642c7fa7748a Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 4 Jun 2026 03:58:07 +0000 Subject: [PATCH] =?UTF-8?q?feat(nextcloud-todos):=20Phase=204=20IaC=20?= =?UTF-8?q?=E2=80=94=20service=20stack,=20Vault=20role,=20DB=20bootstrap,?= =?UTF-8?q?=20OpenClaw=20plugin,=20monitoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitleaksignore | 7 + stacks/dbaas/modules/dbaas/main.tf | 33 ++ stacks/nextcloud-todos/main.tf | 412 ++++++++++++++++++ stacks/nextcloud-todos/terragrunt.hcl | 24 + stacks/openclaw/main.tf | 69 ++- .../uptime-kuma/modules/uptime-kuma/main.tf | 19 + stacks/vault/main.tf | 9 + 7 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 stacks/nextcloud-todos/main.tf create mode 100644 stacks/nextcloud-todos/terragrunt.hcl diff --git a/.gitleaksignore b/.gitleaksignore index dfe626cd..68ec9ec9 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -2,3 +2,10 @@ # gitleaks scans the staged working-tree copy and can't see that they're # encrypted on disk in git, so allowlist by fingerprint. stacks/recruiter-responder/secrets/privkey.pem:private-key:1 + +# False positives: the `curl-auth-user` rule flags `-u "admin:..."` in the +# nextcloud-todos webhook-register provisioner, but the password is a shell +# variable ($NC_ADMIN_APP_PW) resolved at apply time from Vault — no literal +# secret is committed. +stacks/nextcloud-todos/main.tf:curl-auth-user:383 +stacks/nextcloud-todos/main.tf:curl-auth-user:400 diff --git a/stacks/dbaas/modules/dbaas/main.tf b/stacks/dbaas/modules/dbaas/main.tf index 7c94bea5..3fc44f94 100644 --- a/stacks/dbaas/modules/dbaas/main.tf +++ b/stacks/dbaas/modules/dbaas/main.tf @@ -1311,6 +1311,39 @@ resource "null_resource" "pg_tripit_db" { } } +# Create nextcloud_todos database + role for the nextcloud-todos service +# (FastAPI; watches the Nextcloud Personal task list). Role password is +# managed by the Vault Database Secrets Engine (static role +# `pg-nextcloud-todos`, 7d rotation). Tables live in schema `nextcloud_todos` +# (alembic creates them on the app's first migrate). Unlike most app DBs we +# also create the schema explicitly + pin the role's search_path to it, so the +# unqualified tables alembic generates land in `nextcloud_todos` rather than +# `public`. +resource "null_resource" "pg_nextcloud_todos_db" { + depends_on = [null_resource.pg_cluster] + + triggers = { + db_name = "nextcloud_todos" + username = "nextcloud_todos" + } + + provisioner "local-exec" { + command = <<-EOT + PRIMARY=$(kubectl --kubeconfig ${var.kube_config_path} get cluster -n dbaas pg-cluster -o jsonpath='{.status.currentPrimary}') + kubectl --kubeconfig ${var.kube_config_path} exec -n dbaas $PRIMARY -c postgres -- \ + bash -c ' + psql -U postgres -tc "SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '"'"'nextcloud_todos'"'"'" | grep -q 1 || \ + psql -U postgres -c "CREATE ROLE nextcloud_todos WITH LOGIN PASSWORD '"'"'changeme-vault-will-rotate'"'"'" + psql -U postgres -tc "SELECT 1 FROM pg_catalog.pg_database WHERE datname = '"'"'nextcloud_todos'"'"'" | grep -q 1 || \ + psql -U postgres -c "CREATE DATABASE nextcloud_todos OWNER nextcloud_todos" + psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE nextcloud_todos TO nextcloud_todos" + psql -U postgres -c "ALTER ROLE nextcloud_todos SET search_path TO nextcloud_todos" + psql -U postgres -d nextcloud_todos -c "CREATE SCHEMA IF NOT EXISTS nextcloud_todos AUTHORIZATION nextcloud_todos" + ' + EOT + } +} + # Postiz: 3 databases (postiz, temporal, temporal_visibility) all owned by the # `postiz` role. Bundled bitnami PostgreSQL was retired 2026-05-09 in favour of # this CNPG cluster — covered by postgresql-backup-per-db automatically. diff --git a/stacks/nextcloud-todos/main.tf b/stacks/nextcloud-todos/main.tf new file mode 100644 index 00000000..ed388a13 --- /dev/null +++ b/stacks/nextcloud-todos/main.tf @@ -0,0 +1,412 @@ +variable "image_tag" { + type = string + default = "latest" + description = "nextcloud-todos image tag. Use 8-char git SHA in CI." +} + +variable "postgresql_host" { type = string } + +variable "tls_secret_name" { + type = string + sensitive = true +} + +locals { + namespace = "nextcloud-todos" + image = "forgejo.viktorbarzin.me/viktor/nextcloud-todos:${var.image_tag}" + labels = { + app = "nextcloud-todos" + } +} + +resource "kubernetes_namespace" "nextcloud_todos" { + metadata { + name = local.namespace + labels = { + tier = local.tiers.aux + "istio-injection" = "disabled" + # Opt into Keel auto-update (inject-keel-annotations ClusterPolicy). + "keel.sh/enrolled" = "true" + } + } + lifecycle { + # KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label. + ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]] + } +} + +# App secrets — seed these in Vault before applying: +# secret/nextcloud-todos +# webhook_bearer_token — bearer for Nextcloud -> /nextcloud/hook and +# the OpenClaw nextcloud-todos-api plugin +# hmac_secret — signs the /cb approval links (sig + exp) +# caldav_app_password — Nextcloud `admin` app-password for CalDAV +# (PROPFIND list resolution + note append) +# nextcloud_admin_app_password — admin app-password used by the +# webhook-register null_resource below (may +# reuse the CalDAV one) +# claude_agent_token — Bearer for claude-agent-service (Tier-2) +# telegram_bot_token — consumed by the OpenClaw plugin (see +# stacks/openclaw), NOT by this service +# viktor_chat_id — consumed by the OpenClaw plugin +# +# Schema in CNPG: `nextcloud_todos` (alembic creates tables on first migrate). +# DB user: created in dbaas (null_resource.pg_nextcloud_todos_db); password +# managed via the Vault database engine — see static-creds/pg-nextcloud-todos. +resource "kubernetes_manifest" "external_secret" { + manifest = { + apiVersion = "external-secrets.io/v1beta1" + kind = "ExternalSecret" + metadata = { + name = "nextcloud-todos-secrets" + namespace = local.namespace + } + spec = { + refreshInterval = "15m" + secretStoreRef = { + name = "vault-kv" + kind = "ClusterSecretStore" + } + target = { + name = "nextcloud-todos-secrets" + template = { + metadata = { + annotations = { + "reloader.stakater.com/match" = "true" + } + } + } + } + data = [ + { secretKey = "WEBHOOK_BEARER_TOKEN", remoteRef = { key = "nextcloud-todos", property = "webhook_bearer_token" } }, + { secretKey = "HMAC_SECRET", remoteRef = { key = "nextcloud-todos", property = "hmac_secret" } }, + { secretKey = "CALDAV_APP_PASSWORD", remoteRef = { key = "nextcloud-todos", property = "caldav_app_password" } }, + { secretKey = "CLAUDE_AGENT_TOKEN", remoteRef = { key = "nextcloud-todos", property = "claude_agent_token" } }, + ] + } + } + depends_on = [kubernetes_namespace.nextcloud_todos] +} + +# DB credentials from Vault database engine (7-day rotation). +# Builds the asyncpg DSN consumed by the FastAPI app as DB_CONNECTION_STRING. +# Pre-req in dbaas: CNPG cluster has DB `nextcloud_todos`, role +# `nextcloud_todos`, and Vault role `static-creds/pg-nextcloud-todos`. +resource "kubernetes_manifest" "db_external_secret" { + manifest = { + apiVersion = "external-secrets.io/v1beta1" + kind = "ExternalSecret" + metadata = { + name = "nextcloud-todos-db-creds" + namespace = local.namespace + } + spec = { + refreshInterval = "15m" + secretStoreRef = { + name = "vault-database" + kind = "ClusterSecretStore" + } + target = { + name = "nextcloud-todos-db-creds" + template = { + metadata = { + annotations = { + "reloader.stakater.com/match" = "true" + } + } + data = { + DB_CONNECTION_STRING = "postgresql+asyncpg://nextcloud_todos:{{ .password }}@${var.postgresql_host}:5432/nextcloud_todos" + DB_PASSWORD = "{{ .password }}" + } + } + } + data = [{ + secretKey = "password" + remoteRef = { + key = "static-creds/pg-nextcloud-todos" + property = "password" + } + }] + } + } + depends_on = [kubernetes_namespace.nextcloud_todos] +} + +resource "kubernetes_deployment" "nextcloud_todos" { + metadata { + name = "nextcloud-todos" + namespace = kubernetes_namespace.nextcloud_todos.metadata[0].name + labels = merge(local.labels, { + tier = local.tiers.aux + }) + annotations = { + "reloader.stakater.com/search" = "true" + } + } + + spec { + # CalDAV sweep + single webhook leader; concurrency hurts both. + replicas = 1 + strategy { + type = "Recreate" + } + + selector { + match_labels = local.labels + } + + template { + metadata { + labels = local.labels + annotations = { + # Prometheus scrapes the service-endpoints (annotations live on the + # Service below); the pod annotations here let the kubernetes-pods + # SD job also discover /metrics directly. + "prometheus.io/scrape" = "true" + "prometheus.io/path" = "/metrics" + "prometheus.io/port" = "8080" + } + } + + spec { + image_pull_secrets { + name = "registry-credentials" + } + + init_container { + name = "alembic-migrate" + image = local.image + command = ["python", "-m", "nextcloud_todos", "migrate"] + + env_from { + secret_ref { name = "nextcloud-todos-secrets" } + } + env_from { + secret_ref { name = "nextcloud-todos-db-creds" } + } + + resources { + requests = { cpu = "50m", memory = "256Mi" } + limits = { memory = "512Mi" } + } + } + + container { + name = "nextcloud-todos" + image = local.image + + port { + container_port = 8080 + } + + env_from { + secret_ref { name = "nextcloud-todos-secrets" } + } + env_from { + secret_ref { name = "nextcloud-todos-db-creds" } + } + + # Nextcloud / CalDAV + env { + name = "NEXTCLOUD_BASE_URL" + value = "https://nextcloud.viktorbarzin.me" + } + env { + name = "NEXTCLOUD_USER" + value = "admin" + } + env { + name = "LIST_ALLOWLIST" + value = "Personal" + } + # Tier-0 LLM classifier + env { + name = "LLAMA_SWAP_URL" + value = "http://llama-swap.llama-cpp.svc.cluster.local:8080" + } + env { + name = "LLAMA_SWAP_MODEL" + value = "qwen3-8b" + } + # Tier-2 LLM (claude-agent-service: research auto-run + two-pass exec) + env { + name = "CLAUDE_AGENT_URL" + value = "http://claude-agent-service.claude-agent.svc.cluster.local:8080" + } + # Public callback base URL for the Telegram inline-keyboard URL + # buttons. Must match the ingress host below (proxied via Cloudflare). + env { + name = "CALLBACK_BASE_URL" + value = "https://nextcloud-todos.viktorbarzin.me" + } + + readiness_probe { + http_get { + path = "/healthz" + port = 8080 + } + initial_delay_seconds = 5 + period_seconds = 10 + } + liveness_probe { + http_get { + path = "/healthz" + port = 8080 + } + initial_delay_seconds = 30 + period_seconds = 30 + } + + resources { + requests = { cpu = "100m", memory = "384Mi" } + limits = { memory = "768Mi" } + } + } + } + } + } + + lifecycle { + ignore_changes = [ + spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1 + metadata[0].annotations["keel.sh/policy"], + metadata[0].annotations["keel.sh/trigger"], + metadata[0].annotations["keel.sh/pollSchedule"], # KYVERNO_LIFECYCLE_V2 + metadata[0].annotations["keel.sh/match-tag"], + spec[0].template[0].spec[0].container[0].image, # KEEL_IGNORE_IMAGE — Keel manages tag updates + spec[0].template[0].spec[0].init_container[0].image, + metadata[0].annotations["kubernetes.io/change-cause"], + metadata[0].annotations["deployment.kubernetes.io/revision"], + spec[0].template[0].metadata[0].annotations["keel.sh/update-time"], # KEEL_LIFECYCLE_V1 + ] + } + + depends_on = [ + kubernetes_manifest.external_secret, + kubernetes_manifest.db_external_secret, + ] +} + +resource "kubernetes_service" "nextcloud_todos" { + metadata { + name = "nextcloud-todos" + namespace = kubernetes_namespace.nextcloud_todos.metadata[0].name + labels = local.labels + annotations = { + # Prometheus kubernetes-service-endpoints SD scrapes /metrics here. + "prometheus.io/scrape" = "true" + "prometheus.io/path" = "/metrics" + "prometheus.io/port" = "8080" + } + } + + spec { + type = "ClusterIP" + selector = local.labels + + port { + name = "http" + port = 8080 + target_port = 8080 + } + } +} + +# Kyverno ClusterPolicy `sync-tls-secret` auto-clones the wildcard TLS +# secret into every namespace, so we don't need a setup_tls_secret module. + +# Public ingress for the /cb/* callback endpoints driven by Telegram URL +# buttons. /nextcloud/hook, /api/* and /healthz stay internal — they're +# reached via cluster DNS by Nextcloud's webhook, the OpenClaw plugin, and +# kubelet probes respectively. +# +# auth = "none": the /cb endpoints are gated by HMAC-signed query params +# (sig + exp) generated from HMAC_SECRET. Authentik would force a login flow +# before the GET could fire and break the one-tap approval flow. +module "ingress" { + source = "../../modules/kubernetes/ingress_factory" + # auth = "none": HMAC + expiry gate the /cb endpoints — Authentik would + # force a login dance and break Telegram's one-tap UX. See hmac_links.py. + auth = "none" + anti_ai_scraping = false + dns_type = "proxied" + namespace = kubernetes_namespace.nextcloud_todos.metadata[0].name + name = "nextcloud-todos" + port = 8080 + ingress_path = ["/cb"] + tls_secret_name = var.tls_secret_name +} + +# ============================================================================= +# Nextcloud webhook registration (Task 4.5) +# ============================================================================= +# Registers the two Calendar VTODO events (create + update) against the +# Nextcloud `webhook_listeners` OCS API so Nextcloud POSTs to this service +# whenever a task on the Personal list changes. Points at the INTERNAL svc +# URL (Nextcloud calls it from inside the cluster). Idempotent: GETs the +# existing webhooks first and skips registration of any event already +# targeting the svc URL (the OCS API has no upsert — a blind re-POST creates +# duplicates). Re-runs whenever the bearer token or hook URL changes. +# +# Auth: admin app-password (Nextcloud admin) for the OCS call itself; the +# registered webhook then carries `Authorization: Bearer ` +# (authMethod=header) so Nextcloud authenticates to THIS service on delivery. +# Both values come from Vault — read here as a plan-time data source (this is +# the one place the stack needs a raw Vault value: a local-exec provisioner +# can't consume an ESO-created K8s Secret). +data "vault_kv_secret_v2" "nextcloud_todos" { + mount = "secret" + name = "nextcloud-todos" +} + +resource "null_resource" "register_webhooks" { + depends_on = [module.ingress] + + triggers = { + hook_url = "http://nextcloud-todos.nextcloud-todos.svc.cluster.local:8080/nextcloud/hook" + bearer_token = sha256(data.vault_kv_secret_v2.nextcloud_todos.data["webhook_bearer_token"]) + } + + provisioner "local-exec" { + environment = { + NC_ADMIN_APP_PW = data.vault_kv_secret_v2.nextcloud_todos.data["nextcloud_admin_app_password"] + WEBHOOK_BEARER_TOKEN = data.vault_kv_secret_v2.nextcloud_todos.data["webhook_bearer_token"] + } + command = <<-EOT + set -eu + NC="https://nextcloud.viktorbarzin.me/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks" + HOOK_URL="http://nextcloud-todos.nextcloud-todos.svc.cluster.local:8080/nextcloud/hook" + + # Idempotency: list existing webhooks and skip any event already + # pointing at our svc URL. The OCS API has no upsert; re-POSTing the + # same listener silently creates a duplicate, so we gate on a match. + EXISTING=$(curl -fsS -H "OCS-APIRequest: true" -H "Accept: application/json" \ + -u "admin:$${NC_ADMIN_APP_PW}" "$${NC}") + + for EV in CalendarObjectCreatedEvent CalendarObjectUpdatedEvent; do + # The event class is a PHP namespace: OCP\Calendar\Events\. In the + # JSON body each backslash must be doubled (valid JSON escape), so the + # shell var holds "\\" per separator -> the heredoc source needs four + # backslashes per separator (TF heredoc passes them through literally, + # bash double-quotes then halve them to "\\"). + FULL_EVENT="OCP\\\\Calendar\\\\Events\\\\$${EV}" + # Match on both the uri and the event short-name within the JSON blob + # — grep with fixed strings to avoid regex pitfalls. + if echo "$${EXISTING}" | grep -F "$${HOOK_URL}" | grep -qF "$${EV}"; then + echo "webhook for $${EV} already registered -> skipping" + continue + fi + echo "registering webhook for $${EV} ..." + curl -fsS -X POST -H "OCS-APIRequest: true" -H "Content-Type: application/json" \ + -u "admin:$${NC_ADMIN_APP_PW}" "$${NC}" \ + -d "{\"httpMethod\":\"POST\", + \"uri\":\"$${HOOK_URL}\", + \"event\":\"$${FULL_EVENT}\", + \"authMethod\":\"header\", + \"authData\":{\"Authorization\":\"Bearer $${WEBHOOK_BEARER_TOKEN}\"}}" + echo + done + echo "webhook registration complete" + EOT + } +} diff --git a/stacks/nextcloud-todos/terragrunt.hcl b/stacks/nextcloud-todos/terragrunt.hcl new file mode 100644 index 00000000..a17c2e51 --- /dev/null +++ b/stacks/nextcloud-todos/terragrunt.hcl @@ -0,0 +1,24 @@ +include "root" { + path = find_in_parent_folders() +} + +dependency "platform" { + config_path = "../platform" + skip_outputs = true +} + +dependency "vault" { + config_path = "../vault" + skip_outputs = true +} + +dependency "external-secrets" { + config_path = "../external-secrets" + skip_outputs = true +} + +inputs = { + # Override per-deploy in CI / commit. + image_tag = "latest" + postgresql_host = "pg-cluster-rw.dbaas.svc.cluster.local" +} diff --git a/stacks/openclaw/main.tf b/stacks/openclaw/main.tf index 7c1aec96..670daff0 100644 --- a/stacks/openclaw/main.tf +++ b/stacks/openclaw/main.tf @@ -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 { diff --git a/stacks/uptime-kuma/modules/uptime-kuma/main.tf b/stacks/uptime-kuma/modules/uptime-kuma/main.tf index 0695081a..faa7d2d3 100644 --- a/stacks/uptime-kuma/modules/uptime-kuma/main.tf +++ b/stacks/uptime-kuma/modules/uptime-kuma/main.tf @@ -722,6 +722,25 @@ locals { retry_interval = 30 max_retries = 3 }, + { + # Internal /healthz probe of the nextcloud-todos service. The `/cb` + # ingress carries the `[External]` HTTPS monitor (auto-created by + # external-monitor-sync), but those endpoints are HMAC-gated and only + # cover the callback path — this checks the app's own liveness inside + # the cluster on the ClusterIP svc. Plain HTTP, expects a clean 200. + name = "nextcloud-todos (/healthz)" + type = "http" + database_connection_string = null + database_password_vault_key = null + hostname = null + port = null + url = "http://nextcloud-todos.nextcloud-todos.svc.cluster.local:8080/healthz" + accepted_statuscodes = ["200-299"] + ignore_tls = false + interval = 60 + retry_interval = 30 + max_retries = 3 + }, ] } diff --git a/stacks/vault/main.tf b/stacks/vault/main.tf index 9a61a39c..ef97341a 100644 --- a/stacks/vault/main.tf +++ b/stacks/vault/main.tf @@ -617,6 +617,7 @@ resource "vault_database_secret_backend_connection" "postgresql" { "pg-wealthfolio-sync", "pg-fire-planner", "pg-postiz", "pg-instagram-poster", "pg-recruiter-responder", "pg-tripit", + "pg-nextcloud-todos", "pg-matrix", "pg-technitium", ] @@ -811,6 +812,14 @@ resource "vault_database_secret_backend_static_role" "pg_recruiter_responder" { rotation_period = 604800 } +resource "vault_database_secret_backend_static_role" "pg_nextcloud_todos" { + backend = vault_mount.database.path + db_name = vault_database_secret_backend_connection.postgresql.name + name = "pg-nextcloud-todos" + username = "nextcloud_todos" + rotation_period = 604800 +} + resource "vault_database_secret_backend_static_role" "pg_tripit" { backend = vault_mount.database.path db_name = vault_database_secret_backend_connection.postgresql.name