feat(16-01): migrate container status queries to Unraid GraphQL API
- Replace 3 Docker API GET queries with Unraid GraphQL POST queries
- Add GraphQL Response Normalizer after each query (transforms Unraid format to Docker contract)
- Add Container ID Registry update after each normalizer (keeps name-to-PrefixedID mapping fresh)
- Rename HTTP Request nodes: Docker List → Query Containers, Docker Get → Query Container Status
- Wire pattern: HTTP Request → Normalizer → Registry Update → existing Code node
- Downstream Code nodes unchanged (Build Container List, Build Container Submenu, Build Paginated List)
- GraphQL query: docker.containers {id, names, state, image, status}
- State mapping: RUNNING→running, STOPPED→exited, PAUSED→paused
- Authentication: n8n Header Auth credential "Unraid API Key"
- Timeout: 15s for myunraid.net cloud relay
- Workflow nodes: 11 → 17 (added 3 normalizers + 3 registry updates)
This commit is contained in:
+228
-21
@@ -158,18 +158,39 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"options": {}
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": []
|
||||
},
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query {\\n docker {\\n containers {\\n id\\n names\\n state\\n image\\n status\\n }\\n }\\n}\"}",
|
||||
"options": {
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"fullResponse": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "status-docker-list",
|
||||
"name": "Docker List Containers",
|
||||
"name": "Query Containers",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
900,
|
||||
200
|
||||
]
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "unraid-api-key-credential-id",
|
||||
"name": "Unraid API Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -199,18 +220,39 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"options": {}
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": []
|
||||
},
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query {\\n docker {\\n containers {\\n id\\n names\\n state\\n image\\n status\\n }\\n }\\n}\"}",
|
||||
"options": {
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"fullResponse": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "status-docker-single",
|
||||
"name": "Docker Get Container",
|
||||
"name": "Query Container Status",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
900,
|
||||
300
|
||||
]
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "unraid-api-key-credential-id",
|
||||
"name": "Unraid API Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -240,18 +282,39 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"options": {}
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": []
|
||||
},
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query {\\n docker {\\n containers {\\n id\\n names\\n state\\n image\\n status\\n }\\n }\\n}\"}",
|
||||
"options": {
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"fullResponse": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "status-docker-paginate",
|
||||
"name": "Docker List For Paginate",
|
||||
"name": "Query Containers For Paginate",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
900,
|
||||
400
|
||||
]
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "unraid-api-key-credential-id",
|
||||
"name": "Unraid API Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -265,6 +328,84 @@
|
||||
1120,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"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: container.status || dockerState, // Use Unraid status field or fallback to state\n Image: container.image || '', // Unraid provides image field\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 }));"
|
||||
},
|
||||
"id": "status-normalizer-list",
|
||||
"name": "Normalize GraphQL Response (List)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1000,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"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: container.status || dockerState, // Use Unraid status field or fallback to state\n Image: container.image || '', // Unraid provides image field\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 }));"
|
||||
},
|
||||
"id": "status-normalizer-status",
|
||||
"name": "Normalize GraphQL Response (Status)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1000,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"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: container.status || dockerState, // Use Unraid status field or fallback to state\n Image: container.image || '', // Unraid provides image field\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 }));"
|
||||
},
|
||||
"id": "status-normalizer-paginate",
|
||||
"name": "Normalize GraphQL Response (Paginate)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1000,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Update Container ID Registry with fresh container data\nconst containers = $input.all().map(item => item.json);\n\n// Initialize registry using static data with JSON serialization pattern\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst newMap = {};\n\nfor (const container of containers) {\n // Extract container name: strip leading '/', lowercase\n const rawName = container.Names[0];\n const name = rawName.startsWith('/') ? rawName.substring(1).toLowerCase() : rawName.toLowerCase();\n \n // Map name -> {name, unraidId}\n newMap[name] = {\n name: name,\n unraidId: container.Id // Full PrefixedID from normalized data\n };\n}\n\n// Store timestamp\nregistry._lastRefresh = Date.now();\n\n// Serialize (top-level assignment - this is what n8n persists)\nregistry._containerIdMap = JSON.stringify(newMap);\n\n// Pass through all containers unchanged\nreturn $input.all();"
|
||||
},
|
||||
"id": "status-registry-list",
|
||||
"name": "Update Container Registry (List)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1060,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Update Container ID Registry with fresh container data\nconst containers = $input.all().map(item => item.json);\n\n// Initialize registry using static data with JSON serialization pattern\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst newMap = {};\n\nfor (const container of containers) {\n // Extract container name: strip leading '/', lowercase\n const rawName = container.Names[0];\n const name = rawName.startsWith('/') ? rawName.substring(1).toLowerCase() : rawName.toLowerCase();\n \n // Map name -> {name, unraidId}\n newMap[name] = {\n name: name,\n unraidId: container.Id // Full PrefixedID from normalized data\n };\n}\n\n// Store timestamp\nregistry._lastRefresh = Date.now();\n\n// Serialize (top-level assignment - this is what n8n persists)\nregistry._containerIdMap = JSON.stringify(newMap);\n\n// Pass through all containers unchanged\nreturn $input.all();"
|
||||
},
|
||||
"id": "status-registry-status",
|
||||
"name": "Update Container Registry (Status)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1060,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Update Container ID Registry with fresh container data\nconst containers = $input.all().map(item => item.json);\n\n// Initialize registry using static data with JSON serialization pattern\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst newMap = {};\n\nfor (const container of containers) {\n // Extract container name: strip leading '/', lowercase\n const rawName = container.Names[0];\n const name = rawName.startsWith('/') ? rawName.substring(1).toLowerCase() : rawName.toLowerCase();\n \n // Map name -> {name, unraidId}\n newMap[name] = {\n name: name,\n unraidId: container.Id // Full PrefixedID from normalized data\n };\n}\n\n// Store timestamp\nregistry._lastRefresh = Date.now();\n\n// Serialize (top-level assignment - this is what n8n persists)\nregistry._containerIdMap = JSON.stringify(newMap);\n\n// Pass through all containers unchanged\nreturn $input.all();"
|
||||
},
|
||||
"id": "status-registry-paginate",
|
||||
"name": "Update Container Registry (Paginate)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1060,
|
||||
400
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
@@ -308,14 +449,36 @@
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Docker List Containers",
|
||||
"node": "Query Containers",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Docker List Containers": {
|
||||
"Query Containers": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (List)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Normalize GraphQL Response (List)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Container Registry (List)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update Container Registry (List)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
@@ -330,14 +493,36 @@
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Docker Get Container",
|
||||
"node": "Query Container Status",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Docker Get Container": {
|
||||
"Query Container Status": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (Status)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Normalize GraphQL Response (Status)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Container Registry (Status)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update Container Registry (Status)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
@@ -352,14 +537,36 @@
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Docker List For Paginate",
|
||||
"node": "Query Containers For Paginate",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Docker List For Paginate": {
|
||||
"Query Containers For Paginate": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (Paginate)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Normalize GraphQL Response (Paginate)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Container Registry (Paginate)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update Container Registry (Paginate)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user