Phase 03: Container Actions - 4 plans in 4 waves (sequential due to shared workflow file) - Ready for execution
13 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-container-actions | 04 | execute | 4 |
|
|
true |
|
Purpose: Allow users to update containers via "update plex" command. The workflow pulls the latest image, compares digests to detect changes, and recreates the container with the same configuration. Per CONTEXT.md, only notify if an actual update occurred.
Output: Extended n8n workflow with full container update flow.
<execution_context> @/home/luc/.claude/get-shit-done/workflows/execute-plan.md @/home/luc/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-container-actions/03-CONTEXT.md @.planning/phases/03-container-actions/03-RESEARCH.md @.planning/phases/03-container-actions/03-01-SUMMARY.md @n8n-workflow.json Task 1: Add update command routing and container matching n8n-workflow.json Extend the "Route Message" Switch node to handle update commands:-
Add Update Route (to existing Switch node):
- Pattern: message contains "update" (case-insensitive)
- Route to new "Update Branch"
-
Parse Update Command (Code node):
const text = $json.message.text.toLowerCase().trim(); const match = text.match(/^update\s+(.+)$/i); if (!match) { return { json: { error: 'Invalid update format', chatId: $json.message.chat.id } }; } return { json: { containerQuery: match[1].trim(), chatId: $json.message.chat.id, messageId: $json.message.message_id } }; -
Match Container (reuse existing matching pattern):
- Docker List Containers (Execute Command)
- Fuzzy match logic (Code node)
- For updates: only single match supported (no batch update confirmation)
- If 0 matches: "No container found" (can reuse suggestion flow from 03-02 if available)
- If >1 matches: "Update requires exact container name. Found: sonarr, radarr, lidarr"
- If 1 match: proceed to update flow
-
Multiple Match Handler (for update only):
const matches = $json.matches; const names = matches.map(m => m.Names[0].replace(/^\//, '')).join(', '); return { json: { message: `Update requires an exact container name.\n\nFound ${matches.length} matches: ${names}`, chatId: $json.chatId } };Then Send Message node.
-
"update plex" (single match) → should proceed to update flow (may fail at execution, but routing works)
-
"update arr" (multiple matches) → should show "requires exact name" message
-
"update nonexistent" → should show "no container found" Update commands route correctly, single matches proceed to update flow
-
Inspect Container (Execute Command node):
const containerId = $json.matches[0].Id; return { json: { cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/${containerId}/json'`, containerId, containerName: $json.matches[0].Names[0].replace(/^\//, ''), chatId: $json.chatId } }; -
Parse Container Config (Code node): Parse the inspect output and extract what we need:
const inspect = JSON.parse($json.stdout); const imageName = inspect.Config.Image; const currentImageId = inspect.Image; // Extract version from labels if available const labels = inspect.Config.Labels || {}; const currentVersion = labels['org.opencontainers.image.version'] || labels['version'] || currentImageId.substring(7, 19); return { json: { imageName, currentImageId, currentVersion, containerConfig: inspect.Config, hostConfig: inspect.HostConfig, networkSettings: inspect.NetworkSettings, containerName: $json.containerName, containerId: $json.containerId, chatId: $json.chatId } }; -
Pull Image (Execute Command node):
const imageName = $json.imageName; return { json: { cmd: `curl -s --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`, ...($json) // Preserve all context } }; -
Inspect New Image (Execute Command node):
const imageName = $json.imageName; return { json: { cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/images/${encodeURIComponent(imageName)}/json'`, ...($json) } }; -
Compare Digests (Code node):
// Parse new image inspect const newImage = JSON.parse($json.stdout); const newImageId = newImage.Id; const currentImageId = $('Parse Container Config').first().json.currentImageId; if (currentImageId === newImageId) { // No update needed - stay silent per CONTEXT.md return { json: { needsUpdate: false, chatId: $json.chatId } }; } // Extract new version const labels = newImage.Config?.Labels || {}; const newVersion = labels['org.opencontainers.image.version'] || labels['version'] || newImageId.substring(7, 19); return { json: { needsUpdate: true, currentImageId, newImageId, currentVersion: $('Parse Container Config').first().json.currentVersion, newVersion, containerConfig: $('Parse Container Config').first().json.containerConfig, hostConfig: $('Parse Container Config').first().json.hostConfig, networkSettings: $('Parse Container Config').first().json.networkSettings, containerName: $('Parse Container Config').first().json.containerName, containerId: $('Parse Container Config').first().json.containerId, chatId: $json.chatId } }; -
Check If Update Needed (IF node):
- Condition:
{{ $json.needsUpdate }}equals true - True: proceed to recreate
- False: do nothing (silent, no message)
- Condition:
-
"update [container]" with no new image → no message sent (silent)
-
Check workflow logs to confirm pull was attempted and digests compared Image pull works, change detection compares digests correctly
-
Stop Container (Execute Command node):
const containerId = $json.containerId; return { json: { cmd: `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/stop?t=10'`, ...($json) } }; -
Verify Stopped (Code node):
const statusCode = parseInt($json.stdout.trim()); if (statusCode !== 204 && statusCode !== 304) { return { json: { error: true, message: `Failed to stop container: HTTP ${statusCode}`, chatId: $json.chatId } }; } return { json: { ...$json, stopped: true } }; -
Remove Container (Execute Command node):
const containerId = $json.containerId; return { json: { cmd: `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X DELETE 'http://localhost/v1.47/containers/${containerId}'`, ...($json) } }; -
Build Create Body (Code node): Build the container creation request from saved config:
const config = $json.containerConfig; const hostConfig = $json.hostConfig; const networkSettings = $json.networkSettings; // Build NetworkingConfig from NetworkSettings const networks = {}; for (const [name, netConfig] of Object.entries(networkSettings.Networks || {})) { networks[name] = { IPAMConfig: netConfig.IPAMConfig, Links: netConfig.Links, Aliases: netConfig.Aliases }; } const createBody = { ...config, HostConfig: hostConfig, NetworkingConfig: { EndpointsConfig: networks } }; // Remove fields that shouldn't be in create request delete createBody.Hostname; // Let Docker assign delete createBody.Domainname; return { json: { createBody: JSON.stringify(createBody), containerName: $json.containerName, currentVersion: $json.currentVersion, newVersion: $json.newVersion, chatId: $json.chatId } }; -
Create Container (Execute Command node):
const containerName = $json.containerName; const createBody = $json.createBody; // Write body to temp file to avoid shell escaping issues // Or use curl's -d option with proper escaping return { json: { cmd: `echo '${createBody.replace(/'/g, "'\\''")}' | curl -s -X POST --unix-socket /var/run/docker.sock -H "Content-Type: application/json" -d @- 'http://localhost/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`, ...($json) } };Alternative approach if shell escaping is problematic:
// Use a Code node with HTTP request instead of Execute Command // n8n Code nodes can make HTTP requests directly -
Parse Create Response (Code node):
let response; try { response = JSON.parse($json.stdout); } catch (e) { return { json: { error: true, message: `Create failed: ${$json.stdout}`, chatId: $json.chatId } }; } if (response.message) { // Error response return { json: { error: true, message: `Create failed: ${response.message}`, chatId: $json.chatId } }; } return { json: { newContainerId: response.Id, currentVersion: $json.currentVersion, newVersion: $json.newVersion, containerName: $json.containerName, chatId: $json.chatId } }; -
Start New Container (Execute Command node):
const newContainerId = $json.newContainerId; return { json: { cmd: `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${newContainerId}/start'`, ...($json) } }; -
Send Update Result (Telegram Send Message):
const { containerName, currentVersion, newVersion } = $json; const message = `<b>${containerName}</b> updated: ${currentVersion} → ${newVersion}`; return { json: { text: message, chatId: $json.chatId } };
-
"update plex" (when update available):
- Image pulled
- Container stops
- Container removed
- New container created with same config
- Container starts
- Message: "plex updated: v1.32.0 → v1.32.1"
-
"update plex" (when already up to date):
- Image pulled
- Digests compared
- No further action
- No message sent (silent per CONTEXT.md)
-
"update arr" (multiple matches):
- Message: "Update requires exact container name..."
-
"update nonexistent":
- Message: "No container found..."
-
Post-update verification:
- Container running
- Same ports mapped
- Same volumes mounted
- Same network connections
Import updated workflow and test with a real container that has an update available.
<success_criteria>
- Update command parses container name correctly
- Image pull succeeds
- Digest comparison detects changes accurately
- Container recreation preserves Config, HostConfig, Networks
- Version change displayed when detectable
- Silent when no update available
- All error cases report diagnostic details
- Container runs correctly after update </success_criteria>