diff --git a/n8n-actions.json b/n8n-actions.json index 86d9afc..7f941be 100644 --- a/n8n-actions.json +++ b/n8n-actions.json @@ -216,7 +216,23 @@ { "parameters": { "method": "POST", - "url": "=http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/start", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.UNRAID_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 } @@ -234,7 +250,23 @@ { "parameters": { "method": "POST", - "url": "=http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/stop?t=10", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.UNRAID_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 } @@ -252,13 +284,29 @@ { "parameters": { "method": "POST", - "url": "=http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/restart?t=10", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.UNRAID_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 } }, - "id": "http-restart-container", - "name": "Restart Container", + "id": "http-stop-for-restart", + "name": "Stop For Restart", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ @@ -332,6 +380,131 @@ 720, 400 ] + }, + { + "parameters": { + "jsCode": "// Build Start Mutation\nconst data = $('Route Action').item.json;\nconst unraidId = data.unraidId || data.containerId;\nreturn { json: { query: `mutation { docker { start(id: \"${unraidId}\") { id state } } }` } };" + }, + "id": "code-build-start-mutation", + "name": "Build Start Mutation", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1080, + 200 + ] + }, + { + "parameters": { + "jsCode": "// GraphQL Error Handler - Standardized error checking and HTTP status mapping\n// Input: $input.item.json = raw response from HTTP Request node\n// Output: { success, statusCode, alreadyInState, message, data }\n\nconst response = $input.item.json;\n\n// Check GraphQL errors array\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n const message = error.message;\n \n // Map error codes to HTTP equivalents\n if (code === 'ALREADY_IN_STATE') {\n // Maps to Docker API HTTP 304 pattern (used in n8n-actions.json)\n return {\n json: {\n success: true,\n statusCode: 304,\n alreadyInState: true,\n message: 'Container already in desired state'\n }\n };\n }\n \n // Error codes that should throw\n if (code === 'NOT_FOUND') {\n return {\n json: {\n success: false,\n statusCode: 404,\n message: `Container not found: ${message}`\n }\n };\n }\n \n if (code === 'FORBIDDEN' || code === 'UNAUTHORIZED') {\n return {\n json: {\n success: false,\n statusCode: 403,\n message: `Permission denied: ${message}`\n }\n };\n }\n \n // Any other GraphQL error\n return {\n json: {\n success: false,\n statusCode: 500,\n message: `Unraid API error: ${message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode >= 400) {\n return {\n json: {\n success: false,\n statusCode: response.statusCode,\n message: `HTTP ${response.statusCode}: ${response.statusMessage}`\n }\n };\n}\n\n// Check missing data field\nif (!response.data) {\n return {\n json: {\n success: false,\n statusCode: 500,\n message: 'GraphQL response missing data field'\n }\n };\n}\n\n// Success\nreturn {\n json: {\n success: true,\n statusCode: 200,\n alreadyInState: false,\n data: response.data\n }\n};\n" + }, + "id": "code-start-error-handler", + "name": "Start Error Handler", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1280, + 200 + ] + }, + { + "parameters": { + "jsCode": "// Build Stop Mutation\nconst data = $('Route Action').item.json;\nconst unraidId = data.unraidId || data.containerId;\nreturn { json: { query: `mutation { docker { stop(id: \"${unraidId}\") { id state } } }` } };" + }, + "id": "code-build-stop-mutation", + "name": "Build Stop Mutation", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1080, + 300 + ] + }, + { + "parameters": { + "jsCode": "// GraphQL Error Handler - Standardized error checking and HTTP status mapping\n// Input: $input.item.json = raw response from HTTP Request node\n// Output: { success, statusCode, alreadyInState, message, data }\n\nconst response = $input.item.json;\n\n// Check GraphQL errors array\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n const message = error.message;\n \n // Map error codes to HTTP equivalents\n if (code === 'ALREADY_IN_STATE') {\n // Maps to Docker API HTTP 304 pattern (used in n8n-actions.json)\n return {\n json: {\n success: true,\n statusCode: 304,\n alreadyInState: true,\n message: 'Container already in desired state'\n }\n };\n }\n \n // Error codes that should throw\n if (code === 'NOT_FOUND') {\n return {\n json: {\n success: false,\n statusCode: 404,\n message: `Container not found: ${message}`\n }\n };\n }\n \n if (code === 'FORBIDDEN' || code === 'UNAUTHORIZED') {\n return {\n json: {\n success: false,\n statusCode: 403,\n message: `Permission denied: ${message}`\n }\n };\n }\n \n // Any other GraphQL error\n return {\n json: {\n success: false,\n statusCode: 500,\n message: `Unraid API error: ${message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode >= 400) {\n return {\n json: {\n success: false,\n statusCode: response.statusCode,\n message: `HTTP ${response.statusCode}: ${response.statusMessage}`\n }\n };\n}\n\n// Check missing data field\nif (!response.data) {\n return {\n json: {\n success: false,\n statusCode: 500,\n message: 'GraphQL response missing data field'\n }\n };\n}\n\n// Success\nreturn {\n json: {\n success: true,\n statusCode: 200,\n alreadyInState: false,\n data: response.data\n }\n};\n" + }, + "id": "code-stop-error-handler", + "name": "Stop Error Handler", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1280, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Build Stop-for-Restart Mutation\nconst data = $('Route Action').item.json;\nconst unraidId = data.unraidId || data.containerId;\nreturn { json: { query: `mutation { docker { stop(id: \"${unraidId}\") { id state } } }`, unraidId } };" + }, + "id": "code-build-restart-stop-mutation", + "name": "Build Stop-for-Restart Mutation", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1080, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Handle Stop-for-Restart Result\n// Check response: if success OR statusCode 304 (already stopped) -> proceed to start\n// If error -> fail restart\n\nconst response = $input.item.json;\nconst prevData = $('Build Stop-for-Restart Mutation').item.json;\n\n// Check for errors\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n \n // ALREADY_IN_STATE (304) is OK - container already stopped\n if (code === 'ALREADY_IN_STATE') {\n // Continue to start step\n return { json: { query: `mutation { docker { start(id: \"${prevData.unraidId}\") { id state } } }` } };\n }\n \n // Any other error - fail restart\n return {\n json: {\n error: true,\n statusCode: 500,\n message: `Failed to stop container for restart: ${error.message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode && response.statusCode >= 400) {\n return {\n json: {\n error: true,\n statusCode: response.statusCode,\n message: 'Failed to stop container for restart'\n }\n };\n}\n\n// Success - proceed to start\nreturn { json: { query: `mutation { docker { start(id: \"${prevData.unraidId}\") { id state } } }` } };\n" + }, + "id": "code-handle-stop-for-restart", + "name": "Handle Stop-for-Restart Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1280, + 400 + ] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.UNRAID_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", + "options": { + "timeout": 15000 + } + }, + "id": "http-start-after-stop", + "name": "Start After Stop", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1480, + 400 + ], + "onError": "continueRegularOutput" + }, + { + "parameters": { + "jsCode": "// GraphQL Error Handler for Restart (after Start step)\n// Input: $input.item.json = raw response from Start After Stop\n// Output: { success, statusCode, alreadyInState, message, data }\n\nconst response = $input.item.json;\n\n// Check GraphQL errors array\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n const message = error.message;\n \n // Map error codes to HTTP equivalents\n if (code === 'ALREADY_IN_STATE') {\n // Maps to Docker API HTTP 304 pattern (container already running)\n return {\n json: {\n success: true,\n statusCode: 304,\n alreadyInState: true,\n message: 'Container already in desired state'\n }\n };\n }\n \n // Error codes that should throw\n if (code === 'NOT_FOUND') {\n return {\n json: {\n success: false,\n statusCode: 404,\n message: `Container not found: ${message}`\n }\n };\n }\n \n if (code === 'FORBIDDEN' || code === 'UNAUTHORIZED') {\n return {\n json: {\n success: false,\n statusCode: 403,\n message: `Permission denied: ${message}`\n }\n };\n }\n \n // Any other GraphQL error\n return {\n json: {\n success: false,\n statusCode: 500,\n message: `Unraid API error: ${message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode >= 400) {\n return {\n json: {\n success: false,\n statusCode: response.statusCode,\n message: `HTTP ${response.statusCode}: ${response.statusMessage}`\n }\n };\n}\n\n// Check missing data field\nif (!response.data) {\n return {\n json: {\n success: false,\n statusCode: 500,\n message: 'GraphQL response missing data field'\n }\n };\n}\n\n// Success\nreturn {\n json: {\n success: true,\n statusCode: 200,\n alreadyInState: false,\n data: response.data\n }\n};\n" + }, + "id": "code-restart-error-handler", + "name": "Restart Error Handler", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 400 + ] } ], "connections": { @@ -379,21 +552,21 @@ "main": [ [ { - "node": "Start Container", + "node": "Build Start Mutation", "type": "main", "index": 0 } ], [ { - "node": "Stop Container", + "node": "Build Stop Mutation", "type": "main", "index": 0 } ], [ { - "node": "Restart Container", + "node": "Build Stop-for-Restart Mutation", "type": "main", "index": 0 } @@ -404,7 +577,7 @@ "main": [ [ { - "node": "Format Start Result", + "node": "Start Error Handler", "type": "main", "index": 0 } @@ -415,18 +588,7 @@ "main": [ [ { - "node": "Format Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Restart Container": { - "main": [ - [ - { - "node": "Format Restart Result", + "node": "Stop Error Handler", "type": "main", "index": 0 } @@ -465,6 +627,105 @@ } ] ] + }, + "Build Start Mutation": { + "main": [ + [ + { + "node": "Start Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start Error Handler": { + "main": [ + [ + { + "node": "Format Start Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Stop Mutation": { + "main": [ + [ + { + "node": "Stop Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Stop Error Handler": { + "main": [ + [ + { + "node": "Format Stop Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Stop-for-Restart Mutation": { + "main": [ + [ + { + "node": "Stop For Restart", + "type": "main", + "index": 0 + } + ] + ] + }, + "Stop For Restart": { + "main": [ + [ + { + "node": "Handle Stop-for-Restart Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Handle Stop-for-Restart Result": { + "main": [ + [ + { + "node": "Start After Stop", + "type": "main", + "index": 0 + } + ] + ] + }, + "Start After Stop": { + "main": [ + [ + { + "node": "Restart Error Handler", + "type": "main", + "index": 0 + } + ] + ] + }, + "Restart Error Handler": { + "main": [ + [ + { + "node": "Format Restart Result", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {