From 04321c1c9a4922f8d902609848194c8e7f8a8f3a Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 30 Jan 2026 13:26:32 -0500 Subject: [PATCH] feat(03-04): implement container update workflow Add complete update command flow with image pull and container recreation: - Parse update command and extract container query - Match container using fuzzy matching (single match only for update) - Handle no-match and multiple-match cases with appropriate messages - Inspect container to extract current config - Pull latest image from registry - Compare image digests to detect if update is available - Stay silent if no update needed (per CONTEXT.md) - Stop container with graceful 10-second timeout - Remove old container - Create new container preserving Config, HostConfig, Networks - Start new container - Report version change (from image labels or ID substring) Nodes added: - Parse Update Command, Docker List for Update, Match Update Container - Check Update Match Count, Handle Update Multiple, Send Update Error/No Match/Multiple - Build Inspect Command, Inspect Container, Parse Container Config - Build Pull Command, Pull Image, Build Image Inspect, Inspect New Image - Compare Digests, Check If Update Needed - Build Stop Command, Stop Container, Verify Stop Build Remove - Remove Container, Build Create Body, Build Create Command - Create Container, Parse Create Response - Build Start Command, Start New Container - Format Update Result, Send Update Result Closes all 3 tasks of 03-04-PLAN.md --- n8n-workflow.json | 796 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 795 insertions(+), 1 deletion(-) diff --git a/n8n-workflow.json b/n8n-workflow.json index e7d2128..65332c5 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -206,6 +206,29 @@ "combinator": "or" }, "renameOutput": false + }, + { + "id": "update-command-route", + "conditions": { + "options": { + "caseSensitive": false, + "leftValue": "", + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "starts-with-update", + "leftValue": "={{ $json.message.text.toLowerCase().trim() }}", + "rightValue": "update ", + "operator": { + "type": "string", + "operation": "startsWith" + } + } + ], + "combinator": "or" + }, + "renameOutput": false } ] }, @@ -1052,6 +1075,473 @@ "name": "Telegram API" } } + }, + { + "parameters": { + "jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};" + }, + "id": "code-parse-update", + "name": "Parse Update Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [900, 1000] + }, + { + "parameters": { + "command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'", + "options": {} + }, + "id": "exec-docker-list-update", + "name": "Docker List for Update", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [1120, 1000] + }, + { + "parameters": { + "jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" + }, + "id": "code-match-update-container", + "name": "Match Update Container", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1340, 1000] + }, + { + "parameters": { + "rules": { + "values": [ + { + "id": "update-docker-error", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "count-negative", + "leftValue": "={{ $json.matchCount }}", + "rightValue": "0", + "operator": { + "type": "number", + "operation": "lt" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "error" + }, + { + "id": "update-no-match", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "count-zero", + "leftValue": "={{ $json.matchCount }}", + "rightValue": "0", + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "no-match" + }, + { + "id": "update-single-match", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "count-one", + "leftValue": "={{ $json.matchCount }}", + "rightValue": "1", + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "single" + }, + { + "id": "update-multiple-matches", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "count-gt-one", + "leftValue": "={{ $json.matchCount }}", + "rightValue": "1", + "operator": { + "type": "number", + "operation": "gt" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "multiple" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "id": "switch-update-match-count", + "name": "Check Update Match Count", + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, + "position": [1560, 1000] + }, + { + "parameters": { + "resource": "message", + "operation": "sendMessage", + "chatId": "={{ $json.chatId }}", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "HTML" + } + }, + "id": "telegram-send-update-error", + "name": "Send Update Error", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [1780, 900], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};" + }, + "id": "code-update-multiple-handler", + "name": "Handle Update Multiple", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1780, 1100] + }, + { + "parameters": { + "resource": "message", + "operation": "sendMessage", + "chatId": "={{ $json.chatId }}", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "HTML" + } + }, + "id": "telegram-send-update-multiple", + "name": "Send Update Multiple", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [2000, 1100], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "resource": "message", + "operation": "sendMessage", + "chatId": "={{ $json.chatId }}", + "text": "=No container found matching '{{ $json.containerQuery }}'", + "additionalFields": { + "parse_mode": "HTML" + } + }, + "id": "telegram-send-update-no-match", + "name": "Send Update No Match", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [1780, 1000], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "jsCode": "// Build inspect command for the matched container\nconst containerId = $json.matches[0].Id;\nconst containerName = $json.matches[0].Name;\nconst chatId = $json.chatId;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/${containerId}/json'`,\n containerId,\n containerName,\n chatId\n }\n};" + }, + "id": "code-build-inspect-cmd", + "name": "Build Inspect Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1780, 1200] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-inspect-container", + "name": "Inspect Container", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [2000, 1200] + }, + { + "parameters": { + "jsCode": "// Parse container inspect output and extract config\nconst stdout = $input.item.json.stdout;\nconst cmdData = $('Build Inspect Command').item.json;\nconst containerId = cmdData.containerId;\nconst containerName = cmdData.containerName;\nconst chatId = cmdData.chatId;\n\nlet inspect;\ntry {\n inspect = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect ${containerName}: ${e.message}`\n }\n };\n}\n\nconst imageName = inspect.Config.Image;\nconst currentImageId = inspect.Image;\n\n// Extract version from labels if available\nconst labels = inspect.Config.Labels || {};\nconst currentVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || currentImageId.substring(7, 19);\n\nreturn {\n json: {\n imageName,\n currentImageId,\n currentVersion,\n containerConfig: inspect.Config,\n hostConfig: inspect.HostConfig,\n networkSettings: inspect.NetworkSettings,\n containerName,\n containerId,\n chatId\n }\n};" + }, + "id": "code-parse-container-config", + "name": "Parse Container Config", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2220, 1200] + }, + { + "parameters": { + "jsCode": "// Build pull image command\nconst imageName = $json.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,\n imageName,\n currentImageId: $json.currentImageId,\n currentVersion: $json.currentVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n containerName: $json.containerName,\n containerId: $json.containerId,\n chatId: $json.chatId\n }\n};" + }, + "id": "code-build-pull-cmd", + "name": "Build Pull Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2440, 1200] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-pull-image", + "name": "Pull Image", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [2660, 1200] + }, + { + "parameters": { + "jsCode": "// Build inspect image command to get the new image ID\nconst pullData = $('Build Pull Command').item.json;\nconst imageName = pullData.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/images/${encodeURIComponent(imageName)}/json'`,\n imageName,\n currentImageId: pullData.currentImageId,\n currentVersion: pullData.currentVersion,\n containerConfig: pullData.containerConfig,\n hostConfig: pullData.hostConfig,\n networkSettings: pullData.networkSettings,\n containerName: pullData.containerName,\n containerId: pullData.containerId,\n chatId: pullData.chatId\n }\n};" + }, + "id": "code-build-inspect-image-cmd", + "name": "Build Image Inspect", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2880, 1200] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-inspect-image", + "name": "Inspect New Image", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [3100, 1200] + }, + { + "parameters": { + "jsCode": "// Compare old and new image IDs to detect if update is needed\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Image Inspect').item.json;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\n\nlet newImage;\ntry {\n newImage = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect new image: ${e.message}`\n }\n };\n}\n\nconst newImageId = newImage.Id;\n\nif (currentImageId === newImageId) {\n // No update needed - stay silent per CONTEXT.md\n return { json: { needsUpdate: false, chatId } };\n}\n\n// Extract new version from labels\nconst labels = newImage.Config?.Labels || {};\nconst newVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || newImageId.substring(7, 19);\n\nreturn {\n json: {\n needsUpdate: true,\n currentImageId,\n newImageId,\n currentVersion: prevData.currentVersion,\n newVersion,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n containerName: prevData.containerName,\n containerId: prevData.containerId,\n chatId\n }\n};" + }, + "id": "code-compare-digests", + "name": "Compare Digests", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [3320, 1200] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "needs-update", + "leftValue": "={{ $json.needsUpdate }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "if-needs-update", + "name": "Check If Update Needed", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [3540, 1200] + }, + { + "parameters": { + "jsCode": "// Build stop container command\nconst containerId = $json.containerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n chatId: $json.chatId\n }\n};" + }, + "id": "code-build-stop-cmd", + "name": "Build Stop Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [3760, 1200] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-stop-container", + "name": "Stop Container", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [3980, 1200] + }, + { + "parameters": { + "jsCode": "// Verify container stopped and build remove command\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Stop Command').item.json;\nconst containerId = prevData.containerId;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: stopped, 304: already stopped - both OK\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to stop container: HTTP ${statusCode}`\n }\n };\n}\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X DELETE 'http://localhost/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n chatId\n }\n};" + }, + "id": "code-verify-stop-build-remove", + "name": "Verify Stop Build Remove", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [4200, 1200] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-remove-container", + "name": "Remove Container", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [4420, 1200] + }, + { + "parameters": { + "jsCode": "// Build container create request body from saved config\nconst prevData = $('Verify Stop Build Remove').item.json;\nconst config = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\n\n// Build NetworkingConfig from NetworkSettings\nconst networks = {};\nfor (const [name, netConfig] of Object.entries(networkSettings.Networks || {})) {\n networks[name] = {\n IPAMConfig: netConfig.IPAMConfig,\n Links: netConfig.Links,\n Aliases: netConfig.Aliases\n };\n}\n\nconst createBody = {\n ...config,\n HostConfig: hostConfig,\n NetworkingConfig: {\n EndpointsConfig: networks\n }\n};\n\n// Remove fields that shouldn't be in create request\ndelete createBody.Hostname;\ndelete createBody.Domainname;\n\nreturn {\n json: {\n createBody: JSON.stringify(createBody),\n containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n chatId\n }\n};" + }, + "id": "code-build-create-body", + "name": "Build Create Body", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [4640, 1200] + }, + { + "parameters": { + "jsCode": "// Build the create container command using a temp file approach\nconst createBody = $json.createBody;\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\n\n// Write body to command using here-doc to avoid shell escaping issues\nconst cmd = `curl -s -X POST --unix-socket /var/run/docker.sock -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://localhost/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n cmd,\n containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n chatId\n }\n};" + }, + "id": "code-build-create-cmd", + "name": "Build Create Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [4860, 1200] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-create-container", + "name": "Create Container", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [5080, 1200] + }, + { + "parameters": { + "jsCode": "// Parse create response and extract new container ID\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Create Command').item.json;\nconst chatId = prevData.chatId;\n\nlet response;\ntry {\n response = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${stdout}`\n }\n };\n}\n\nif (response.message) {\n // Error response from Docker\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${response.message}`\n }\n };\n}\n\nreturn {\n json: {\n newContainerId: response.Id,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n chatId\n }\n};" + }, + "id": "code-parse-create-response", + "name": "Parse Create Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [5300, 1200] + }, + { + "parameters": { + "jsCode": "// Build start command for new container\nconst newContainerId = $json.newContainerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${newContainerId}/start'`,\n newContainerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n chatId: $json.chatId\n }\n};" + }, + "id": "code-build-start-cmd", + "name": "Build Start Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [5520, 1200] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-start-new-container", + "name": "Start New Container", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [5740, 1200] + }, + { + "parameters": { + "jsCode": "// Parse start result and format success message\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Start Command').item.json;\nconst containerName = prevData.containerName;\nconst currentVersion = prevData.currentVersion;\nconst newVersion = prevData.newVersion;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n chatId,\n text: `Container created but failed to start: HTTP ${statusCode}`\n }\n };\n}\n\nconst message = `${containerName} updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message\n }\n};" + }, + "id": "code-format-update-result", + "name": "Format Update Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [5960, 1200] + }, + { + "parameters": { + "resource": "message", + "operation": "sendMessage", + "chatId": "={{ $json.chatId }}", + "text": "={{ $json.text }}", + "additionalFields": { + "parse_mode": "HTML" + } + }, + "id": "telegram-send-update-result", + "name": "Send Update Result", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [6180, 1200], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } } ], "connections": { @@ -1266,7 +1756,13 @@ "index": 0 } ], - [], + [ + { + "node": "Parse Update Command", + "type": "main", + "index": 0 + } + ], [ { "node": "Format Echo", @@ -1546,6 +2042,304 @@ } ] ] + }, + "Parse Update Command": { + "main": [ + [ + { + "node": "Docker List for Update", + "type": "main", + "index": 0 + } + ] + ] + }, + "Docker List for Update": { + "main": [ + [ + { + "node": "Match Update Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Match Update Container": { + "main": [ + [ + { + "node": "Check Update Match Count", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Update Match Count": { + "main": [ + [ + { + "node": "Send Update Error", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send Update No Match", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Build Inspect Command", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Handle Update Multiple", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "Handle Update Multiple": { + "main": [ + [ + { + "node": "Send Update Multiple", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Inspect Command": { + "main": [ + [ + { + "node": "Inspect Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Inspect Container": { + "main": [ + [ + { + "node": "Parse Container Config", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Container Config": { + "main": [ + [ + { + "node": "Build Pull Command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Pull Command": { + "main": [ + [ + { + "node": "Pull Image", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pull Image": { + "main": [ + [ + { + "node": "Build Image Inspect", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Image Inspect": { + "main": [ + [ + { + "node": "Inspect New Image", + "type": "main", + "index": 0 + } + ] + ] + }, + "Inspect New Image": { + "main": [ + [ + { + "node": "Compare Digests", + "type": "main", + "index": 0 + } + ] + ] + }, + "Compare Digests": { + "main": [ + [ + { + "node": "Check If Update Needed", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check If Update Needed": { + "main": [ + [ + { + "node": "Build Stop Command", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "Build Stop Command": { + "main": [ + [ + { + "node": "Stop Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Stop Container": { + "main": [ + [ + { + "node": "Verify Stop Build Remove", + "type": "main", + "index": 0 + } + ] + ] + }, + "Verify Stop Build Remove": { + "main": [ + [ + { + "node": "Remove Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Remove Container": { + "main": [ + [ + { + "node": "Build Create Body", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Create Body": { + "main": [ + [ + { + "node": "Build Create Command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Create Command": { + "main": [ + [ + { + "node": "Create Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Container": { + "main": [ + [ + { + "node": "Parse Create Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Create Response": { + "main": [ + [ + { + "node": "Build Start Command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Start Command": { + "main": [ + [ + { + "node": "Start New Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start New Container": { + "main": [ + [ + { + "node": "Format Update Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Update Result": { + "main": [ + [ + { + "node": "Send Update Result", + "type": "main", + "index": 0 + } + ] + ] } }, "pinData": {},