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>
158 lines
5.4 KiB
YAML
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
|