Compare commits
33
Commits
f059470fde
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d011a4c50f | ||
|
|
8a5ccbebdb | ||
|
|
09643943cb | ||
|
|
2abc398a10 | ||
|
|
376eab68b6 | ||
|
|
5c43a59884 | ||
|
|
c99d0a978e | ||
|
|
c364d46ac6 | ||
|
|
39ea16fe91 | ||
|
|
e9eb8c4ae2 | ||
|
|
ed3d886090 | ||
|
|
4c0b2d4fa4 | ||
|
|
a85c82a903 | ||
|
|
005b13bd91 | ||
|
|
1b9a7bdddc | ||
|
|
871af5df11 | ||
|
|
d903fd4631 | ||
|
|
43e607629f | ||
|
|
7e60b5e8d5 | ||
|
|
831d686b5a | ||
|
|
6416c6c3af | ||
|
|
ad3716bf80 | ||
|
|
29b3fb3e5a | ||
|
|
989064d332 | ||
|
|
91bf7d2909 | ||
|
|
cbfb1ba039 | ||
|
|
43c78246a8 | ||
|
|
395102a85e | ||
|
|
9dbcb0896e | ||
|
|
f29256ca55 | ||
|
|
e1a25329d4 | ||
|
|
d1d2fac287 | ||
|
|
ae0b5f9492 |
@@ -0,0 +1,95 @@
|
|||||||
|
# RESTORE.md — Runbook de restauración del homelab
|
||||||
|
|
||||||
|
> Última revisión: 2026-07-15. Si cambias `backup.sh`, actualiza este runbook.
|
||||||
|
|
||||||
|
## Dónde están las copias
|
||||||
|
|
||||||
|
- **Mega** (`mega:k3s-backups/`): espejo diario de `/data/backups/backups` del master
|
||||||
|
(retención 7 por servicio; 3 para `uptime-kuma` y `home-master`), más el subdir
|
||||||
|
`reddit-intel/` que sube el CronJob de n97.
|
||||||
|
- **Backblaze B2** (`b2:chemavx-k3s-backups/crown-jewels/` + `reddit-intel/`):
|
||||||
|
copia reducida "crown jewels" (últimas 2 de cada servicio irremplazable, todo
|
||||||
|
menos uptime-kuma). Segundo proveedor por si Mega falla.
|
||||||
|
- Acceso: credenciales rclone en Infisical (`/backup-system`, key `RCLONE_CONF`).
|
||||||
|
**Si el cluster está muerto, Infisical también** → usa el login de Mega/B2 de la
|
||||||
|
cuenta personal (Vaultwarden... que también está en el backup: guarda estas dos
|
||||||
|
contraseñas TAMBIÉN en un gestor externo al homelab, p. ej. el llavero del móvil).
|
||||||
|
|
||||||
|
## ⚠️ La cadena de dependencias (leer antes de restaurar nada)
|
||||||
|
|
||||||
|
```
|
||||||
|
token k3s ──> state.db (todos los Secrets k8s) ──> infisical-secrets (ENCRYPTION_KEY)
|
||||||
|
│
|
||||||
|
v
|
||||||
|
dump infisical-pg (secrets de apps)
|
||||||
|
│
|
||||||
|
v
|
||||||
|
resto de apps (n8n, ghost smtp, ddns, runner…)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. `k3s-db_*.tar.gz` contiene `state.db` **+ `/var/lib/rancher/k3s/server/token` +
|
||||||
|
`/etc/rancher/k3s/`**. Sin ese token, un k3s nuevo NO puede leer el state.db
|
||||||
|
(los datos de bootstrap van cifrados con él).
|
||||||
|
2. El Secret `infisical-secrets` (ns `infisical`: `ENCRYPTION_KEY`, `AUTH_SECRET`)
|
||||||
|
está DENTRO de state.db. Sin él, el dump `infisical-pg` es indescifrable.
|
||||||
|
3. n8n es autosuficiente: su `encryptionKey` va dentro de `n8n_*.tar.gz`
|
||||||
|
(`.n8n/config`).
|
||||||
|
|
||||||
|
## Escenario A — Perder un PVC / una app (lo habitual)
|
||||||
|
|
||||||
|
Restaurar solo el artefacto de esa app (ver tabla). El cluster sigue vivo.
|
||||||
|
|
||||||
|
## Escenario B — Muere el disco del master (chemavx-k8)
|
||||||
|
|
||||||
|
1. Reinstalar Ubuntu + k3s server (misma versión mayor; ver skill k8s-infra).
|
||||||
|
2. Restaurar del `k3s-db_*.tar.gz`: `/etc/rancher/k3s/`, el fichero `token` en
|
||||||
|
`/var/lib/rancher/k3s/server/token` y `state.db*` en
|
||||||
|
`/var/lib/rancher/k3s/server/db/` (con k3s parado). Arrancar k3s.
|
||||||
|
3. Con el API server vivo: los manifests los trae ArgoCD desde Gitea → pero Gitea
|
||||||
|
está en el master: restaurar primero Gitea (tabla) para que ArgoCD sincronice.
|
||||||
|
4. Restaurar los datos de apps del master (tabla). Reinstalar Tailscale a mano
|
||||||
|
(está fuera de k8s).
|
||||||
|
|
||||||
|
## Escenario C — Muere el disco del worker (n97)
|
||||||
|
|
||||||
|
Reinstalar Ubuntu + k3s agent (token del master), dejar que los pods se
|
||||||
|
reprogramen y restaurar: ghost-en, zona-exclusion, researchowl, trilium,
|
||||||
|
umami-pg, roswell-pg (la BD corre en n97) y `/opt/reddit-intel` + Docker.
|
||||||
|
|
||||||
|
## Tabla de restauración por artefacto
|
||||||
|
|
||||||
|
| Artefacto | Cómo restaurar |
|
||||||
|
|---|---|
|
||||||
|
| `n8n_*.tar.gz` | Extraer en `/data/n8n` (hostPath master), reiniciar deploy. Incluye encryptionKey. |
|
||||||
|
| `openclaw_*.tar.gz` | Extraer en el PVC `openclaw-pvc` (pod parado). |
|
||||||
|
| `homarr_*.tar.gz` | `db/db.sqlite` → PVC db; `redis/dump.rdb` → PVC redis. |
|
||||||
|
| `authentik-pg_*.sql.gz` | `gunzip -c \| kubectl exec -i postgresql-0 -n authentik -- psql -U authentik authentik` (BD vacía primero). |
|
||||||
|
| `grafana_*.tar.gz` | `grafana.db` → PVC de grafana, reiniciar pod. |
|
||||||
|
| `k3s-db_*.tar.gz` | Ver Escenario B. **No** restaurar sobre un cluster sano. |
|
||||||
|
| `gitea_*.tar.gz` | Extraer en el PVC de gitea; **renombrar `gitea/gitea-backup.db` → `gitea/gitea.db`**. El registry (packages) NO está: re-push de imágenes vía CI (re-run de Gitea Actions por app). |
|
||||||
|
| `ghost-*-db_*.db.gz` | `gunzip` → `content/data/ghost.db` (pod parado). |
|
||||||
|
| `ghost-*-content_*.tar.gz` | Extraer en `/var/lib/ghost/content` (incluye imágenes, temas, `data/redirects.yaml`; el `data/ghost.db` del tar es copia en caliente — usa el `-db_*.db.gz`, que es la consistente). |
|
||||||
|
| `researchowl_*.db.gz` | `gunzip` → `/data/researchowl.db` en el PVC (pod parado; borrar `-wal`/`-shm` viejos). |
|
||||||
|
| `roswell-pg_*.sql.gz` / `umami-pg_*.sql.gz` | Como authentik: `psql` contra el deploy/postgres de su namespace. |
|
||||||
|
| `infisical-pg_*.sql.gz` | **Primero** debe existir el Secret `infisical-secrets` original (viene en state.db; si no, exportarlo ANTES de perderlo). Luego psql contra `infisicalDB` en `postgresql-0`. |
|
||||||
|
| `trilium_*.tar.gz` | Extraer en `/home/node/trilium-data` (PVC, pod parado). |
|
||||||
|
| `vaultwarden_*.tar.gz` | Extraer en el PVC (pod parado). Incluye db, attachments y claves RSA. |
|
||||||
|
| `uptime-kuma_*.db.gz` | `gunzip` → `/app/data/kuma.db` (pod parado, borrar `-wal`/`-shm`). |
|
||||||
|
| `filebrowser-db_*.tar.gz` | Extraer en el PVC `filebrowser-db`. |
|
||||||
|
| `home-master_*.tar.gz` | Extraer en `/home/chemavx`. **No incluye `~/.ssh`** (regenerar claves) ni `~/bin` ni venvs/caches (reinstalar). |
|
||||||
|
| `reddit-intel/reddit-intel_*.tar.gz` | Extraer en `/opt/reddit-intel` de n97, `docker compose up -d`. |
|
||||||
|
|
||||||
|
## Qué NO está en el backup (reconstruible, asumido)
|
||||||
|
|
||||||
|
- Registry de imágenes de Gitea (re-build por CI), modelos Ollama (re-pull),
|
||||||
|
hf-cache de roswell, Prometheus TSDB (histórico de métricas se pierde),
|
||||||
|
registry-cache, SO de los nodos, Tailscale (re-join), certificados TLS
|
||||||
|
(cert-manager los re-emite), DNS (favonia lo reconstruye).
|
||||||
|
- `~/.ssh` del master: excluido a propósito — regenerar par ed25519 y
|
||||||
|
re-autorizar en n97/Gitea.
|
||||||
|
|
||||||
|
## Simulacro
|
||||||
|
|
||||||
|
Tras cualquier cambio gordo en `backup.sh`, repetir el simulacro: bajar de Mega
|
||||||
|
el `ghost-en-db` más reciente, `PRAGMA integrity_check` + `SELECT count(*) FROM posts`,
|
||||||
|
y extraer un `home-master` en /tmp comprobando que `~/.claude/` está dentro.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: backup-system
|
||||||
|
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: backup-system
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: backup-system
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: backup-role
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
- pods/exec
|
||||||
|
- persistentvolumeclaims
|
||||||
|
- persistentvolumes
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- create
|
||||||
|
# kubectl exec deploy/<nombre> resuelve el Deployment antes de elegir pod
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources:
|
||||||
|
- deployments
|
||||||
|
- statefulsets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: backup-role-binding
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: backup-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: backup-sa
|
||||||
|
namespace: backup-system
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
backup.sh: |
|
||||||
|
#!/bin/bash
|
||||||
|
# Backup diario del homelab. Imagen: bitnami/kubectl (bash GNU, corre en chemavx-k8).
|
||||||
|
# Regla: cada artefacto se valida (gzip -t + tamaño mínimo) — un tar vacío
|
||||||
|
# como el bug histórico de openclaw (104 bytes) cuenta como FALLO y el job sale 1
|
||||||
|
# (→ alerta Grafana homelab-backup-job-failed → Telegram).
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# kubectl usa el ServiceAccount in-cluster (backup-sa + ClusterRole backup-role).
|
||||||
|
# NO usar el k3s.yaml del host: su server es https://127.0.0.1:6443, inalcanzable
|
||||||
|
# desde la red del pod (el script antiguo "funcionaba" porque apuntaba KUBECONFIG
|
||||||
|
# a un fichero inexistente y kubectl caía en fallback al ServiceAccount).
|
||||||
|
unset KUBECONFIG
|
||||||
|
|
||||||
|
BACKUP_DIR="/data/backups/backups"
|
||||||
|
DATE=$(date +%Y-%m-%d_%H-%M-%S)
|
||||||
|
STORAGE=/var/lib/rancher/k3s/storage
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||||
|
fail() { log "ERROR: $1 FAILED"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
# Valida artefacto: gzip íntegro y tamaño >= mínimo esperado
|
||||||
|
check() { # $1=fichero $2=bytes_minimos $3=servicio
|
||||||
|
if gzip -t "$1" 2>/dev/null && [ "$(stat -c%s "$1" 2>/dev/null || echo 0)" -ge "$2" ]; then
|
||||||
|
log "$3 OK ($(du -h "$1" | cut -f1))"
|
||||||
|
else
|
||||||
|
rm -f "$1"
|
||||||
|
fail "$3 (artefacto corrupto o menor de $2 bytes)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resuelve la ruta local-path de un PVC en el host
|
||||||
|
pvpath() { # $1=ns $2=pvc
|
||||||
|
local pv
|
||||||
|
pv=$(kubectl get pvc -n "$1" "$2" -o jsonpath='{.spec.volumeName}' 2>/dev/null) || return 1
|
||||||
|
echo "$STORAGE/${pv}_$1_$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copia consistente de la ghost.db (VACUUM INTO vía node del pod) + tar del content
|
||||||
|
backup_ghost() { # $1=ns $2=deploy $3=prefijo
|
||||||
|
local f="$BACKUP_DIR/${3}-db_${DATE}.db.gz"
|
||||||
|
kubectl exec -i -n "$1" "deploy/$2" -- sh -c \
|
||||||
|
'rm -f /tmp/ghost-backup.db && NODE_PATH=/var/lib/ghost/current/node_modules node -' \
|
||||||
|
< /scripts/ghost-db-backup.js >/dev/null 2>&1 \
|
||||||
|
&& kubectl exec -n "$1" "deploy/$2" -- cat /tmp/ghost-backup.db 2>/dev/null | gzip > "$f"
|
||||||
|
kubectl exec -n "$1" "deploy/$2" -- rm -f /tmp/ghost-backup.db 2>/dev/null || true
|
||||||
|
check "$f" 200000 "${3}-db"
|
||||||
|
local c="$BACKUP_DIR/${3}-content_${DATE}.tar.gz"
|
||||||
|
kubectl exec -n "$1" "deploy/$2" -- tar czf - -C /var/lib/ghost/content . > "$c" 2>/dev/null
|
||||||
|
check "$c" 5000000 "${3}-content"
|
||||||
|
}
|
||||||
|
|
||||||
|
# === n8n (hostPath /data/n8n — incluye .n8n/config con la encryptionKey) ===
|
||||||
|
log "Backing up n8n..."
|
||||||
|
tar -czf "$BACKUP_DIR/n8n_${DATE}.tar.gz" -C /data/n8n . 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/n8n_${DATE}.tar.gz" 10000000 n8n
|
||||||
|
|
||||||
|
# === openclaw (PVC local-path; el hostPath /data/openclaw era un stub vacío) ===
|
||||||
|
log "Backing up openclaw..."
|
||||||
|
OC=$(pvpath openclaw openclaw-pvc)
|
||||||
|
tar -czf "$BACKUP_DIR/openclaw_${DATE}.tar.gz" -C "$OC" . 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/openclaw_${DATE}.tar.gz" 50000 openclaw
|
||||||
|
|
||||||
|
# === homarr ===
|
||||||
|
log "Backing up homarr..."
|
||||||
|
HOMARR_DB=$(pvpath homarr homarr-db-pvc)
|
||||||
|
HOMARR_REDIS=$(pvpath homarr homarr-redis-pvc)
|
||||||
|
mkdir -p /tmp/backup-homarr/db /tmp/backup-homarr/redis
|
||||||
|
cp "$HOMARR_DB/db.sqlite" /tmp/backup-homarr/db/ 2>/dev/null || true
|
||||||
|
cp "$HOMARR_REDIS/dump.rdb" /tmp/backup-homarr/redis/ 2>/dev/null || true
|
||||||
|
tar -czf "$BACKUP_DIR/homarr_${DATE}.tar.gz" -C /tmp/backup-homarr . 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/homarr_${DATE}.tar.gz" 10000 homarr
|
||||||
|
rm -rf /tmp/backup-homarr
|
||||||
|
|
||||||
|
# === authentik PostgreSQL ===
|
||||||
|
log "Backing up authentik postgres..."
|
||||||
|
kubectl exec -n authentik postgresql-0 -- pg_dump -U authentik authentik 2>/dev/null | \
|
||||||
|
gzip > "$BACKUP_DIR/authentik-pg_${DATE}.sql.gz"
|
||||||
|
check "$BACKUP_DIR/authentik-pg_${DATE}.sql.gz" 10000 authentik-pg
|
||||||
|
|
||||||
|
# === grafana ===
|
||||||
|
log "Backing up grafana..."
|
||||||
|
GRAFANA=$(pvpath monitoring kube-prometheus-stack-grafana)
|
||||||
|
mkdir -p /tmp/backup-grafana
|
||||||
|
cp "$GRAFANA/grafana.db" /tmp/backup-grafana/ 2>/dev/null || true
|
||||||
|
tar -czf "$BACKUP_DIR/grafana_${DATE}.tar.gz" -C /tmp/backup-grafana . 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/grafana_${DATE}.tar.gz" 100000 grafana
|
||||||
|
rm -rf /tmp/backup-grafana
|
||||||
|
|
||||||
|
# === k3s: state.db + token del server + /etc/rancher/k3s ===
|
||||||
|
# Sin el token, state.db NO arranca en un k3s nuevo (cadena de restauración).
|
||||||
|
log "Backing up k3s (state.db + token + /etc/rancher/k3s)..."
|
||||||
|
tar -czf "$BACKUP_DIR/k3s-db_${DATE}.tar.gz" \
|
||||||
|
/data/k3s-db/state.db* /host/k3s-token /host/etc-rancher-k3s 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/k3s-db_${DATE}.tar.gz" 1000000 k3s-db
|
||||||
|
|
||||||
|
# === Gitea: sqlite .backup consistente + repos + conf (SIN gitea/packages, 6.4G de registry reconstruible por CI) ===
|
||||||
|
log "Backing up gitea..."
|
||||||
|
kubectl exec -n gitea gitea-0 -- sh -c \
|
||||||
|
'rm -f /data/gitea/gitea-backup.db && sqlite3 /data/gitea/gitea.db ".backup /data/gitea/gitea-backup.db"' 2>/dev/null \
|
||||||
|
&& kubectl exec -n gitea gitea-0 -- tar czf - -C /data \
|
||||||
|
git ssh gitea/conf gitea/jwt gitea/avatars gitea/repo-avatars gitea/gitea-backup.db \
|
||||||
|
> "$BACKUP_DIR/gitea_${DATE}.tar.gz" 2>/dev/null
|
||||||
|
kubectl exec -n gitea gitea-0 -- rm -f /data/gitea/gitea-backup.db 2>/dev/null || true
|
||||||
|
check "$BACKUP_DIR/gitea_${DATE}.tar.gz" 5000000 gitea
|
||||||
|
|
||||||
|
# === Ghost EN + ES (db consistente + content con imágenes/temas/redirects) ===
|
||||||
|
log "Backing up ghost-en..."
|
||||||
|
backup_ghost ghost-en ghost-en ghost-en
|
||||||
|
log "Backing up zona-exclusion..."
|
||||||
|
backup_ghost zona-exclusion zona-exclusion zona-exclusion
|
||||||
|
|
||||||
|
# === ResearchOwl (sqlite .backup vía python3 del pod) ===
|
||||||
|
log "Backing up researchowl..."
|
||||||
|
kubectl exec -i -n researchowl deploy/researchowl -- sh -c \
|
||||||
|
'rm -f /tmp/ro-backup.db && python3 -' < /scripts/sqlite-stdin-backup.py >/dev/null 2>&1 \
|
||||||
|
&& kubectl exec -n researchowl deploy/researchowl -- cat /tmp/ro-backup.db 2>/dev/null | \
|
||||||
|
gzip > "$BACKUP_DIR/researchowl_${DATE}.db.gz"
|
||||||
|
kubectl exec -n researchowl deploy/researchowl -- rm -f /tmp/ro-backup.db 2>/dev/null || true
|
||||||
|
check "$BACKUP_DIR/researchowl_${DATE}.db.gz" 5000000 researchowl
|
||||||
|
|
||||||
|
# === Roswell (corpus) PostgreSQL ===
|
||||||
|
log "Backing up roswell postgres..."
|
||||||
|
kubectl exec -n roswell deploy/postgres -- sh -c \
|
||||||
|
'PGPASSWORD="${POSTGRES_PASSWORD:-}" pg_dump -U "$POSTGRES_USER" "${POSTGRES_DB:-$POSTGRES_USER}"' 2>/dev/null | \
|
||||||
|
gzip > "$BACKUP_DIR/roswell-pg_${DATE}.sql.gz"
|
||||||
|
check "$BACKUP_DIR/roswell-pg_${DATE}.sql.gz" 5000 roswell-pg
|
||||||
|
|
||||||
|
# === Umami PostgreSQL ===
|
||||||
|
log "Backing up umami postgres..."
|
||||||
|
kubectl exec -n umami deploy/postgres -- sh -c \
|
||||||
|
'PGPASSWORD="${POSTGRES_PASSWORD:-}" pg_dump -U "$POSTGRES_USER" "${POSTGRES_DB:-$POSTGRES_USER}"' 2>/dev/null | \
|
||||||
|
gzip > "$BACKUP_DIR/umami-pg_${DATE}.sql.gz"
|
||||||
|
check "$BACKUP_DIR/umami-pg_${DATE}.sql.gz" 5000 umami-pg
|
||||||
|
|
||||||
|
# === Infisical PostgreSQL (RESTAURAR REQUIERE infisical-secrets — está dentro de k3s-db/state.db, ver RESTORE.md) ===
|
||||||
|
log "Backing up infisical postgres..."
|
||||||
|
kubectl exec -n infisical postgresql-0 -- sh -c \
|
||||||
|
'PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -U "$POSTGRES_USER" infisicalDB' 2>/dev/null | \
|
||||||
|
gzip > "$BACKUP_DIR/infisical-pg_${DATE}.sql.gz"
|
||||||
|
check "$BACKUP_DIR/infisical-pg_${DATE}.sql.gz" 10000 infisical-pg
|
||||||
|
|
||||||
|
# === Trilium ===
|
||||||
|
log "Backing up trilium..."
|
||||||
|
kubectl exec -n trilium deploy/trilium -- tar czf - -C /home/node/trilium-data . \
|
||||||
|
> "$BACKUP_DIR/trilium_${DATE}.tar.gz" 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/trilium_${DATE}.tar.gz" 500000 trilium
|
||||||
|
|
||||||
|
# === Vaultwarden (db + attachments + claves RSA; copia en frío ~02:00) ===
|
||||||
|
log "Backing up vaultwarden..."
|
||||||
|
VW=$(pvpath vaultwarden vaultwarden-pvc)
|
||||||
|
tar -czf "$BACKUP_DIR/vaultwarden_${DATE}.tar.gz" -C "$VW" . 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/vaultwarden_${DATE}.tar.gz" 100000 vaultwarden
|
||||||
|
|
||||||
|
# === Uptime-Kuma (sqlite .backup vía CLI del pod; retención corta, es el artefacto gordo) ===
|
||||||
|
log "Backing up uptime-kuma..."
|
||||||
|
kubectl exec -n monitoring deploy/uptime-kuma -- sh -c \
|
||||||
|
'rm -f /tmp/kuma-backup.db && sqlite3 /app/data/kuma.db ".backup /tmp/kuma-backup.db"' 2>/dev/null \
|
||||||
|
&& kubectl exec -n monitoring deploy/uptime-kuma -- cat /tmp/kuma-backup.db 2>/dev/null | \
|
||||||
|
gzip > "$BACKUP_DIR/uptime-kuma_${DATE}.db.gz"
|
||||||
|
kubectl exec -n monitoring deploy/uptime-kuma -- rm -f /tmp/kuma-backup.db 2>/dev/null || true
|
||||||
|
check "$BACKUP_DIR/uptime-kuma_${DATE}.db.gz" 1000000 uptime-kuma
|
||||||
|
|
||||||
|
# === Filebrowser (solo su BD; los ficheros servidos van en home-master/imagefinder) ===
|
||||||
|
log "Backing up filebrowser db..."
|
||||||
|
FB=$(pvpath filebrowser filebrowser-db)
|
||||||
|
tar -czf "$BACKUP_DIR/filebrowser-db_${DATE}.tar.gz" -C "$FB" . 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/filebrowser-db_${DATE}.tar.gz" 1000 filebrowser-db
|
||||||
|
|
||||||
|
# === Home del master (~/.claude memoria+skills, imagefinder, proyectos sueltos, RUNBOOK...) ===
|
||||||
|
# Excluye ~/.ssh (claves privadas, decisión explícita), caches y binarios reproducibles.
|
||||||
|
log "Backing up home-master..."
|
||||||
|
tar -czf "$BACKUP_DIR/home-master_${DATE}.tar.gz" -C /host/home-chemavx \
|
||||||
|
--exclude='./.ssh' --exclude='./.cache' --exclude='./.venvs' --exclude='./.npm' \
|
||||||
|
--exclude='./.nvm' --exclude='./bin' --exclude='./.docker' --exclude='./snap' \
|
||||||
|
--exclude='*/node_modules' --exclude='./.local/share/pnpm' \
|
||||||
|
--exclude='./.warp' --exclude='./.local/share/claude' --exclude='./.local/lib' \
|
||||||
|
. 2>/dev/null
|
||||||
|
check "$BACKUP_DIR/home-master_${DATE}.tar.gz" 50000000 home-master
|
||||||
|
|
||||||
|
# === Retención (7 por defecto; 3 para los artefactos gordos) ===
|
||||||
|
log "Cleaning old backups..."
|
||||||
|
retain() { # $1=prefijo $2=copias
|
||||||
|
find "$BACKUP_DIR" -maxdepth 1 \
|
||||||
|
\( -name "${1}_*.tar.gz" -o -name "${1}_*.sql.gz" -o -name "${1}_*.db.gz" \) | \
|
||||||
|
sort | head -n -"$2" | xargs -r rm -f
|
||||||
|
}
|
||||||
|
for p in n8n openclaw homarr authentik-pg grafana k3s-db gitea \
|
||||||
|
ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content \
|
||||||
|
researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db; do
|
||||||
|
retain "$p" 7
|
||||||
|
done
|
||||||
|
retain uptime-kuma 3
|
||||||
|
retain home-master 3
|
||||||
|
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
log "Backup terminado con $FAIL fallo(s) — el job sale con error para disparar la alerta."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Backup complete!"
|
||||||
|
ghost-db-backup.js: |
|
||||||
|
// Copia consistente de la ghost.db usando el driver sqlite3 del propio Ghost.
|
||||||
|
// Se ejecuta DENTRO del pod vía: kubectl exec -i ... node - (script por stdin)
|
||||||
|
const sqlite3 = require('sqlite3');
|
||||||
|
const src = '/var/lib/ghost/content/data/ghost.db';
|
||||||
|
const dst = '/tmp/ghost-backup.db';
|
||||||
|
const db = new sqlite3.Database(src, sqlite3.OPEN_READONLY, (err) => {
|
||||||
|
if (err) { console.error(err.message); process.exit(1); }
|
||||||
|
});
|
||||||
|
db.run("VACUUM INTO '" + dst + "'", (err) => {
|
||||||
|
if (err) { console.error(err.message); process.exit(1); }
|
||||||
|
db.close(() => console.log('ghost db backup ok'));
|
||||||
|
});
|
||||||
|
sqlite-stdin-backup.py: |
|
||||||
|
# Copia consistente de researchowl.db (API backup de sqlite3, stdlib).
|
||||||
|
# Se ejecuta DENTRO del pod vía: kubectl exec -i ... python3 -
|
||||||
|
import sqlite3
|
||||||
|
src = sqlite3.connect('file:/data/researchowl.db?mode=ro', uri=True)
|
||||||
|
dst = sqlite3.connect('/tmp/ro-backup.db')
|
||||||
|
with dst:
|
||||||
|
src.backup(dst)
|
||||||
|
dst.close()
|
||||||
|
src.close()
|
||||||
|
print('researchowl db backup ok')
|
||||||
|
rclone-mega.sh: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||||
|
|
||||||
|
log "Iniciando sync a Mega..."
|
||||||
|
log "Origen: /data/backups/backups"
|
||||||
|
log "Destino: mega:k3s-backups/"
|
||||||
|
|
||||||
|
# --exclude reddit-intel: ese subdir remoto lo escribe el CronJob de n97
|
||||||
|
# directamente; un sync sin exclude lo borraría (sync = espejo exacto).
|
||||||
|
rclone sync /data/backups/backups mega:k3s-backups/ \
|
||||||
|
--exclude "/reddit-intel/**" \
|
||||||
|
--config /rclone/rclone.conf \
|
||||||
|
--log-level INFO \
|
||||||
|
--stats 30s
|
||||||
|
|
||||||
|
log "Sync completado."
|
||||||
|
log "Archivos en Mega:"
|
||||||
|
rclone ls mega:k3s-backups/ --config /rclone/rclone.conf
|
||||||
|
b2-crown-jewels.sh: |
|
||||||
|
#!/bin/sh
|
||||||
|
# Segunda copia offsite (Backblaze B2): "crown jewels" — las 2 copias más
|
||||||
|
# recientes de cada servicio irremplazable (~2GB, cabe en los 10GB gratis).
|
||||||
|
# Todo menos uptime-kuma (622M de historial de heartbeats, prescindible).
|
||||||
|
# Imagen BusyBox — sin GNU-ismos. Config: RCLONE_CONF de Infisical (mega+b2).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||||
|
BACKUP_DIR=/data/backups/backups
|
||||||
|
CROWN="n8n openclaw homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db home-master"
|
||||||
|
|
||||||
|
KEEP=/tmp/crown-jewels
|
||||||
|
rm -rf "$KEEP"; mkdir -p "$KEEP"
|
||||||
|
for p in $CROWN; do
|
||||||
|
for f in $(ls -1 "$BACKUP_DIR/${p}"_* 2>/dev/null | sort | tail -2); do
|
||||||
|
ln -s "$f" "$KEEP/$(basename "$f")"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
N=$(ls -1 "$KEEP" | wc -l)
|
||||||
|
log "Sync de $N artefactos crown-jewels a B2..."
|
||||||
|
# -L: resuelve los symlinks; sync = espejo → la retención remota (2/servicio) es automática
|
||||||
|
rclone sync "$KEEP" b2:chemavx-k3s-backups/crown-jewels/ -L \
|
||||||
|
--config /rclone/rclone.conf --log-level INFO --stats 60s
|
||||||
|
rm -rf "$KEEP"
|
||||||
|
log "B2 crown-jewels completado ($N artefactos)."
|
||||||
|
reddit-intel.sh: |
|
||||||
|
#!/bin/sh
|
||||||
|
# Corre en chemavx-n97 (imagen rclone, BusyBox — sin GNU-ismos).
|
||||||
|
# Sube /opt/reddit-intel a Mega directamente: es el único dato del host n97
|
||||||
|
# y su cron local escribe en el MISMO disco (inútil contra fallo de disco).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||||
|
DATE=$(date +%Y-%m-%d_%H-%M-%S)
|
||||||
|
F="/tmp/reddit-intel_${DATE}.tar.gz"
|
||||||
|
|
||||||
|
log "Empaquetando /opt/reddit-intel..."
|
||||||
|
tar -czf "$F" -C /opt/reddit-intel .
|
||||||
|
gzip -t "$F"
|
||||||
|
log "Subiendo a mega:k3s-backups/reddit-intel/..."
|
||||||
|
rclone copy "$F" mega:k3s-backups/reddit-intel/ --config /rclone/rclone.conf --log-level INFO
|
||||||
|
log "Podando copias remotas de más de 8 días..."
|
||||||
|
rclone delete mega:k3s-backups/reddit-intel/ --min-age 8d --config /rclone/rclone.conf
|
||||||
|
log "Subiendo a B2 (crown jewels, retención 3 días)..."
|
||||||
|
rclone copy "$F" b2:chemavx-k3s-backups/reddit-intel/ --config /rclone/rclone.conf --log-level INFO
|
||||||
|
rclone delete b2:chemavx-k3s-backups/reddit-intel/ --min-age 3d --config /rclone/rclone.conf
|
||||||
|
rm -f "$F"
|
||||||
|
log "reddit-intel OK"
|
||||||
|
verify.sh: |
|
||||||
|
#!/bin/sh
|
||||||
|
# Imagen BusyBox (rclone) — evitar GNU-ismos (date -d 'yesterday' NO funciona).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||||
|
|
||||||
|
EXPECTED_SERVICES="n8n openclaw homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden uptime-kuma filebrowser-db home-master"
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Ventana de frescura: backup y sync son diarios → un backup es "reciente"
|
||||||
|
# si su fecha está en los últimos 3 días (margen para jobs nocturnos largos).
|
||||||
|
NOW=$(date +%s)
|
||||||
|
ACCEPTED_DATES=""
|
||||||
|
for i in $(seq 0 2); do
|
||||||
|
ACCEPTED_DATES="$ACCEPTED_DATES $(date -d @$(( NOW - i * 86400 )) +%Y-%m-%d)"
|
||||||
|
done
|
||||||
|
|
||||||
|
log "=== Verificación de backups en Mega ==="
|
||||||
|
FILES=$(rclone ls mega:k3s-backups/ --config /rclone/rclone.conf 2>&1)
|
||||||
|
|
||||||
|
log "Fechas aceptadas (ventana de 3 días):$ACCEPTED_DATES"
|
||||||
|
|
||||||
|
for svc in $EXPECTED_SERVICES; do
|
||||||
|
FOUND=0
|
||||||
|
for d in $ACCEPTED_DATES; do
|
||||||
|
if echo "$FILES" | grep -q "${svc}_${d}"; then
|
||||||
|
FOUND=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$FOUND" -eq 1 ]; then
|
||||||
|
log "[OK] $svc"
|
||||||
|
else
|
||||||
|
log "[WARN] $svc — sin backup en los últimos 3 días"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# reddit-intel vive en su subdir (lo sube el CronJob de n97)
|
||||||
|
FOUND=0
|
||||||
|
for d in $ACCEPTED_DATES; do
|
||||||
|
if echo "$FILES" | grep -q "reddit-intel/reddit-intel_${d}"; then
|
||||||
|
FOUND=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$FOUND" -eq 1 ]; then
|
||||||
|
log "[OK] reddit-intel"
|
||||||
|
else
|
||||||
|
log "[WARN] reddit-intel — sin backup en los últimos 3 días"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "---"
|
||||||
|
TOTAL=$(echo "$FILES" | grep -c "tar.gz\|sql.gz\|db.gz" || true)
|
||||||
|
log "Total archivos en Mega: $TOTAL"
|
||||||
|
|
||||||
|
# === Segundo offsite: B2 crown-jewels (todo menos uptime-kuma) ===
|
||||||
|
log "=== Verificación de crown-jewels en B2 ==="
|
||||||
|
CROWN="n8n openclaw homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db home-master"
|
||||||
|
B2FILES=$(rclone ls b2:chemavx-k3s-backups/ --config /rclone/rclone.conf 2>&1)
|
||||||
|
for svc in $CROWN; do
|
||||||
|
FOUND=0
|
||||||
|
for d in $ACCEPTED_DATES; do
|
||||||
|
if echo "$B2FILES" | grep -q "${svc}_${d}"; then
|
||||||
|
FOUND=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$FOUND" -eq 1 ]; then
|
||||||
|
log "[OK] b2/$svc"
|
||||||
|
else
|
||||||
|
log "[WARN] b2/$svc — sin backup en los últimos 3 días"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
log "RESULTADO: $ERRORS servicio(s) sin backup reciente. Revisar."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "RESULTADO: Todos los backups verificados correctamente (Mega + B2)."
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: backup-scripts
|
||||||
|
namespace: backup-system
|
||||||
@@ -4,7 +4,8 @@ metadata:
|
|||||||
name: backup-verify
|
name: backup-verify
|
||||||
namespace: backup-system
|
namespace: backup-system
|
||||||
spec:
|
spec:
|
||||||
schedule: "0 4 * * 1"
|
# Diario desde 2026-07-15 (el sync a Mega ya es diario; ventana de frescura 3 días)
|
||||||
|
schedule: "0 5 * * *"
|
||||||
concurrencyPolicy: Forbid
|
concurrencyPolicy: Forbid
|
||||||
failedJobsHistoryLimit: 3
|
failedJobsHistoryLimit: 3
|
||||||
successfulJobsHistoryLimit: 3
|
successfulJobsHistoryLimit: 3
|
||||||
@@ -19,7 +20,8 @@ spec:
|
|||||||
runAsUser: 0
|
runAsUser: 0
|
||||||
containers:
|
containers:
|
||||||
- name: verify
|
- name: verify
|
||||||
image: rclone/rclone:latest
|
# OJO: imagen BusyBox — verify.sh debe evitar GNU-ismos (date -d 'yesterday')
|
||||||
|
image: docker.io/rclone/rclone:1.74.3
|
||||||
command: ["/bin/sh", "/scripts/verify.sh"]
|
command: ["/bin/sh", "/scripts/verify.sh"]
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -29,16 +31,19 @@ spec:
|
|||||||
cpu: 50m
|
cpu: 50m
|
||||||
memory: 64Mi
|
memory: 64Mi
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /rclone/rclone.conf
|
- mountPath: /rclone
|
||||||
name: rclone-conf
|
name: rclone-conf
|
||||||
- mountPath: /scripts
|
- mountPath: /scripts
|
||||||
name: scripts
|
name: scripts
|
||||||
volumes:
|
volumes:
|
||||||
|
# RCLONE_CONF (Infisical /backup-system) contiene mega+b2 desde 2026-07-15
|
||||||
- name: rclone-conf
|
- name: rclone-conf
|
||||||
hostPath:
|
secret:
|
||||||
path: /home/chemavx/.config/rclone/rclone.conf
|
secretName: rclone-conf-infisical
|
||||||
type: File
|
items:
|
||||||
|
- key: RCLONE_CONF
|
||||||
|
path: rclone.conf
|
||||||
- name: scripts
|
- name: scripts
|
||||||
hostPath:
|
configMap:
|
||||||
path: /data/backups/scripts
|
name: backup-scripts
|
||||||
type: Directory
|
defaultMode: 0755
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ spec:
|
|||||||
failedJobsHistoryLimit: 1
|
failedJobsHistoryLimit: 1
|
||||||
jobTemplate:
|
jobTemplate:
|
||||||
spec:
|
spec:
|
||||||
|
# Sin reintentos automáticos: un reintento re-crea TODOS los artefactos con
|
||||||
|
# otro timestamp y la retención por número podría expulsar copias buenas.
|
||||||
|
backoffLimit: 1
|
||||||
|
activeDeadlineSeconds: 3600
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
restartPolicy: OnFailure
|
restartPolicy: Never
|
||||||
serviceAccountName: backup-sa
|
serviceAccountName: backup-sa
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsUser: 0
|
runAsUser: 0
|
||||||
@@ -20,18 +24,17 @@ spec:
|
|||||||
kubernetes.io/hostname: chemavx-k8
|
kubernetes.io/hostname: chemavx-k8
|
||||||
containers:
|
containers:
|
||||||
- name: backup
|
- name: backup
|
||||||
image: bitnami/kubectl:latest
|
# bitnami/kubectl v1.36.2 — pinneado por digest: los tags de bitnami
|
||||||
|
# en Docker Hub ya no son fiables (migración Broadcom/bitnamilegacy)
|
||||||
|
image: docker.io/bitnami/kubectl@sha256:558420daf32bbc382e3e9af4537f4073085b336ddd47399a3b70e70087115978
|
||||||
command: ["/bin/bash", "/scripts/backup.sh"]
|
command: ["/bin/bash", "/scripts/backup.sh"]
|
||||||
env:
|
|
||||||
- name: KUBECONFIG
|
|
||||||
value: /kubeconfig/k3s.yaml
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 200m
|
||||||
memory: 128Mi
|
memory: 256Mi
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: "1"
|
||||||
memory: 512Mi
|
memory: 1Gi
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: backup-dir
|
- name: backup-dir
|
||||||
mountPath: /data/backups/backups
|
mountPath: /data/backups/backups
|
||||||
@@ -43,11 +46,18 @@ spec:
|
|||||||
mountPath: /scripts
|
mountPath: /scripts
|
||||||
- name: k3s-storage
|
- name: k3s-storage
|
||||||
mountPath: /var/lib/rancher/k3s/storage
|
mountPath: /var/lib/rancher/k3s/storage
|
||||||
- name: kubeconfig
|
|
||||||
mountPath: /kubeconfig/k3s.yaml
|
|
||||||
- name: k3s-db
|
- name: k3s-db
|
||||||
mountPath: /data/k3s-db
|
mountPath: /data/k3s-db
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
- name: k3s-token
|
||||||
|
mountPath: /host/k3s-token
|
||||||
|
readOnly: true
|
||||||
|
- name: etc-rancher-k3s
|
||||||
|
mountPath: /host/etc-rancher-k3s
|
||||||
|
readOnly: true
|
||||||
|
- name: home-chemavx
|
||||||
|
mountPath: /host/home-chemavx
|
||||||
|
readOnly: true
|
||||||
volumes:
|
volumes:
|
||||||
- name: backup-dir
|
- name: backup-dir
|
||||||
hostPath:
|
hostPath:
|
||||||
@@ -62,18 +72,26 @@ spec:
|
|||||||
path: /data/openclaw
|
path: /data/openclaw
|
||||||
type: DirectoryOrCreate
|
type: DirectoryOrCreate
|
||||||
- name: scripts
|
- name: scripts
|
||||||
hostPath:
|
configMap:
|
||||||
path: /data/backups/scripts
|
name: backup-scripts
|
||||||
type: Directory
|
defaultMode: 0755
|
||||||
- name: k3s-storage
|
- name: k3s-storage
|
||||||
hostPath:
|
hostPath:
|
||||||
path: /var/lib/rancher/k3s/storage
|
path: /var/lib/rancher/k3s/storage
|
||||||
type: Directory
|
type: Directory
|
||||||
- name: kubeconfig
|
|
||||||
hostPath:
|
|
||||||
path: /etc/rancher/k3s/k3s.yaml
|
|
||||||
type: File
|
|
||||||
- name: k3s-db
|
- name: k3s-db
|
||||||
hostPath:
|
hostPath:
|
||||||
path: /var/lib/rancher/k3s/server/db
|
path: /var/lib/rancher/k3s/server/db
|
||||||
type: Directory
|
type: Directory
|
||||||
|
- name: k3s-token
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/rancher/k3s/server/token
|
||||||
|
type: File
|
||||||
|
- name: etc-rancher-k3s
|
||||||
|
hostPath:
|
||||||
|
path: /etc/rancher/k3s
|
||||||
|
type: Directory
|
||||||
|
- name: home-chemavx
|
||||||
|
hostPath:
|
||||||
|
path: /home/chemavx
|
||||||
|
type: Directory
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: rclone-b2-backup
|
||||||
|
namespace: backup-system
|
||||||
|
spec:
|
||||||
|
# Segunda copia offsite (crown jewels → Backblaze B2), tras el sync a Mega
|
||||||
|
schedule: "0 4 * * *"
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
failedJobsHistoryLimit: 2
|
||||||
|
successfulJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 1
|
||||||
|
activeDeadlineSeconds: 3600
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: chemavx-k8
|
||||||
|
restartPolicy: Never
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
containers:
|
||||||
|
- name: rclone
|
||||||
|
# OJO: imagen BusyBox — el script debe evitar GNU-ismos
|
||||||
|
image: docker.io/rclone/rclone:1.74.3
|
||||||
|
command: ["/bin/sh", "/scripts/b2-crown-jewels.sh"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data/backups/backups
|
||||||
|
name: backup-dir
|
||||||
|
readOnly: true
|
||||||
|
- mountPath: /rclone
|
||||||
|
name: rclone-conf
|
||||||
|
- mountPath: /scripts
|
||||||
|
name: scripts
|
||||||
|
volumes:
|
||||||
|
- name: backup-dir
|
||||||
|
hostPath:
|
||||||
|
path: /data/backups/backups
|
||||||
|
type: Directory
|
||||||
|
# RCLONE_CONF (Infisical /backup-system) contiene mega+b2 desde 2026-07-15
|
||||||
|
- name: rclone-conf
|
||||||
|
secret:
|
||||||
|
secretName: rclone-conf-infisical
|
||||||
|
items:
|
||||||
|
- key: RCLONE_CONF
|
||||||
|
path: rclone.conf
|
||||||
|
- name: scripts
|
||||||
|
configMap:
|
||||||
|
name: backup-scripts
|
||||||
|
defaultMode: 0755
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: rclone-mega-backup
|
||||||
|
namespace: backup-system
|
||||||
|
spec:
|
||||||
|
# Diario desde 2026-07-15 (antes semanal: un fallo de disco entre domingos
|
||||||
|
# perdía hasta 6 días). Corre tras el backup de las 02:00.
|
||||||
|
schedule: "30 3 * * *"
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
failedJobsHistoryLimit: 2
|
||||||
|
successfulJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: chemavx-k8
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
containers:
|
||||||
|
- name: rclone
|
||||||
|
image: docker.io/rclone/rclone:1.74.3
|
||||||
|
command: ["/bin/sh", "/scripts/rclone-mega.sh"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data/backups/backups
|
||||||
|
name: backup-dir
|
||||||
|
- mountPath: /rclone
|
||||||
|
name: rclone-conf
|
||||||
|
- mountPath: /scripts
|
||||||
|
name: scripts
|
||||||
|
volumes:
|
||||||
|
- name: backup-dir
|
||||||
|
hostPath:
|
||||||
|
path: /data/backups/backups
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: rclone-conf
|
||||||
|
secret:
|
||||||
|
secretName: rclone-conf-infisical
|
||||||
|
items:
|
||||||
|
- key: RCLONE_CONF
|
||||||
|
path: rclone.conf
|
||||||
|
- name: scripts
|
||||||
|
configMap:
|
||||||
|
name: backup-scripts
|
||||||
|
defaultMode: 0755
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: reddit-intel-backup
|
||||||
|
namespace: backup-system
|
||||||
|
spec:
|
||||||
|
# Único CronJob de backup que corre en n97: /opt/reddit-intel vive fuera de k3s
|
||||||
|
# (Docker en el host) y su cron local escribe en el mismo disco.
|
||||||
|
schedule: "15 3 * * *"
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
failedJobsHistoryLimit: 2
|
||||||
|
successfulJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 1
|
||||||
|
activeDeadlineSeconds: 1800
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: chemavx-n97
|
||||||
|
restartPolicy: Never
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
containers:
|
||||||
|
- name: rclone
|
||||||
|
# OJO: imagen BusyBox — el script debe evitar GNU-ismos
|
||||||
|
image: docker.io/rclone/rclone:1.74.3
|
||||||
|
command: ["/bin/sh", "/scripts/reddit-intel.sh"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /opt/reddit-intel
|
||||||
|
name: reddit-intel
|
||||||
|
readOnly: true
|
||||||
|
- mountPath: /rclone
|
||||||
|
name: rclone-conf
|
||||||
|
- mountPath: /scripts
|
||||||
|
name: scripts
|
||||||
|
volumes:
|
||||||
|
- name: reddit-intel
|
||||||
|
hostPath:
|
||||||
|
path: /opt/reddit-intel
|
||||||
|
type: Directory
|
||||||
|
# RCLONE_CONF (Infisical /backup-system) contiene mega+b2 desde 2026-07-15
|
||||||
|
- name: rclone-conf
|
||||||
|
secret:
|
||||||
|
secretName: rclone-conf-infisical
|
||||||
|
items:
|
||||||
|
- key: RCLONE_CONF
|
||||||
|
path: rclone.conf
|
||||||
|
- name: scripts
|
||||||
|
configMap:
|
||||||
|
name: backup-scripts
|
||||||
|
defaultMode: 0755
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: secrets.infisical.com/v1beta1
|
||||||
|
kind: InfisicalStaticSecret
|
||||||
|
metadata:
|
||||||
|
name: rclone-conf
|
||||||
|
namespace: backup-system
|
||||||
|
spec:
|
||||||
|
# Identidad compartida read-only (sin write grant) — el valor RCLONE_CONF
|
||||||
|
# se gestiona desde la UI de Infisical (proyecto homelab / prod / /backup-system).
|
||||||
|
infisicalAuthRef:
|
||||||
|
name: infisical-auth
|
||||||
|
namespace: infisical-operator
|
||||||
|
sources:
|
||||||
|
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
|
||||||
|
environmentSlug: prod
|
||||||
|
secretPath: /backup-system
|
||||||
|
syncOptions:
|
||||||
|
refreshInterval: 60s
|
||||||
|
targets:
|
||||||
|
- kind: Secret
|
||||||
|
name: rclone-conf-infisical
|
||||||
|
namespace: backup-system
|
||||||
|
creationPolicy: Owner
|
||||||
|
# El Secret lleva la key RCLONE_CONF con el contenido íntegro de rclone.conf;
|
||||||
|
# los CronJobs la montan como fichero /rclone/rclone.conf (items en el volume).
|
||||||
@@ -83,6 +83,47 @@ data:
|
|||||||
conditions:
|
conditions:
|
||||||
- evaluator: {params: [0], type: gt}
|
- evaluator: {params: [0], type: gt}
|
||||||
|
|
||||||
|
- uid: homelab-backup-job-failed
|
||||||
|
title: "Backup job fallido"
|
||||||
|
condition: C
|
||||||
|
for: 1m
|
||||||
|
noDataState: OK
|
||||||
|
execErrState: Error
|
||||||
|
annotations:
|
||||||
|
summary: "🗄️ Backup FALLIDO: {{ $labels.job_name }}\nNamespace: {{ $labels.namespace }}"
|
||||||
|
description: "El job {{ $labels.job_name }} del sistema de backups ha fallado. Cubre los CronJobs backup, rclone-mega-backup y backup-verify."
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
isPaused: false
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange: {from: 300, to: 0}
|
||||||
|
datasourceUid: prometheus
|
||||||
|
model:
|
||||||
|
editorMode: code
|
||||||
|
expr: "kube_job_status_failed{namespace=\"backup-system\"}"
|
||||||
|
instant: true
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
relativeTimeRange: {from: 0, to: 0}
|
||||||
|
datasourceUid: "-100"
|
||||||
|
model:
|
||||||
|
type: reduce
|
||||||
|
refId: B
|
||||||
|
expression: A
|
||||||
|
reducer: last
|
||||||
|
settings:
|
||||||
|
mode: ""
|
||||||
|
- refId: C
|
||||||
|
relativeTimeRange: {from: 0, to: 0}
|
||||||
|
datasourceUid: "-100"
|
||||||
|
model:
|
||||||
|
type: threshold
|
||||||
|
refId: C
|
||||||
|
expression: B
|
||||||
|
conditions:
|
||||||
|
- evaluator: {params: [0], type: gt}
|
||||||
|
|
||||||
- uid: homelab-disk-high
|
- uid: homelab-disk-high
|
||||||
title: "Disco > 80%"
|
title: "Disco > 80%"
|
||||||
condition: C
|
condition: C
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ kind: Deployment
|
|||||||
metadata:
|
metadata:
|
||||||
name: n8n
|
name: n8n
|
||||||
namespace: n8n
|
namespace: n8n
|
||||||
|
annotations:
|
||||||
|
# El operator de Infisical redespliega este Deployment cuando cambia
|
||||||
|
# n8n-secret-infisical (p.ej. rotación de GETXAPI_TOKEN) — sin kubectl manual.
|
||||||
|
secrets.infisical.com/auto-reload: "true"
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
@@ -34,6 +38,30 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: n8n-secret-infisical
|
name: n8n-secret-infisical
|
||||||
key: encryption-key
|
key: encryption-key
|
||||||
|
# Token getxapi (X autopost) — rotado 2026-07-09; el workflow lo lee
|
||||||
|
# con la expresión {{ $env.GETXAPI_TOKEN }}, nunca inline en el nodo.
|
||||||
|
- name: GETXAPI_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: n8n-secret-infisical
|
||||||
|
key: GETXAPI_TOKEN
|
||||||
|
# Token del bot @chemavx_bot (notificaciones) — rotado 2026-07-09.
|
||||||
|
# Lo consume el workflow "Health Check General" vía process.env,
|
||||||
|
# ya no hardcodeado. Fuente: Infisical homelab/prod/telegram.
|
||||||
|
- name: TELEGRAM_BOT_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: telegram-notify-infisical
|
||||||
|
key: TELEGRAM_BOT_TOKEN
|
||||||
|
- name: TELEGRAM_CHAT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: telegram-notify-infisical
|
||||||
|
key: TELEGRAM_CHAT_ID
|
||||||
|
# n8n ≥1.x bloquea $env en expresiones por defecto; sin esto el nodo
|
||||||
|
# HTTP del autopost no puede leer GETXAPI_TOKEN. Instancia single-user.
|
||||||
|
- name: N8N_BLOCK_ENV_ACCESS_IN_NODE
|
||||||
|
value: "false"
|
||||||
- name: N8N_HOST
|
- name: N8N_HOST
|
||||||
value: n8n.chemavx.xyz
|
value: n8n.chemavx.xyz
|
||||||
- name: N8N_PORT
|
- name: N8N_PORT
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ return await (async () => {
|
|||||||
|
|
||||||
// ── Configuración ────────────────────────────────────────────────────────
|
// ── Configuración ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const TELEGRAM_TOKEN = '8611913802:AAFlrFtc0vYISOliO_W8B4c-W1ue0hG9Fio';
|
const TELEGRAM_TOKEN = process.env.TELEGRAM_BOT_TOKEN /* rotado: leer de env, no hardcodear */;
|
||||||
const TELEGRAM_CHAT = '5138407666';
|
const TELEGRAM_CHAT = '5138407666';
|
||||||
const RESTART_WEBHOOK = 'https://n8n.chemavx.xyz/webhook/uptime-kuma-restart';
|
const RESTART_WEBHOOK = 'https://n8n.chemavx.xyz/webhook/uptime-kuma-restart';
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"dmPolicy": "pairing",
|
"dmPolicy": "pairing",
|
||||||
"botToken": "8611913802:AAFlrFtc0vYISOliO_W8B4c-W1ue0hG9Fio",
|
"botToken": "<set-via-infisical: homelab/prod/telegram TELEGRAM_BOT_TOKEN>",
|
||||||
"groupPolicy": "allowlist",
|
"groupPolicy": "allowlist",
|
||||||
"streaming": {
|
"streaming": {
|
||||||
"mode": "partial"
|
"mode": "partial"
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: bot-config
|
|
||||||
namespace: polymarket-bot
|
|
||||||
# Non-secret config split out of the old bot-secrets Secret. PAPER_* are plain
|
|
||||||
# operating params; WALLET_PRIVATE_KEY and COINGECKO_API_KEY are kept here as
|
|
||||||
# empty placeholders (preserving the "present-but-empty" env semantics the bot
|
|
||||||
# was started with — avoids a KeyError if the code reads them unconditionally).
|
|
||||||
data:
|
|
||||||
PAPER_BANKROLL: "10000"
|
|
||||||
PAPER_MODE: "true"
|
|
||||||
WALLET_PRIVATE_KEY: ""
|
|
||||||
COINGECKO_API_KEY: ""
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
apiVersion: batch/v1
|
|
||||||
kind: CronJob
|
|
||||||
metadata:
|
|
||||||
name: metrics-retention
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
# Daily at 00:10 UTC: collapse every past UTC day in metrics_daily to its
|
|
||||||
# last snapshot (the "daily close"). The bot writes one snapshot per cycle
|
|
||||||
# (~1,300/day); only the close of past days is read back (DISTINCT ON in
|
|
||||||
# get_metrics_history / get_daily_pnl_closes), so intraday rows of past
|
|
||||||
# days are dead weight. Today's rows are never touched: the latest one
|
|
||||||
# feeds /api/summary and the close is still evolving.
|
|
||||||
schedule: "10 0 * * *"
|
|
||||||
suspend: true
|
|
||||||
concurrencyPolicy: Forbid
|
|
||||||
successfulJobsHistoryLimit: 3
|
|
||||||
failedJobsHistoryLimit: 3
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
backoffLimit: 2
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
containers:
|
|
||||||
- name: prune
|
|
||||||
image: postgres:16-alpine
|
|
||||||
env:
|
|
||||||
- name: DATABASE_URL
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: bot-secrets-infisical
|
|
||||||
key: DATABASE_URL
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "
|
|
||||||
DELETE FROM metrics_daily m
|
|
||||||
USING (
|
|
||||||
SELECT timestamp::date AS day, MAX(timestamp) AS keep_ts
|
|
||||||
FROM metrics_daily
|
|
||||||
WHERE timestamp::date < CURRENT_DATE
|
|
||||||
GROUP BY 1
|
|
||||||
) k
|
|
||||||
WHERE m.timestamp::date = k.day
|
|
||||||
AND m.timestamp < k.keep_ts;
|
|
||||||
"
|
|
||||||
psql "$DATABASE_URL" -c "VACUUM ANALYZE metrics_daily;"
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 10m
|
|
||||||
memory: 32Mi
|
|
||||||
limits:
|
|
||||||
memory: 128Mi
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
apiVersion: batch/v1
|
|
||||||
kind: CronJob
|
|
||||||
metadata:
|
|
||||||
name: outcomes-joiner
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
# Daily at 00:30 UTC (after metrics-retention at 00:10, no overlap): fetch
|
|
||||||
# UMA-final resolutions for archived markets from the Gamma API into
|
|
||||||
# market_outcomes, then print the calibration report (Brier/log-loss of the
|
|
||||||
# model vs the market price). Replay R2 joiner — analysis only: reads the
|
|
||||||
# signals archive, writes only market_outcomes, never touches trading
|
|
||||||
# tables. Safe to also run by hand any time (kubectl create job --from=...):
|
|
||||||
# the upsert is idempotent per market_id (ON CONFLICT DO UPDATE) and the
|
|
||||||
# fetch only queries markets that still lack an outcome.
|
|
||||||
schedule: "30 0 * * *"
|
|
||||||
suspend: true
|
|
||||||
concurrencyPolicy: Forbid
|
|
||||||
successfulJobsHistoryLimit: 3
|
|
||||||
failedJobsHistoryLimit: 3
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
backoffLimit: 2
|
|
||||||
# A hung Gamma call must not block tomorrow's run through Forbid
|
|
||||||
activeDeadlineSeconds: 900
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: gitea-registry-infisical
|
|
||||||
containers:
|
|
||||||
- name: outcomes
|
|
||||||
# Same image as the bot deployment; CI bumps this tag on every
|
|
||||||
# bot image build (see polymarket-bot repo .gitea/workflows/ci.yml)
|
|
||||||
image: git.chemavx.xyz/chemavx/polymarket-bot:2b326ad5
|
|
||||||
imagePullPolicy: Always
|
|
||||||
command: ["python3", "-m", "bot.outcomes"]
|
|
||||||
envFrom:
|
|
||||||
- secretRef:
|
|
||||||
name: bot-secrets-infisical
|
|
||||||
- configMapRef:
|
|
||||||
name: bot-config
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 25m
|
|
||||||
memory: 64Mi
|
|
||||||
limits:
|
|
||||||
memory: 256Mi
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: api
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
progressDeadlineSeconds: 600
|
|
||||||
replicas: 0
|
|
||||||
revisionHistoryLimit: 10
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: api
|
|
||||||
strategy:
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 25%
|
|
||||||
maxUnavailable: 25%
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: api
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: gitea-registry-infisical
|
|
||||||
containers:
|
|
||||||
- command:
|
|
||||||
- uvicorn
|
|
||||||
- api.main:app
|
|
||||||
- --host
|
|
||||||
- 0.0.0.0
|
|
||||||
- --port
|
|
||||||
- "8000"
|
|
||||||
envFrom:
|
|
||||||
- secretRef:
|
|
||||||
name: bot-secrets-infisical
|
|
||||||
- configMapRef:
|
|
||||||
name: bot-config
|
|
||||||
image: git.chemavx.xyz/chemavx/polymarket-bot-api:7804d25c
|
|
||||||
imagePullPolicy: Always
|
|
||||||
name: api
|
|
||||||
ports:
|
|
||||||
- containerPort: 8000
|
|
||||||
protocol: TCP
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 256Mi
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
securityContext: {}
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: bot
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
progressDeadlineSeconds: 600
|
|
||||||
replicas: 0
|
|
||||||
revisionHistoryLimit: 10
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: bot
|
|
||||||
strategy:
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 25%
|
|
||||||
maxUnavailable: 25%
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: bot
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: gitea-registry-infisical
|
|
||||||
containers:
|
|
||||||
- command:
|
|
||||||
- python3
|
|
||||||
- -m
|
|
||||||
- bot.main
|
|
||||||
envFrom:
|
|
||||||
- secretRef:
|
|
||||||
name: bot-secrets-infisical
|
|
||||||
- configMapRef:
|
|
||||||
name: bot-config
|
|
||||||
image: git.chemavx.xyz/chemavx/polymarket-bot:7804d25c
|
|
||||||
imagePullPolicy: Always
|
|
||||||
name: bot
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 256Mi
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
securityContext: {}
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: dashboard
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
progressDeadlineSeconds: 600
|
|
||||||
replicas: 0
|
|
||||||
revisionHistoryLimit: 10
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: dashboard
|
|
||||||
strategy:
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 25%
|
|
||||||
maxUnavailable: 25%
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: dashboard
|
|
||||||
spec:
|
|
||||||
imagePullSecrets:
|
|
||||||
- name: gitea-registry-infisical
|
|
||||||
containers:
|
|
||||||
- image: git.chemavx.xyz/chemavx/polymarket-bot-dashboard:7804d25c
|
|
||||||
imagePullPolicy: Always
|
|
||||||
name: dashboard
|
|
||||||
ports:
|
|
||||||
- containerPort: 80
|
|
||||||
protocol: TCP
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 200m
|
|
||||||
memory: 128Mi
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 64Mi
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
securityContext: {}
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
apiVersion: secrets.infisical.com/v1beta1
|
|
||||||
kind: InfisicalStaticSecret
|
|
||||||
metadata:
|
|
||||||
name: bot-secrets
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
# Shared, read-only machine identity (no write grant, no PushSecret).
|
|
||||||
infisicalAuthRef:
|
|
||||||
name: infisical-auth
|
|
||||||
namespace: infisical-operator
|
|
||||||
sources:
|
|
||||||
# App-specific secrets (4): DATABASE_URL, POLYMARKET_API_KEY/SECRET/PASSPHRASE
|
|
||||||
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
|
|
||||||
environmentSlug: prod
|
|
||||||
secretPath: /polymarket-bot
|
|
||||||
# Telegram (2): TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID — reused from the shared
|
|
||||||
# /telegram path (deduplicated; same token as the other 5 namespaces).
|
|
||||||
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0
|
|
||||||
environmentSlug: prod
|
|
||||||
secretPath: /telegram
|
|
||||||
syncOptions:
|
|
||||||
refreshInterval: 60s
|
|
||||||
targets:
|
|
||||||
- kind: Secret
|
|
||||||
name: bot-secrets-infisical # parallel name — old bot-secrets stays live until 3c
|
|
||||||
namespace: polymarket-bot
|
|
||||||
creationPolicy: Owner
|
|
||||||
# key passthrough: synced Secret carries all 6 keys verbatim.
|
|
||||||
# PAPER_* and the empty WALLET_PRIVATE_KEY/COINGECKO_API_KEY are config,
|
|
||||||
# not secrets — they live in the bot-config ConfigMap, not here.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
apiVersion: secrets.infisical.com/v1beta1
|
|
||||||
kind: InfisicalStaticSecret
|
|
||||||
metadata:
|
|
||||||
name: telegram-notify
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
# Shared, read-only machine identity (no write grant, no PushSecret).
|
|
||||||
# NOTE: this migrates only the STANDALONE telegram-notify secret. The same
|
|
||||||
# Telegram token also embedded in bot-secrets is migration #3 — not touched here.
|
|
||||||
infisicalAuthRef:
|
|
||||||
name: infisical-auth
|
|
||||||
namespace: infisical-operator
|
|
||||||
sources:
|
|
||||||
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
|
|
||||||
environmentSlug: prod
|
|
||||||
secretPath: /telegram # shared across all 5 namespaces
|
|
||||||
syncOptions:
|
|
||||||
refreshInterval: 60s
|
|
||||||
targets:
|
|
||||||
- kind: Secret
|
|
||||||
name: telegram-notify-infisical # parallel name — old telegram-notify stays live until 3c
|
|
||||||
namespace: polymarket-bot
|
|
||||||
creationPolicy: Owner
|
|
||||||
# key passthrough: synced Secret carries TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID verbatim
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: polymarket-bot
|
|
||||||
namespace: polymarket-bot
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
|
||||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
|
||||||
spec:
|
|
||||||
ingressClassName: traefik
|
|
||||||
rules:
|
|
||||||
- host: polymarket.chemavx.xyz
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- backend:
|
|
||||||
service:
|
|
||||||
name: api
|
|
||||||
port:
|
|
||||||
number: 8000
|
|
||||||
path: /api
|
|
||||||
pathType: Prefix
|
|
||||||
- backend:
|
|
||||||
service:
|
|
||||||
name: dashboard
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- polymarket.chemavx.xyz
|
|
||||||
secretName: polymarket-tls
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
# Never let ArgoCD prune this PVC: it holds the paper-trading postgres data.
|
|
||||||
argocd.argoproj.io/sync-options: Prune=false
|
|
||||||
pv.kubernetes.io/bind-completed: 'yes'
|
|
||||||
pv.kubernetes.io/bound-by-controller: 'yes'
|
|
||||||
volume.beta.kubernetes.io/storage-provisioner: rancher.io/local-path
|
|
||||||
volume.kubernetes.io/selected-node: chemavx-k8
|
|
||||||
volume.kubernetes.io/storage-provisioner: rancher.io/local-path
|
|
||||||
labels:
|
|
||||||
app: postgres
|
|
||||||
name: data-postgres-0
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
storageClassName: local-path
|
|
||||||
volumeMode: Filesystem
|
|
||||||
volumeName: pvc-327bbc6d-dcc6-46b4-9d78-db950f71eb98
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: api
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
clusterIP: 10.43.201.4
|
|
||||||
clusterIPs:
|
|
||||||
- 10.43.201.4
|
|
||||||
internalTrafficPolicy: Cluster
|
|
||||||
ipFamilies:
|
|
||||||
- IPv4
|
|
||||||
ipFamilyPolicy: SingleStack
|
|
||||||
ports:
|
|
||||||
- port: 8000
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 8000
|
|
||||||
selector:
|
|
||||||
app: api
|
|
||||||
sessionAffinity: None
|
|
||||||
type: ClusterIP
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: dashboard
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
clusterIP: 10.43.62.6
|
|
||||||
clusterIPs:
|
|
||||||
- 10.43.62.6
|
|
||||||
internalTrafficPolicy: Cluster
|
|
||||||
ipFamilies:
|
|
||||||
- IPv4
|
|
||||||
ipFamilyPolicy: SingleStack
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 80
|
|
||||||
selector:
|
|
||||||
app: dashboard
|
|
||||||
sessionAffinity: None
|
|
||||||
type: ClusterIP
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: postgres
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
clusterIP: 10.43.238.53
|
|
||||||
clusterIPs:
|
|
||||||
- 10.43.238.53
|
|
||||||
internalTrafficPolicy: Cluster
|
|
||||||
ipFamilies:
|
|
||||||
- IPv4
|
|
||||||
ipFamilyPolicy: SingleStack
|
|
||||||
ports:
|
|
||||||
- port: 5432
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 5432
|
|
||||||
selector:
|
|
||||||
app: postgres
|
|
||||||
sessionAffinity: None
|
|
||||||
type: ClusterIP
|
|
||||||
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: postgres
|
|
||||||
namespace: polymarket-bot
|
|
||||||
spec:
|
|
||||||
persistentVolumeClaimRetentionPolicy:
|
|
||||||
whenDeleted: Retain
|
|
||||||
whenScaled: Retain
|
|
||||||
podManagementPolicy: OrderedReady
|
|
||||||
replicas: 0
|
|
||||||
revisionHistoryLimit: 10
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: postgres
|
|
||||||
serviceName: postgres
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: postgres
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- env:
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
value: bot
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
value: bot
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
value: polymarket
|
|
||||||
image: postgres:16-alpine
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
name: postgres
|
|
||||||
ports:
|
|
||||||
- containerPort: 5432
|
|
||||||
protocol: TCP
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- pg_isready
|
|
||||||
- -U
|
|
||||||
- bot
|
|
||||||
failureThreshold: 3
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 1
|
|
||||||
resources: {}
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /var/lib/postgresql/data
|
|
||||||
name: data
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
securityContext: {}
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
updateStrategy:
|
|
||||||
rollingUpdate:
|
|
||||||
partition: 0
|
|
||||||
type: RollingUpdate
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: data
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
volumeMode: Filesystem
|
|
||||||
status:
|
|
||||||
phase: Pending
|
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ data:
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="ChemaVX — homelab k3s self-hosted: GitOps con ArgoCD, secrets con Infisical, backups offsite duales y publicación bilingüe." />
|
||||||
<title>ChemaVX</title>
|
<title>ChemaVX</title>
|
||||||
<style>
|
<style>
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@@ -37,7 +38,7 @@ data:
|
|||||||
/* ── Header ─────────────────────────────────── */
|
/* ── Header ─────────────────────────────────── */
|
||||||
header {
|
header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 3.5rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 80px; height: 80px;
|
width: 80px; height: 80px;
|
||||||
@@ -59,6 +60,18 @@ data:
|
|||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.4; }
|
50% { opacity: 0.4; }
|
||||||
}
|
}
|
||||||
|
.badges {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.5rem;
|
||||||
|
justify-content: center; margin-top: 1.1rem;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
font-size: 0.7rem; color: var(--muted);
|
||||||
|
border: 1px solid var(--border); border-radius: 20px;
|
||||||
|
padding: 3px 11px; background: var(--surface);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.badge b { color: var(--text); font-weight: 600; }
|
||||||
|
|
||||||
/* ── Section title ───────────────────────────── */
|
/* ── Section title ───────────────────────────── */
|
||||||
.section-title {
|
.section-title {
|
||||||
@@ -79,7 +92,7 @@ data:
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 1rem 1.1rem;
|
padding: 0.9rem 1.1rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
|
transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
|
||||||
@@ -91,7 +104,8 @@ data:
|
|||||||
box-shadow: 0 4px 16px rgba(88,166,255,0.1);
|
box-shadow: 0 4px 16px rgba(88,166,255,0.1);
|
||||||
}
|
}
|
||||||
.service-icon { font-size: 1.3rem; flex-shrink: 0; }
|
.service-icon { font-size: 1.3rem; flex-shrink: 0; }
|
||||||
.service-name { font-size: 0.9rem; font-weight: 500; }
|
.service-name { font-size: 0.9rem; font-weight: 500; line-height: 1.25; }
|
||||||
|
.service-sub { font-size: 0.68rem; color: var(--muted); margin-top: 1px; }
|
||||||
|
|
||||||
/* ── Polymarket card ─────────────────────────── */
|
/* ── Polymarket card ─────────────────────────── */
|
||||||
.poly-wrapper { width: 100%; max-width: 720px; }
|
.poly-wrapper { width: 100%; max-width: 720px; }
|
||||||
@@ -167,6 +181,13 @@ data:
|
|||||||
<p class="tagline">
|
<p class="tagline">
|
||||||
<span class="dot"></span>Homelab · k3s · Self-hosted
|
<span class="dot"></span>Homelab · k3s · Self-hosted
|
||||||
</p>
|
</p>
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge"><b>k3s</b> · 2 nodos</span>
|
||||||
|
<span class="badge"><b>GitOps</b> · ArgoCD</span>
|
||||||
|
<span class="badge"><b>Secrets</b> · Infisical</span>
|
||||||
|
<span class="badge"><b>Backups</b> · Mega + B2</span>
|
||||||
|
<span class="badge"><b>Observabilidad</b> · Grafana + Kuma</span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
@@ -175,35 +196,51 @@ data:
|
|||||||
<div class="services-grid">
|
<div class="services-grid">
|
||||||
<a class="service-card" href="https://grafana.chemavx.xyz" target="_blank" rel="noopener">
|
<a class="service-card" href="https://grafana.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
<span class="service-icon">📊</span>
|
<span class="service-icon">📊</span>
|
||||||
<span class="service-name">Grafana</span>
|
<span><span class="service-name">Grafana</span><div class="service-sub">Dashboards y alertas</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://status.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">💓</span>
|
||||||
|
<span><span class="service-name">Uptime Kuma</span><div class="service-sub">Estado de servicios</div></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="service-card" href="https://n8n.chemavx.xyz" target="_blank" rel="noopener">
|
<a class="service-card" href="https://n8n.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
<span class="service-icon">⚙️</span>
|
<span class="service-icon">⚙️</span>
|
||||||
<span class="service-name">n8n</span>
|
<span><span class="service-name">n8n</span><div class="service-sub">Automatización</div></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="service-card" href="https://home.chemavx.xyz" target="_blank" rel="noopener">
|
<a class="service-card" href="https://home.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
<span class="service-icon">🏠</span>
|
<span class="service-icon">🏠</span>
|
||||||
<span class="service-name">Homarr</span>
|
<span><span class="service-name">Homarr</span><div class="service-sub">Panel del homelab</div></span>
|
||||||
</a>
|
|
||||||
<a class="service-card" href="https://git.chemavx.xyz/chemavx/polymarket-bot" target="_blank" rel="noopener">
|
|
||||||
<span class="service-icon">📈</span>
|
|
||||||
<span class="service-name">Polymarket Bot (archived)</span>
|
|
||||||
</a>
|
|
||||||
<a class="service-card" href="https://chat.chemavx.xyz" target="_blank" rel="noopener">
|
|
||||||
<span class="service-icon">🤖</span>
|
|
||||||
<span class="service-name">Open WebUI</span>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="service-card" href="https://git.chemavx.xyz" target="_blank" rel="noopener">
|
<a class="service-card" href="https://git.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
<span class="service-icon">🐙</span>
|
<span class="service-icon">🐙</span>
|
||||||
<span class="service-name">Gitea</span>
|
<span><span class="service-name">Gitea</span><div class="service-sub">Git + CI + registry</div></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="service-card" href="https://argocd.chemavx.xyz" target="_blank" rel="noopener">
|
<a class="service-card" href="https://argocd.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
<span class="service-icon">🔄</span>
|
<span class="service-icon">🔄</span>
|
||||||
<span class="service-name">ArgoCD</span>
|
<span><span class="service-name">ArgoCD</span><div class="service-sub">GitOps del cluster</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://infisical.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">🗝️</span>
|
||||||
|
<span><span class="service-name">Infisical</span><div class="service-sub">Gestor de secretos</div></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="service-card" href="https://vaultwarden.chemavx.xyz" target="_blank" rel="noopener">
|
<a class="service-card" href="https://vaultwarden.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
<span class="service-icon">🔐</span>
|
<span class="service-icon">🔐</span>
|
||||||
<span class="service-name">Vaultwarden</span>
|
<span><span class="service-name">Vaultwarden</span><div class="service-sub">Contraseñas</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://trilium.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">📝</span>
|
||||||
|
<span><span class="service-name">Trilium</span><div class="service-sub">Notas</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://files.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">📁</span>
|
||||||
|
<span><span class="service-name">Files</span><div class="service-sub">Filebrowser</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://umami.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">📈</span>
|
||||||
|
<span><span class="service-name">Umami</span><div class="service-sub">Analytics sin cookies</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://ollama.chemavx.xyz" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">🦙</span>
|
||||||
|
<span><span class="service-name">Ollama</span><div class="service-sub">LLM local</div></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,9 +249,21 @@ data:
|
|||||||
<div style="width:100%;max-width:720px;margin-bottom:2.5rem;">
|
<div style="width:100%;max-width:720px;margin-bottom:2.5rem;">
|
||||||
<p class="section-title">Projects</p>
|
<p class="section-title">Projects</p>
|
||||||
<div class="services-grid">
|
<div class="services-grid">
|
||||||
<a class="service-card" href="/ze">
|
<a class="service-card" href="https://theexclusionzone.com" target="_blank" rel="noopener">
|
||||||
<span class="service-icon">🛸</span>
|
<span class="service-icon">🛸</span>
|
||||||
<span class="service-name">ze</span>
|
<span><span class="service-name">The Exclusion Zone</span><div class="service-sub">UAP research · EN</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://zonadeexclusion.com" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">📡</span>
|
||||||
|
<span><span class="service-name">Zona de Exclusión</span><div class="service-sub">Investigación UAP · ES</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="https://git.chemavx.xyz/chemavx/researchowl" target="_blank" rel="noopener">
|
||||||
|
<span class="service-icon">🦉</span>
|
||||||
|
<span><span class="service-name">ResearchOwl</span><div class="service-sub">Bot de investigación</div></span>
|
||||||
|
</a>
|
||||||
|
<a class="service-card" href="/ze/">
|
||||||
|
<span class="service-icon">⌨️</span>
|
||||||
|
<span><span class="service-name">ze</span><div class="service-sub">CLI · manual</div></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,7 +274,7 @@ data:
|
|||||||
<div class="poly-card">
|
<div class="poly-card">
|
||||||
<div class="poly-header">
|
<div class="poly-header">
|
||||||
<div class="poly-title">
|
<div class="poly-title">
|
||||||
<span>📈</span>
|
<span>📉</span>
|
||||||
<span>Paper Trading — Final Results</span>
|
<span>Paper Trading — Final Results</span>
|
||||||
<span class="poly-badge">ARCHIVED</span>
|
<span class="poly-badge">ARCHIVED</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,6 +313,7 @@ data:
|
|||||||
<p>
|
<p>
|
||||||
Hosted on <strong>k3s</strong> —
|
Hosted on <strong>k3s</strong> —
|
||||||
<a href="https://grafana.chemavx.xyz/d/chemavx-homelab-v1/chemavx-homelab-overview" target="_blank">Cluster Dashboard</a>
|
<a href="https://grafana.chemavx.xyz/d/chemavx-homelab-v1/chemavx-homelab-overview" target="_blank">Cluster Dashboard</a>
|
||||||
|
— <a href="https://status.chemavx.xyz" target="_blank">Status</a>
|
||||||
— <a href="https://git.chemavx.xyz" target="_blank">Gitea</a>
|
— <a href="https://git.chemavx.xyz" target="_blank">Gitea</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+4
-4
@@ -2,7 +2,7 @@ apiVersion: secrets.infisical.com/v1beta1
|
|||||||
kind: InfisicalStaticSecret
|
kind: InfisicalStaticSecret
|
||||||
metadata:
|
metadata:
|
||||||
name: gitea-registry
|
name: gitea-registry
|
||||||
namespace: polymarket-bot
|
namespace: reddit-intel
|
||||||
spec:
|
spec:
|
||||||
# References the SHARED auth layer in infisical-operator (cross-namespace).
|
# References the SHARED auth layer in infisical-operator (cross-namespace).
|
||||||
infisicalAuthRef:
|
infisicalAuthRef:
|
||||||
@@ -11,13 +11,13 @@ spec:
|
|||||||
sources:
|
sources:
|
||||||
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
|
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
|
||||||
environmentSlug: prod
|
environmentSlug: prod
|
||||||
secretPath: /registry # shared /registry v3 fields (same as researchowl)
|
secretPath: /registry
|
||||||
syncOptions:
|
syncOptions:
|
||||||
refreshInterval: 60s
|
refreshInterval: 60s
|
||||||
targets:
|
targets:
|
||||||
- kind: Secret
|
- kind: Secret
|
||||||
name: gitea-registry-infisical # parallel name — old gitea-registry stays live until cutover
|
name: gitea-registry
|
||||||
namespace: polymarket-bot
|
namespace: reddit-intel
|
||||||
creationPolicy: Owner
|
creationPolicy: Owner
|
||||||
secretType: kubernetes.io/dockerconfigjson
|
secretType: kubernetes.io/dockerconfigjson
|
||||||
template:
|
template:
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -44,7 +44,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: researchowl
|
- name: researchowl
|
||||||
image: git.chemavx.xyz/chemavx/researchowl:d31badeb
|
image: git.chemavx.xyz/chemavx/researchowl:dadc030d
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
env:
|
env:
|
||||||
- name: TELEGRAM_BOT_TOKEN
|
- name: TELEGRAM_BOT_TOKEN
|
||||||
@@ -104,7 +104,7 @@ spec:
|
|||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
cpu: "100m"
|
cpu: "100m"
|
||||||
limits:
|
limits:
|
||||||
memory: "1Gi"
|
memory: "2Gi"
|
||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
|
|||||||
@@ -1,18 +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
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata:
|
metadata:
|
||||||
name: polymarket-bot
|
name: roswell
|
||||||
namespace: argocd
|
namespace: argocd
|
||||||
spec:
|
spec:
|
||||||
destination:
|
|
||||||
namespace: polymarket-bot
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
project: default
|
project: default
|
||||||
source:
|
source:
|
||||||
path: polymarket-bot
|
|
||||||
repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests
|
repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests
|
||||||
targetRevision: main
|
targetRevision: HEAD
|
||||||
|
path: roswell
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: roswell
|
||||||
syncPolicy:
|
syncPolicy:
|
||||||
automated:
|
automated:
|
||||||
prune: true
|
prune: true
|
||||||
selfHeal: true
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -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-infisical
|
||||||
|
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
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Bot + scheduler en un pod (PLAN §Fase 6). Los secretos vienen de Infisical
|
||||||
|
# (carpeta /roswell del proyecto homelab) vía infisical-roswell-secrets.yaml.
|
||||||
|
---
|
||||||
|
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:09457a47
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: TELEGRAM_BOT_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: roswell-secrets-infisical
|
||||||
|
key: telegram-bot-token
|
||||||
|
- name: TELEGRAM_ALLOWED_USERS
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: roswell-secrets-infisical
|
||||||
|
key: telegram-allowed-users
|
||||||
|
- name: ANTHROPIC_API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: roswell-secrets-infisical
|
||||||
|
key: anthropic-api-key
|
||||||
|
- name: DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: roswell-secrets-infisical
|
||||||
|
key: database-url
|
||||||
|
- name: PODCASTINDEX_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: roswell-secrets-infisical
|
||||||
|
key: podcastindex-key
|
||||||
|
- name: PODCASTINDEX_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: roswell-secrets-infisical
|
||||||
|
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 INT8 en CPU: el pico real supera los 4Gi que
|
||||||
|
# estimaba el PLAN §10 (OOMKilled verificado el 2026-07-13).
|
||||||
|
# Sin límite de CPU: la transcripción usa los cores libres.
|
||||||
|
memory: "6Gi"
|
||||||
|
# Nodo grande (16 cores / 29Gi): transcribe a ~1x tiempo real y comparte
|
||||||
|
# nodo con los PVC de datos y backups. El nodo pequeño (4 cores/12Gi)
|
||||||
|
# se saturaba con Whisper.
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: chemavx-k8
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: roswell-data
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitea-registry
|
||||||
@@ -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 }}"}}}'
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Secretos de la app respaldados por Infisical (patrón researchowl).
|
||||||
|
#
|
||||||
|
# Claves esperadas en /roswell (proyecto homelab, entorno prod):
|
||||||
|
# TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_USERS, ANTHROPIC_API_KEY,
|
||||||
|
# PODCASTINDEX_KEY, PODCASTINDEX_SECRET, POSTGRES_PASSWORD
|
||||||
|
#
|
||||||
|
# Un InfisicalStaticSecret por target: el operator (versión actual) solo
|
||||||
|
# procesa el primer elemento de `targets`, comprobado el 2026-07-13.
|
||||||
|
#
|
||||||
|
# CORTE (cuando los valores estén verificados): cambiar las referencias de
|
||||||
|
# deployment.yaml, postgres.yaml y backup-cronjob.yaml a los nombres
|
||||||
|
# *-infisical y borrar los planos:
|
||||||
|
# kubectl delete secret roswell-secrets roswell-pg-secret -n roswell
|
||||||
|
apiVersion: secrets.infisical.com/v1beta1
|
||||||
|
kind: InfisicalStaticSecret
|
||||||
|
metadata:
|
||||||
|
name: roswell-secrets
|
||||||
|
namespace: roswell
|
||||||
|
spec:
|
||||||
|
# Identidad compartida de solo lectura (sin write, sin PushSecret).
|
||||||
|
infisicalAuthRef:
|
||||||
|
name: infisical-auth
|
||||||
|
namespace: infisical-operator
|
||||||
|
sources:
|
||||||
|
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
|
||||||
|
environmentSlug: prod
|
||||||
|
secretPath: /roswell
|
||||||
|
syncOptions:
|
||||||
|
refreshInterval: 60s
|
||||||
|
targets:
|
||||||
|
- kind: Secret
|
||||||
|
name: roswell-secrets-infisical
|
||||||
|
namespace: roswell
|
||||||
|
creationPolicy: Owner
|
||||||
|
template:
|
||||||
|
engineVersion: v1
|
||||||
|
data:
|
||||||
|
telegram-bot-token: '{{ .TELEGRAM_BOT_TOKEN.Value }}'
|
||||||
|
telegram-allowed-users: '{{ .TELEGRAM_ALLOWED_USERS.Value }}'
|
||||||
|
anthropic-api-key: '{{ .ANTHROPIC_API_KEY.Value }}'
|
||||||
|
podcastindex-key: '{{ .PODCASTINDEX_KEY.Value }}'
|
||||||
|
podcastindex-secret: '{{ .PODCASTINDEX_SECRET.Value }}'
|
||||||
|
# La URL completa se reconstruye: un solo sitio para la password.
|
||||||
|
database-url: 'postgresql://roswell:{{ .POSTGRES_PASSWORD.Value }}@postgres.roswell.svc.cluster.local:5432/roswell'
|
||||||
|
---
|
||||||
|
apiVersion: secrets.infisical.com/v1beta1
|
||||||
|
kind: InfisicalStaticSecret
|
||||||
|
metadata:
|
||||||
|
name: roswell-pg-secret
|
||||||
|
namespace: roswell
|
||||||
|
spec:
|
||||||
|
infisicalAuthRef:
|
||||||
|
name: infisical-auth
|
||||||
|
namespace: infisical-operator
|
||||||
|
sources:
|
||||||
|
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
|
||||||
|
environmentSlug: prod
|
||||||
|
secretPath: /roswell
|
||||||
|
syncOptions:
|
||||||
|
refreshInterval: 60s
|
||||||
|
targets:
|
||||||
|
- kind: Secret
|
||||||
|
name: roswell-pg-secret-infisical
|
||||||
|
namespace: roswell
|
||||||
|
creationPolicy: Owner
|
||||||
|
template:
|
||||||
|
engineVersion: v1
|
||||||
|
data:
|
||||||
|
password: '{{ .POSTGRES_PASSWORD.Value }}'
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Postgres 16 + pgvector para el corpus (namespace roswell).
|
||||||
|
# El Secret roswell-pg-secret se crea imperativamente (PLAN Fase 6);
|
||||||
|
# migración a Infisical preparada en infisical-roswell-secrets.yaml.
|
||||||
|
# ClusterIP (cierre Fase 6): con el bot ya en el clúster el NodePort 30432
|
||||||
|
# sobraba. Acceso local puntual: desde un nodo del clúster la ClusterIP
|
||||||
|
# responde directa; desde fuera, kubectl port-forward -n roswell svc/postgres 15432:5432.
|
||||||
|
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-infisical
|
||||||
|
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: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: roswell-postgres
|
||||||
|
ports:
|
||||||
|
- port: 5432
|
||||||
|
targetPort: 5432
|
||||||
Reference in New Issue
Block a user