From d0b03b71591df1ccc68f77bc079eb8ebe63ba7c1 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 4 Feb 2026 13:08:37 -0500 Subject: [PATCH] feat(10-02): wire main workflow to use container-update sub-workflow - Removed 54 duplicate update nodes from text and callback paths - Added Execute Sub-workflow nodes for both update entry points - Text path: Check Update Match Count -> Prepare Text Update Input -> Execute Text Update - Callback path: Route Confirm Action -> Prepare Callback Update Input -> Execute Callback Update - Workflow reduced from 246 to 200 nodes (-19%) - Line count reduced from ~8485 to 7209 (-15%) DEBT-03 (duplicated update flow) resolved - update logic now in one place --- n8n-workflow.json | 1618 ++++++--------------------------------------- 1 file changed, 195 insertions(+), 1423 deletions(-) diff --git a/n8n-workflow.json b/n8n-workflow.json index 3fc655a..4cd7fc9 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -1887,479 +1887,6 @@ } } }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=Updating {{ $json.matches[0].Name }}...", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-started", - "name": "Send Update Started", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1560, - 1200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build inspect command for the matched container\n// Pass through original data from Match Update Container\nconst matchData = $('Check Update Match Count').item.json;\nconst containerId = matchData.matches[0].Id;\nconst containerName = matchData.matches[0].Name;\nconst chatId = matchData.chatId;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 5 'http://docker-socket-proxy:2375/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\nlet imageName = inspect.Config.Image;\nconst currentImageId = inspect.Image;\n\n// CRITICAL: Ensure image has a tag, otherwise Docker pulls ALL tags!\n// If no tag specified, default to :latest\nif (imageName && !imageName.includes(':') && !imageName.includes('@')) {\n imageName = imageName + ':latest';\n}\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\n// Pipe through tail to only keep last 10KB - avoids memory issues with large pulls\n// Error/success messages appear at the end of the stream\nreturn {\n json: {\n cmd: `curl -s --max-time 600 -X POST 'http://docker-socket-proxy:2375/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}' | tail -c 10000`,\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": { - "timeout": 660 - } - }, - "id": "exec-pull-image", - "name": "Pull Image", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2660, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Check pull response for errors\nconst stdout = $input.item.json.stdout || '';\nconst pullData = $('Build Pull Command').item.json;\nconst chatId = pullData.chatId;\nconst containerName = pullData.containerName;\n\n// Docker pull streams JSON objects, check for error messages\n// Rate limit error: {\"message\":\"toomanyrequests: ...\"}\n// Other errors: {\"message\":\"...\"}\nif (stdout.includes('\"message\"') && (stdout.includes('toomanyrequests') || stdout.includes('error') || stdout.includes('denied'))) {\n // Extract error message\n let errorMsg = 'Pull failed';\n try {\n const match = stdout.match(/\"message\"\\s*:\\s*\"([^\"]+)\"/); if (match) errorMsg = match[1];\n } catch (e) {}\n \n return {\n json: {\n pullError: true,\n chatId,\n text: `Failed to update ${containerName}: ${errorMsg.substring(0, 100)}`\n }\n };\n}\n\n// Success - pass through data for next node\nreturn {\n json: {\n pullError: false,\n imageName: pullData.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-check-pull-response", - "name": "Check Pull Response", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2770, - 1200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "pull-error-check", - "leftValue": "={{ $json.pullError }}", - "rightValue": false, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-pull-success", - "name": "Check Pull Success", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2880, - 1200 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-pull-error", - "name": "Send Pull Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 3100, - 1350 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build inspect image command to get the new image ID\nconst imageName = $json.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/images/${encodeURIComponent(imageName)}/json'`,\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-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 (Text)", - "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 - notify user\n return { json: { needsUpdate: false, chatId, containerName: prevData.containerName } };\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": "// Format 'already up to date' message\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\n\nreturn {\n json: {\n chatId,\n text: `${containerName} is already up to date`\n }\n};" - }, - "id": "code-format-no-update", - "name": "Format No Update", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3540, - 1400 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-no-update", - "name": "Send No Update", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 3760, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build stop container command\nconst containerId = $json.containerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n currentImageId: $json.currentImageId,\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}\" --max-time 5 -X DELETE 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n currentImageId: prevData.currentImageId,\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 currentImageId: prevData.currentImageId,\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 --max-time 5 -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://docker-socket-proxy:2375/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n cmd,\n containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n currentImageId: $json.currentImageId,\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 currentImageId: prevData.currentImageId,\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}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${newContainerId}/start'`,\n newContainerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n currentImageId: $json.currentImageId,\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 currentImageId = prevData.currentImageId;\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: `Failed to update ${containerName}`,\n currentImageId: null\n }\n };\n}\n\nconst message = `${containerName} updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message,\n currentImageId\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": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build remove old image command (fire and forget)\n// Reference Format Update Result since Telegram node doesn't pass through our data\nconst currentImageId = $('Format Update Result').item.json.currentImageId;\n\n// Skip if no image ID (error case) - use no-op command\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-remove-image", - "name": "Build Remove Image Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 6400, - 1200 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-remove-old-image", - "name": "Remove Old Image", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 6620, - 1200 - ] - }, { "parameters": { "jsCode": "// Parse logs command from message\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Extract container name and optional line count\n// Formats: \"logs plex\", \"logs plex 100\", \"show logs plex\"\nlet containerQuery = '';\nlet lineCount = 50; // default\n\n// Remove \"show\" prefix if present\nlet cleanText = text.replace(/^show\\s+/, '');\n// Remove \"logs\" keyword\ncleanText = cleanText.replace(/^logs\\s*/, '');\n\n// Check for line count at the end\nconst parts = cleanText.trim().split(/\\s+/);\nif (parts.length > 1) {\n const lastPart = parts[parts.length - 1];\n if (/^\\d+$/.test(lastPart)) {\n lineCount = Math.min(Math.max(parseInt(lastPart), 1), 1000);\n parts.pop();\n }\n}\ncontainerQuery = parts.join(' ').trim();\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n chatId: chatId,\n text: 'Please specify a container name (e.g., \"logs plex\" or \"logs plex 100\")'\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: lineCount,\n chatId: chatId,\n messageId: messageId\n }\n};" @@ -3620,394 +3147,6 @@ } } }, - { - "parameters": { - "jsCode": "// Build update progress message - removes buttons during operation\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Progress message with no buttons (prevents duplicate actions)\nconst text = `\\u2B06\\uFE0F Updating ${containerName}...\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.`;\n\n// Empty keyboard removes all buttons during update\nconst reply_markup = { inline_keyboard: [] };\n\nreturn {\n json: {\n containerName,\n chatId,\n messageId,\n progressText: text,\n reply_markup\n }\n};" - }, - "id": "code-prepare-confirmed-update", - "name": "Prepare Confirmed Update", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1650 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.progressText, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-show-update-progress", - "name": "Show Update Progress", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1650 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-update", - "name": "Get Container For Update", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1650 - ] - }, - { - "parameters": { - "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", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1650 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/{{ $json.containerId }}/json", - "options": {} - }, - "id": "http-inspect-container-for-update", - "name": "Inspect Container For Update", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1650 - ] - }, - { - "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\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", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3100, - 1650 - ] - }, - { - "parameters": { - "command": "={{ $json.pullCmd }}", - "options": {} - }, - "id": "exec-pull-update-image", - "name": "Pull Update Image", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 3320, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Check pull result and get new image ID\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Parse Update Container Config').item.json;\nconst containerId = prevData.containerId;\nconst containerName = prevData.containerName;\nconst fullContainerName = prevData.fullContainerName;\nconst imageName = prevData.imageName;\nconst currentImageId = prevData.currentImageId;\nconst currentDigest = prevData.currentDigest;\nconst containerConfig = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Check if pull succeeded (contains status messages)\nconst pullSuccess = stdout.includes('Pulling') || stdout.includes('Downloaded') || stdout.includes('Status:') || stdout.includes('Digest:');\n\nif (!pullSuccess && stdout.includes('error')) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Failed to pull image for ${containerName}`\n }\n };\n}\n\nreturn {\n json: {\n inspectCmd: `curl -s --max-time 10 'http://docker-socket-proxy:2375/v1.47/images/${encodeURIComponent(imageName)}/json'`,\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-check-pull-result", - "name": "Check Pull Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3540, - 1650 - ] - }, - { - "parameters": { - "command": "={{ $json.inspectCmd }}", - "options": {} - }, - "id": "exec-inspect-new-image", - "name": "Inspect New Image", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 3760, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Compare image digests and decide if update needed\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Check Pull Result').item.json;\nconst containerId = prevData.containerId;\nconst containerName = prevData.containerName;\nconst fullContainerName = prevData.fullContainerName;\nconst imageName = prevData.imageName;\nconst currentImageId = prevData.currentImageId;\nconst currentDigest = prevData.currentDigest;\nconst containerConfig = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nlet imageData;\ntry {\n imageData = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Failed to inspect new image for ${containerName}`\n }\n };\n}\n\nconst newImageId = imageData.Id;\nconst newDigest = newImageId.substring(7, 19);\n\n// Check if image changed\nconst needsUpdate = newImageId !== currentImageId;\n\nif (!needsUpdate) {\n return {\n json: {\n needsUpdate: false,\n chatId,\n messageId,\n containerName,\n text: `${containerName} is already up to date.`\n }\n };\n}\n\n// Proceed with update\nreturn {\n json: {\n needsUpdate: true,\n stopCmd: `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,\n fullContainerName,\n imageName,\n currentImageId,\n newImageId,\n currentDigest,\n newDigest,\n containerConfig,\n hostConfig,\n networkSettings,\n chatId,\n messageId\n }\n};" - }, - "id": "code-compare-update-images", - "name": "Compare Update Images", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3980, - 1650 - ] - }, - { - "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 Needs Update", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 4200, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Container is already up to date - show completion with back button only\nconst data = $input.item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Completion message shows only navigation button (removes action buttons)\nconst keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n};\n\nconst text = `\\u2705 ${containerName} already up to date\\n\\nNo changes needed.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-format-no-update-needed", - "name": "Format No Update Needed", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4420, - 1750 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-no-update-needed", - "name": "Send No Update Needed", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 4640, - 1750 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "command": "={{ $json.stopCmd }}", - "options": {} - }, - "id": "exec-stop-for-update", - "name": "Stop For Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 4420, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Verify stop and build remove command\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Compare Update Images').item.json;\nconst containerId = prevData.containerId;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst containerName = prevData.containerName;\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 messageId,\n containerName,\n text: `Failed to stop container: HTTP ${statusCode}`\n }\n };\n}\n\nreturn {\n json: {\n removeCmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X DELETE 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n fullContainerName: prevData.fullContainerName,\n imageName: prevData.imageName,\n currentImageId: prevData.currentImageId,\n newImageId: prevData.newImageId,\n currentDigest: prevData.currentDigest,\n newDigest: prevData.newDigest,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n chatId,\n messageId\n }\n};" - }, - "id": "code-verify-update-stop", - "name": "Verify Update Stop", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4640, - 1650 - ] - }, - { - "parameters": { - "command": "={{ $json.removeCmd }}", - "options": {} - }, - "id": "exec-remove-for-update", - "name": "Remove For Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 4860, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Build container create request body\nconst prevData = $('Verify Update Stop').item.json;\nconst config = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst containerName = prevData.fullContainerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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 shortName: prevData.containerName,\n currentDigest: prevData.currentDigest,\n newDigest: prevData.newDigest,\n currentImageId: prevData.currentImageId,\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-update-create-body", - "name": "Build Update Create Body", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5080, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Build create container command\nconst createBody = $json.createBody;\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\n\nconst cmd = `curl -s -X POST --max-time 5 -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://docker-socket-proxy:2375/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n createCmd: cmd,\n containerName,\n shortName: $json.shortName,\n currentDigest: $json.currentDigest,\n newDigest: $json.newDigest,\n currentImageId: $json.currentImageId,\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-update-create-cmd", - "name": "Build Update Create Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5300, - 1650 - ] - }, - { - "parameters": { - "command": "={{ $json.createCmd }}", - "options": {} - }, - "id": "exec-create-for-update", - "name": "Create For Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 5520, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Parse create response and extract new container ID\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Update Create Command').item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst containerName = prevData.shortName;\n\nlet response;\ntry {\n response = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Create failed: ${stdout}`\n }\n };\n}\n\nif (response.message) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Create failed: ${response.message}`\n }\n };\n}\n\nreturn {\n json: {\n startCmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${response.Id}/start'`,\n newContainerId: response.Id,\n containerName,\n currentDigest: prevData.currentDigest,\n newDigest: prevData.newDigest,\n currentImageId: prevData.currentImageId,\n chatId,\n messageId\n }\n};" - }, - "id": "code-parse-update-create-response", - "name": "Parse Update Create Response", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5740, - 1650 - ] - }, - { - "parameters": { - "command": "={{ $json.startCmd }}", - "options": {} - }, - "id": "exec-start-after-update", - "name": "Start After Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 5960, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Build update completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Parse Update Create Response').item.json;\nconst containerName = prevData.containerName;\nconst currentDigest = prevData.currentDigest;\nconst newDigest = prevData.newDigest;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nconst success = statusCode === 204 || statusCode === 304;\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = `\\u2705 ${containerName} updated successfully\\n\\n${currentDigest} \\u2192 ${newDigest}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to start ${containerName} after update`;\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `confirm:update:${containerName}:${timestamp}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard,\n currentImageId\n }\n};" - }, - "id": "code-format-update-complete", - "name": "Format Update Complete", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 6180, - 1650 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-update-complete", - "name": "Send Update Complete", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 6400, - 1650 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "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", @@ -5770,6 +4909,139 @@ 2000, 4200 ] + }, + { + "parameters": { + "jsCode": "// Prepare input for container-update sub-workflow (text mode)\nconst matchData = $('Check Update Match Count').item.json;\nconst containerId = matchData.matches[0].Id;\nconst containerName = matchData.matches[0].Name;\nconst chatId = matchData.chatId;\n\nreturn {\n json: {\n containerId,\n containerName,\n chatId,\n messageId: 0, // No message to edit in text mode\n responseMode: 'text'\n }\n};" + }, + "id": "code-prepare-text-update", + "name": "Prepare Text Update Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + 1200 + ] + }, + { + "parameters": { + "resource": "message", + "operation": "sendMessage", + "chatId": "={{ $('Check Update Match Count').item.json.chatId }}", + "text": "=Updating {{ $('Check Update Match Count').item.json.matches[0].Name }}...", + "additionalFields": { + "parse_mode": "HTML" + } + }, + "id": "telegram-text-update-started", + "name": "Send Text Update Started", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1780, + 1400 + ], + "credentials": { + "telegramApi": { + "id": "I0xTTiASl7C1NZhJ", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "source": "database", + "workflowId": "={{ $env.CONTAINER_UPDATE_WORKFLOW_ID }}", + "mode": "once", + "options": { + "waitForSubWorkflow": true + } + }, + "id": "exec-text-update-subworkflow", + "name": "Execute Text Update", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [ + 2000, + 1200 + ] + }, + { + "parameters": { + "jsCode": "// Prepare input for container-update sub-workflow (inline mode)\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n containerId: '', // Will be resolved by sub-workflow from name\n containerName,\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" + }, + "id": "code-prepare-callback-update", + "name": "Prepare Callback Update Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2000, + 1650 + ] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: '\\u2B06\\uFE0F Updating ' + $json.containerName + '...\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.', parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } }) }}", + "options": {} + }, + "id": "http-callback-update-progress", + "name": "Show Callback Update Progress", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2220, + 1650 + ] + }, + { + "parameters": { + "method": "GET", + "url": "=http://docker-socket-proxy:2375/containers/json?all=true", + "options": {} + }, + "id": "http-get-container-callback", + "name": "Get Container For Callback Update", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2440, + 1650 + ] + }, + { + "parameters": { + "jsCode": "// Find container ID from name for callback update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Callback Update Input').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 not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" + }, + "id": "code-find-container-callback", + "name": "Find Container For Callback Update", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2660, + 1650 + ] + }, + { + "parameters": { + "source": "database", + "workflowId": "={{ $env.CONTAINER_UPDATE_WORKFLOW_ID }}", + "mode": "once", + "options": { + "waitForSubWorkflow": true + } + }, + "id": "exec-callback-update-subworkflow", + "name": "Execute Callback Update", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [ + 2880, + 1650 + ] } ], "connections": { @@ -6486,7 +5758,7 @@ ], [ { - "node": "Send Update Started", + "node": "Prepare Text Update Input", "type": "main", "index": 0 } @@ -6512,317 +5784,6 @@ ] ] }, - "Send Update Started": { - "main": [ - [ - { - "node": "Build Inspect Command", - "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": "Check Pull Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Pull Response": { - "main": [ - [ - { - "node": "Check Pull Success", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Pull Success": { - "main": [ - [ - { - "node": "Build Image Inspect", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Pull Error", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Image Inspect": { - "main": [ - [ - { - "node": "Inspect New Image (Text)", - "type": "main", - "index": 0 - } - ] - ] - }, - "Inspect New Image (Text)": { - "main": [ - [ - { - "node": "Compare Digests", - "type": "main", - "index": 0 - } - ] - ] - }, - "Compare Digests": { - "main": [ - [ - { - "node": "Check If Update Needed", - "type": "main", - "index": 0 - } - ] - ] - }, - "Inspect New Image": { - "main": [ - [ - { - "node": "Compare Update Images", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check If Update Needed": { - "main": [ - [ - { - "node": "Build Stop Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Format No Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format No Update": { - "main": [ - [ - { - "node": "Send No Update", - "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 - } - ] - ] - }, - "Send Update Result": { - "main": [ - [ - { - "node": "Build Remove Image Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Remove Image Command": { - "main": [ - [ - { - "node": "Remove Old Image", - "type": "main", - "index": 0 - } - ] - ] - }, "Parse Logs Command": { "main": [ [ @@ -7475,7 +6436,7 @@ ], [ { - "node": "Prepare Confirmed Update", + "node": "Prepare Callback Update Input", "type": "main", "index": 0 } @@ -7537,255 +6498,6 @@ ] ] }, - "Prepare Confirmed Update": { - "main": [ - [ - { - "node": "Show Update Progress", - "type": "main", - "index": 0 - } - ] - ] - }, - "Show Update Progress": { - "main": [ - [ - { - "node": "Get Container For Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Update": { - "main": [ - [ - { - "node": "Find Container For Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Container For Update": { - "main": [ - [ - { - "node": "Inspect Container For Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Inspect Container For Update": { - "main": [ - [ - { - "node": "Parse Update Container Config", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Update Container Config": { - "main": [ - [ - { - "node": "Pull Update Image", - "type": "main", - "index": 0 - } - ] - ] - }, - "Pull Update Image": { - "main": [ - [ - { - "node": "Check Pull Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Pull Result": { - "main": [ - [ - { - "node": "Inspect New Image", - "type": "main", - "index": 0 - } - ] - ] - }, - "Compare Update Images": { - "main": [ - [ - { - "node": "Check If Needs Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check If Needs Update": { - "main": [ - [ - { - "node": "Stop For Update", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Format No Update Needed", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format No Update Needed": { - "main": [ - [ - { - "node": "Send No Update Needed", - "type": "main", - "index": 0 - } - ] - ] - }, - "Stop For Update": { - "main": [ - [ - { - "node": "Verify Update Stop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Verify Update Stop": { - "main": [ - [ - { - "node": "Remove For Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Remove For Update": { - "main": [ - [ - { - "node": "Build Update Create Body", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update Create Body": { - "main": [ - [ - { - "node": "Build Update Create Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update Create Command": { - "main": [ - [ - { - "node": "Create For Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Create For Update": { - "main": [ - [ - { - "node": "Parse Update Create Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Update Create Response": { - "main": [ - [ - { - "node": "Start After Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Start After Update": { - "main": [ - [ - { - "node": "Format Update Complete", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Update Complete": { - "main": [ - [ - { - "node": "Send Update Complete", - "type": "main", - "index": 0 - } - ] - ] - }, - "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": [ [ @@ -8425,6 +7137,66 @@ } ] ] + }, + "Prepare Text Update Input": { + "main": [ + [ + { + "node": "Send Text Update Started", + "type": "main", + "index": 0 + }, + { + "node": "Execute Text Update", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Callback Update Input": { + "main": [ + [ + { + "node": "Show Callback Update Progress", + "type": "main", + "index": 0 + } + ] + ] + }, + "Show Callback Update Progress": { + "main": [ + [ + { + "node": "Get Container For Callback Update", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Container For Callback Update": { + "main": [ + [ + { + "node": "Find Container For Callback Update", + "type": "main", + "index": 0 + } + ] + ] + }, + "Find Container For Callback Update": { + "main": [ + [ + { + "node": "Execute Callback Update", + "type": "main", + "index": 0 + } + ] + ] } }, "pinData": {},