Skip to content

feat: add container update action with image pull and recreate 🚀#4588

Merged
amir20 merged 20 commits intomasterfrom
feat/container-update
Apr 5, 2026
Merged

feat: add container update action with image pull and recreate 🚀#4588
amir20 merged 20 commits intomasterfrom
feat/container-update

Conversation

@amir20
Copy link
Copy Markdown
Owner

@amir20 amir20 commented Apr 5, 2026

Summary

  • Add "Update" container action that pulls the latest image and recreates the container with the same config
  • Streams real-time pull progress to the UI via SSE, displayed as toast notifications
  • Supports standalone containers (stop → remove → create → start) and Swarm services (service update)
  • Skips recreation if the image is already up-to-date (no new layers)
  • Uses the exact image reference from container inspect (no tag manipulation)
  • Gated behind enableActions config flag, same as start/stop/restart
  • Agent mode support via server-streaming gRPC RPC
  • Exposed as update_container cloud tool for Dozzle Cloud

Test plan

  • Verify "Update" button appears in container action dropdown when actions are enabled
  • Test updating a container with an outdated image — should pull, recreate, and show progress toasts
  • Test updating a container with a current image — should show "Already up to date" toast
  • Test with a Swarm service container — should use service update instead of recreate
  • Verify go test -race ./... passes
  • Verify pnpm test and pnpm typecheck pass

🤖 Generated with Claude Code

amir20 and others added 8 commits April 5, 2026 11:17
…tService implementation

Adds UpdateProgress type, new Client interface methods (ImagePull, ContainerInspectRaw,
ContainerRemove, ContainerCreate, ServiceUpdate), Docker SDK thin wrappers, and the
UpdateContainer orchestration logic that handles image pull, container recreation, and
swarm service updates with progress reporting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
K8sClient now implements ImagePull, ContainerInspectRaw, ContainerRemove,
ContainerCreate, and ServiceUpdate (all return "not supported" errors).
K8sClientService now implements UpdateContainer similarly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ainer update progress

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sistant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 5, 2026

Bugs:

  • updated flag false-negative (docker_service.go:1374): relies on per-layer "Download complete"/"Pull complete" events, but misses the case where a new image has a different manifest yet all layers are cached — Docker reports "Already exists" for all layers and the container won't be recreated. Should check the terminal status event: "Status: Downloaded newer image for ..." vs "Status: Image is up to date for ...".

  • Network config on recreate (docker_service.go:1196-1199): passes inspectResp.NetworkSettings.Networks (runtime state with IP addresses, gateways) as EndpointsConfig. This can cause failures if the previous IP is still allocated or conflict with IPAM. Should pass only network names/aliases, not the runtime endpoint state.

Design:

  • Five new methods on container.Client interface (ImagePull, ContainerInspectRaw, ContainerRemove, ContainerCreate, ServiceUpdate) are Docker-specific and belong in DockerClientService, not the shared interface. ContainerInspectRaw returning any that's immediately type-asserted in the caller breaks the abstraction. K8s/Agent stubs just return errors. The update logic should live entirely within DockerClientService without polluting the Client interface.

Minor:

  • containerActions.ts:88,167: error toasts use hardcoded English strings instead of i18n keys, inconsistent with the rest of the file.
  • No timeout on image pull — stalled pulls hold the SSE connection open indefinitely.

@claude
Copy link
Copy Markdown

claude bot commented Apr 5, 2026

