4.9 KiB
4.9 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
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 propagation
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.
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/
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.