feat: add Docker Swarm support (MODE=swarm) #4

Merged
shcizo merged 10 commits from worktree-docker-swarm-support into main 2026-07-04 18:54:18 +00:00
Owner

Summary

  • Adds a MODE env var (compose default, or swarm) selecting once per deployment between the existing Docker Compose subprocess flow and a new Docker-API-based Swarm service update flow.
  • New SwarmDiscovery / SwarmExecutor satisfy the existing api.Finder / updater.Executor interfaces, so the HTTP layer and FIFO queue needed no changes — only cmd/server/main.go branches on cfg.Mode.
  • Swarm mode's security gate is opt-in label on the Swarm service itself (Spec.Labels), replacing the STACKS_ROOT path-check that only makes sense for Compose (Swarm services have no local compose file to check a path against).
  • Fixes a bug found during final whole-branch review: the request's tag field was never plumbed through, so swarm mode would have deployed an untagged (:latest) image regardless of what was actually pushed.

Process

Implemented via subagent-driven development from docs/superpowers/plans/2026-07-04-docker-swarm-support.md: 7 tasks, each with an independent implementer + task-level spec/quality review (all approved clean), followed by a final whole-branch review that caught the tag-plumbing gap (fixed and re-reviewed clean).

Test plan

  • go build ./...
  • go test ./... (all packages green)
  • Manual smoke test against a real Swarm cluster (not done — no cluster available in this environment; MODE=swarm wiring is verified via build + unit tests only)
## Summary - Adds a `MODE` env var (`compose` default, or `swarm`) selecting once per deployment between the existing Docker Compose subprocess flow and a new Docker-API-based Swarm service update flow. - New `SwarmDiscovery` / `SwarmExecutor` satisfy the existing `api.Finder` / `updater.Executor` interfaces, so the HTTP layer and FIFO queue needed no changes — only `cmd/server/main.go` branches on `cfg.Mode`. - Swarm mode's security gate is opt-in label on the Swarm service itself (`Spec.Labels`), replacing the `STACKS_ROOT` path-check that only makes sense for Compose (Swarm services have no local compose file to check a path against). - Fixes a bug found during final whole-branch review: the request's `tag` field was never plumbed through, so swarm mode would have deployed an untagged (`:latest`) image regardless of what was actually pushed. ## Process Implemented via subagent-driven development from `docs/superpowers/plans/2026-07-04-docker-swarm-support.md`: 7 tasks, each with an independent implementer + task-level spec/quality review (all approved clean), followed by a final whole-branch review that caught the tag-plumbing gap (fixed and re-reviewed clean). ## Test plan - [x] `go build ./...` - [x] `go test ./...` (all packages green) - [ ] Manual smoke test against a real Swarm cluster (not done — no cluster available in this environment; `MODE=swarm` wiring is verified via build + unit tests only)
shcizo added 10 commits 2026-07-04 18:45:53 +00:00
Verified the Docker SDK v28.5.2+incompatible ServiceList signature via go doc
before implementing: the options type is swarm.ServiceListOptions, not
types.ServiceListOptions as the brief assumed. Everything else (Service.ID,
Service.Spec via embedded Annotations for Name/Labels, TaskTemplate.ContainerSpec.Image)
matched the brief exactly.

Reuses discovery.ImagesMatch and discovery.HasOptIn rather than duplicating
matching/opt-in logic. Opt-in label is read from the service spec's own
labels since Swarm services have no local compose file to anchor a
STACKS_ROOT path check against (unlike Compose mode).
Adds the Swarm-mode counterpart to ComposeExecutor: updates a Swarm
service's image directly through the Docker SDK (ServiceInspectWithRaw +
ServiceUpdate with QueryRegistry=true so floating tags resolve to a fresh
digest), gated by the same Refused guard used in Compose mode. Satisfies
the existing Executor interface unchanged, so Queue needs no changes.
req.Tag was only echoed in the HTTP response, never used to match jobs.
Compose mode didn't care (ComposeExecutor re-pulls the compose file's own
pinned tag), but SwarmExecutor sets the service image directly from
Job.Image, which was built from the untagged req.Image alone -- so a
Swarm deploy silently rewrote the service to :latest instead of the
requested tag. Build the full image:tag reference once in the handler
and pass it into FindJobs; NormaliseImage/ImagesMatch already strip
tags before matching, so this doesn't change which jobs match in either
mode.
shcizo merged commit 50ef1a9a07 into main 2026-07-04 18:54:18 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shcizo/package-updater#4