Files
2026-02-09 08:08:25 -05:00

654 lines
26 KiB
Markdown

# Architecture Research: Unraid GraphQL API Integration
**Domain:** n8n workflow system migration from Docker socket proxy to Unraid GraphQL API
**Researched:** 2026-02-09
**Confidence:** HIGH
## Integration Architecture Overview
### Current Architecture (Docker Socket Proxy)
```
┌─────────────────────────────────────────────────────────────┐
│ n8n Workflow System │
├─────────────────────────────────────────────────────────────┤
│ Main Workflow (169 nodes) │
│ ├─── Execute Workflow → Update Sub-workflow │
│ ├─── Execute Workflow → Actions Sub-workflow │
│ ├─── Execute Workflow → Logs Sub-workflow │
│ ├─── Execute Workflow → Status Sub-workflow │
│ └─── Execute Workflow → Matching Sub-workflow │
│ │
│ Sub-workflows call Docker API: │
│ HTTP Request → docker-socket-proxy:2375/v1.47/... │
│ Execute Command → curl docker-socket-proxy:2375/... │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ docker-socket-proxy (tecnativa/...) │
│ Security layer: Exposes only safe endpoints │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Docker Engine │
│ /var/run/docker.sock │
└─────────────────────────────────────────────────────────────┘
```
### Target Architecture (Unraid GraphQL API)
```
┌─────────────────────────────────────────────────────────────┐
│ n8n Workflow System │
├─────────────────────────────────────────────────────────────┤
│ Main Workflow (169 nodes) │
│ ├─── Execute Workflow → Update Sub-workflow │
│ ├─── Execute Workflow → Actions Sub-workflow │
│ ├─── Execute Workflow → Logs Sub-workflow │
│ ├─── Execute Workflow → Status Sub-workflow │
│ └─── Execute Workflow → Matching Sub-workflow │
│ │
│ Sub-workflows call Unraid GraphQL API: │
│ HTTP Request → UNRAID_HOST/graphql (POST) │
│ Header Auth: x-api-key │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Unraid GraphQL API Endpoint │
│ https://192-168-x-x.hash.myunraid.net:8443/graphql │
│ Authentication: x-api-key header │
│ Permission: DOCKER:UPDATE_ANY │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Unraid Docker Service │
│ Native container management + status sync │
└─────────────────────────────────────────────────────────────┘
```
**Key architectural change:** Single unified API replaces two-layer proxy system. Unraid GraphQL API provides both Docker operations AND native Unraid integration (update status sync).
## Component Modification Map
### Sub-workflows Requiring Modification
| Sub-workflow | Docker API Nodes | Operations | Complexity | Priority |
|--------------|------------------|------------|------------|----------|
| **n8n-update.json** | 9 nodes | Pull, stop, remove, create, start, inspect, cleanup | HIGH | 1 |
| **n8n-actions.json** | 4 nodes | List, start, stop, restart | LOW | 2 |
| **n8n-status.json** | 3 nodes | List, inspect | LOW | 3 |
| **n8n-logs.json** | 2 nodes (Execute Command) | Container logs retrieval | LOW | 4 |
| **n8n-matching.json** | 0 nodes | Pure data transformation | NONE | N/A |
| **n8n-batch-ui.json** | 5 nodes (inherited) | Calls Update/Actions | NONE | N/A |
| **n8n-confirmation.json** | 0 nodes | Pure UI logic | NONE | N/A |
**Total impact:** 4 sub-workflows modified (18 Docker API nodes), 3 sub-workflows unchanged
### Sub-workflows NOT Requiring Modification
- **n8n-matching.json** — Container name matching logic operates on data shape, not API source
- **n8n-batch-ui.json** — Pure Telegram keyboard UI, no direct Docker API calls
- **n8n-confirmation.json** — Confirmation dialogs, no Docker operations
These workflows receive transformed data from modified workflows and operate on contracts (input/output shapes) that remain stable.
## Node Type Changes
### HTTP Request Node Pattern (Actions, Status)
**Before (Docker API):**
```javascript
// n8n HTTP Request node
{
method: 'POST',
url: 'http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/start',
responseFormat: 'json'
}
```
**After (Unraid GraphQL API):**
```javascript
// n8n HTTP Request node
{
method: 'POST',
url: '={{ $env.UNRAID_HOST }}/graphql',
authentication: 'genericCredentialType',
genericAuthType: 'httpHeaderAuth',
credential: 'Unraid API Key', // Header Auth: x-api-key
sendBody: true,
bodyContentType: 'json',
body: {
query: 'mutation { docker { start(id: "{{ $json.unraidContainerId }}") { id state } } }'
},
options: {
ignoreSSLIssues: true
}
}
```
**Node type:** Same (HTTP Request), but parameters change
**Count:** ~15 HTTP Request nodes across Actions, Status, Update workflows
### Execute Command → HTTP Request (Logs, Update)
**Before (Execute Command with curl):**
```javascript
// n8n Execute Command node (Pull Image)
{
command: '=curl -s --max-time 600 -X POST \'http://docker-socket-proxy:2375/v1.47/images/create?fromImage={{ encodeURIComponent($json.imageName) }}\' | tail -c 10000'
}
```
**After (HTTP Request with GraphQL mutation):**
```javascript
// n8n HTTP Request node
{
method: 'POST',
url: '={{ $env.UNRAID_HOST }}/graphql',
authentication: 'genericCredentialType',
genericAuthType: 'httpHeaderAuth',
credential: 'Unraid API Key',
sendBody: true,
bodyContentType: 'json',
body: {
query: 'mutation { docker { updateContainer(id: "{{ $json.unraidContainerId }}") { id imageId state } } }'
},
timeout: 600000 // 10 minutes
}
```
**Node type change:** Execute Command → HTTP Request
**Count:** 3 Execute Command nodes (1 in Update for image pull, 2 in Logs for log retrieval)
**Impact:** Simpler — no shell escaping, native n8n timeout handling, structured GraphQL errors
## Data Transformation Requirements
### Container ID Mapping
**Docker API format:**
- Container ID: 64-character hex string (`8a9907a245766012...`)
- Container names: Array with `/` prefix (`["/n8n"]`)
**Unraid GraphQL format:**
- Container ID: `PrefixedID` scalar — `{server_hash}:{container_hash}` (128 chars + colon)
- Example: `1639d2f04f44841b...a558d85071fa23e0:8a9907a245766012...840cefdec67af6b7`
- Container names: Array WITH `/` prefix (`["/n8n"]`) — GraphQL returns Docker's raw value
**Mapping strategy:**
1. **Container matching (Matching sub-workflow):**
- User input: "n8n" (plain name)
- Docker API query: Returns names `["/n8n"]` → strip `/` for comparison
- Unraid GraphQL query: Returns names `["/n8n"]` → strip `/` for comparison
- **No change needed** — name matching logic already strips `/` prefix
2. **Container identification:**
- Docker API: Use Docker container ID (64 chars)
- Unraid GraphQL: Use Unraid PrefixedID (128+ chars)
- **Strategy:** Store Unraid ID from initial query, pass through workflow
- **Where:** Matching sub-workflow output, Update/Actions/Status inputs
3. **Workflow contract stability:**
- Field name stays `containerId` throughout all workflows
- Value changes from Docker ID → Unraid PrefixedID
- No contract changes needed — ID is opaque token to workflows
### Response Shape Transformation
**Docker API response (GET /containers/json):**
```json
[
{
"Id": "8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925",
"Names": ["/n8n"],
"Image": "n8nio/n8n:latest",
"ImageID": "sha256:abc123...",
"State": "running",
"Status": "Up 2 hours"
}
]
```
**Unraid GraphQL response:**
```json
{
"data": {
"docker": {
"containers": [
{
"id": "1639d2f04f44841b...a558d85071fa23e0:8a9907a245766012...840cefdec67af6b7",
"names": ["/n8n"],
"image": "n8nio/n8n:latest",
"imageId": "sha256:abc123...",
"state": "RUNNING",
"status": "Up 2 hours",
"isUpdateAvailable": false
}
]
}
}
}
```
**Key differences:**
| Field | Docker API | Unraid GraphQL | Impact |
|-------|------------|----------------|--------|
| Container ID | `Id` | `id` | Lowercase (minor) |
| ID format | 64 hex chars | PrefixedID (128+) | Length check updates |
| Names | `Names` | `names` | Lowercase (minor) |
| Image ID | `ImageID` | `imageId` | Lowercase (minor) |
| State | `"running"` (lowercase) | `"RUNNING"` (uppercase) | Case handling in comparisons |
| Status | `Status` | `status` | Lowercase (minor) |
| Update available | Not available | `isUpdateAvailable` | NEW field (benefit!) |
**Transformation pattern:**
Add a **"Normalize Container Data"** Code node after Unraid GraphQL queries to map to workflow's expected shape:
```javascript
// Code node: Normalize Unraid GraphQL Response
const graphqlResponse = $input.item.json;
// Check for GraphQL errors first
if (graphqlResponse.errors) {
throw new Error(`GraphQL error: ${graphqlResponse.errors.map(e => e.message).join(', ')}`);
}
// Extract containers from GraphQL response structure
const containers = graphqlResponse.data?.docker?.containers || [];
// Map to workflow's expected format
return containers.map(c => ({
json: {
containerId: c.id, // Unraid PrefixedID
containerName: (c.names?.[0] || '').replace(/^\//, ''), // Strip leading /
imageName: c.image,
imageId: c.imageId,
state: c.state.toLowerCase(), // RUNNING → running (for existing comparisons)
status: c.status,
updateAvailable: c.isUpdateAvailable || false // NEW
}
}));
```
**Where to add:** After every `docker.containers` GraphQL query in:
- Status sub-workflow (container list query)
- Matching sub-workflow (container list for matching)
- Actions sub-workflow (container list before action)
## Integration Patterns
### Pattern 1: Direct Mutation (Simple Operations)
**Use for:** Start, stop, restart, pause (single-step operations)
**Before (Docker API):**
```javascript
// HTTP Request node: POST /containers/{id}/start
// Response: HTTP 204 No Content (empty body)
// Success check: response.statusCode === 204
```
**After (Unraid GraphQL):**
```javascript
// HTTP Request node: POST /graphql
// Body: { query: 'mutation { docker { start(id: "...") { id state } } }' }
// Response: { data: { docker: { start: { id: "...", state: "RUNNING" } } } }
// Success check: response.data?.docker?.start?.state === "RUNNING"
```
**Error handling:**
- Docker API: HTTP status codes (404 = not found, 304 = already running)
- Unraid GraphQL: `response.errors[]` array with structured error messages
- **New pattern:** Check `response.errors` first, then validate `response.data` structure
**Example implementation (Actions sub-workflow):**
```javascript
// Code node: Validate Start Result
const response = $input.item.json;
// Check GraphQL errors
if (response.errors) {
const errorMsg = response.errors.map(e => e.message).join(', ');
return {
json: {
success: false,
message: `Start failed: ${errorMsg}`,
action: 'start'
}
};
}
// Validate response structure
const startResult = response.data?.docker?.start;
if (!startResult) {
return {
json: {
success: false,
message: 'Invalid GraphQL response: missing start result',
action: 'start'
}
};
}
// Success
return {
json: {
success: true,
action: 'start',
containerId: startResult.id,
state: startResult.state,
message: 'Container started successfully'
}
};
```
### Pattern 2: Complex Multi-Step Operations (Update Workflow)
**Docker API approach (9 nodes):**
1. Get container list → Find container
2. Inspect container → Extract config
3. Pull image (Execute Command with curl)
4. Inspect new image → Get digest
5. Stop container
6. Remove container
7. Create container (with old config)
8. Start container
9. Remove old image
**Unraid GraphQL approach (1-2 nodes):**
1. Call `updateContainer` mutation → Unraid handles all steps atomically
2. (Optional) Query container status after update
**Simplification:**
```javascript
// Single HTTP Request node: Update Container
{
method: 'POST',
url: '={{ $env.UNRAID_HOST }}/graphql',
authentication: 'genericCredentialType',
genericAuthType: 'httpHeaderAuth',
credential: 'Unraid API Key',
sendBody: true,
bodyContentType: 'json',
body: {
query: 'mutation { docker { updateContainer(id: "{{ $json.containerId }}") { id imageId state isUpdateAvailable } } }'
},
timeout: 600000 // 10 minutes (pull can be slow)
}
// Code node: Validate Update Result
const response = $input.item.json;
if (response.errors) {
const errorMsg = response.errors.map(e => e.message).join(', ');
return {
json: {
success: false,
updated: false,
message: `Update failed: ${errorMsg}`
}
};
}
const updateResult = response.data?.docker?.updateContainer;
if (!updateResult) {
return {
json: {
success: false,
updated: false,
message: 'Invalid GraphQL response'
}
};
}
// Check if update was needed
if (updateResult.isUpdateAvailable === false) {
return {
json: {
success: true,
updated: false,
message: 'Container is already up to date'
}
};
}
// Success
return {
json: {
success: true,
updated: true,
message: `Container updated successfully`,
newImageId: updateResult.imageId,
state: updateResult.state
}
};
```
**Impact:** Update sub-workflow shrinks from 34 nodes to ~15-20 nodes (remove 9 Docker API nodes, add 1-2 GraphQL nodes + data normalization)
**Benefit:** Unraid's `updateContainer` handles image pull, container stop/remove/create/start atomically AND syncs update status internally (solves v1.3's "apply update" badge issue automatically).
### Pattern 3: Log Retrieval (Logs Sub-workflow)
**Before (Execute Command):**
```javascript
// Execute Command node: Get Logs
{
command: '=curl -s "http://docker-socket-proxy:2375/v1.47/containers/{{ $json.containerId }}/logs?stdout=true&stderr=true&tail={{ $json.lineCount || 50 }}"'
}
```
**After (Unraid GraphQL):**
```javascript
// HTTP Request node: Query Logs
{
method: 'POST',
url: '={{ $env.UNRAID_HOST }}/graphql',
authentication: 'genericCredentialType',
genericAuthType: 'httpHeaderAuth',
credential: 'Unraid API Key',
sendBody: true,
bodyContentType: 'json',
body: {
query: 'query { docker { logs(id: "{{ $json.containerId }}", tail: {{ $json.lineCount || 50 }}) { entries } } }'
}
}
// Code node: Format Logs
const response = $input.item.json;
if (response.errors) {
throw new Error(`Log retrieval failed: ${response.errors.map(e => e.message).join(', ')}`);
}
const logData = response.data?.docker?.logs;
if (!logData) {
throw new Error('Invalid GraphQL response: missing logs');
}
const logs = logData.entries || [];
return {
json: {
success: true,
logs: logs.join('\n'),
lineCount: logs.length
}
};
```
**Benefit:** Native GraphQL logs query returns structured data (array of log entries) instead of raw Docker log format with control characters. Simpler parsing.
## Modified vs. New Components
### Modified Components
| Component | File | Modification Type | Node Count Change | Reason |
|-----------|------|-------------------|-------------------|--------|
| Container List Query | n8n-status.json | Replace HTTP Request URL + add normalization node | 3 → 4 nodes | GraphQL endpoint + response shape |
| Container Actions | n8n-actions.json | Replace HTTP Request URLs + update mutation body | 4 → 5 nodes | GraphQL mutations + error handling |
| Container Update | n8n-update.json | Replace 9 Docker API nodes with 2 GraphQL nodes | 34 → 27 nodes | Unraid atomic update |
| Container Logs | n8n-logs.json | Replace Execute Command with HTTP Request | 2 → 3 nodes | Native logs query + formatting |
| Container Matching | n8n-matching.json | Add normalization node after list query | 23 → 24 nodes | Data shape consistency |
**Total node count change:** 290 nodes → ~284 nodes (net reduction of ~6 nodes)
### New Components
| Component | Purpose | Where Added | Why New |
|-----------|---------|-------------|---------|
| Normalize Container Data | Map GraphQL response to workflow contract | All modified sub-workflows | Data shape consistency |
| GraphQL Error Handler | Validate `response.errors` array | Inline in validation Code nodes | GraphQL-specific error handling |
| Container ID Resolver | Map container name → Unraid PrefixedID | n8n-matching.json | ID format difference |
**Pattern:** Each modified sub-workflow gains 1-2 new Code nodes for normalization/validation.
## Suggested Build Order
### Phase 1: Infrastructure (Low Risk) — Already Complete
1. **Credential setup** — Completed in Phase 14
- `.env.unraid-api` file
- n8n Header Auth credential
- Test query validated
- Network connectivity proven (myunraid.net relay)
2. **Data normalization library** — Create reusable template
- Build "Normalize Unraid Response" Code node
- Test with Phase 14's test query
- Template ready for copy-paste into sub-workflows
### Phase 2: Simple Sub-workflows (Low Complexity)
**Order:** Actions → Status → Logs → Matching
3. **Actions sub-workflow** (4 Docker API nodes → 5 GraphQL nodes)
- Start/stop/restart mutations
- Direct 1:1 mapping
- Add normalization + error handling
- Test: `start <container>`, `stop <container>`, `restart <container>`
4. **Status sub-workflow** (3 Docker API nodes → 4 GraphQL nodes)
- Container list query
- Single container query
- Add normalization
- Test: `status`, tap container in list
5. **Logs sub-workflow** (2 Execute Command → 3 HTTP Request)
- Logs query with tail parameter
- Add formatting Code node
- Test: `logs <container>`
6. **Matching sub-workflow** (0 API nodes → 1 normalization node)
- Container list source changes (inherited from Status)
- Add normalization if not already present
- Test: All name matching patterns
### Phase 3: Complex Sub-workflow (High Impact)
7. **Update sub-workflow** (9 Docker API nodes → 2 GraphQL nodes)
- Replace multi-step Docker API flow with single `updateContainer` mutation
- Unraid handles atomic update AND status sync
- Validate success/failure cases
- Test: `update <container>`, verify image pull + recreate + status cleared
### Phase 4: Dependency Cleanup (Post-Integration)
8. **Remove docker-socket-proxy dependency**
- Update Docker Compose / Unraid template
- Remove container from deployment
- Remove volume mount for Docker socket
- Update architecture docs
### Phase 5: Validation
9. **Full workflow testing**
- All text commands (status, start, stop, restart, update, logs)
- All inline keyboard flows (tap container → action)
- Batch operations (stop multiple, update all)
- Error cases (invalid container, network failure, API errors)
- Verify Unraid update badges clear automatically after bot updates
**Rationale:** Start simple (Actions = 4 nodes, low risk), build confidence, tackle complex Update workflow last when patterns are proven.
## What NOT to Change
### Stable Components (DO NOT MODIFY)
1. **Main workflow structure** — Keyword Router, Auth IF, Correlation ID generator
- Reason: No Docker API calls, pure orchestration logic
2. **Sub-workflow contracts** — Input/output shapes remain the same
- Example: Update sub-workflow still receives `{ containerId, containerName, chatId, messageId, responseMode }`
- Reason: Preserves integration points with main workflow
- Note: `containerId` value changes (Docker ID → Unraid PrefixedID) but field name/type stable
3. **Telegram response nodes** — All "Send Message" and "Edit Message" nodes
- Reason: User-facing messages, no Docker/Unraid API dependency
4. **Matching logic algorithm** — Container name matching strategy
- Reason: Operates on container names array, agnostic to API source
- Note: Data SOURCE changes, algorithm does not
5. **Batch UI logic** — Selection keyboard, bitmap encoding
- Reason: Pure UI state management, no Docker operations
6. **Confirmation dialogs** — Stop/update confirmation flows
- Reason: Pure UI logic, orchestration only
7. **Error logging patterns** — Structured error returns, correlation IDs
- Reason: API-agnostic observability infrastructure
### Anti-Patterns to Avoid
- **Do NOT change sub-workflow contracts** — Main workflow dispatch nodes depend on stable input shapes
- **Do NOT mix Docker API and Unraid GraphQL** in same workflow — Pick one, commit fully, test, ship
- **Do NOT remove correlation IDs** — Observability is critical for debugging GraphQL errors
- **Do NOT change Telegram message formats** — User-facing stability matters
- **Do NOT optimize prematurely** — Get it working with Unraid GraphQL first, optimize later
- **Do NOT skip data normalization** — Consistent data shapes prevent cascading failures
## Risk Assessment
| Component | Risk Level | Mitigation |
|-----------|------------|------------|
| Actions sub-workflow | LOW | Simple 1:1 mutation mapping, test early |
| Status sub-workflow | LOW | Query-only, add normalization layer |
| Logs sub-workflow | LOW | GraphQL logs query is simpler than Docker API |
| Update sub-workflow | MEDIUM | Complex flow → atomic mutation (big simplification but needs thorough testing) |
| Container ID mapping | MEDIUM | ID format difference requires careful validation |
| Matching sub-workflow | LOW | No API changes, only data source changes |
| Main workflow | NONE | No modifications needed |
| docker-socket-proxy removal | LOW | Remove after all sub-workflows migrated, test thoroughly |
**Overall risk:** LOW-MEDIUM — Most changes are direct API replacements with simpler GraphQL equivalents. Update workflow is the only complex migration but Unraid's atomic operation actually simplifies the flow.
## Key Benefits of Migration
1. **Simplified Update Flow** — 9 Docker API nodes → 2 GraphQL nodes (atomic operation)
2. **Automatic Status Sync** — Unraid update badges clear automatically (solves v1.3's manual sync issue)
3. **Better Error Messages** — Structured GraphQL errors vs. HTTP status codes
4. **Native Integration** — Direct Unraid API vs. Docker proxy layer
5. **Update Detection**`isUpdateAvailable` field enables proactive notifications
6. **Simpler Log Parsing** — Structured entries vs. raw Docker log format with control characters
7. **Security Improvement** — API key with granular permissions vs. Docker socket proxy
8. **Fewer Dependencies** — Remove docker-socket-proxy container from deployment
## Sources
- [Unraid GraphQL Schema](https://raw.githubusercontent.com/unraid/api/main/api/generated-schema.graphql) — Complete API specification (HIGH confidence)
- `.planning/phases/14-unraid-api-access/14-RESEARCH.md` — Phase 14 connectivity research (HIGH confidence)
- `ARCHITECTURE.md` — Existing workflow architecture, contracts (HIGH confidence)
- `n8n-workflow.json`, `n8n-*.json` — Actual workflow implementations (HIGH confidence)
- [n8n GraphQL Node Documentation](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.graphql/) — HTTP Request patterns (HIGH confidence)
- [Docker API Documentation](https://docs.docker.com/engine/api/v1.47/) — Current Docker API patterns (HIGH confidence)
---
*Architecture research for: Unraid Docker Manager v1.4 — Unraid API Native*
*Researched: 2026-02-09*
*Confidence: HIGH — Unraid GraphQL schema verified, Phase 14 connectivity proven, workflow structure analyzed*