fix(08): resolve n8n deployment issues and clean up orphan nodes
Bug fixes: - Fix array handling in Code nodes ($input.all() vs $input.item.json) - Build Logs Action Command - Build Confirmed Stop Command - Build Immediate Action Command - Find Container For Update - Add timestamp to logs refresh to avoid "message not modified" error - Add :latest tag protection to callback update flow - Add image cleanup after callback updates Cleanup: - Remove orphan nodes: Parse and Match, Format Response, Send Docker Response - Rename duplicate "Inspect New Image" to "Inspect New Image (Text)" - Restore Compare Digests to fix text update command flow - Add callback image cleanup nodes (Build Callback Remove Image, Callback Remove Old Image) Both text commands and inline keyboard buttons now work correctly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+70
-83
@@ -347,57 +347,6 @@
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Get Docker API response and message\nconst dockerOutput = $input.item.json.stdout;\nconst message = $('Keyword Router').item.json.message;\nconst chatId = 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: \"Cannot connect to Docker\"\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// Check if user specified a container name in message (e.g., \"status plex\")\nconst text = (message.text || '').toLowerCase().trim();\nlet requestedName = null;\n\n// Extract container name if message is more than just \"status\"\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n // Join all parts except \"status\" as the container query\n requestedName = parts.filter(p => p !== 'status').join(' ');\n}\n\n// If no container name specified, return summary\nif (!requestedName) {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n \n const total = containers.length;\n const running = counts['running'] || 0;\n const stopped = counts['exited'] || 0;\n const other = total - running - stopped;\n \n let summary = '<b>Docker Containers</b>\\n\\n';\n summary += 'Running: ' + running + '/' + total + '\\n';\n if (stopped > 0) summary += 'Stopped: ' + stopped + '\\n';\n if (other > 0) summary += 'Other: ' + other + '\\n';\n \n // List running containers\n const runningContainers = containers\n .filter(c => c.State === 'running')\n .map(c => {\n const name = normalizeName(c.Names[0]);\n const uptime = c.Status;\n return ' \\u2022 ' + name + ' - ' + uptime;\n });\n \n if (runningContainers.length > 0) {\n summary += '\\n<b>Running:</b>\\n' + runningContainers.join('\\n');\n }\n \n return [{\n json: {\n chatId: chatId,\n text: summary\n }\n }];\n}\n\n// Find matching containers\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n});\n\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: 'No container found matching \"' + requestedName + '\"'\n }\n }];\n}\n\nif (matches.length === 1) {\n const container = matches[0];\n const name = normalizeName(container.Names[0]);\n const state = container.State;\n const status = container.Status;\n const image = container.Image;\n \n let text = '<b>' + name + '</b>\\n';\n text += 'State: ' + state + '\\n';\n text += 'Status: ' + status + '\\n';\n text += 'Image: ' + image;\n \n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Multiple matches - list them\nconst matchList = matches.map(c => {\n const name = normalizeName(c.Names[0]);\n return ' \\u2022 ' + name + ' (' + c.State + ')';\n}).join('\\n');\n\nreturn [{\n json: {\n chatId: chatId,\n text: 'Multiple matches for \"' + requestedName + '\":\\n' + matchList\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": "I0xTTiASl7C1NZhJ",
|
||||
"name": "Telegram account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Parse action from message text directly\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action from the text\nlet requestedAction = null;\nlet containerQuery = '';\n\n// Parse action and container name from message\nif (text.startsWith('start ')) {\n requestedAction = 'start';\n containerQuery = text.substring(6).trim();\n} else if (text.startsWith('stop ')) {\n requestedAction = 'stop';\n containerQuery = text.substring(5).trim();\n} else if (text.startsWith('restart ')) {\n requestedAction = 'restart';\n containerQuery = text.substring(8).trim();\n} else if (text.includes('start')) {\n requestedAction = 'start';\n containerQuery = text.replace('start', '').trim();\n} else if (text.includes('stop')) {\n requestedAction = 'stop';\n containerQuery = text.replace('stop', '').trim();\n} else if (text.includes('restart')) {\n requestedAction = 'restart';\n containerQuery = text.replace('restart', '').trim();\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify action and container name (e.g., \"start plex\")',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: requestedAction,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};"
|
||||
@@ -1825,7 +1774,7 @@
|
||||
"options": {}
|
||||
},
|
||||
"id": "exec-inspect-image",
|
||||
"name": "Inspect New Image",
|
||||
"name": "Inspect New Image (Text)",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
@@ -2897,7 +2846,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Find container and execute action\nconst containers = $input.item.json;\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};"
|
||||
"jsCode": "// Find container and execute action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};"
|
||||
},
|
||||
"id": "code-build-immediate-action-cmd",
|
||||
"name": "Build Immediate Action Command",
|
||||
@@ -2989,7 +2938,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Find container and build logs command\nconst containers = $input.item.json;\nconst prevData = $('Prepare Logs Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 10 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/logs?stdout=true&stderr=true&tail=${lines}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n containerState: container.State,\n lines,\n chatId,\n messageId\n }\n};"
|
||||
"jsCode": "// Find container and build logs command\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Logs Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 10 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/logs?stdout=true&stderr=true&tail=${lines}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n containerState: container.State,\n lines,\n chatId,\n messageId\n }\n};"
|
||||
},
|
||||
"id": "code-build-logs-action-cmd",
|
||||
"name": "Build Logs Action Command",
|
||||
@@ -3016,7 +2965,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Parse and format logs output\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Build Logs Action Command').item.json;\nconst containerName = prevData.containerName;\nconst containerState = prevData.containerState;\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Docker logs API returns binary stream with 8-byte header per line\n// Strip non-printable characters and clean up\nlet logs = stdout\n .split('\\n')\n .map(line => {\n // Remove Docker stream header (first 8 bytes of each frame)\n // The header contains stream type and length info\n if (line.length > 8) {\n const cleaned = line.substring(8).replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n return cleaned;\n }\n return line.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n })\n .filter(line => line.trim().length > 0)\n .slice(-lines)\n .join('\\n');\n\nif (!logs || logs.trim().length === 0) {\n logs = '(no recent logs)';\n}\n\n// Truncate if too long for Telegram (max ~4096 chars)\nif (logs.length > 3800) {\n logs = '...' + logs.slice(-3800);\n}\n\n// Build keyboard for navigation\nconst keyboard = [];\nif (containerState === 'running') {\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: '\\u25B6\\uFE0F Start', callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: '\\u{1F4CB} Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\\u2B06\\uFE0F Update', callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n]);\n\nconst stateIcon = containerState === 'running' ? '\\u{1F7E2}' : '\\u26AA';\nlet text = `${stateIcon} <b>${containerName}</b> - Logs\\n\\n<pre>${logs}</pre>`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n};"
|
||||
"jsCode": "// Parse and format logs output\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Build Logs Action Command').item.json;\nconst containerName = prevData.containerName;\nconst containerState = prevData.containerState;\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Docker logs API returns binary stream with 8-byte header per line\n// Strip non-printable characters and clean up\nlet logs = stdout\n .split('\\n')\n .map(line => {\n // Remove Docker stream header (first 8 bytes of each frame)\n // The header contains stream type and length info\n if (line.length > 8) {\n const cleaned = line.substring(8).replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n return cleaned;\n }\n return line.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n })\n .filter(line => line.trim().length > 0)\n .slice(-lines)\n .join('\\n');\n\nif (!logs || logs.trim().length === 0) {\n logs = '(no recent logs)';\n}\n\n// Truncate if too long for Telegram (max ~4096 chars)\nif (logs.length > 3800) {\n logs = '...' + logs.slice(-3800);\n}\n\n// Build keyboard for navigation\nconst keyboard = [];\nif (containerState === 'running') {\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: '\\u25B6\\uFE0F Start', callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: '\\u{1F4CB} Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\\u2B06\\uFE0F Update', callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n]);\n\nconst stateIcon = containerState === 'running' ? '\\u{1F7E2}' : '\\u26AA';\nconst timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\nlet text = `${stateIcon} <b>${containerName}</b> - Logs <i>(${timestamp})</i>\\n\\n<pre>${logs}</pre>`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n};"
|
||||
},
|
||||
"id": "code-format-logs-action-result",
|
||||
"name": "Format Logs Action Result",
|
||||
@@ -3313,7 +3262,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Find container and build stop command\nconst containers = $input.item.json;\nconst prevData = $('Prepare Confirmed Stop').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId\n }\n};"
|
||||
"jsCode": "// Find container and build stop command\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Stop').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId\n }\n};"
|
||||
},
|
||||
"id": "code-build-confirmed-stop-cmd",
|
||||
"name": "Build Confirmed Stop Command",
|
||||
@@ -3429,7 +3378,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Find container and get inspect data for update\nconst containers = $input.item.json;\nconst prevData = $('Prepare Confirmed Update').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst fullName = container.Names[0].replace(/^\\//, '');\n\nreturn {\n json: {\n containerId,\n containerName: normalizeName(container.Names[0]),\n fullContainerName: fullName,\n imageName: container.Image,\n chatId,\n messageId\n }\n};"
|
||||
"jsCode": "// Find container and get inspect data for update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Update').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst fullName = container.Names[0].replace(/^\\//, '');\n\nreturn {\n json: {\n containerId,\n containerName: normalizeName(container.Names[0]),\n fullContainerName: fullName,\n imageName: container.Image,\n chatId,\n messageId\n }\n};"
|
||||
},
|
||||
"id": "code-find-container-for-update",
|
||||
"name": "Find Container For Update",
|
||||
@@ -3457,7 +3406,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Parse container config and build pull command\nconst inspectData = $input.item.json;\nconst prevData = $('Find Container For Update').item.json;\nconst containerId = prevData.containerId;\nconst containerName = prevData.containerName;\nconst fullContainerName = prevData.fullContainerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Extract image info\nconst imageName = inspectData.Config.Image;\nconst currentImageId = inspectData.Image;\n\n// Extract config for recreation\nconst containerConfig = inspectData.Config;\nconst hostConfig = inspectData.HostConfig;\nconst networkSettings = inspectData.NetworkSettings;\n\n// Get current version from image digest or tag\nconst currentDigest = currentImageId.substring(7, 19);\n\nreturn {\n json: {\n pullCmd: `curl -s --max-time 120 -X POST 'http://docker-socket-proxy:2375/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,\n containerId,\n containerName,\n fullContainerName,\n imageName,\n currentImageId,\n currentDigest,\n containerConfig,\n hostConfig,\n networkSettings,\n chatId,\n messageId\n }\n};"
|
||||
"jsCode": "// Parse container config and build pull command\nconst inspectData = $input.item.json;\nconst prevData = $('Find Container For Update').item.json;\nconst containerId = prevData.containerId;\nconst containerName = prevData.containerName;\nconst fullContainerName = prevData.fullContainerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Extract image info\nlet imageName = inspectData.Config.Image;\nconst currentImageId = inspectData.Image;\n\n// CRITICAL: Ensure image has a tag, otherwise Docker pulls ALL tags!\nif (imageName && !imageName.includes(':') && !imageName.includes('@')) {\n imageName = imageName + ':latest';\n}\n\n// Extract config for recreation\nconst containerConfig = inspectData.Config;\nconst hostConfig = inspectData.HostConfig;\nconst networkSettings = inspectData.NetworkSettings;\n\n// Get current version from image digest or tag\nconst currentDigest = currentImageId.substring(7, 19);\n\nreturn {\n json: {\n pullCmd: `curl -s --max-time 120 -X POST 'http://docker-socket-proxy:2375/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,\n containerId,\n containerName,\n fullContainerName,\n imageName,\n currentImageId,\n currentDigest,\n containerConfig,\n hostConfig,\n networkSettings,\n chatId,\n messageId\n }\n};"
|
||||
},
|
||||
"id": "code-parse-update-container-config",
|
||||
"name": "Parse Update Container Config",
|
||||
@@ -3736,6 +3685,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build remove old image command (fire and forget)\nconst currentImageId = $('Format Update Complete').item.json.currentImageId;\n\n// Skip if no image ID (error case)\nif (!currentImageId) {\n return { json: { cmd: 'true', skip: true } };\n}\n\n// Remove the old image - ignore errors (image might be used by another container)\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X DELETE 'http://docker-socket-proxy:2375/v1.47/images/${currentImageId}?force=false'`,\n currentImageId\n }\n};"
|
||||
},
|
||||
"id": "code-build-callback-remove-image",
|
||||
"name": "Build Callback Remove Image",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
6620,
|
||||
1650
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "={{ $json.cmd }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "exec-callback-remove-old-image",
|
||||
"name": "Callback Remove Old Image",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
6840,
|
||||
1650
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
@@ -4203,28 +4179,6 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse and Match": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Docker Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Format Response": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Docker Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Docker List for Action": {
|
||||
"main": [
|
||||
[
|
||||
@@ -4617,18 +4571,18 @@
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Inspect New Image",
|
||||
"node": "Inspect New Image (Text)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Inspect New Image": {
|
||||
"Inspect New Image (Text)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compare Update Images",
|
||||
"node": "Compare Digests",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
@@ -4646,6 +4600,17 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"Inspect New Image": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Compare Update Images",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check If Update Needed": {
|
||||
"main": [
|
||||
[
|
||||
@@ -5531,6 +5496,28 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send Update Complete": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build Callback Remove Image",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Build Callback Remove Image": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Callback Remove Old Image",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer Cancel Confirm Callback": {
|
||||
"main": [
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user