From ae0b5f94928b05738974c775c562d2b178137ad4 Mon Sep 17 00:00:00 2001 From: chemavx Date: Mon, 6 Jul 2026 17:14:06 +0000 Subject: [PATCH] =?UTF-8?q?feat(reddit-intel):=20manifiestos=20preparados,?= =?UTF-8?q?=20INACTIVOS=20hasta=20promoci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App corre en docker compose en n97 (semana de calibración). El app-of-apps no es recursivo: nada de este directorio se sincroniza hasta aplicar a mano argocd-app.yaml. Checklist de activación en PROMOTION.md (imagen construida, secrets en Infisical /reddit-intel, copia de la BD al PVC). Co-Authored-By: Claude Fable 5 --- reddit-intel/PROMOTION.md | 61 +++++++++ reddit-intel/argocd-app.yaml | 26 ++++ reddit-intel/backup-cronjob.yaml | 80 +++++++++++ reddit-intel/deployment.yaml | 124 ++++++++++++++++++ reddit-intel/infisical-gitea-registry.yaml | 27 ++++ .../infisical-reddit-intel-secrets.yaml | 26 ++++ reddit-intel/service.yaml | 14 ++ 7 files changed, 358 insertions(+) create mode 100644 reddit-intel/PROMOTION.md create mode 100644 reddit-intel/argocd-app.yaml create mode 100644 reddit-intel/backup-cronjob.yaml create mode 100644 reddit-intel/deployment.yaml create mode 100644 reddit-intel/infisical-gitea-registry.yaml create mode 100644 reddit-intel/infisical-reddit-intel-secrets.yaml create mode 100644 reddit-intel/service.yaml diff --git a/reddit-intel/PROMOTION.md b/reddit-intel/PROMOTION.md new file mode 100644 index 0000000..a20a75f --- /dev/null +++ b/reddit-intel/PROMOTION.md @@ -0,0 +1,61 @@ +# reddit-intel — promoción de docker compose (n97) a k3s + +Estado: **INACTIVO**. Los manifiestos de este directorio existen solo en git; +el app-of-apps `k8s-manifests` no es recursivo, así que ArgoCD no los +sincroniza. La app corre en docker compose en chemavx-n97 +(`/opt/reddit-intel`, semana de calibración desde 2026-07-06). + +## Checklist de activación (en este orden) + +1. **Imagen en el registro.** Primer build manual, o `workflow_dispatch` del + workflow "Build reddit-intel image" en el repo + `git.chemavx.xyz/chemavx/reddit-intel` (requiere secrets `CI_TOKEN` y + `TELEGRAM_BOT_TOKEN` configurados en ese repo). El workflow actualiza el + tag de `deployment.yaml` aquí; si el build es manual, sustituir el + placeholder `:00000000` a mano por el sha8 real. + +2. **Secrets en Infisical** (proyecto "homelab", env `prod`, path + `/reddit-intel`) — mismos valores que el `.env` del compose: + `reddit-client-id`, `reddit-client-secret`, `llm-api-key`, + `dashboard-auth-token`, `telegram-bot-token`, `telegram-chat-id` + (los dos de telegram pueden ir vacíos). + +3. **Congelar el compose y copiar la BD al PVC:** + ```bash + # en n97 + cd /opt/reddit-intel && docker compose down # para la app (checkpoint WAL limpio) + /opt/reddit-intel/scripts/backup.sh # backup final pre-migración + + # activar la app (crea PVCs y arranca el pod con BD vacía) + kubectl apply -f reddit-intel/argocd-app.yaml + kubectl -n reddit-intel rollout status deploy/reddit-intel + + # parar el pod y volcar la BD del host al PVC + kubectl -n reddit-intel scale deploy/reddit-intel --replicas=0 + kubectl -n reddit-intel run pvc-loader --image=busybox --restart=Never \ + --overrides='{"spec":{"nodeSelector":{"kubernetes.io/hostname":"chemavx-n97"},"containers":[{"name":"pvc-loader","image":"busybox","command":["sleep","600"],"volumeMounts":[{"name":"data","mountPath":"/data"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"reddit-intel-data"}}]}}' + kubectl -n reddit-intel wait --for=condition=Ready pod/pvc-loader + kubectl -n reddit-intel cp /opt/reddit-intel/data/reddit-intel.db pvc-loader:/data/reddit-intel.db + kubectl -n reddit-intel delete pod pvc-loader + kubectl -n reddit-intel scale deploy/reddit-intel --replicas=1 + ``` + (El `docker compose down` previo garantiza que no hay `-wal`/`-shm` + pendientes: basta copiar el `.db`.) + +4. **Verificar:** `kubectl -n reddit-intel logs deploy/reddit-intel` debe + mostrar `scheduler_start jobs=monitor,detector,analyzer,karma,learning`; + el dashboard responde vía `svc/reddit-intel` (port-forward o el ingress + que se decida — pendiente de elegir exposición). + +5. **Desmantelar la fase compose:** quitar la línea del crontab de chemavx en + n97 (el CronJob `reddit-intel-db-backup` de aquí la reemplaza) y archivar + `/opt/reddit-intel` cuando la semana de convivencia confirme que todo va bien. + +## Notas + +- El CronJob de backup (`backup-cronjob.yaml`) replica el patrón researchowl: + `sqlite3 .backup` + `integrity_check` + retención 7, contra el PVC. +- Un solo replica siempre (`strategy: Recreate`): SQLite WAL + APScheduler + in-process no admiten dos pods. +- El namespace `reddit-intel` ya existe en el clúster (creado durante la + preparación); `CreateNamespace=true` lo cubre igualmente. diff --git a/reddit-intel/argocd-app.yaml b/reddit-intel/argocd-app.yaml new file mode 100644 index 0000000..5311494 --- /dev/null +++ b/reddit-intel/argocd-app.yaml @@ -0,0 +1,26 @@ +# INACTIVO hasta la promoción: el app-of-apps k8s-manifests NO es recursivo, +# así que este fichero solo existe en git — nadie lo aplica automáticamente. +# La activación es manual: ver PROMOTION.md en este directorio. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: reddit-intel + namespace: argocd + annotations: + notifications.argoproj.io/subscribe.on-sync-failed.telegram: "5138407666" + notifications.argoproj.io/subscribe.on-health-degraded.telegram: "5138407666" +spec: + project: default + source: + repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests + targetRevision: HEAD + path: reddit-intel + destination: + server: https://kubernetes.default.svc + namespace: reddit-intel + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/reddit-intel/backup-cronjob.yaml b/reddit-intel/backup-cronjob.yaml new file mode 100644 index 0000000..08e197d --- /dev/null +++ b/reddit-intel/backup-cronjob.yaml @@ -0,0 +1,80 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + # Never let ArgoCD prune this PVC: it holds the DB backups. + argocd.argoproj.io/sync-options: Prune=false + name: reddit-intel-backups + namespace: reddit-intel +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: reddit-intel-db-backup + namespace: reddit-intel + labels: + app: reddit-intel +spec: + schedule: "0 3 * * *" + timeZone: "Europe/Madrid" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + activeDeadlineSeconds: 600 + template: + metadata: + labels: + app: reddit-intel-db-backup + spec: + restartPolicy: Never + containers: + - name: backup + image: keinos/sqlite3:latest + command: + - /bin/sh + - -c + - | + set -eu + STAMP=$(date +%Y%m%d) + DEST="/backups/reddit-intel-${STAMP}.db" + echo "Backing up /data/reddit-intel.db -> ${DEST}" + sqlite3 -cmd ".timeout 30000" /data/reddit-intel.db ".backup '${DEST}'" + RESULT=$(sqlite3 "${DEST}" "PRAGMA integrity_check;") + if [ "${RESULT}" != "ok" ]; then + echo "Integrity check FAILED: ${RESULT}" + rm -f "${DEST}" + exit 1 + fi + echo "Integrity check ok" + # Retention: keep the 7 most recent backups + for f in $(ls -1t /backups/reddit-intel-*.db | tail -n +8); do + echo "Pruning old backup ${f}" + rm -f "${f}" + done + ls -lh /backups/ + volumeMounts: + # NOT readOnly: the source DB runs in WAL mode and sqlite readers + # must be able to touch the -shm/-wal sidecar files. The job only + # ever reads the DB (.backup); it never modifies application data. + - name: data + mountPath: /data + - name: backups + mountPath: /backups + volumes: + - name: data + persistentVolumeClaim: + claimName: reddit-intel-data + - name: backups + persistentVolumeClaim: + claimName: reddit-intel-backups diff --git a/reddit-intel/deployment.yaml b/reddit-intel/deployment.yaml new file mode 100644 index 0000000..a7fb7d1 --- /dev/null +++ b/reddit-intel/deployment.yaml @@ -0,0 +1,124 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: reddit-intel + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + # Never let ArgoCD prune this PVC: it holds the SQLite DB. + argocd.argoproj.io/sync-options: Prune=false + name: reddit-intel-data + namespace: reddit-intel +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: reddit-intel + namespace: reddit-intel + labels: + app: reddit-intel +spec: + replicas: 1 + # Single-replica app on a RWO local-path SQLite DB (WAL) with an in-process + # APScheduler: Recreate avoids two pods briefly sharing /data/reddit-intel.db. + strategy: + type: Recreate + selector: + matchLabels: + app: reddit-intel + template: + metadata: + labels: + app: reddit-intel + spec: + containers: + - name: reddit-intel + # Placeholder tag: el workflow manual (repo reddit-intel) lo sustituye + # por el sha8 real en cada build. Ver PROMOTION.md. + image: git.chemavx.xyz/chemavx/reddit-intel:00000000 + imagePullPolicy: Always + ports: + - containerPort: 8080 + env: + - name: DB_PATH + value: "/data/reddit-intel.db" + - name: REDDIT_USERNAME + value: "ExclusionZoneUAP" + - name: REDDIT_USER_AGENT + value: "linux:reddit-intel:v1.0 (by /u/ExclusionZoneUAP)" + - name: LLM_PROVIDER + value: "anthropic" + - name: LLM_MODEL_SCORING + value: "claude-haiku-4-5" + - name: LLM_MODEL_DRAFTING + value: "claude-sonnet-5" + - name: DAILY_LLM_BUDGET + value: "60" + - name: REDDIT_CLIENT_ID + valueFrom: + secretKeyRef: + name: reddit-intel-secrets + key: reddit-client-id + - name: REDDIT_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: reddit-intel-secrets + key: reddit-client-secret + - name: LLM_API_KEY + valueFrom: + secretKeyRef: + name: reddit-intel-secrets + key: llm-api-key + - name: DASHBOARD_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: reddit-intel-secrets + key: dashboard-auth-token + - name: TELEGRAM_BOT_TOKEN + valueFrom: + secretKeyRef: + name: reddit-intel-secrets + key: telegram-bot-token + - name: TELEGRAM_CHAT_ID + valueFrom: + secretKeyRef: + name: reddit-intel-secrets + key: telegram-chat-id + volumeMounts: + - name: data + mountPath: /data + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 20 + periodSeconds: 60 + resources: + requests: + memory: "128Mi" + cpu: "50m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: data + persistentVolumeClaim: + claimName: reddit-intel-data + imagePullSecrets: + - name: gitea-registry diff --git a/reddit-intel/infisical-gitea-registry.yaml b/reddit-intel/infisical-gitea-registry.yaml new file mode 100644 index 0000000..b85ec59 --- /dev/null +++ b/reddit-intel/infisical-gitea-registry.yaml @@ -0,0 +1,27 @@ +apiVersion: secrets.infisical.com/v1beta1 +kind: InfisicalStaticSecret +metadata: + name: gitea-registry + namespace: reddit-intel +spec: + # References the SHARED auth layer in infisical-operator (cross-namespace). + infisicalAuthRef: + name: infisical-auth + namespace: infisical-operator + sources: + - projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab" + environmentSlug: prod + secretPath: /registry + syncOptions: + refreshInterval: 60s + targets: + - kind: Secret + name: gitea-registry + namespace: reddit-intel + creationPolicy: Owner + secretType: kubernetes.io/dockerconfigjson + template: + engineVersion: v1 + data: + # Reconstruct the docker pull-secret blob from the two discrete /registry fields. + .dockerconfigjson: '{"auths":{"git.chemavx.xyz":{"username":"{{ .GITEA_REGISTRY_USERNAME.Value }}","password":"{{ .GITEA_REGISTRY_TOKEN.Value }}","auth":"{{ printf "%s:%s" .GITEA_REGISTRY_USERNAME.Value .GITEA_REGISTRY_TOKEN.Value | encodeBase64 }}"}}}' diff --git a/reddit-intel/infisical-reddit-intel-secrets.yaml b/reddit-intel/infisical-reddit-intel-secrets.yaml new file mode 100644 index 0000000..6abe2a2 --- /dev/null +++ b/reddit-intel/infisical-reddit-intel-secrets.yaml @@ -0,0 +1,26 @@ +apiVersion: secrets.infisical.com/v1beta1 +kind: InfisicalStaticSecret +metadata: + name: reddit-intel-secrets + namespace: reddit-intel +spec: + # Shared, read-only machine identity (no write grant, no PushSecret). + # Source is the /reddit-intel folder. Expected keys (lowercase-hyphenated, + # straight passthrough to the deployment's secretKeyRef values): + # reddit-client-id, reddit-client-secret, llm-api-key, + # dashboard-auth-token, telegram-bot-token, telegram-chat-id + # telegram-* pueden crearse vacíos (las notificaciones son opcionales). + infisicalAuthRef: + name: infisical-auth + namespace: infisical-operator + sources: + - projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab" + environmentSlug: prod + secretPath: /reddit-intel + syncOptions: + refreshInterval: 60s + targets: + - kind: Secret + name: reddit-intel-secrets + namespace: reddit-intel + creationPolicy: Owner diff --git a/reddit-intel/service.yaml b/reddit-intel/service.yaml new file mode 100644 index 0000000..6abca2f --- /dev/null +++ b/reddit-intel/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: reddit-intel + namespace: reddit-intel + labels: + app: reddit-intel +spec: + selector: + app: reddit-intel + ports: + - name: http + port: 80 + targetPort: 8080