diff --git a/n8n-update.json b/n8n-update.json index 2cc7478..b3cef3e 100644 --- a/n8n-update.json +++ b/n8n-update.json @@ -75,26 +75,45 @@ }, { "parameters": { - "url": "http://docker-socket-proxy:2375/v1.47/containers/json?all=true", + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "none", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.UNRAID_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\"query\": \"query { docker { containers { id names state image imageId } } }\"}", "options": { - "timeout": 5000 + "timeout": 15000 } }, - "id": "http-get-containers", - "name": "Get All Containers", + "id": "http-query-containers", + "name": "Query All Containers", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 560, 400 - ] + ], + "onError": "continueRegularOutput" }, { "parameters": { - "jsCode": "// Find container by name and resolve ID\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerName = triggerData.containerName;\nconst containers = $input.all();\n\n// Normalize function to strip leading slash\nconst normalizeName = (name) => name.replace(/^\\//, '').toLowerCase();\nconst searchName = normalizeName(containerName);\n\n// Find matching container\nlet matched = null;\nfor (const item of containers) {\n const c = item.json;\n if (c.Names && c.Names.length > 0) {\n const cName = normalizeName(c.Names[0]);\n if (cName === searchName || cName.includes(searchName)) {\n matched = c;\n break;\n }\n }\n}\n\nif (!matched) {\n throw new Error(`Container '${containerName}' not found`);\n}\n\nreturn {\n json: {\n ...triggerData,\n containerId: matched.Id\n }\n};" + "jsCode": "// GraphQL Response Normalizer - Transform Unraid GraphQL to Docker API contract\n// Input: $input.item.json = raw GraphQL response\n// Output: Array of normalized containers\n\nconst response = $input.item.json;\n\n// Validation: Check for GraphQL errors\nif (response.errors && response.errors.length > 0) {\n const messages = response.errors.map(e => e.message).join('; ');\n throw new Error(`GraphQL error: ${messages}`);\n}\n\n// Validation: Check response structure\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure: missing data.docker.containers');\n}\n\n// State mapping: Unraid UPPERCASE -> Docker lowercase\nconst stateMap = {\n 'RUNNING': 'running',\n 'STOPPED': 'exited', // Docker convention: stopped = exited\n 'PAUSED': 'paused'\n};\n\nfunction normalizeState(unraidState) {\n return stateMap[unraidState] || unraidState.toLowerCase();\n}\n\n// Transform each container\nconst containers = response.data.docker.containers;\nconst normalized = containers.map(container => {\n const dockerState = normalizeState(container.state);\n \n return {\n // Core fields matching Docker API contract\n Id: container.id, // Keep full PrefixedID (registry handles translation)\n Names: container.names, // Already has '/' prefix (Phase 14 verified)\n State: dockerState, // Normalized lowercase state\n Status: dockerState, // Docker has separate Status field\n Image: '', // Not available in basic query\n \n // Debug field: preserve original Unraid ID\n _unraidId: container.id\n };\n});\n\n// Return as array of items (n8n multi-item output format)\nreturn normalized.map(container => ({ json: container }));\n" }, - "id": "code-resolve-id", - "name": "Resolve Container ID", + "id": "code-normalize-response", + "name": "Normalize GraphQL Response", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ @@ -103,57 +122,99 @@ ] }, { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/{{ $json.containerId }}/json", - "options": { - "timeout": 5000 - } - }, - "id": "http-inspect-container", - "name": "Inspect Container", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 460, - 300 - ] - }, - { - "parameters": { - "jsCode": "// Parse container config and prepare for pull\nconst inspectData = $input.item.json;\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerId = triggerData.containerId;\nconst containerName = triggerData.containerName;\nconst chatId = triggerData.chatId;\nconst messageId = triggerData.messageId;\nconst responseMode = triggerData.responseMode;\nconst correlationId = triggerData.correlationId || '';\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 labels or image digest\nconst labels = containerConfig.Labels || {};\nconst currentVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || currentImageId.substring(7, 19);\n\nreturn {\n json: {\n containerId,\n containerName,\n chatId,\n messageId,\n responseMode,\n imageName,\n currentImageId,\n currentVersion,\n containerConfig,\n hostConfig,\n networkSettings,\n correlationId\n }\n};" - }, - "id": "code-parse-config", - "name": "Parse Container Config", + "id": "code-update-registry", + "name": "Update Container ID Registry", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 680, - 300 + 880, + 400 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "// Container ID Registry - Update action only\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst containers = $input.all().map(item => item.json);\nconst containerMap = JSON.parse(registry._containerIdMap);\n\n// Update map from normalized containers\nfor (const container of containers) {\n const name = (container.Names?.[0] || '').replace(/^\\//, '').toLowerCase();\n if (name && container.Id) {\n containerMap[name] = {\n name: name,\n unraidId: container.Id,\n timestamp: Date.now()\n };\n }\n}\n\nregistry._containerIdMap = JSON.stringify(containerMap);\nregistry._lastRefresh = Date.now();\n\n// Pass through all containers\nreturn containers.map(c => ({ json: c }));\n" + } + }, + { + "parameters": { + "jsCode": "// Find container by name and resolve ID\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerName = triggerData.containerName;\nconst containers = $input.all();\n\n// Normalize function to strip leading slash\nconst normalizeName = (name) => name.replace(/^\\//, '').toLowerCase();\nconst searchName = normalizeName(containerName);\n\n// Find matching container\nlet matched = null;\nfor (const item of containers) {\n const c = item.json;\n if (c.Names && c.Names.length > 0) {\n const cName = normalizeName(c.Names[0]);\n if (cName === searchName || cName.includes(searchName)) {\n matched = c;\n break;\n }\n }\n}\n\nif (!matched) {\n throw new Error(`Container '${containerName}' not found`);\n}\n\nreturn {\n json: {\n ...triggerData,\n containerId: matched.Id,\n unraidId: matched.Id, // Full PrefixedID for GraphQL mutation\n currentImageId: matched.imageId || '', // For later comparison\n currentImage: matched.image || ''\n }\n};\n" + }, + "id": "code-resolve-id", + "name": "Resolve Container ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1040, + 400 ] }, { "parameters": { - "command": "=curl -s --max-time 600 -X POST 'http://docker-socket-proxy:2375/v1.47/images/create?fromImage={{ encodeURIComponent($json.imageName) }}' | tail -c 10000", + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "none", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.UNRAID_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\"query\": \"query { docker { containers(filter: { id: \\\"\" + $json.containerId + \"\\\" }) { id names state image imageId } } }\"} }}", "options": { - "timeout": 660 + "timeout": 15000 } }, - "id": "exec-pull-image", - "name": "Pull Image", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, + "id": "http-query-single", + "name": "Query Single Container", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, "position": [ - 900, + 560, + 300 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "jsCode": "// GraphQL Response Normalizer - Transform Unraid GraphQL to Docker API contract\n// Input: $input.item.json = raw GraphQL response\n// Output: Array of normalized containers\n\nconst response = $input.item.json;\n\n// Validation: Check for GraphQL errors\nif (response.errors && response.errors.length > 0) {\n const messages = response.errors.map(e => e.message).join('; ');\n throw new Error(`GraphQL error: ${messages}`);\n}\n\n// Validation: Check response structure\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure: missing data.docker.containers');\n}\n\n// State mapping: Unraid UPPERCASE -> Docker lowercase\nconst stateMap = {\n 'RUNNING': 'running',\n 'STOPPED': 'exited', // Docker convention: stopped = exited\n 'PAUSED': 'paused'\n};\n\nfunction normalizeState(unraidState) {\n return stateMap[unraidState] || unraidState.toLowerCase();\n}\n\n// Transform each container\nconst containers = response.data.docker.containers;\nconst normalized = containers.map(container => {\n const dockerState = normalizeState(container.state);\n \n return {\n // Core fields matching Docker API contract\n Id: container.id, // Keep full PrefixedID (registry handles translation)\n Names: container.names, // Already has '/' prefix (Phase 14 verified)\n State: dockerState, // Normalized lowercase state\n Status: dockerState, // Docker has separate Status field\n Image: '', // Not available in basic query\n \n // Debug field: preserve original Unraid ID\n _unraidId: container.id\n };\n});\n\n// Return as array of items (n8n multi-item output format)\nreturn normalized.map(container => ({ json: container }));\n" + }, + "id": "code-normalize-single", + "name": "Normalize Single Container", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 720, 300 ] }, { "parameters": { - "jsCode": "// Check pull response for errors\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Parse Container Config').item.json;\n\n// Docker pull streams JSON objects, check for error messages\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*\"([^\"]+)\"/);\n if (match) errorMsg = match[1];\n } catch (e) {}\n \n return {\n json: {\n pullError: true,\n errorMessage: errorMsg.substring(0, 100),\n ...prevData\n }\n };\n}\n\n// Success - pass through data\nreturn {\n json: {\n pullError: false,\n ...prevData\n }\n};" + "jsCode": "// Capture pre-update state from input\nconst data = $input.item.json;\nconst triggerData = $('When executed by another workflow').item.json;\n\n// Check if we have container data already (from Resolve path) or need to extract (from direct ID path)\nlet unraidId, containerName, currentImageId, currentImage;\n\nif (data.unraidId) {\n // From Resolve Container ID path\n unraidId = data.unraidId;\n containerName = data.containerName;\n currentImageId = data.currentImageId;\n currentImage = data.currentImage;\n} else if (data.Id) {\n // From Query Single Container path (normalized)\n unraidId = data.Id;\n containerName = (data.Names?.[0] || '').replace(/^\\//, '');\n currentImageId = data.imageId || '';\n currentImage = data.image || '';\n} else {\n throw new Error('No container data found');\n}\n\nreturn {\n json: {\n unraidId,\n containerName,\n currentImageId,\n currentImage,\n chatId: triggerData.chatId,\n messageId: triggerData.messageId,\n responseMode: triggerData.responseMode,\n correlationId: triggerData.correlationId || ''\n }\n};\n" }, - "id": "code-check-pull", - "name": "Check Pull Response", + "id": "code-capture-state", + "name": "Capture Pre-Update State", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 920, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Build GraphQL updateContainer mutation\nconst data = $input.item.json;\nreturn {\n json: {\n ...data,\n query: `mutation { docker { updateContainer(id: \"${data.unraidId}\") { id state image imageId } } }`\n }\n};\n" + }, + "id": "code-build-mutation", + "name": "Build Update Mutation", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ @@ -161,6 +222,54 @@ 300 ] }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "none", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.UNRAID_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\"query\": $json.query} }}", + "options": { + "timeout": 60000 + } + }, + "id": "http-update-container", + "name": "Update Container", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1320, + 300 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "jsCode": "// Handle updateContainer mutation response\nconst response = $input.item.json;\nconst prevData = $('Capture Pre-Update State').item.json;\n\n// Check for GraphQL errors\nif (response.errors) {\n const error = response.errors[0];\n return {\n json: {\n success: false,\n error: true,\n errorMessage: error.message || 'Update failed',\n ...prevData\n }\n };\n}\n\n// Extract updated container from response\nconst updated = response.data?.docker?.updateContainer;\nif (!updated) {\n return {\n json: {\n success: false,\n error: true,\n errorMessage: 'No response from update mutation',\n ...prevData\n }\n };\n}\n\n// Compare imageId to determine if update happened\nconst newImageId = updated.imageId || '';\nconst oldImageId = prevData.currentImageId || '';\nconst wasUpdated = (newImageId !== oldImageId);\n\nreturn {\n json: {\n success: true,\n needsUpdate: wasUpdated,\n updated: wasUpdated,\n containerName: prevData.containerName,\n currentVersion: prevData.currentImage,\n newVersion: updated.image,\n currentImageId: oldImageId,\n newImageId: newImageId,\n chatId: prevData.chatId,\n messageId: prevData.messageId,\n responseMode: prevData.responseMode,\n correlationId: prevData.correlationId\n }\n};\n" + }, + "id": "code-handle-response", + "name": "Handle Update Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1520, + 300 + ] + }, { "parameters": { "conditions": { @@ -171,12 +280,12 @@ }, "conditions": [ { - "id": "pull-success", - "leftValue": "={{ $json.pullError }}", - "rightValue": false, + "id": "is-success", + "leftValue": "={{ $json.error }}", + "rightValue": true, "operator": { "type": "boolean", - "operation": "equals" + "operation": "notEquals" } } ], @@ -184,45 +293,15 @@ }, "options": {} }, - "id": "if-pull-success", - "name": "Check Pull Success", + "id": "if-update-success", + "name": "Check Update Success", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ - 1340, + 1720, 300 ] }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/v1.47/images/{{ encodeURIComponent($json.imageName) }}/json", - "options": { - "timeout": 5000 - } - }, - "id": "http-inspect-new-image", - "name": "Inspect New Image", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 200 - ] - }, - { - "parameters": { - "jsCode": "// Compare old and new image IDs\nconst newImage = $input.item.json;\nconst prevData = $('Check Pull Success').item.json;\nconst currentImageId = prevData.currentImageId;\n\nconst newImageId = newImage.Id;\n\nif (currentImageId === newImageId) {\n // No update needed\n return {\n json: {\n needsUpdate: false,\n ...prevData\n }\n };\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 newImageId,\n newVersion,\n ...prevData\n }\n};" - }, - "id": "code-compare-digests", - "name": "Compare Digests", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 200 - ] - }, { "parameters": { "conditions": { @@ -233,7 +312,7 @@ }, "conditions": [ { - "id": "needs-update", + "id": "was-updated", "leftValue": "={{ $json.needsUpdate }}", "rightValue": true, "operator": { @@ -246,126 +325,26 @@ }, "options": {} }, - "id": "if-needs-update", - "name": "Check If Update Needed", + "id": "if-was-updated", + "name": "Check If Updated", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ - 2000, + 1920, 200 ] }, { "parameters": { - "method": "POST", - "url": "=http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/stop?t=10", - "options": { - "timeout": 15000 - } - }, - "id": "http-stop-container", - "name": "Stop Container", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 100 - ], - "onError": "continueRegularOutput" - }, - { - "parameters": { - "method": "DELETE", - "url": "=http://docker-socket-proxy:2375/v1.47/containers/{{ $('Check If Update Needed').item.json.containerId }}", - "options": { - "timeout": 5000 - } - }, - "id": "http-remove-container", - "name": "Remove Container", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 100 - ], - "onError": "continueRegularOutput" - }, - { - "parameters": { - "jsCode": "// Build container create request body from saved config\nconst prevData = $('Check If Update Needed').item.json;\nconst config = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst containerName = prevData.containerName;\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,\n containerName,\n ...prevData\n }\n};" - }, - "id": "code-build-create-body", - "name": "Build Create Body", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=http://docker-socket-proxy:2375/v1.47/containers/create?name={{ encodeURIComponent($json.containerName) }}", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json.createBody) }}", - "options": { - "timeout": 5000 - } - }, - "id": "http-create-container", - "name": "Create Container", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 100 - ] - }, - { - "parameters": { - "jsCode": "// Parse create response and extract new container ID\nconst response = $input.item.json;\nconst prevData = $('Build Create Body').item.json;\n\nif (response.message) {\n // Error response from Docker\n return {\n json: {\n createError: true,\n errorMessage: response.message,\n ...prevData\n }\n };\n}\n\nreturn {\n json: {\n createError: false,\n newContainerId: response.Id,\n ...prevData\n }\n};" - }, - "id": "code-parse-create", - "name": "Parse Create Response", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3100, - 100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=http://docker-socket-proxy:2375/v1.47/containers/{{ $json.newContainerId }}/start", - "options": { - "timeout": 5000 - } - }, - "id": "http-start-container", - "name": "Start Container", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3320, - 100 - ], - "onError": "continueRegularOutput" - }, - { - "parameters": { - "jsCode": "// Format update success result and clean up old image\nconst prevData = $('Parse Create Response').item.json;\nconst containerName = prevData.containerName;\nconst currentVersion = prevData.currentVersion;\nconst newVersion = prevData.newVersion;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst responseMode = prevData.responseMode;\nconst correlationId = prevData.correlationId || '';\n\nconst message = `${containerName} updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n success: true,\n updated: true,\n message,\n oldDigest: currentVersion,\n newDigest: newVersion,\n currentImageId,\n chatId,\n messageId,\n responseMode,\n containerName,\n correlationId\n }\n};" + "jsCode": "// Format update success result\nconst data = $('Handle Update Response').item.json;\nconst containerName = data.containerName;\nconst currentVersion = data.currentVersion;\nconst newVersion = data.newVersion;\n\nconst message = `${containerName} updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n success: true,\n updated: true,\n message,\n oldDigest: currentVersion,\n newDigest: newVersion,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: data.responseMode,\n containerName: containerName,\n correlationId: data.correlationId || ''\n }\n};\n" }, "id": "code-format-success", "name": "Format Update Success", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3540, - 100 + 1960, + 200 ] }, { @@ -429,8 +408,8 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ - 3760, - 100 + 2180, + 200 ] }, { @@ -447,8 +426,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 3980, - 0 + 2400, + 100 ] }, { @@ -466,8 +445,8 @@ "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ - 3980, - 200 + 2400, + 300 ], "credentials": { "telegramApi": { @@ -476,24 +455,6 @@ } } }, - { - "parameters": { - "method": "DELETE", - "url": "=http://docker-socket-proxy:2375/v1.47/images/{{ $('Format Update Success').item.json.currentImageId }}?force=false", - "options": { - "timeout": 5000 - } - }, - "id": "http-remove-old-image-success", - "name": "Remove Old Image (Success)", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 4200, - 100 - ], - "onError": "continueRegularOutput" - }, { "parameters": { "jsCode": "// Return final success result\nconst data = $('Format Update Success').item.json;\nreturn {\n json: {\n success: true,\n updated: true,\n message: data.message,\n oldDigest: data.oldDigest,\n newDigest: data.newDigest,\n correlationId: data.correlationId || ''\n }\n};" @@ -503,21 +464,21 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 4420, - 100 + 2620, + 200 ] }, { "parameters": { - "jsCode": "// Format 'already up to date' result\nconst prevData = $('Check If Update Needed').item.json;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst responseMode = prevData.responseMode;\nconst correlationId = prevData.correlationId || '';\n\nconst message = `${containerName} is already up to date`;\n\nreturn {\n json: {\n success: true,\n updated: false,\n message,\n chatId,\n messageId,\n responseMode,\n containerName,\n correlationId\n }\n};" + "jsCode": "// Format 'already up to date' result\nconst data = $('Handle Update Response').item.json;\nconst containerName = data.containerName;\n\nconst message = `${containerName} is already up to date`;\n\nreturn {\n json: {\n success: true,\n updated: false,\n message,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: data.responseMode,\n containerName: containerName,\n correlationId: data.correlationId || ''\n }\n};\n" }, "id": "code-format-no-update", "name": "Format No Update Needed", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 2220, - 300 + 1960, + 400 ] }, { @@ -581,8 +542,8 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ - 2440, - 300 + 2180, + 400 ] }, { @@ -599,8 +560,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 2660, - 200 + 2400, + 400 ] }, { @@ -618,8 +579,8 @@ "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ - 2660, - 400 + 2400, + 500 ], "credentials": { "telegramApi": { @@ -637,21 +598,21 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 2880, - 300 + 2620, + 400 ] }, { "parameters": { - "jsCode": "// Format pull error result\nconst prevData = $('Check Pull Success').item.json;\nconst containerName = prevData.containerName;\nconst errorMessage = prevData.errorMessage;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst responseMode = prevData.responseMode;\nconst correlationId = prevData.correlationId || '';\n\nconst message = `Failed to update ${containerName}: ${errorMessage}`;\n\nreturn {\n json: {\n success: false,\n updated: false,\n message,\n error: {\n workflow: 'n8n-update',\n node: 'Pull Image',\n message: errorMessage,\n httpCode: null,\n rawResponse: errorMessage\n },\n correlationId,\n chatId,\n messageId,\n responseMode,\n containerName\n }\n};" + "jsCode": "// Format update error result\nconst data = $('Handle Update Response').item.json;\nconst containerName = data.containerName;\nconst errorMessage = data.errorMessage;\n\nconst message = `Failed to update ${containerName}: ${errorMessage}`;\n\nreturn {\n json: {\n success: false,\n updated: false,\n message,\n error: {\n workflow: 'n8n-update',\n node: 'Update Container',\n message: errorMessage,\n httpCode: null,\n rawResponse: errorMessage\n },\n correlationId: data.correlationId || '',\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: data.responseMode,\n containerName: containerName\n }\n};\n" }, "id": "code-format-pull-error", - "name": "Format Pull Error", + "name": "Format Update Error", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1560, - 400 + 1540, + 500 ] }, { @@ -715,8 +676,8 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ - 1780, - 400 + 1760, + 500 ] }, { @@ -733,8 +694,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 2000, - 300 + 1980, + 500 ] }, { @@ -752,8 +713,8 @@ "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ - 2000, - 500 + 1980, + 600 ], "credentials": { "telegramApi": { @@ -771,8 +732,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 2220, - 400 + 2200, + 500 ] } ], @@ -792,21 +753,65 @@ "main": [ [ { - "node": "Inspect Container", + "node": "Query Single Container", "type": "main", "index": 0 } ], [ { - "node": "Get All Containers", + "node": "Query All Containers", "type": "main", "index": 0 } ] ] }, - "Get All Containers": { + "Query Single Container": { + "main": [ + [ + { + "node": "Normalize Single Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Single Container": { + "main": [ + [ + { + "node": "Capture Pre-Update State", + "type": "main", + "index": 0 + } + ] + ] + }, + "Query All Containers": { + "main": [ + [ + { + "node": "Normalize GraphQL Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize GraphQL Response": { + "main": [ + [ + { + "node": "Update Container ID Registry", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Container ID Registry": { "main": [ [ { @@ -821,102 +826,62 @@ "main": [ [ { - "node": "Inspect Container", + "node": "Capture Pre-Update State", "type": "main", "index": 0 } ] ] }, - "Inspect Container": { + "Capture Pre-Update State": { "main": [ [ { - "node": "Parse Container Config", + "node": "Build Update Mutation", "type": "main", "index": 0 } ] ] }, - "Parse Container Config": { + "Build Update Mutation": { "main": [ [ { - "node": "Pull Image", + "node": "Update Container", "type": "main", "index": 0 } ] ] }, - "Pull Image": { + "Update Container": { "main": [ [ { - "node": "Check Pull Response", + "node": "Handle Update Response", "type": "main", "index": 0 } ] ] }, - "Check Pull Response": { + "Handle Update Response": { "main": [ [ { - "node": "Check Pull Success", + "node": "Check Update Success", "type": "main", "index": 0 } ] ] }, - "Check Pull Success": { + "Check If Updated": { "main": [ [ { - "node": "Inspect New Image", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Format Pull Error", - "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": "Stop Container", + "node": "Format Update Success", "type": "main", "index": 0 } @@ -930,72 +895,6 @@ ] ] }, - "Stop Container": { - "main": [ - [ - { - "node": "Remove Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Remove Container": { - "main": [ - [ - { - "node": "Build Create Body", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Create Body": { - "main": [ - [ - { - "node": "Create Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Create Container": { - "main": [ - [ - { - "node": "Parse Create Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Create Response": { - "main": [ - [ - { - "node": "Start Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Start Container": { - "main": [ - [ - { - "node": "Format Update Success", - "type": "main", - "index": 0 - } - ] - ] - }, "Format Update Success": { "main": [ [ @@ -1018,7 +917,7 @@ ], [ { - "node": "Remove Old Image (Success)", + "node": "Return Success", "type": "main", "index": 0 } @@ -1036,7 +935,7 @@ "main": [ [ { - "node": "Remove Old Image (Success)", + "node": "Return Success", "type": "main", "index": 0 } @@ -1044,17 +943,6 @@ ] }, "Send Text Success": { - "main": [ - [ - { - "node": "Remove Old Image (Success)", - "type": "main", - "index": 0 - } - ] - ] - }, - "Remove Old Image (Success)": { "main": [ [ { @@ -1123,7 +1011,7 @@ ] ] }, - "Format Pull Error": { + "Format Update Error": { "main": [ [ { @@ -1180,6 +1068,24 @@ } ] ] + }, + "Check Update Success": { + "main": [ + [ + { + "node": "Check If Updated", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Format Update Error", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {