n8n: copia en git del workflow del auto-reinicio
Los workflows viven solo en la SQLite, que ya se revirtio sola una vez (16-jul, a un snapshot de abril) y se llevo por delante mes y medio de trabajo. Este en concreto es el que reacciona a las caidas, asi que conviene tenerlo fuera. Ya no hay nada secreto que ocultar: los tres nodos de Telegram leen el token de $env.TELEGRAM_BOT_TOKEN en vez de llevarlo incrustado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"name": "Uptime Kuma → K8s Auto-Restart",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "uptime-kuma-restart",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-1111-1111-1111-000000000001",
|
||||||
|
"name": "Uptime Kuma Webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
256,
|
||||||
|
304
|
||||||
|
],
|
||||||
|
"webhookId": "uptime-kuma-restart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const body = $input.first().json.body || $input.first().json;\nconst monitor = body.monitor || {};\nconst heartbeat = body.heartbeat || {};\n\n// Solo lo que existe de verdad en el cluster (revisado 2026-07-30): fuera\n// OpenClaw, los tres polymarket-* y open-webui, decomisados en julio.\n// 'kind' es el plural que va en la ruta del API: Gitea es un StatefulSet,\n// no un Deployment, y con 'deployments' fijo daba 404 aunque hubiera permisos.\nconst SERVICE_MAP = {\n 'n8n': { kind: 'deployments', name: 'n8n', namespace: 'n8n' },\n 'Vaultwarden': { kind: 'deployments', name: 'vaultwarden', namespace: 'vaultwarden' },\n 'Grafana': { kind: 'deployments', name: 'kube-prometheus-stack-grafana', namespace: 'monitoring' },\n 'Uptime Kuma': { kind: 'deployments', name: 'uptime-kuma', namespace: 'monitoring' },\n 'Authentik': { kind: 'deployments', name: 'authentik-server', namespace: 'authentik' },\n 'Homarr': { kind: 'deployments', name: 'homarr', namespace: 'homarr' },\n 'ArgoCD': { kind: 'deployments', name: 'argocd-server', namespace: 'argocd' },\n 'ArgoCD repo-server': { kind: 'deployments', name: 'argocd-repo-server', namespace: 'argocd' },\n 'Ollama': { kind: 'deployments', name: 'ollama', namespace: 'ollama' },\n 'Gitea': { kind: 'statefulsets', name: 'gitea', namespace: 'gitea' },\n // Los blogs (2026-07-30). La clave es el NOMBRE EXACTO del monitor en Kuma,\n // que se busca sin distinguir mayusculas -- si se renombra alli, aqui deja de\n // encontrarse y el workflow se va por la rama de \"sin mapeo\".\n // El monitor \"www.zonadeexclusion.com (301->apex)\" queda FUERA a proposito:\n // que ese 301 falle es cosa de Traefik o del DNS, no de Ghost, y reiniciar el\n // blog no arreglaria nada.\n 'theexclusionzone.com (Ghost EN)': { kind: 'deployments', name: 'ghost-en', namespace: 'ghost-en' },\n 'zonadeexclusion.com (Ghost ES)': { kind: 'deployments', name: 'zona-exclusion', namespace: 'zona-exclusion' },\n};\n\nconst serviceName = monitor.name || 'desconocido';\nconst status = heartbeat.status;\n\nif (status !== 0) {\n return [];\n}\n\n// Busqueda case-insensitive\nconst k8s = SERVICE_MAP[serviceName] ||\n Object.entries(SERVICE_MAP).find(([k]) => k.toLowerCase() === serviceName.toLowerCase())?.[1];\n\nif (!k8s) {\n return [{ json: {\n serviceName,\n canRestart: false,\n error: 'Sin mapeo K8s para: ' + serviceName\n }}];\n}\n\n// Se sigue llamando 'deployment' aunque a veces sea un StatefulSet: el nodo IF\n// \"Tiene mapeo K8s\" comprueba ese campo, y renombrarlo lo romperia en silencio.\nreturn [{ json: {\n serviceName,\n canRestart: true,\n deployment: k8s.name,\n kind: k8s.kind,\n namespace: k8s.namespace\n}}];\n"
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-2222-2222-2222-000000000002",
|
||||||
|
"name": "Parse & Map Service",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
480,
|
||||||
|
304
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"value1": "={{ $json.deployment || '' }}",
|
||||||
|
"operation": "isNotEmpty"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-3333-3333-3333-000000000003",
|
||||||
|
"name": "Tiene mapeo K8s",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
704,
|
||||||
|
304
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const https = require('https');\nconst fs = require('fs');\n\nconst token = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8').trim();\nconst ca = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt');\n\nconst deployment = $input.first().json.deployment;\nconst namespace = $input.first().json.namespace;\nconst serviceName = $input.first().json.serviceName;\n// Sin kind, deployments: asi un workflow viejo en vuelo no se rompe.\nconst kind = $input.first().json.kind || 'deployments';\n\nconst now = new Date().toISOString();\nconst patchBody = JSON.stringify({\n spec: { template: { metadata: { annotations: { 'kubectl.kubernetes.io/restartedAt': now } } } }\n});\n\nreturn new Promise((resolve) => {\n const req = https.request({\n hostname: 'kubernetes.default.svc',\n port: 443,\n path: `/apis/apps/v1/namespaces/${namespace}/${kind}/${deployment}`,\n method: 'PATCH',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/strategic-merge-patch+json',\n 'Content-Length': Buffer.byteLength(patchBody)\n },\n ca\n }, (res) => {\n let data = '';\n res.on('data', c => data += c);\n res.on('end', () => {\n const ok = res.statusCode >= 200 && res.statusCode < 300;\n resolve([{ json: { serviceName, deployment, kind, namespace, restartOk: ok, statusCode: res.statusCode } }]);\n });\n });\n req.on('error', (e) => resolve([{ json: { serviceName, deployment, kind, namespace, restartOk: false, error: e.message } }]));\n req.write(patchBody);\n req.end();\n});"
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-4444-4444-4444-000000000004",
|
||||||
|
"name": "K8s API Restart",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
912,
|
||||||
|
192
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"amount": 60,
|
||||||
|
"unit": "seconds"
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-5555-5555-5555-000000000005",
|
||||||
|
"name": "Esperar 60s",
|
||||||
|
"type": "n8n-nodes-base.wait",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1136,
|
||||||
|
192
|
||||||
|
],
|
||||||
|
"webhookId": "wait-resume-uptime-001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const https = require('https');\nconst fs = require('fs');\n\nconst token = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8').trim();\nconst ca = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt');\n\nconst deployment = $input.first().json.deployment;\nconst namespace = $input.first().json.namespace;\nconst serviceName = $input.first().json.serviceName;\n// Sin kind, deployments: asi un workflow viejo en vuelo no se rompe.\nconst kind = $input.first().json.kind || 'deployments';\n\nreturn new Promise((resolve) => {\n const req = https.request({\n hostname: 'kubernetes.default.svc',\n port: 443,\n path: `/apis/apps/v1/namespaces/${namespace}/${kind}/${deployment}`,\n method: 'GET',\n headers: { 'Authorization': `Bearer ${token}` },\n ca\n }, (res) => {\n let data = '';\n res.on('data', c => data += c);\n res.on('end', () => {\n try {\n const dep = JSON.parse(data);\n const desired = dep.spec.replicas || 1;\n const ready = dep.status.readyReplicas || 0;\n const updated = dep.status.updatedReplicas || 0;\n const statusOk = ready >= desired && updated >= desired;\n resolve([{ json: { serviceName, deployment, kind, namespace, statusOk, ready, desired } }]);\n } catch (e) {\n resolve([{ json: { serviceName, deployment, kind, namespace, statusOk: false, error: e.message } }]);\n }\n });\n });\n req.on('error', (e) => resolve([{ json: { serviceName, deployment, kind, namespace, statusOk: false, error: e.message } }]));\n req.end();\n});"
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-6666-6666-6666-000000000006",
|
||||||
|
"name": "K8s API Check Status",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1360,
|
||||||
|
192
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"value1": "={{ $json.statusOk ? 'yes' : 'no' }}",
|
||||||
|
"value2": "yes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-7777-7777-7777-000000000007",
|
||||||
|
"name": "Reinicio Exitoso",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1584,
|
||||||
|
192
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "chat_id",
|
||||||
|
"value": "5138407666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"value": "={{ '✅ Servicio *' + $json.serviceName + '* caído detectado — reiniciado correctamente' }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "parse_mode",
|
||||||
|
"value": "Markdown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-8888-8888-8888-000000000008",
|
||||||
|
"name": "Telegram Exito",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4,
|
||||||
|
"position": [
|
||||||
|
1792,
|
||||||
|
80
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "chat_id",
|
||||||
|
"value": "5138407666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"value": "={{ '❌ Servicio *' + $json.serviceName + '* caído — reinicio fallido, intervención manual necesaria' }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "parse_mode",
|
||||||
|
"value": "Markdown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-9999-9999-9999-000000000009",
|
||||||
|
"name": "Telegram Fallo",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4,
|
||||||
|
"position": [
|
||||||
|
1792,
|
||||||
|
304
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "chat_id",
|
||||||
|
"value": "5138407666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"value": "={{ '⚠️ Alerta Uptime Kuma: Servicio *' + $json.serviceName + '* caído pero sin mapeo K8s configurado. No se puede reiniciar automáticamente.' }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "parse_mode",
|
||||||
|
"value": "Markdown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "d1a2b3c4-aaaa-aaaa-aaaa-00000000000a",
|
||||||
|
"name": "Telegram Sin Mapeo",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4,
|
||||||
|
"position": [
|
||||||
|
912,
|
||||||
|
432
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Uptime Kuma Webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse & Map Service",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Parse & Map Service": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Tiene mapeo K8s",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Tiene mapeo K8s": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "K8s API Restart",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Telegram Sin Mapeo",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"K8s API Restart": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Esperar 60s",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Esperar 60s": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "K8s API Check Status",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"K8s API Check Status": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Reinicio Exitoso",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Reinicio Exitoso": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Telegram Exito",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Telegram Fallo",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"callerPolicy": "workflowsFromSameOwner",
|
||||||
|
"availableInMCP": false,
|
||||||
|
"binaryMode": "separate"
|
||||||
|
},
|
||||||
|
"_activeVersionId": "ebe6d3e6-5cf8-4265-9435-2a63fd555748",
|
||||||
|
"_workflowId": "m8fZG86J5JJzVQn9"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user