23ce6a88d5
Add defensive error handling to throw clear error if chatId is missing before attempting to send Telegram message. This helps debug 'resource not found' errors by identifying data flow issues.
2626 lines
91 KiB
Plaintext
2626 lines
91 KiB
Plaintext
{
|
|
"name": "Docker Manager Bot",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"updates": [
|
|
"message",
|
|
"callback_query"
|
|
]
|
|
},
|
|
"id": "telegram-trigger",
|
|
"name": "Telegram Trigger",
|
|
"type": "n8n-nodes-base.telegramTrigger",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
240,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "route-message",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-message",
|
|
"leftValue": "={{ $json.message }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "object",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "message"
|
|
},
|
|
{
|
|
"id": "route-callback",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-callback",
|
|
"leftValue": "={{ $json.callback_query }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "object",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "callback_query"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-update-type",
|
|
"name": "Route Update Type",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
460,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "user-auth-condition",
|
|
"leftValue": "={{ $json.message.from.id.toString() }}",
|
|
"rightValue": "563878771",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-auth",
|
|
"name": "IF User Authenticated",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
680,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "callback-auth-condition",
|
|
"leftValue": "={{ $json.callback_query.from.id.toString() }}",
|
|
"rightValue": "563878771",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-callback-auth",
|
|
"name": "IF Callback Authenticated",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
680,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "docker-query-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-status",
|
|
"leftValue": "={{ $json.message.text.toLowerCase() }}",
|
|
"rightValue": "status",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "action-command-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "starts-with-start",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "start ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
},
|
|
{
|
|
"id": "starts-with-stop",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "stop ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
},
|
|
{
|
|
"id": "starts-with-restart",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "restart ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "update-command-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "starts-with-update",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "update ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-command-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "matches-logs",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "^(show\\s+)?logs\\s+",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "regex"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-route",
|
|
"name": "Route Message",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
900,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list",
|
|
"name": "Docker List Containers",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and user input\nconst dockerOutput = $input.item.json.stdout;\nconst userMessage = $('Telegram Trigger').item.json.message.text.toLowerCase().trim();\nconst chatId = $('Telegram Trigger').item.json.message.chat.id;\n\n// Parse JSON response - only error if we can't parse valid JSON from stdout\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Extract container name from user query\n// Remove common query words to get the container name\nconst queryWords = ['status', 'show', 'check', 'container', 'docker', 'what', 'is', 'the', 'of', 'for'];\nconst words = userMessage.split(/\\s+/).filter(word => !queryWords.includes(word));\nconst requestedName = words.join(' ').trim();\n\n// If no container name specified, return summary\nif (!requestedName || requestedName === '') {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n\n const parts = [];\n if (counts.running) parts.push(`${counts.running} running`);\n if (counts.exited) parts.push(`${counts.exited} stopped`);\n if (counts.paused) parts.push(`${counts.paused} paused`);\n if (counts.restarting) parts.push(`${counts.restarting} restarting`);\n\n const summary = parts.length > 0 ? parts.join(', ') : 'No containers found';\n\n return [{\n json: {\n chatId: chatId,\n summary: true,\n containers: containers,\n text: `Container summary: ${summary}`\n }\n }];\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = requestedName.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Handle no matches\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: `No container found matching \"${requestedName}\".\\n\\nTry \"status\" to see all containers.`\n }\n }];\n}\n\n// Handle multiple matches\nif (matches.length > 1) {\n const names = matches.map(c => c.Names[0].replace(/^\\//, '')).join('\\n- ');\n return [{\n json: {\n chatId: chatId,\n multipleMatches: true,\n matches: matches,\n text: `Found ${matches.length} matches:\\n\\n- ${names}\\n\\nPlease be more specific.`\n }\n }];\n}\n\n// Single match - return container details\nconst container = matches[0];\nreturn [{\n json: {\n chatId: chatId,\n singleMatch: true,\n container: {\n id: container.Id,\n name: container.Names[0].replace(/^\\//, ''),\n state: container.State,\n status: container.Status,\n image: container.Image\n }\n }\n}];"
|
|
},
|
|
"id": "code-parse-match",
|
|
"name": "Parse and Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get the data from previous node\nconst data = $input.item.json;\nconst chatId = data.chatId;\n\n// If error or summary, pass through as-is\nif (data.error || data.summary || data.multipleMatches) {\n return [{\n json: {\n chatId: chatId,\n text: data.text\n }\n }];\n}\n\n// Format single container details\nif (data.singleMatch) {\n const container = data.container;\n\n // State indicator mapping\n const stateIndicator = {\n 'running': '[OK]',\n 'exited': '[STOPPED]',\n 'paused': '[PAUSED]',\n 'restarting': '[RESTARTING]',\n 'dead': '[DEAD]'\n };\n\n const indicator = stateIndicator[container.state] || '[?]';\n\n // Format detailed response\n const text = `${indicator} <b>${container.name}</b>\\n\\n` +\n `<b>State:</b> ${container.state}\\n` +\n `<b>Status:</b> ${container.status}\\n` +\n `<b>Image:</b> ${container.image}\\n` +\n `<b>ID:</b> ${container.id.substring(0, 12)}`;\n\n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Fallback\nreturn [{\n json: {\n chatId: chatId,\n text: \"Unexpected response format\"\n }\n}];"
|
|
},
|
|
"id": "code-format-response",
|
|
"name": "Format Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-docker",
|
|
"name": "Send Docker Response",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1560,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const message = $input.item.json.message;\nconst timestamp = new Date().toISOString();\nconst text = message.text || '(no text)';\n\nreturn {\n json: {\n chatId: message.chat.id,\n text: `Got: ${text}\\n\\nProcessed: ${timestamp}`\n }\n};"
|
|
},
|
|
"id": "code-format-echo",
|
|
"name": "Format Echo",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send",
|
|
"name": "Send Echo",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1120,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse action command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match action pattern: start/stop/restart followed by container name\nconst match = text.match(/^(start|stop|restart)\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid action format. Use: start/stop/restart <container-name>',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: match[1].toLowerCase(),\n containerQuery: match[2].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-action",
|
|
"name": "Parse Action",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-action",
|
|
"name": "Docker List for Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and action info from Parse Action\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action').item.json;\nconst action = actionData.action;\nconst containerQuery = actionData.containerQuery;\nconst chatId = actionData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results with all necessary context\n// Include allContainers for suggestion finding when no matches\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
|
|
},
|
|
"id": "code-match-container",
|
|
"name": "Match Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-gt-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-match-count",
|
|
"name": "Check Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};"
|
|
},
|
|
"id": "code-build-action-cmd",
|
|
"name": "Build Action Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-action",
|
|
"name": "Execute Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${stderr.trim()}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `<b>${containerName}</b> ${verb} successfully`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${errorMsg}`\n }\n};"
|
|
},
|
|
"id": "code-parse-action-result",
|
|
"name": "Parse Action Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-action-result",
|
|
"name": "Send Action Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
400
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};"
|
|
},
|
|
"id": "code-find-closest",
|
|
"name": "Find Closest Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-suggestion",
|
|
"leftValue": "={{ $json.hasSuggestion }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-has-suggestion",
|
|
"name": "Check Suggestion",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// callback_data must be <=64 bytes - use short keys\n// a=action (1 char: s=start, t=stop, r=restart)\n// c=container short ID\n// t=timestamp\nconst actionCode = action === 'start' ? 's' : action === 'stop' ? 't' : 'r';\nconst callbackData = JSON.stringify({ a: actionCode, c: suggestedId, t: timestamp });\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '<b>${query}</b>' found.\\n\\nDid you mean <b>${suggestedName}</b>?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n }\n }\n};"
|
|
},
|
|
"id": "code-build-suggestion",
|
|
"name": "Build Suggestion Keyboard",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-suggestion",
|
|
"name": "Send Suggestion",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2440,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "=No container found matching '<b>{{ $json.query }}</b>'",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-no-match",
|
|
"name": "Send No Match",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2220,
|
|
400
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst shortIds = matches.map(m => m.Id.substring(0, 12));\n\n// Build callback_data - must be <=64 bytes\n// For batch: a=action code, c=array of short IDs, t=timestamp\nconst actionCode = action === 'start' ? 's' : action === 'stop' ? 't' : 'r';\nconst timestamp = Date.now();\n\n// Check size - if too many containers, callback_data might exceed 64 bytes\n// Each short ID is 12 chars, plus overhead. Max ~3-4 containers safely\nlet callbackData;\nlet limitedCount = shortIds.length;\nif (shortIds.length <= 4) {\n callbackData = JSON.stringify({ a: actionCode, c: shortIds, t: timestamp });\n} else {\n // Too many containers - limit to first 4\n callbackData = JSON.stringify({ a: actionCode, c: shortIds.slice(0, 4), t: timestamp });\n limitedCount = 4;\n}\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found <b>${matches.length}</b> containers matching '<b>${query}</b>':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};"
|
|
},
|
|
"id": "code-build-batch-keyboard",
|
|
"name": "Build Batch Keyboard",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-batch-confirm",
|
|
"name": "Send Batch Confirmation",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-docker-error",
|
|
"name": "Send Docker Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse callback data from button click (single suggestion or batch)\nconst callback = $json.callback_query;\nlet data;\ntry {\n data = JSON.parse(callback.data);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds, // Array for batch support\n containerId: containerIds[0] || null, // For single-container compat\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel'\n }\n};"
|
|
},
|
|
"id": "code-parse-callback",
|
|
"name": "Parse Callback Data",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "is-cancel",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "cancel-true",
|
|
"leftValue": "={{ $json.isCancel }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "cancel"
|
|
},
|
|
{
|
|
"id": "is-expired",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "expired-true",
|
|
"leftValue": "={{ $json.expired }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "expired"
|
|
},
|
|
{
|
|
"id": "is-batch",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "batch-true",
|
|
"leftValue": "={{ $json.isBatch }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "batch"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-route-callback",
|
|
"name": "Route Callback",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1120,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};"
|
|
},
|
|
"id": "code-handle-cancel",
|
|
"name": "Handle Cancel",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-cancel",
|
|
"name": "Answer Cancel Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-cancel-msg",
|
|
"name": "Delete Cancel Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};"
|
|
},
|
|
"id": "code-handle-expired",
|
|
"name": "Handle Expired",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-expired",
|
|
"name": "Answer Expired Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
600
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-expired-msg",
|
|
"name": "Delete Expired Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
600
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};"
|
|
},
|
|
"id": "code-build-callback-cmd",
|
|
"name": "Build Callback Action",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-callback-action",
|
|
"name": "Execute Callback Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1560,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${stderr.trim()}`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${errorMsg}`,\n answerText: 'Action failed'\n }\n};"
|
|
},
|
|
"id": "code-parse-callback-result",
|
|
"name": "Parse Callback Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-action",
|
|
"name": "Answer Action Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-suggestion-msg",
|
|
"name": "Delete Suggestion Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $('Parse Callback Result').item.json.chatId }}",
|
|
"text": "={{ $('Parse Callback Result').item.json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-callback-result",
|
|
"name": "Send Callback Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Execute batch action on all containers sequentially\nconst containerIds = $json.containerIds;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\nconst queryId = $json.queryId;\n\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\nconst results = [];\n\n// Execute each container action sequentially using fetch\nfor (const containerId of containerIds) {\n try {\n // Use n8n's built-in $http or construct command for later execution\n // Since we can't use execSync easily, we'll build commands for chained execution\n results.push({\n containerId,\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`\n });\n } catch (err) {\n results.push({ containerId, error: err.message });\n }\n}\n\nreturn {\n json: {\n commands: results,\n action,\n queryId,\n chatId,\n messageId,\n totalCount: containerIds.length\n }\n};"
|
|
},
|
|
"id": "code-build-batch-commands",
|
|
"name": "Build Batch Commands",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Execute all batch commands and collect results\nconst { commands, action, queryId, chatId, messageId, totalCount } = $json;\nconst results = [];\n\n// Build a single shell command that runs all curl commands sequentially\n// and outputs results in a parseable format\nconst allCommands = commands.map((c, i) => \n `echo \"RESULT_${i}:$(${c.cmd})\"`\n).join(' && ');\n\nreturn {\n json: {\n batchCmd: allCommands,\n containerIds: commands.map(c => c.containerId),\n action,\n queryId,\n chatId,\n messageId,\n totalCount\n }\n};"
|
|
},
|
|
"id": "code-prepare-batch-exec",
|
|
"name": "Prepare Batch Execution",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.batchCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-batch-action",
|
|
"name": "Execute Batch Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1780,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse batch execution results\nconst stdout = $input.item.json.stdout || '';\nconst stderr = $input.item.json.stderr || '';\nconst prevData = $('Prepare Batch Execution').item.json;\nconst { containerIds, action, queryId, chatId, messageId, totalCount } = prevData;\n\n// Parse results from output like: RESULT_0:204 RESULT_1:204 RESULT_2:304\nconst results = [];\nfor (let i = 0; i < containerIds.length; i++) {\n const match = stdout.match(new RegExp(`RESULT_${i}:(\\\\d+)`));\n if (match) {\n const statusCode = parseInt(match[1]);\n results.push({\n containerId: containerIds[i],\n success: statusCode === 204 || statusCode === 304,\n statusCode\n });\n } else {\n results.push({\n containerId: containerIds[i],\n success: false,\n error: 'No response'\n });\n }\n}\n\nconst successCount = results.filter(r => r.success).length;\nconst failCount = results.length - successCount;\n\nreturn {\n json: {\n results,\n successCount,\n failCount,\n totalCount: results.length,\n action,\n queryId,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-batch-result",
|
|
"name": "Parse Batch Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Format batch result message\nconst { successCount, failCount, totalCount, action } = $json;\nconst verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n\nlet message;\nif (failCount === 0) {\n message = `Successfully ${verb} ${successCount} container${successCount > 1 ? 's' : ''}`;\n} else if (successCount === 0) {\n message = `Failed to ${action} all ${totalCount} containers`;\n} else {\n message = `${verb.charAt(0).toUpperCase() + verb.slice(1)} ${successCount}/${totalCount} containers (${failCount} failed)`;\n}\n\nreturn {\n json: {\n message,\n chatId: $json.chatId,\n queryId: $json.queryId,\n messageId: $json.messageId\n }\n};"
|
|
},
|
|
"id": "code-format-batch-result",
|
|
"name": "Format Batch Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-batch-query",
|
|
"name": "Answer Batch Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2440,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-batch-confirm-msg",
|
|
"name": "Delete Batch Confirm Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2660,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $('Format Batch Result').item.json.chatId }}",
|
|
"text": "={{ $('Format Batch Result').item.json.message }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-batch-result",
|
|
"name": "Send Batch Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2880,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update <container-name>',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-update",
|
|
"name": "Parse Update Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-update",
|
|
"name": "Docker List for Update",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
|
|
},
|
|
"id": "code-match-update-container",
|
|
"name": "Match Update Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "update-docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "error"
|
|
},
|
|
{
|
|
"id": "update-no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "no-match"
|
|
},
|
|
{
|
|
"id": "update-single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "single"
|
|
},
|
|
{
|
|
"id": "update-multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-gt-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "multiple"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-update-match-count",
|
|
"name": "Check Update Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-error",
|
|
"name": "Send Update Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
900
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};"
|
|
},
|
|
"id": "code-update-multiple-handler",
|
|
"name": "Handle Update Multiple",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
1100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-multiple",
|
|
"name": "Send Update Multiple",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2000,
|
|
1100
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "=No container found matching '<b>{{ $json.containerQuery }}</b>'",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-no-match",
|
|
"name": "Send Update No Match",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
1000
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect command for the matched container\nconst containerId = $json.matches[0].Id;\nconst containerName = $json.matches[0].Name;\nconst chatId = $json.chatId;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/${containerId}/json'`,\n containerId,\n containerName,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-inspect-cmd",
|
|
"name": "Build Inspect Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-inspect-container",
|
|
"name": "Inspect Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse container inspect output and extract config\nconst stdout = $input.item.json.stdout;\nconst cmdData = $('Build Inspect Command').item.json;\nconst containerId = cmdData.containerId;\nconst containerName = cmdData.containerName;\nconst chatId = cmdData.chatId;\n\nlet inspect;\ntry {\n inspect = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect ${containerName}: ${e.message}`\n }\n };\n}\n\nconst imageName = inspect.Config.Image;\nconst currentImageId = inspect.Image;\n\n// Extract version from labels if available\nconst labels = inspect.Config.Labels || {};\nconst currentVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || currentImageId.substring(7, 19);\n\nreturn {\n json: {\n imageName,\n currentImageId,\n currentVersion,\n containerConfig: inspect.Config,\n hostConfig: inspect.HostConfig,\n networkSettings: inspect.NetworkSettings,\n containerName,\n containerId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-parse-container-config",
|
|
"name": "Parse Container Config",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build pull image command\nconst imageName = $json.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,\n imageName,\n currentImageId: $json.currentImageId,\n currentVersion: $json.currentVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n containerName: $json.containerName,\n containerId: $json.containerId,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-pull-cmd",
|
|
"name": "Build Pull Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2440,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-pull-image",
|
|
"name": "Pull Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2660,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect image command to get the new image ID\nconst pullData = $('Build Pull Command').item.json;\nconst imageName = pullData.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/images/${encodeURIComponent(imageName)}/json'`,\n imageName,\n currentImageId: pullData.currentImageId,\n currentVersion: pullData.currentVersion,\n containerConfig: pullData.containerConfig,\n hostConfig: pullData.hostConfig,\n networkSettings: pullData.networkSettings,\n containerName: pullData.containerName,\n containerId: pullData.containerId,\n chatId: pullData.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-inspect-image-cmd",
|
|
"name": "Build Image Inspect",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2880,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-inspect-image",
|
|
"name": "Inspect New Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3100,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Compare old and new image IDs to detect if update is needed\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Image Inspect').item.json;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\n\nlet newImage;\ntry {\n newImage = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect new image: ${e.message}`\n }\n };\n}\n\nconst newImageId = newImage.Id;\n\nif (currentImageId === newImageId) {\n // No update needed - stay silent per CONTEXT.md\n return { json: { needsUpdate: false, chatId } };\n}\n\n// Extract new version from labels\nconst labels = newImage.Config?.Labels || {};\nconst newVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || newImageId.substring(7, 19);\n\nreturn {\n json: {\n needsUpdate: true,\n currentImageId,\n newImageId,\n currentVersion: prevData.currentVersion,\n newVersion,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n containerName: prevData.containerName,\n containerId: prevData.containerId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-compare-digests",
|
|
"name": "Compare Digests",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3320,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "needs-update",
|
|
"leftValue": "={{ $json.needsUpdate }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-needs-update",
|
|
"name": "Check If Update Needed",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3540,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build stop container command\nconst containerId = $json.containerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-stop-cmd",
|
|
"name": "Build Stop Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3760,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-stop-container",
|
|
"name": "Stop Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3980,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Verify container stopped and build remove command\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Stop Command').item.json;\nconst containerId = prevData.containerId;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: stopped, 304: already stopped - both OK\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to stop container: HTTP ${statusCode}`\n }\n };\n}\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X DELETE 'http://localhost/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-verify-stop-build-remove",
|
|
"name": "Verify Stop Build Remove",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4200,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-remove-container",
|
|
"name": "Remove Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4420,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build container create request body from saved config\nconst prevData = $('Verify Stop Build Remove').item.json;\nconst config = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\n\n// Build NetworkingConfig from NetworkSettings\nconst networks = {};\nfor (const [name, netConfig] of Object.entries(networkSettings.Networks || {})) {\n networks[name] = {\n IPAMConfig: netConfig.IPAMConfig,\n Links: netConfig.Links,\n Aliases: netConfig.Aliases\n };\n}\n\nconst createBody = {\n ...config,\n HostConfig: hostConfig,\n NetworkingConfig: {\n EndpointsConfig: networks\n }\n};\n\n// Remove fields that shouldn't be in create request\ndelete createBody.Hostname;\ndelete createBody.Domainname;\n\nreturn {\n json: {\n createBody: JSON.stringify(createBody),\n containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-create-body",
|
|
"name": "Build Create Body",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4640,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the create container command using a temp file approach\nconst createBody = $json.createBody;\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\n\n// Write body to command using here-doc to avoid shell escaping issues\nconst cmd = `curl -s -X POST --unix-socket /var/run/docker.sock -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://localhost/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n cmd,\n containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-create-cmd",
|
|
"name": "Build Create Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4860,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-create-container",
|
|
"name": "Create Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5080,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse create response and extract new container ID\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Create Command').item.json;\nconst chatId = prevData.chatId;\n\nlet response;\ntry {\n response = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${stdout}`\n }\n };\n}\n\nif (response.message) {\n // Error response from Docker\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${response.message}`\n }\n };\n}\n\nreturn {\n json: {\n newContainerId: response.Id,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-parse-create-response",
|
|
"name": "Parse Create Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5300,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build start command for new container\nconst newContainerId = $json.newContainerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${newContainerId}/start'`,\n newContainerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-start-cmd",
|
|
"name": "Build Start Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5520,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-start-new-container",
|
|
"name": "Start New Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5740,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse start result and format success message\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Start Command').item.json;\nconst containerName = prevData.containerName;\nconst currentVersion = prevData.currentVersion;\nconst newVersion = prevData.newVersion;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n chatId,\n text: `Container created but failed to start: HTTP ${statusCode}`\n }\n };\n}\n\nconst message = `<b>${containerName}</b> updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message\n }\n};"
|
|
},
|
|
"id": "code-format-update-result",
|
|
"name": "Format Update Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5960,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-result",
|
|
"name": "Send Update Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
6180,
|
|
1200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse logs command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match logs pattern: (show )logs <container> (optional line count)\n// Examples:\n// - \"logs plex\" -> { container: \"plex\", lines: 50 }\n// - \"show logs sonarr\" -> { container: \"sonarr\", lines: 50 }\n// - \"logs nginx 100\" -> { container: \"nginx\", lines: 100 }\n// - \"show logs radarr last 200\" -> { container: \"radarr\", lines: 200 }\n\nconst match = text.match(/^(?:show\\s+)?logs\\s+(.+?)(?:\\s+(?:last\\s+)?(\\d+))?$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid logs format. Use: logs <container> [line-count]',\n chatId: chatId\n }\n };\n}\n\nconst containerQuery = match[1].trim();\nconst lines = match[2] ? parseInt(match[2], 10) : 50;\n\n// Validate line count (reasonable limits)\nconst validatedLines = Math.min(Math.max(lines, 1), 1000);\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: validatedLines,\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-logs",
|
|
"name": "Parse Logs Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
600
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"Telegram Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Update Type",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Update Type": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "IF User Authenticated",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "IF Callback Authenticated",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"IF User Authenticated": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"IF Callback Authenticated": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Callback Data",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Parse Callback Data": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Callback": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Handle Cancel",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Handle Expired",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Commands",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Callback Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Cancel": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Cancel Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Cancel Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Cancel Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Expired": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Expired Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Expired Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Expired Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Callback Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Callback Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Callback Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Callback Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Callback Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Action Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Action Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Suggestion Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Delete Suggestion Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Callback Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List Containers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Update Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format Echo",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List Containers": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse and Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse and Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Echo": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Echo",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Find Closest Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Keyboard",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Find Closest Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Suggestion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Suggestion": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Suggestion Keyboard",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Suggestion Keyboard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Suggestion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Action Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Action Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Keyboard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Confirmation",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Commands": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Batch Execution",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Batch Execution": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Batch Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Batch Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Batch Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Batch Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Batch Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Batch Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Batch Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Batch Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Batch Confirm Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Delete Batch Confirm Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Update Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Update Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Update Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Update Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Update Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Update No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Inspect Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Handle Update Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Handle Update Multiple": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Inspect Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Container Config",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Container Config": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Pull Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Pull Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Pull Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Pull Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Image Inspect",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Image Inspect": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect New Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect New Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compare Digests",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compare Digests": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check If Update Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check If Update Needed": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Stop Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Build Stop Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Stop Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Stop Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Verify Stop Build Remove",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Verify Stop Build Remove": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Remove Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Remove Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Create Body",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Create Body": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Create Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Create Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Create Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Create Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Start Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Start Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Start New Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Start New Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Update Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Update Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"pinData": {},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"staticData": null,
|
|
"tags": [],
|
|
"triggerCount": 1,
|
|
"active": false
|
|
} |