From 9881812eca1e130accfbd83cc3ffb6891143cfd5 Mon Sep 17 00:00:00 2001 From: Samuel Enocsson Date: Sat, 4 Jul 2026 20:32:22 +0200 Subject: [PATCH] docs: document MODE=swarm support and its opt-in label / manager-node requirements --- CLAUDE.md | 10 ++++++++++ README.md | 24 +++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7414ea9..9b0153b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,9 @@ curl -sH "Authorization: Bearer $UPDATER_API_KEY" \ - `internal/api` — HTTP handlers, bearer-token auth, request-id + access-log middleware - `internal/config` — env-var loading; fails fast if `UPDATER_API_KEY` missing - `internal/discovery`— given an image, return Compose `Job`s to run (label parsing, path check, dedup) +- `internal/discovery/swarm.go` — SwarmDiscovery: same FindJobs signature, lists `docker service ls`, gates on service-level opt-in label instead of STACKS_ROOT path-check - `internal/updater` — FIFO queue + single worker + `docker compose` subprocess executor +- `internal/updater/swarm_executor.go` — SwarmExecutor: `docker service update --image` via the Docker API instead of a `docker compose` subprocess - `internal/selfupdate` — flush-then-exec wrapper for updating ourselves (NOT wired in live, see gotcha) - `internal/metrics` — Prometheus collectors - `internal/logging` — slog JSON, request-id context propagation @@ -45,6 +47,10 @@ curl -sH "Authorization: Bearer $UPDATER_API_KEY" \ Tests rely on this — don't drop the nil-check. - **Stateless**: no DB, no config file, no on-disk audit log. Docker daemon is the source of truth. +- **Compose mode and Swarm mode are selected once per deployment via `MODE`**, never + mixed at request time. Swarm mode's security gate is opt-in label only — there is + no STACKS_ROOT-equivalent path check, since Swarm services have no local compose + file. Don't add one; don't weaken Compose mode's three-factor gate to match. ## Gotchas @@ -63,6 +69,10 @@ curl -sH "Authorization: Bearer $UPDATER_API_KEY" \ without the other has caused a fix commit already. - **`` C# convention from global CLAUDE.md does not apply here** — this is Go. Use idiomatic GoDoc (`// FuncName does X.`). +- **Swarm mode requires manager-node API access.** `docker service update` fails + with a permission error against a worker-only node. This is an operator/deployment + concern (point `DOCKER_HOST` at a manager, or schedule the updater on a manager), + not something the code can detect or work around. ## References diff --git a/README.md b/README.md index b481e41..8a41057 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,25 @@ A container is eligible for update only if it has **both**: Defense in depth: a valid bearer token AND the opt-in label must both be present before any container is touched. +## Swarm mode + +Set `MODE=swarm` to update Docker Swarm services instead of Compose stacks. The +service runs `docker service update --image` via the Docker API instead of +shelling out to `docker compose`. + +Two things to get right when running in Swarm mode: + +- **The opt-in label goes on the service, not the container/task.** Swarm mode + reads `Service.Spec.Labels`, so add it with + `docker service update --label-add se.shcizo.auto-update=true ` or + set it under `deploy.labels` in the stack file — a plain `labels:` entry on + the service (container-level) is not visible to Swarm mode's discovery. +- **The updater must talk to a Swarm manager.** `docker service update` + requires manager API access, so either point `DOCKER_HOST` at a manager node + or schedule the updater container itself on a manager with the socket + mounted. This is a deployment concern the service cannot detect or work + around. + ## Quick start 1. Build and push the image (e.g. via your own CI). @@ -49,6 +68,7 @@ All via environment variables. | `LOG_LEVEL` | no | `info` | `debug` / `info` / `warn` / `error`. | | `UPDATE_TIMEOUT` | no | `5m` | Per-job timeout (Go duration). | | `OPT_IN_LABEL` | no | `se.shcizo.auto-update` | Label name to check; value must equal `"true"`. | +| `MODE` | no | `compose` | `compose` or `swarm`. Selects the update mechanism for the whole deployment; not mixed per-request. | ## Endpoints @@ -80,5 +100,7 @@ These are tracked in the spec's section 2 and section 15 as deliberate out-of-sc - **Self-update wiring**: `internal/selfupdate.Wrapped` exists and is unit-tested but is not wired into the live queue. The HTTP response flush ordering for self-replacement is a future enhancement; for now, expect to manually rerun `docker compose up -d` on the host if pushing a new image of `package-updater` itself causes a mid-response interruption. - **No rollback**: Compose's "keep old container if new fails to start" is the only safety net. -- **Single host only**. +- **Single host only in Compose mode**. Swarm mode (`MODE=swarm`) is the + multi-node path, but only from a manager node's point of view — the updater + itself still needs manager API access (see "Swarm mode" above). - **No per-repo API keys**: a single shared bearer token is used.