feat(16-03): replace 5-step Docker update with single updateContainer GraphQL mutation

- Replace Docker API container lookup with GraphQL containers query
- Add GraphQL Response Normalizer and Container ID Registry update
- Replace 5-step update flow (stop/remove/create/start) with single updateContainer mutation
- 60-second timeout for large image pulls (was 600s for docker pull)
- ImageId comparison determines update success (not digest comparison)
- Preserve all 15 messaging nodes (Format/Check/Send/Return)
- Remove Docker socket proxy dependencies (zero references)
- Remove Execute Command node (docker pull eliminated)
- Reduce from 34 to 29 nodes (~15% reduction)
This commit is contained in:
Lucas Berger
2026-02-09 10:23:29 -05:00
parent 1f6de5542a
commit 6caa0f171f
+275 -369
View File
@@ -75,26 +75,45 @@
}, },
{ {
"parameters": { "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": { "options": {
"timeout": 5000 "timeout": 15000
} }
}, },
"id": "http-get-containers", "id": "http-query-containers",
"name": "Get All Containers", "name": "Query All Containers",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2, "typeVersion": 4.2,
"position": [ "position": [
560, 560,
400 400
] ],
"onError": "continueRegularOutput"
}, },
{ {
"parameters": { "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", "id": "code-normalize-response",
"name": "Resolve Container ID", "name": "Normalize GraphQL Response",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
@@ -103,57 +122,99 @@
] ]
}, },
{ {
"parameters": { "id": "code-update-registry",
"method": "GET", "name": "Update Container ID Registry",
"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",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
680, 880,
300 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": { "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": { "options": {
"timeout": 660 "timeout": 15000
} }
}, },
"id": "exec-pull-image", "id": "http-query-single",
"name": "Pull Image", "name": "Query Single Container",
"type": "n8n-nodes-base.executeCommand", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 1, "typeVersion": 4.2,
"position": [ "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 300
] ]
}, },
{ {
"parameters": { "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", "id": "code-capture-state",
"name": "Check Pull Response", "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", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
@@ -161,6 +222,54 @@
300 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": { "parameters": {
"conditions": { "conditions": {
@@ -171,12 +280,12 @@
}, },
"conditions": [ "conditions": [
{ {
"id": "pull-success", "id": "is-success",
"leftValue": "={{ $json.pullError }}", "leftValue": "={{ $json.error }}",
"rightValue": false, "rightValue": true,
"operator": { "operator": {
"type": "boolean", "type": "boolean",
"operation": "equals" "operation": "notEquals"
} }
} }
], ],
@@ -184,45 +293,15 @@
}, },
"options": {} "options": {}
}, },
"id": "if-pull-success", "id": "if-update-success",
"name": "Check Pull Success", "name": "Check Update Success",
"type": "n8n-nodes-base.if", "type": "n8n-nodes-base.if",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1340, 1720,
300 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": { "parameters": {
"conditions": { "conditions": {
@@ -233,7 +312,7 @@
}, },
"conditions": [ "conditions": [
{ {
"id": "needs-update", "id": "was-updated",
"leftValue": "={{ $json.needsUpdate }}", "leftValue": "={{ $json.needsUpdate }}",
"rightValue": true, "rightValue": true,
"operator": { "operator": {
@@ -246,126 +325,26 @@
}, },
"options": {} "options": {}
}, },
"id": "if-needs-update", "id": "if-was-updated",
"name": "Check If Update Needed", "name": "Check If Updated",
"type": "n8n-nodes-base.if", "type": "n8n-nodes-base.if",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
2000, 1920,
200 200
] ]
}, },
{ {
"parameters": { "parameters": {
"method": "POST", "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 = `<b>${containerName}</b> 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"
"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 = `<b>${containerName}</b> 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};"
}, },
"id": "code-format-success", "id": "code-format-success",
"name": "Format Update Success", "name": "Format Update Success",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
3540, 1960,
100 200
] ]
}, },
{ {
@@ -429,8 +408,8 @@
"type": "n8n-nodes-base.switch", "type": "n8n-nodes-base.switch",
"typeVersion": 3.2, "typeVersion": 3.2,
"position": [ "position": [
3760, 2180,
100 200
] ]
}, },
{ {
@@ -447,8 +426,8 @@
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2, "typeVersion": 4.2,
"position": [ "position": [
3980, 2400,
0 100
] ]
}, },
{ {
@@ -466,8 +445,8 @@
"type": "n8n-nodes-base.telegram", "type": "n8n-nodes-base.telegram",
"typeVersion": 1.2, "typeVersion": 1.2,
"position": [ "position": [
3980, 2400,
200 300
], ],
"credentials": { "credentials": {
"telegramApi": { "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": { "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};" "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", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
4420, 2620,
100 200
] ]
}, },
{ {
"parameters": { "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 = `<b>${containerName}</b> 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 = `<b>${containerName}</b> 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", "id": "code-format-no-update",
"name": "Format No Update Needed", "name": "Format No Update Needed",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
2220, 1960,
300 400
] ]
}, },
{ {
@@ -581,8 +542,8 @@
"type": "n8n-nodes-base.switch", "type": "n8n-nodes-base.switch",
"typeVersion": 3.2, "typeVersion": 3.2,
"position": [ "position": [
2440, 2180,
300 400
] ]
}, },
{ {
@@ -599,8 +560,8 @@
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2, "typeVersion": 4.2,
"position": [ "position": [
2660, 2400,
200 400
] ]
}, },
{ {
@@ -618,8 +579,8 @@
"type": "n8n-nodes-base.telegram", "type": "n8n-nodes-base.telegram",
"typeVersion": 1.2, "typeVersion": 1.2,
"position": [ "position": [
2660, 2400,
400 500
], ],
"credentials": { "credentials": {
"telegramApi": { "telegramApi": {
@@ -637,21 +598,21 @@
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
2880, 2620,
300 400
] ]
}, },
{ {
"parameters": { "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 <b>${containerName}</b>: ${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 <b>${containerName}</b>: ${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", "id": "code-format-pull-error",
"name": "Format Pull Error", "name": "Format Update Error",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1560, 1540,
400 500
] ]
}, },
{ {
@@ -715,8 +676,8 @@
"type": "n8n-nodes-base.switch", "type": "n8n-nodes-base.switch",
"typeVersion": 3.2, "typeVersion": 3.2,
"position": [ "position": [
1780, 1760,
400 500
] ]
}, },
{ {
@@ -733,8 +694,8 @@
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2, "typeVersion": 4.2,
"position": [ "position": [
2000, 1980,
300 500
] ]
}, },
{ {
@@ -752,8 +713,8 @@
"type": "n8n-nodes-base.telegram", "type": "n8n-nodes-base.telegram",
"typeVersion": 1.2, "typeVersion": 1.2,
"position": [ "position": [
2000, 1980,
500 600
], ],
"credentials": { "credentials": {
"telegramApi": { "telegramApi": {
@@ -771,8 +732,8 @@
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
2220, 2200,
400 500
] ]
} }
], ],
@@ -792,21 +753,65 @@
"main": [ "main": [
[ [
{ {
"node": "Inspect Container", "node": "Query Single Container",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
], ],
[ [
{ {
"node": "Get All Containers", "node": "Query All Containers",
"type": "main", "type": "main",
"index": 0 "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": [ "main": [
[ [
{ {
@@ -821,102 +826,62 @@
"main": [ "main": [
[ [
{ {
"node": "Inspect Container", "node": "Capture Pre-Update State",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
] ]
] ]
}, },
"Inspect Container": { "Capture Pre-Update State": {
"main": [ "main": [
[ [
{ {
"node": "Parse Container Config", "node": "Build Update Mutation",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
] ]
] ]
}, },
"Parse Container Config": { "Build Update Mutation": {
"main": [ "main": [
[ [
{ {
"node": "Pull Image", "node": "Update Container",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
] ]
] ]
}, },
"Pull Image": { "Update Container": {
"main": [ "main": [
[ [
{ {
"node": "Check Pull Response", "node": "Handle Update Response",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
] ]
] ]
}, },
"Check Pull Response": { "Handle Update Response": {
"main": [ "main": [
[ [
{ {
"node": "Check Pull Success", "node": "Check Update Success",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
] ]
] ]
}, },
"Check Pull Success": { "Check If Updated": {
"main": [ "main": [
[ [
{ {
"node": "Inspect New Image", "node": "Format Update Success",
"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",
"type": "main", "type": "main",
"index": 0 "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": { "Format Update Success": {
"main": [ "main": [
[ [
@@ -1018,7 +917,7 @@
], ],
[ [
{ {
"node": "Remove Old Image (Success)", "node": "Return Success",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -1036,7 +935,7 @@
"main": [ "main": [
[ [
{ {
"node": "Remove Old Image (Success)", "node": "Return Success",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -1044,17 +943,6 @@
] ]
}, },
"Send Text Success": { "Send Text Success": {
"main": [
[
{
"node": "Remove Old Image (Success)",
"type": "main",
"index": 0
}
]
]
},
"Remove Old Image (Success)": {
"main": [ "main": [
[ [
{ {
@@ -1123,7 +1011,7 @@
] ]
] ]
}, },
"Format Pull Error": { "Format Update Error": {
"main": [ "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": { "settings": {