Files
k8s-manifests/n8n/cronjob-workflow-monitor.yaml
T
chemavxandClaude Opus 4.8 ef75792c18 n8n: vigilante contra reversiones de BD (alerta Telegram)
CronJob externo a n8n que lee la SQLite en solo-lectura por el hostPath y avisa
si el workflow del autopost desaparece (firma de la reversión del 2026-07-16) o
si el total de workflows se desploma. La reversión SUBE el nº de activos, así que
un umbral de activos no la cazaría; el disparador es la desaparición del autopost.
Cada 6h. Reutiliza telegram-notify-infisical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:45:20 +00:00

158 lines
5.4 KiB
YAML

# Vigilante de n8n contra reversiones de BD (como la del 2026-07-16, en que la
# SQLite de n8n se revirtió a un snapshot de abril durante los reinicios de
# kernel: desaparecieron los workflows de julio —incluido el autopost de Ghost
# EN— y volvieron activados los bots viejos, sin que nadie lo notara en una
# semana, hasta que la copia buena se purgó de los backups por la retención).
#
# Lee la SQLite EN SOLO LECTURA por el hostPath /data/n8n (independiente del pod
# de n8n: funciona aunque n8n esté roto) y avisa por Telegram si el workflow del
# autopost DESAPARECE (la firma de la reversión; ojo: la reversión SUBE el nº de
# activos, así que un umbral de "activos" no la cazaría) o si el total de
# workflows se desploma. Cada 6 h: caza dentro del día, deja margen de sobra
# antes de que la retención de 7 días borre la última copia buena.
---
apiVersion: v1
kind: ConfigMap
metadata:
name: n8n-monitor-script
namespace: n8n
data:
n8n_monitor.py: |
#!/usr/bin/env python3
"""Vigilante de n8n: caza una reversión de BD. Sin deps externas."""
import os
import sqlite3
import sys
import urllib.parse
import urllib.request
DB = os.environ.get("N8N_DB", "/n8n/database.sqlite")
TOTAL_FLOOR = int(os.environ.get("TOTAL_FLOOR", "18"))
AUTOPOST_MATCH = os.environ.get("AUTOPOST_MATCH", "%X Autopost%")
def send_telegram(text):
token = os.environ["TELEGRAM_BOT_TOKEN"]
chat = os.environ["TELEGRAM_CHAT_ID"]
data = urllib.parse.urlencode(
{"chat_id": chat, "text": text, "disable_web_page_preview": "true"}
).encode()
req = urllib.request.Request(
f"https://api.telegram.org/bot{token}/sendMessage", data=data
)
with urllib.request.urlopen(req, timeout=20) as r:
r.read()
def main():
if "--install-test" in sys.argv:
send_telegram("✅ n8n workflow monitor instalado y funcionando (prueba).")
print("mensaje de prueba enviado")
return
try:
con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True, timeout=30)
total = con.execute("SELECT COUNT(*) FROM workflow_entity").fetchone()[0]
active = con.execute(
"SELECT COUNT(*) FROM workflow_entity WHERE active=1"
).fetchone()[0]
ap = con.execute(
"SELECT active FROM workflow_entity WHERE name LIKE ?", (AUTOPOST_MATCH,)
).fetchone()
con.close()
except Exception as e: # noqa: BLE001
send_telegram(
f"🚨 n8n monitor: no pude leer la BD de n8n ({e}). ¿Pod/DB caídos?"
)
print("ERROR leyendo la BD:", e)
sys.exit(1)
problems = []
if ap is None:
problems.append(
"el workflow «ExclusionZone → X Autopost» ha DESAPARECIDO — "
"firma de reversion de BD (como el 2026-07-16)"
)
if total < TOTAL_FLOOR:
problems.append(
f"solo {total} workflows (esperados >={TOTAL_FLOOR}) — posible perdida de datos"
)
if problems:
msg = (
"🚨 ALERTA n8n:\n"
+ "\n".join("• " + p for p in problems)
+ f"\n\n(total={total}, activos={active})\n"
+ "Runbook: k8s-manifests/backup-system/RESTORE.md (Escenario A)"
)
send_telegram(msg)
print("ALERTA enviada:", problems)
sys.exit(1)
print(f"OK: total={total} activos={active} autopost={'ON' if ap[0] else 'OFF'}")
if __name__ == "__main__":
main()
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: n8n-workflow-monitor
namespace: n8n
spec:
schedule: "0 */6 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 120
template:
metadata:
labels:
app: n8n-workflow-monitor
spec:
restartPolicy: Never
# /data/n8n vive en chemavx-k8; hay que leerlo desde ahí.
nodeSelector:
kubernetes.io/hostname: chemavx-k8
securityContext:
runAsUser: 1000
runAsGroup: 1000
containers:
- name: monitor
image: python:3.12-alpine@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df
command: ["python3", "/scripts/n8n_monitor.py"]
envFrom:
# TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID (sincronizado de Infisical)
- secretRef:
name: telegram-notify-infisical
env:
- name: N8N_DB
value: /n8n/database.sqlite
- name: TOTAL_FLOOR
value: "18"
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: n8n-data
mountPath: /n8n
readOnly: true
- name: script
mountPath: /scripts
volumes:
- name: n8n-data
hostPath:
path: /data/n8n
type: Directory
- name: script
configMap:
name: n8n-monitor-script