fix: real registry token

This commit is contained in:
2026-04-19 19:53:17 +02:00
parent f17778655f
commit a4b4a3dc85
3 changed files with 714 additions and 3 deletions
+3 -3
View File
@@ -4,7 +4,7 @@ metadata:
name: ENC[AES256_GCM,data:tuyp95XjnJ7hmselZQ==,iv:Qdb1EA8uQaWzHE1xcHvf0ht+iyHmya9AaoNsUVeVCnw=,tag:2tYxvILbN41gOA4jYqAGcQ==,type:str] name: ENC[AES256_GCM,data:tuyp95XjnJ7hmselZQ==,iv:Qdb1EA8uQaWzHE1xcHvf0ht+iyHmya9AaoNsUVeVCnw=,tag:2tYxvILbN41gOA4jYqAGcQ==,type:str]
namespace: ENC[AES256_GCM,data:VkkF3Q==,iv:Kegn4sgJEllCpAbn9GMzUknaQIC96ke608qsBNfjgQ4=,tag:cdZv3OkvkPDjFGCKhwuJ8Q==,type:str] namespace: ENC[AES256_GCM,data:VkkF3Q==,iv:Kegn4sgJEllCpAbn9GMzUknaQIC96ke608qsBNfjgQ4=,tag:cdZv3OkvkPDjFGCKhwuJ8Q==,type:str]
stringData: stringData:
token: ENC[AES256_GCM,data:dWN3baAcMkqhWndpz9L5ZG8HvA==,iv:poc+qXxeloPeg0YlMBbnc74GKDg0KsF5bMIZIFQ4XM0=,tag:+2w+RfVShElHQqANpm8xdQ==,type:str] token: ENC[AES256_GCM,data:sjNXGCiW5sKi45zCg+lfNCsxHUD7A0I5/AHuHNM5Vhwlrk+qoTLJxQ==,iv:arjjTTUal/W5jLrarKIMk4bYVWs/PUW5Yr+sn04mNb8=,tag:wbdluuXxrZ4ZvZB1lC1toA==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
@@ -20,8 +20,8 @@ sops:
SWM2K2VNRDRaRmpjY3pGdVZjbEM4ZlkKcJ2Ytx9kzQaHbuw8saYIUwwojCqxiOQc SWM2K2VNRDRaRmpjY3pGdVZjbEM4ZlkKcJ2Ytx9kzQaHbuw8saYIUwwojCqxiOQc
gIQe/FlvZ0m8Azfr5BL5NKUcWvMYSQo6si7lngRpYBDIGw/VY6g7AA== gIQe/FlvZ0m8Azfr5BL5NKUcWvMYSQo6si7lngRpYBDIGw/VY6g7AA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-19T17:37:29Z" lastmodified: "2026-04-19T17:52:35Z"
mac: ENC[AES256_GCM,data:9tj7ezR4Pmp/336Dos7O0znnGQSC5YqHDdukpstN7Ep6dJXB4rStWqgtQgJklVtcqZ8VB2xxqvzI/P945YxLGeWf9EsXpzF2q+eSD94JUPTcRIumaq0HiSWjK74w3MyZFO1SKWOlYC9xpUsrsF4elILyhnnraY+bY/DUpAdKWgA=,iv:oD2HeT+w0Zhzo0w5UzXC1yEsgXiandiMP2JWmmRD79Q=,tag:zqrtW47PDP6tFrXWvzMxVQ==,type:str] mac: ENC[AES256_GCM,data:ZKmWO8CyAp48Bnv10c+kNBksF27oDLfkKEQ9JsBNcGb171Cxf7r21YZ9g5WGXGPxdFntWDZVDmevRVIRIb4KKMeb7UfukqKz9S4yNHaYzCo637XDohl7B9jyr9yN59kk67csYFW3fiptn96l5+NLimSAU6nSaaDEkB4HlCmi7xs=,iv:2hqcGeezi0T4jgqTVGzLNzBQkiig7mUAfSZ6qWVer60=,tag:bKovesylpjQy29lVOvzo4Q==,type:str]
pgp: [] pgp: []
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.9.4 version: 3.9.4
@@ -0,0 +1,671 @@
{
"name": "Jarvis — MCP Homelab Agent",
"nodes": [
{
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"id": "telegram-trigger",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [
200,
300
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.message.text }}",
"operation": "startsWith",
"value2": "/jarvis"
}
]
}
},
"id": "check-jarvis-prefix",
"name": "Nur /jarvis Nachrichten",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
420,
300
]
},
{
"parameters": {
"jsCode": "// Extrahiere die eigentliche Frage nach '/jarvis '\nconst text = $input.first().json.message.text || '';\nconst question = text.replace(/^\\/jarvis\\s*/i, '').trim();\nconst chatId = $input.first().json.message.chat.id;\nconst userId = $input.first().json.message.from.id;\n\nif (!question) {\n throw new Error('Keine Frage nach /jarvis gefunden.');\n}\n\nreturn [{ json: { question, chatId, userId } }];"
},
"id": "extract-question",
"name": "Frage extrahieren",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
220
]
},
{
"parameters": {
"method": "POST",
"url": "http://mcp-homelab.mcp-homelab.svc.cluster.local:8000/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "jsonrpc",
"value": "2.0"
},
{
"name": "method",
"value": "tools/list"
},
{
"name": "id",
"value": "1"
}
]
},
"options": {}
},
"id": "mcp-tools-list",
"name": "MCP: Tools auflisten",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
860,
220
],
"notes": "Holt die aktuelle Tool-Liste vom MCP-Server. Diese wird dem LLM als Kontext mitgegeben."
},
{
"parameters": {
"jsCode": "// Baue den System-Prompt mit Tool-Beschreibungen\nconst tools = $input.first().json.result?.tools || [];\nconst question = $('Frage extrahieren').first().json.question;\n\nconst toolDescriptions = tools.map(t => \n `- ${t.name}: ${t.description}`\n).join('\\n');\n\nconst systemPrompt = `Du bist Jarvis, ein präziser Heimserver-Assistent.\n\nDu hast Zugriff auf folgende MCP-Tools:\n${toolDescriptions}\n\nRegeln:\n- Lies immer erst relevante Daten bevor du antwortest.\n- Antworte auf Deutsch, präzise und ohne Fülltext.\n- Wenn du einen Kubernetes-Fehler siehst, erkläre was ihn verursacht und was als nächstes zu tun ist.\n- Du darfst NIEMALS etwas schreiben oder verändern — nur lesen.\n- Bei Änderungsbedarf sagst du explizit: 'Ich würde empfehlen, folgenden Git-Commit zu machen: ...'`;\n\nreturn [{ json: { systemPrompt, question, tools } }];"
},
"id": "build-system-prompt",
"name": "System-Prompt bauen",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1080,
220
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OLLAMA_BASE_URL }}/api/chat",
"sendBody": true,
"contentType": "json",
"body": "={\n \"model\": \"qwen2.5-coder:14b\",\n \"messages\": [\n { \"role\": \"system\", \"content\": {{ JSON.stringify($json.systemPrompt) }} },\n { \"role\": \"user\", \"content\": {{ JSON.stringify($json.question) }} }\n ],\n \"stream\": false,\n \"tools\": {{ JSON.stringify($json.tools) }}\n}",
"options": {
"timeout": 120000
}
},
"id": "ollama-call",
"name": "Ollama (lokal, qwen2.5-coder:14b)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1300,
100
],
"notes": "Primär: lokales Ollama auf dem Windows/RTX4070 Rechner. OLLAMA_BASE_URL z.B. http://192.168.2.XXX:11434"
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "x-api-key",
"value": "={{ $env.ANTHROPIC_API_KEY }}"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"body": "={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 2048,\n \"system\": {{ JSON.stringify($('Build System-Prompt').first().json.systemPrompt) }},\n \"messages\": [\n { \"role\": \"user\", \"content\": {{ JSON.stringify($('Build System-Prompt').first().json.question) }} }\n ]\n}",
"options": {
"timeout": 60000
}
},
"id": "anthropic-fallback",
"name": "Anthropic API (Fallback)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1300,
340
],
"notes": "Fallback wenn Ollama nicht erreichbar oder Frage zu komplex. ANTHROPIC_API_KEY als n8n-Variable setzen."
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ $('Ollama (lokal, qwen2.5:14b)').first().json.error ? 1 : 0 }}",
"operation": "equal",
"value2": 1
}
]
}
},
"id": "ollama-failed",
"name": "Ollama fehlgeschlagen?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1520,
220
]
},
{
"parameters": {
"jsCode": "// Normalisiere Antwort aus Ollama oder Anthropic\nlet answer = '';\n\ntry {\n // Ollama-Format\n const ollamaData = $('Ollama (lokal, qwen2.5:14b)').first().json;\n if (ollamaData?.message?.content) {\n answer = ollamaData.message.content;\n }\n} catch(e) {}\n\nif (!answer) {\n try {\n // Anthropic-Format\n const anthropicData = $('Anthropic API (Fallback)').first().json;\n if (anthropicData?.content?.[0]?.text) {\n answer = anthropicData.content[0].text;\n }\n } catch(e) {}\n}\n\nif (!answer) answer = 'Fehler: Keine Antwort von LLM erhalten.';\n\n// Kuerze auf Telegram-Limit (4096 Zeichen)\nif (answer.length > 4000) {\n answer = answer.substring(0, 3990) + '\\n\\n[... gekuerzt]';\n}\n\nconst chatId = $('Frage extrahieren').first().json.chatId;\nreturn [{ json: { answer, chatId } }];"
},
"id": "normalize-answer",
"name": "Antwort normalisieren",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1740,
220
]
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.answer }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "telegram-reply",
"name": "Telegram Antwort senden",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1960,
220
]
},
{
"parameters": {
"path": "prometheus-alert",
"responseMode": "onReceived",
"options": {}
},
"id": "prometheus-webhook",
"name": "Prometheus Alert Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
200,
600
],
"webhookId": "prometheus-alert-receiver",
"notes": "Webhook-URL in Alertmanager konfigurieren: http://n8n.n8n.svc.cluster.local:5678/webhook/prometheus-alert"
},
{
"parameters": {
"jsCode": "// Alertmanager sendet ein Array von Alerts\nconst body = $input.first().json.body || $input.first().json;\nconst alerts = body.alerts || [body];\n\nreturn alerts.map(alert => {\n const severity = alert.labels?.severity || 'unknown';\n const alertname = alert.labels?.alertname || 'UnknownAlert';\n const namespace = alert.labels?.namespace || 'unknown';\n const pod = alert.labels?.pod || alert.labels?.container || '';\n const message = alert.annotations?.message || alert.annotations?.summary || '';\n const status = alert.status || 'firing';\n\n // Severity-Level numerisch fuer spaeteren Vergleich\n const levels = { 'critical': 4, 'high': 3, 'warning': 2, 'info': 1 };\n const level = levels[severity.toLowerCase()] || 2;\n\n return { json: { severity, level, alertname, namespace, pod, message, status } };\n});"
},
"id": "parse-alert",
"name": "Alert parsen",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
420,
600
]
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ $json.level }}",
"operation": "largerEqual",
"value2": 3
}
]
}
},
"id": "is-high-or-critical",
"name": "High oder Critical?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
640,
600
]
},
{
"parameters": {
"method": "POST",
"url": "http://mcp-homelab.mcp-homelab.svc.cluster.local:8000/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"body": "={\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"kubectl_events\",\n \"arguments\": { \"namespace\": {{ JSON.stringify($json.namespace) }} }\n },\n \"id\": \"alert-diag-1\"\n}",
"options": {
"timeout": 15000
}
},
"id": "mcp-get-events",
"name": "MCP: Events holen",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
860,
500
]
},
{
"parameters": {
"method": "POST",
"url": "http://mcp-homelab.mcp-homelab.svc.cluster.local:8000/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"body": "={\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"flux_status\",\n \"arguments\": {}\n },\n \"id\": \"alert-diag-2\"\n}",
"options": {
"timeout": 15000
}
},
"id": "mcp-flux-status",
"name": "MCP: Flux-Status holen",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
860,
620
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "x-api-key",
"value": "={{ $env.ANTHROPIC_API_KEY }}"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"body": "={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 1024,\n \"system\": \"Du bist ein Kubernetes-Experte. Analysiere den Alert und die bereitgestellten Cluster-Daten. Antworte auf Deutsch. Gib aus: 1) Wahrscheinliche Ursache, 2) Empfohlene Untersuchungsschritte, 3) Falls ein GitOps-Fix nötig ist: genaue YAML-Änderung die als PR eingereicht werden sollte.\",\n \"messages\": [{\n \"role\": \"user\",\n \"content\": {{ JSON.stringify(\n 'Alert: ' + $('Alert parsen').first().json.alertname +\n ' (Severity: ' + $('Alert parsen').first().json.severity + ')\\n' +\n 'Namespace: ' + $('Alert parsen').first().json.namespace + '\\n' +\n 'Message: ' + $('Alert parsen').first().json.message + '\\n\\n' +\n 'Kubernetes Events:\\n' + JSON.stringify($('MCP: Events holen').first().json.result, null, 2) + '\\n\\n' +\n 'Flux Status:\\n' + JSON.stringify($('MCP: Flux-Status holen').first().json.result, null, 2)\n ) }}\n }]\n}",
"options": {
"timeout": 60000
}
},
"id": "analyze-alert-llm",
"name": "LLM: Alert analysieren",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1080,
560
]
},
{
"parameters": {
"jsCode": "const alert = $('Alert parsen').first().json;\nconst analysis = $('LLM: Alert analysieren').first().json;\nconst analysisText = analysis?.content?.[0]?.text || 'Keine LLM-Analyse verfügbar.';\n\n// Severity-Emoji\nconst emoji = { critical: '🔴', high: '🟠', warning: '🟡', info: '🔵' }[alert.severity] || '⚪';\n\nconst msg = `${emoji} *${alert.alertname}* (${alert.severity.toUpperCase()})\n` +\n `Namespace: \\`${alert.namespace}\\`\\n` +\n (alert.pod ? `Pod: \\`${alert.pod}\\`\\n` : '') +\n `\\n${alert.message}\\n\\n` +\n `*Analyse:*\\n${analysisText.substring(0, 2500)}`;\n\nreturn [{ json: { message: msg, severity: alert.severity, level: alert.level } }];"
},
"id": "format-alert-message",
"name": "Alert-Nachricht formatieren",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1300,
560
]
},
{
"parameters": {
"chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
"text": "={{ $json.message }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "telegram-alert",
"name": "Telegram Alert senden",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1520,
560
]
},
{
"parameters": {
"jsCode": "// Warning/Info: einfacher Alert ohne LLM-Analyse\nconst alert = $json;\nconst emoji = { warning: '🟡', info: '🔵' }[alert.severity] || '⚪';\n\nconst msg = `${emoji} *${alert.alertname}* (${alert.severity})\n` +\n `Namespace: \\`${alert.namespace}\\`\\n` +\n alert.message;\n\nreturn [{ json: { message: msg } }];"
},
"id": "format-low-alert",
"name": "Einfacher Alert formatieren",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
860,
720
]
},
{
"parameters": {
"chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
"text": "={{ $json.message }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "telegram-low-alert",
"name": "Telegram Low-Alert senden",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1080,
720
]
},
{
"parameters": {
"jsCode": "const allowed = ($env.TELEGRAM_ALLOWED_IDS || '').split(',').map(s => s.trim()).filter(Boolean);\nconst chatId = String($input.first().json.message?.chat?.id || '');\nconst userId = String($input.first().json.message?.from?.id || '');\nif (allowed.length > 0 && !allowed.includes(chatId) && !allowed.includes(userId)) {\n return [];\n}\nreturn $input.all();"
},
"id": "chat-id-whitelist",
"name": "Chat-ID Whitelist",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
310,
300
]
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Chat-ID Whitelist",
"type": "main",
"index": 0
}
]
]
},
"Nur /jarvis Nachrichten": {
"main": [
[
{
"node": "Frage extrahieren",
"type": "main",
"index": 0
}
],
[]
]
},
"Frage extrahieren": {
"main": [
[
{
"node": "MCP: Tools auflisten",
"type": "main",
"index": 0
}
]
]
},
"MCP: Tools auflisten": {
"main": [
[
{
"node": "System-Prompt bauen",
"type": "main",
"index": 0
}
]
]
},
"System-Prompt bauen": {
"main": [
[
{
"node": "Ollama (lokal, qwen2.5:14b)",
"type": "main",
"index": 0
}
]
]
},
"Ollama (lokal, qwen2.5:14b)": {
"main": [
[
{
"node": "Ollama fehlgeschlagen?",
"type": "main",
"index": 0
}
]
]
},
"Ollama fehlgeschlagen?": {
"main": [
[
{
"node": "Anthropic API (Fallback)",
"type": "main",
"index": 0
}
],
[
{
"node": "Antwort normalisieren",
"type": "main",
"index": 0
}
]
]
},
"Anthropic API (Fallback)": {
"main": [
[
{
"node": "Antwort normalisieren",
"type": "main",
"index": 0
}
]
]
},
"Antwort normalisieren": {
"main": [
[
{
"node": "Telegram Antwort senden",
"type": "main",
"index": 0
}
]
]
},
"Prometheus Alert Webhook": {
"main": [
[
{
"node": "Alert parsen",
"type": "main",
"index": 0
}
]
]
},
"Alert parsen": {
"main": [
[
{
"node": "High oder Critical?",
"type": "main",
"index": 0
}
]
]
},
"High oder Critical?": {
"main": [
[
{
"node": "MCP: Events holen",
"type": "main",
"index": 0
},
{
"node": "MCP: Flux-Status holen",
"type": "main",
"index": 0
}
],
[
{
"node": "Einfacher Alert formatieren",
"type": "main",
"index": 0
}
]
]
},
"MCP: Events holen": {
"main": [
[
{
"node": "LLM: Alert analysieren",
"type": "main",
"index": 0
}
]
]
},
"MCP: Flux-Status holen": {
"main": [
[
{
"node": "LLM: Alert analysieren",
"type": "main",
"index": 0
}
]
]
},
"LLM: Alert analysieren": {
"main": [
[
{
"node": "Alert-Nachricht formatieren",
"type": "main",
"index": 0
}
]
]
},
"Alert-Nachricht formatieren": {
"main": [
[
{
"node": "Telegram Alert senden",
"type": "main",
"index": 0
}
]
]
},
"Einfacher Alert formatieren": {
"main": [
[
{
"node": "Telegram Low-Alert senden",
"type": "main",
"index": 0
}
]
]
},
"Chat-ID Whitelist": {
"main": [
[
{
"node": "Nur /jarvis Nachrichten",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"saveExecutionProgress": true
},
"staticData": null,
"tags": [
"homelab",
"jarvis",
"mcp"
],
"pinData": {}
}
@@ -0,0 +1,40 @@
# Ergaenze deine bestehende Alertmanager-Konfiguration um diesen Receiver.
# Einzufuegen in: clusters/homelab/infrastructure/kube-prometheus-stack/
# (wo du Alertmanager konfigurierst — z.B. values.yaml oder alertmanager-config.yaml)
# alertmanager.yaml Receiver-Abschnitt:
receivers:
- name: "jarvis-webhook"
webhook_configs:
- url: "http://n8n.n8n.svc.cluster.local:5678/webhook/prometheus-alert"
send_resolved: true
max_alerts: 10
route:
receiver: "jarvis-webhook"
group_by: ["alertname", "namespace"]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# Critical: sofort, kein Grouping-Delay
- matchers:
- severity="critical"
receiver: "jarvis-webhook"
group_wait: 0s
repeat_interval: 1h
# High: 30 Sekunden warten, dann senden
- matchers:
- severity="high"
receiver: "jarvis-webhook"
group_wait: 30s
repeat_interval: 2h
# Warning/Info: normale Rate, nicht zu oft
- matchers:
- severity=~"warning|info"
receiver: "jarvis-webhook"
group_wait: 2m
repeat_interval: 6h