diff --git a/n8n-workflow.json b/n8n-workflow.json index 22e7b3e..f553245 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -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 = 'Docker Containers\\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 += '\\nRunning:\\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 = '' + name + '\\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} ${container.name}\\n\\n` +\n `State: ${container.state}\\n` +\n `Status: ${container.status}\\n` +\n `Image: ${container.image}\\n` +\n `ID: ${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} ${containerName} - Logs\\n\\n
${logs}
`;\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} ${containerName} - Logs (${timestamp})\\n\\n
${logs}
`;\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": [ [