From 6416c6c3af6b529606cb3b006e72ea5a568bf0d9 Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Mon, 13 Jul 2026 16:29:19 +0000 Subject: [PATCH] =?UTF-8?q?feat(roswell):=20despliegue=20GitOps=20del=20co?= =?UTF-8?q?rpus=20=E2=80=94=20bot+scheduler,=20backup=20pg=5Fdump=20diario?= =?UTF-8?q?,=20postgres=20adoptado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- roswell/argocd-app.yaml | 22 +++++ roswell/backup-cronjob.yaml | 78 +++++++++++++++++ roswell/deployment.yaml | 117 ++++++++++++++++++++++++++ roswell/infisical-gitea-registry.yaml | 27 ++++++ roswell/postgres.yaml | 90 ++++++++++++++++++++ 5 files changed, 334 insertions(+) create mode 100644 roswell/argocd-app.yaml create mode 100644 roswell/backup-cronjob.yaml create mode 100644 roswell/deployment.yaml create mode 100644 roswell/infisical-gitea-registry.yaml create mode 100644 roswell/postgres.yaml diff --git a/roswell/argocd-app.yaml b/roswell/argocd-app.yaml new file mode 100644 index 0000000..d99ee50 --- /dev/null +++ b/roswell/argocd-app.yaml @@ -0,0 +1,22 @@ +# Se aplica una vez imperativamente (kubectl apply -f); después ArgoCD +# se autogestiona porque este fichero también vive en k8s-manifests/roswell/. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: roswell + namespace: argocd +spec: + project: default + source: + repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests + targetRevision: HEAD + path: roswell + destination: + server: https://kubernetes.default.svc + namespace: roswell + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/roswell/backup-cronjob.yaml b/roswell/backup-cronjob.yaml new file mode 100644 index 0000000..d5159b2 --- /dev/null +++ b/roswell/backup-cronjob.yaml @@ -0,0 +1,78 @@ +# Backup diario de la BD del corpus con retención 7 (PLAN §Fase 6). +# pg_dump en formato custom (-Fc, comprimido) verificado con pg_restore --list. +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + # ArgoCD no debe podar este PVC: contiene los backups de la BD. + argocd.argoproj.io/sync-options: Prune=false + name: roswell-backups + namespace: roswell +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi + +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: roswell-db-backup + namespace: roswell + labels: + app: roswell +spec: + schedule: "30 3 * * *" + timeZone: "Europe/Madrid" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + activeDeadlineSeconds: 900 + template: + metadata: + labels: + app: roswell-db-backup + spec: + restartPolicy: Never + containers: + - name: backup + # Misma imagen que el postgres del corpus: pg_dump 16 garantizado + # y ya está en la caché del nodo. + image: pgvector/pgvector:pg16 + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: roswell-pg-secret + key: password + command: + - /bin/sh + - -c + - | + set -eu + STAMP=$(date +%Y%m%d) + DEST="/backups/roswell-${STAMP}.dump" + echo "pg_dump roswell -> ${DEST}" + pg_dump -h postgres.roswell.svc.cluster.local -U roswell \ + -d roswell -Fc -f "${DEST}" + # Verificación: un dump corrupto no lista su TOC. + pg_restore --list "${DEST}" > /dev/null + echo "Dump verificado ($(du -h ${DEST} | cut -f1))" + # Retención: conservar los 7 más recientes + for f in $(ls -1t /backups/roswell-*.dump | tail -n +8); do + echo "Podando backup antiguo ${f}" + rm -f "${f}" + done + ls -lh /backups/ + volumeMounts: + - name: backups + mountPath: /backups + volumes: + - name: backups + persistentVolumeClaim: + claimName: roswell-backups diff --git a/roswell/deployment.yaml b/roswell/deployment.yaml new file mode 100644 index 0000000..d074690 --- /dev/null +++ b/roswell/deployment.yaml @@ -0,0 +1,117 @@ +# Bot + scheduler en un pod (PLAN §Fase 6). El Secret roswell-secrets se crea +# imperativamente (nunca en git): +# kubectl create secret generic roswell-secrets -n roswell \ +# --from-literal=telegram-bot-token=... \ +# --from-literal=telegram-allowed-users=... \ +# --from-literal=anthropic-api-key=... \ +# --from-literal=database-url=postgresql://roswell:@postgres.roswell.svc.cluster.local:5432/roswell \ +# --from-literal=podcastindex-key=... \ +# --from-literal=podcastindex-secret=... +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + # ArgoCD no debe podar este PVC: guarda la caché del modelo Whisper (~3GB) + # y los PDFs subidos por Telegram. + argocd.argoproj.io/sync-options: Prune=false + name: roswell-data + namespace: roswell +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: roswell + namespace: roswell + labels: + app: roswell +spec: + replicas: 1 + # Dos pods haciendo polling del mismo token de Telegram entran en conflicto: + # Recreate garantiza que el viejo muere antes de arrancar el nuevo. + strategy: + type: Recreate + selector: + matchLabels: + app: roswell + template: + metadata: + labels: + app: roswell + spec: + containers: + - name: roswell + # Tag placeholder: el workflow de Gitea Actions lo sustituye por el + # sha8 real en cada build (ver .gitea/workflows/build.yml del repo). + image: git.chemavx.xyz/chemavx/roswell-corpus:00000000 + imagePullPolicy: Always + env: + - name: TELEGRAM_BOT_TOKEN + valueFrom: + secretKeyRef: + name: roswell-secrets + key: telegram-bot-token + - name: TELEGRAM_ALLOWED_USERS + valueFrom: + secretKeyRef: + name: roswell-secrets + key: telegram-allowed-users + - name: ANTHROPIC_API_KEY + valueFrom: + secretKeyRef: + name: roswell-secrets + key: anthropic-api-key + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: roswell-secrets + key: database-url + - name: PODCASTINDEX_KEY + valueFrom: + secretKeyRef: + name: roswell-secrets + key: podcastindex-key + - name: PODCASTINDEX_SECRET + valueFrom: + secretKeyRef: + name: roswell-secrets + key: podcastindex-secret + - name: OLLAMA_URL + value: "http://ollama.ollama.svc.cluster.local:11434" + - name: SEARXNG_URL + value: "http://searxng-svc.researchowl.svc.cluster.local:8080" + - name: WHISPER_MODEL + value: "large-v3" + - name: WHISPER_DEVICE + value: "cpu" + - name: DATA_DIR + value: "/data" + - name: FEED_CHECK_HOURS + value: "24" + # Caché de modelos Whisper en el PVC: la descarga (~3GB) sobrevive + # a los redeploys. + - name: HF_HOME + value: "/data/hf-cache" + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + # Whisper large-v3 en CPU ≈ 3-4 GB (PLAN §10). Sin límite de CPU: + # la transcripción aprovecha los cores libres del nodo. + memory: "4Gi" + volumes: + - name: data + persistentVolumeClaim: + claimName: roswell-data + imagePullSecrets: + - name: gitea-registry diff --git a/roswell/infisical-gitea-registry.yaml b/roswell/infisical-gitea-registry.yaml new file mode 100644 index 0000000..eca0e86 --- /dev/null +++ b/roswell/infisical-gitea-registry.yaml @@ -0,0 +1,27 @@ +apiVersion: secrets.infisical.com/v1beta1 +kind: InfisicalStaticSecret +metadata: + name: gitea-registry + namespace: roswell +spec: + # Referencia la capa de auth COMPARTIDA en 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: roswell + creationPolicy: Owner + secretType: kubernetes.io/dockerconfigjson + template: + engineVersion: v1 + data: + # Reconstruye el pull-secret docker desde los dos campos de /registry. + .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/roswell/postgres.yaml b/roswell/postgres.yaml new file mode 100644 index 0000000..cf33ada --- /dev/null +++ b/roswell/postgres.yaml @@ -0,0 +1,90 @@ +# Postgres 16 + pgvector para el corpus (namespace roswell). +# El Secret roswell-pg-secret se crea imperativamente (PLAN Fase 6): +# kubectl create secret generic roswell-pg-secret -n roswell \ +# --from-literal=password= +# NodePort 30432 para acceso desde la LAN/nodos durante el desarrollo local. +apiVersion: v1 +kind: Namespace +metadata: + name: roswell +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + # ArgoCD no debe podar nunca este PVC: contiene la BD del corpus. + argocd.argoproj.io/sync-options: Prune=false + name: roswell-pgdata + namespace: roswell +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 20Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: roswell +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: roswell-postgres + template: + metadata: + labels: + app: roswell-postgres + spec: + containers: + - name: postgres + image: pgvector/pgvector:pg16 + env: + - name: POSTGRES_DB + value: roswell + - name: POSTGRES_USER + value: roswell + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: roswell-pg-secret + key: password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + volumeMounts: + - name: pgdata + mountPath: /var/lib/postgresql/data + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + readinessProbe: + exec: + command: ["pg_isready", "-U", "roswell", "-d", "roswell"] + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: pgdata + persistentVolumeClaim: + claimName: roswell-pgdata +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: roswell +spec: + type: NodePort + selector: + app: roswell-postgres + ports: + - port: 5432 + targetPort: 5432 + nodePort: 30432