feat: add Docker Swarm support (MODE=swarm) #4
@@ -0,0 +1,37 @@
|
|||||||
|
name: Docker Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.shcizo.se
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.PACKAGES_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v5
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: gitea.shcizo.se/shcizo/package-updater
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 → http
|
||||||
|
- `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/updater` — FIFO queue + single worker + `docker compose` subprocess executor
|
||||||
|
- `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
|
||||||
|
|
||||||
|
## Design intent (do not break without discussion)
|
||||||
|
|
||||||
|
- **Single FIFO worker by design** (`internal/updater/queue.go`). Spec §5.7. Never
|
||||||
|
parallelise the queue — two `docker compose` calls 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 `/update` only**. `/healthz`, `/metrics`, `/version` are intentionally
|
||||||
|
unauthenticated (internal network, scraper/healthcheck need them). See
|
||||||
|
`cmd/server/main.go` `routeAuth`.
|
||||||
|
- **`metrics *Metrics` parameters 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.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **`internal/selfupdate.Wrapped` is 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:v1` where the first colon is a port, not a tag. See
|
||||||
|
`internal/discovery/matching.go` `NormaliseImage`.
|
||||||
|
- **Path safety uses `filepath.Rel` + ".." prefix check**, NOT `strings.HasPrefix`.
|
||||||
|
Prevents the `/foo-evil` vs `/foo` confusion. See `internal/discovery/pathcheck.go`.
|
||||||
|
- **`STACKS_ROOT` defaults to `/home/shcizo/self-hosted`** in prod but `env.sample`
|
||||||
|
suggests `/tmp` for smoke tests. The default in `config.go` is the prod value
|
||||||
|
— set explicitly in test/dev envs.
|
||||||
|
- **Go 1.26.3** (`go.mod`). Dockerfile pins `golang: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.`).
|
||||||
|
|
||||||
|
## 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.
|
||||||
Reference in New Issue
Block a user