Review: feat/container-update

  • internal/container/client.go: Adding ImagePull, ContainerInspectRaw, ContainerRemove, ContainerCreate, ServiceUpdate to the Client interface leaks Docker-specific primitives into the generic abstraction. K8s is forced to stub 5 no-ops. The update logic already lives in DockerClientService — these methods do not belong on the interface.

  • ContainerCreate(ctx, details any, name string): any parameter with a type assertion inside the implementation breaks interface contracts. The Docker inspect type should stay in the Docker layer entirely.

  • Up-to-date detection is unreliable (docker_service.go): updated only flips on Pull complete / Download complete. If nginx:latest resolves to a new digest but all layers are already cached (shared with another image), every layer reports Already exists, updated=false, and the container is wrongly skipped. Use the final summary event Status: Downloaded newer image for... vs Status: Image is up to date for... instead — these are reliable.

  • Hardcoded English error strings in containerActions.ts lines 88, 161, 167: unable to update container, Update Failed, something went wrong bypass i18n.

  • Network config on recreate: EndpointsConfig from inspect is reused verbatim, which may fail if the network IP assignment conflicts or the network no longer exists. Worth documenting as a known limitation.

Instead of retrying with normal backoff when the API key is invalid,
pause for 1 hour before retrying to avoid hammering the server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 5, 2026

Review

Bugs / Design Issues

  • ContainerInspectRaw / ContainerCreate use any (internal/container/client.go): Leaking Docker SDK types through the container.Client interface as any breaks the abstraction. K8s/Agent stubs cannot implement these correctly. Consider a typed ContainerConfig struct or move this logic entirely into DockerClientService.

  • updated detection is fragile (docker_service.go ~L130): Relying on Download complete / Pull complete events to decide whether an image changed can give false negatives. Docker also emits Already exists for cached layers of a newer image, which won't set updated = true. More reliable: compare image digests before/after pull via ImageInspect.

  • Stop to Remove to Create is not atomic (docker_service.go): Context cancellation or any error after ContainerRemove but before ContainerCreate leaves the container permanently destroyed with no recovery. At minimum document this; ideally handle the error gracefully.

  • Inconsistent channel buffering: progressCh is unbuffered in agent/server.go but buffered (100) in the cloud tool (tools_actions.go). If SSE write is slow in the agent path, the goroutine stalls.

Minor

  • containerActions.ts:88: Error strings are hardcoded English, not going through t() like the rest of the file.
  • docker_service.go ServiceUpdate: uses original tag name, not a digest -- may not force re-pull on all Swarm nodes depending on pull policy.
  • The pulling case in the frontend switch is a no-op -- layer/progress info is never surfaced to the user.

amir20 and others added 5 commits April 5, 2026 12:24
Check 'Status: Downloaded newer image' instead of per-layer events to
reliably detect when a new image was actually pulled. Fixes false negatives
when all layers are cached but the manifest changed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only preserve network names and aliases in EndpointsConfig, not runtime
fields like IP addresses, gateways, and MAC addresses that can cause
conflicts on recreate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…face

Define DockerUpdateClient interface in docker_service.go that extends
container.Client with ImagePull, ContainerInspectRaw, ContainerRemove,
ContainerCreate, and ServiceUpdate. Removes K8s stubs that just returned
errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cales

Display "Update Service" instead of "Update" in the dropdown when the
container is a Docker Swarm service. Add update and error i18n keys
to all 17 locale files with proper translations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 5, 2026

Critical bug: k8s_service.go UpdateContainer returns without closing progressCh, causing a goroutine deadlock in every caller that does for progress := range progressCh (web/actions.go:1579, agent/server.go:974). Add defer close(progressCh) like the Docker implementation.

Minor issues:

  • docker_service.go: ContainerInspectRaw returns any then immediately type-asserts to docker_types.InspectResponse in ContainerCreate — the DockerUpdateClient interface loses type safety for no benefit. Consider a typed ContainerInspect method instead.
  • docker_service.go:1435: strings.HasPrefix(event.Status, "Status: Downloaded newer image") relies on Docker daemon response strings. strings.Contains would be more robust.
  • tools_actions.go:1117: buffered channel of 100 for progress — fine for now, but could silently block if the cloud tool takes a long time and fills up before errCh is consumed.
  • containerActions.ts: manual SSE stream parsing via fetch + body reader works but skips the event: field — the handler emits update-progress events, but the client only checks data.status. This is fine as long as no other event types are mixed in.

