6.8 KiB
6.8 KiB
package-updater — Claude context
Webhook-driven Docker Compose updater. Go service, single binary, deployed as
a container with /var/run/docker.sock mounted.
See README.md for user-facing docs. This file is for working ON the code.
Commands
go test ./... # all tests, fast (no docker required)
go test -run TestFoo ./internal/api # single test
go build ./cmd/server # produces ./server
go build ./cmd/master ./cmd/agent # new master/agent binaries (phase 1: inventory)
DATABASE_URL=postgres://... go test -tags integration ./internal/master/store/ # Postgres tests
docker build -t package-updater:dev . # multi-stage, builds golang:1.26-alpine
# Local smoke test (builds from Dockerfile, runs against /var/run/docker.sock):
cp env.sample .env && $EDITOR .env # fill UPDATER_API_KEY
docker compose -f docker-compose.local.yml up --build
curl -sH "Authorization: Bearer $UPDATER_API_KEY" \
-d '{"image":"foo"}' http://localhost:8080/update | jq
Architecture (one-line per package)
cmd/server— wiring only: config → docker client → discovery → queue → httpinternal/api— HTTP handlers, bearer-token auth, request-id + access-log middlewareinternal/config— env-var loading; fails fast ifUPDATER_API_KEYmissinginternal/discovery— given an image, return ComposeJobs to run (label parsing, path check, dedup)internal/discovery/swarm.go— SwarmDiscovery: same FindJobs signature, listsdocker service ls, gates on service-level opt-in label instead of STACKS_ROOT path-checkinternal/updater— FIFO queue + single worker +docker composesubprocess executorinternal/updater/swarm_executor.go— SwarmExecutor:docker service update --imagevia the Docker API instead of adocker composesubprocessinternal/selfupdate— flush-then-exec wrapper for updating ourselves (NOT wired in live, see gotcha)internal/metrics— Prometheus collectorsinternal/logging— slog JSON, request-id context propagationcmd/master,cmd/agent— master/agent architecture (spec2026-10-12-master-agent-design.md), phase 1 = inventory only.cmd/serverkeeps doing updates until phase 3.internal/protocol— agent↔master WebSocket messagesinternal/pin— master key pair, self-signed cert, fingerprint-pinned TLS clientinternal/agenttoken— per-agent tokens (pua_prefix, only the SHA-256 hash is stored)internal/imageref— splitrepo:tag@digestinternal/agent/...— inventory collection, Docker event debounce, connection loopinternal/master/...— Postgres store, agent hub, change broker, OIDC auth, htmx UI
Design intent (do not break without discussion)
- Single FIFO worker by design (
internal/updater/queue.go). Spec §5.7. Never parallelise the queue — twodocker composecalls against the same stack race. - Defense in depth: token AND opt-in label AND STACKS_ROOT prefix must all hold before a container is touched. Weakening any of these breaks the security model — discuss before changing.
- Auth on
/updateonly./healthz,/metrics,/versionare intentionally unauthenticated (internal network, scraper/healthcheck need them). Seecmd/server/main.gorouteAuth. metrics *Metricsparameters may be nil; constructors and call sites check. 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
internal/selfupdate.Wrappedis implemented and unit-tested but NOT wired into the live queue. Spec §15 calls this out as a v1 gap. If you "fix" this, read the package doc — flush-before-exec ordering is subtle.- Image matching is case-sensitive and tag/digest-agnostic. The tricky case
is
localhost:5000/foo:v1where the first colon is a port, not a tag. Seeinternal/discovery/matching.goNormaliseImage. - Path safety uses
filepath.Rel+ ".." prefix check, NOTstrings.HasPrefix. Prevents the/foo-evilvs/fooconfusion. Seeinternal/discovery/pathcheck.go. STACKS_ROOTdefaults to/home/shcizo/self-hostedin prod butenv.samplesuggests/tmpfor smoke tests. The default inconfig.gois the prod value — set explicitly in test/dev envs.- Go 1.26.3 (
go.mod). Dockerfile pinsgolang:1.26-alpine. Bumping one without the other has caused a fix commit already. <summary>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 updatefails with a permission error against a worker-only node. This is an operator/deployment concern (pointDOCKER_HOSTat a manager, or schedule the updater on a manager), not something the code can detect or work around. - WebSocket read limit is raised to 1 MiB on both sides. coder/websocket defaults to 32 KiB, which a full snapshot from a busy machine exceeds — symptom is an agent stuck in a reconnect loop.
- The master's key lives in Postgres (
master_identity). Wiping the database means every agent needs a newMASTER_KEY. - SSE responses must flush through every wrapper.
web.eventsuseshttp.ResponseController; a middleware that wrapsResponseWriterwithoutUnwrap()silently breaks live updates. Nginx needsX-Accel-Buffering: no(set by the handler). - The "stateless" and "label gate" design intents above describe
cmd/serveronly. The master is stateful by design and gates updates per service in the UI (spec 2026-10-12). - The master does OIDC discovery once at startup and exits if the issuer is unreachable. It does not retry; Swarm restarts the container, so a short Authentik outage during deploy resolves itself.
References
- Design spec:
docs/superpowers/specs/2026-05-22-package-updater-design.md(489 lines, authoritative) - Implementation plan:
docs/superpowers/plans/2026-05-22-package-updater-implementation.md - Consumer-side CI integration:
gitea-action/ - Master/agent spec:
docs/superpowers/specs/2026-10-12-master-agent-design.md - Phase 1 plan:
docs/superpowers/plans/2026-10-12-master-agent-phase-1-inventory.md
Conventions
- Conventional Commits (
feat:,fix:,docs:,ci:,build:,chore:). - One package = one responsibility; interfaces defined at consumer site
(
api.Finder,api.Submitter,api.Pinger) for testability. - Table-driven tests with
stretchr/testify. No mocks beyond hand-written fakes.