amir20 and others added 3 commits April 5, 2026 12:29
- Buffer progressCh (50) in HTTP handler to prevent goroutine leak when
  SSE client disconnects mid-update
- Add defer close(progressCh) in K8s UpdateContainer to prevent hung
  requests in K8s mode
- Add nil check for NetworkSettings in ContainerCreate to prevent panic
  on containers with unusual configs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… cases

- Extract findContainerWithActions helper in actions.go to deduplicate
  auth/permission check block shared by containerActions and containerUpdate
- Change ContainerInspectRaw(any) to ContainerInspect(InspectResponse) and
  ContainerCreate(any) to ContainerCreate(InspectResponse), eliminating
  double type assertions and the any parameter
- Collapse near-identical done/up-to-date switch cases in containerActions.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@amir20 amir20 enabled auto-merge (squash) April 5, 2026 19:37
@claude
Copy link
Copy Markdown

claude bot commented Apr 5, 2026

Critical issues:

  • Destructive recreate without rollback (docker_service.go): If ContainerCreate fails after ContainerRemove succeeds, the container is permanently lost. Consider saving the inspect response and attempting to restore, or at minimum surfacing a clearer error message.

  • Goroutine leak / errCh deadlock (actions.go): If the SSE write fails and the handler returns early, the goroutine writing to errCh may block indefinitely since errCh is never drained. Use defer with context cancellation or a select with ctx.Done() to avoid this.

  • Frontend: missing AbortController (containerActions.ts): fetch() for the update stream has no AbortController, so if the component unmounts mid-stream the request continues in the background. Pass a signal tied to the composable's lifecycle.

  • Frontend: reader not closed on error (containerActions.ts): The ReadableStreamDefaultReader from response.body.getReader() is never explicitly closed in the error path. Add reader.cancel() in a finally block.

  • actionStates.update may not reset (containerActions.ts): Any unhandled exception before the finally block (or lack thereof) can leave the Update button permanently disabled. Wrap the whole body in try/finally to guarantee the flag resets.

  • Fragile string-based image update detection (docker_service.go): strings.HasPrefix(event.Status, "Status: Downloaded newer image") is locale- and version-sensitive. Consider comparing image digests before/after pull instead.

@amir20 amir20 disabled auto-merge April 5, 2026 19:39
@amir20 amir20 changed the title feat: add container update action with image pull and recreate feat: add container update action with image pull and recreate 🚀 Apr 5, 2026
amir20 and others added 2 commits April 5, 2026 12:55
@amir20 amir20 enabled auto-merge (squash) April 5, 2026 19:56
@claude
Copy link
Copy Markdown

claude bot commented Apr 5, 2026

Critical issues:

  • Goroutine leak internal/web/actions.go: If SSE write fails, handler returns but the goroutine sending to progressCh (cap=50) blocks indefinitely. Need to drain channel or cancel context on early exit.
  • Data loss risk internal/support/docker/docker_service.go: Container is removed before ContainerCreate is guaranteed to succeed. If create fails (resources, storage), the original container is gone with no recovery path.
  • Nil panic internal/docker/client.go: inspectResp.Config.Image accessed without nil-checking Config.
  • No concurrent update guard: Two simultaneous update requests on same container will both proceed through stop→remove→create, leaving undefined state.
  • Cloud tool blocks internal/cloud/tools_actions.go: executeUpdateContainer drains entire progressCh (can take minutes for large images) before returning response — poor for Cloud UX.
  • Fragile update detection: Checking strings.HasPrefix(event.Status, "Status: Downloaded newer image") is brittle; compare image digests before/after pull instead.
  • No operation timeout: Image pull can hang indefinitely; no context.WithTimeout on the update operation.

@amir20 amir20 merged commit 284822c into master Apr 5, 2026
12 checks passed
@amir20 amir20 deleted the feat/container-update branch April 5, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant