fcd5908992
One instance per server, each owning its local Docker daemon; Gitea CI posts /update to every instance. Records why MODE exclusivity is correct rather than a limitation, and the alternatives rejected. Claude-Session: https://claude.ai/code/session_01S3aqJ4tvaPezQhsGNCybut
112 lines
5.3 KiB
Markdown
112 lines
5.3 KiB
Markdown
# Multi-Server Deployment & CI Fan-Out — Design
|
|
|
|
**Date:** 2026-08-04
|
|
**Status:** Approved, ready for implementation planning
|
|
|
|
## Problem
|
|
|
|
package-updater was built as a single instance talking to a single local Docker
|
|
socket. Two things surfaced at once:
|
|
|
|
1. Swarm support (branch `worktree-docker-swarm-support`) added `MODE=compose|swarm`,
|
|
selecting one paradigm per deployment. This looked like an artificial limitation:
|
|
"why can't one instance do both?"
|
|
2. The real constraint is narrower and harder: an instance can only reach the Docker
|
|
daemon it is configured against. Compose stacks live on other, separate servers.
|
|
No amount of generalising the application makes one instance reach them.
|
|
|
|
The second point dissolves the first. The limitation was never in the application —
|
|
it was in the deployment topology (one instance for a fleet of servers).
|
|
|
|
## Decision
|
|
|
|
**One package-updater instance per server. CI fans out to all of them.**
|
|
|
|
- Each server runs its own instance against its own local Docker socket.
|
|
- The Swarm manager node runs an instance with `MODE=swarm`.
|
|
- Each standalone Compose host runs an instance with `MODE=compose`.
|
|
- The Gitea CI action posts `/update` to every instance.
|
|
|
|
`MODE` exclusivity is therefore correct, not a defect: each instance owns exactly one
|
|
machine running exactly one paradigm. No composite Finder or routing Executor is built.
|
|
|
|
### Rejected alternatives
|
|
|
|
| Alternative | Why rejected |
|
|
|---|---|
|
|
| Composite Finder + routing Executor (both modes in one instance) | Solves nothing — a single instance still cannot reach other servers' daemons. Only justified if a Swarm manager node *also* ran standalone Compose stacks locally, which it does not. |
|
|
| One instance holding N Docker clients (`ssh://` / TCP+TLS to each host) | Technically viable (`client.FromEnv` already supports it) but requires `Job` to carry host identity, discovery to iterate clients, and creates a single point of failure with broad network credentials. Buys nothing over per-server instances given CI can reach every server. |
|
|
| Controller + agents (pull model) | Only needed if servers cannot accept inbound traffic. They can. Would be a genuinely different application. |
|
|
|
|
### Assumptions
|
|
|
|
These held at design time; revisit the decision if any changes:
|
|
|
|
- Every server is reachable from Gitea CI over HTTP.
|
|
- Per-server outcomes are sufficient; no aggregated cross-server report is needed.
|
|
- The Swarm manager node runs Swarm services only, no local Compose stacks.
|
|
- All instances share one bearer token (org-level Gitea secret `UPDATER_TOKEN`).
|
|
|
|
## Scope
|
|
|
|
### Part 1 — Merge Swarm support to main
|
|
|
|
Branch `worktree-docker-swarm-support` (10 commits) is complete, builds clean, and
|
|
its tests pass. No code changes required. Documentation is reframed so `MODE`
|
|
describes *what paradigm this machine runs*, not a product-level restriction, and the
|
|
one-instance-per-server deployment model is written down — it is the expensive insight
|
|
of this design and is not derivable from the code.
|
|
|
|
Documentation changes:
|
|
|
|
- `README.md` — describe `MODE` per-instance; add a deployment-topology section
|
|
covering one instance per server and CI fan-out.
|
|
- `CLAUDE.md` — reword the "Compose mode and Swarm mode are selected once per
|
|
deployment" design-intent entry to state the reason (each instance owns one daemon),
|
|
so a future reader does not re-litigate "make it handle both".
|
|
|
|
### Part 2 — CI fan-out in `gitea-action/`
|
|
|
|
`gitea-action/action.yml`'s `endpoint` input accepts a **newline-separated list** of
|
|
`/update` URLs instead of a single URL.
|
|
|
|
Behaviour:
|
|
|
|
- Post to each endpoint in list order.
|
|
- **Continue through the whole list even when one fails.** With `set -e` and a naive
|
|
loop, an unreachable server B leaves server C never updated — a half-deployed fleet
|
|
where the failure also hides which hosts succeeded.
|
|
- Print HTTP status and response body per endpoint, so the CI log shows the fleet
|
|
state at a glance.
|
|
- Exit non-zero at the end if any endpoint returned 4xx/5xx or was unreachable.
|
|
|
|
A single URL remains a one-element list and keeps working unchanged. This is a
|
|
property of the format, not backward-compatibility work.
|
|
|
|
Newline rather than comma as separator: URLs may legally contain commas but never
|
|
line breaks, so the separator cannot collide with the data. Choosing an impossible
|
|
delimiter is cheaper than building escaping. This also matches the Actions convention
|
|
for list inputs (`paths`, `files`).
|
|
|
|
Blank lines and surrounding whitespace are ignored, so YAML block scalars with
|
|
trailing newlines and indentation behave as expected.
|
|
|
|
`README.md` in `gitea-action/` documents the list form with a worked multi-server
|
|
example.
|
|
|
|
## Out of scope
|
|
|
|
- Composite/routing discovery within one instance — not needed under this topology.
|
|
- Aggregated cross-server reporting — per-endpoint CI output is sufficient.
|
|
- Per-endpoint distinct tokens — all instances share one token.
|
|
- Wiring `internal/selfupdate` into the live queue — a pre-existing v1 gap, unrelated.
|
|
|
|
## Testing
|
|
|
|
- Part 1: existing suite (`go test ./...`) must stay green after merge. No new tests —
|
|
no new code.
|
|
- Part 2: the action is shell in composite YAML with no test harness in this repo.
|
|
Verification is manual: run the loop logic against a mix of reachable and
|
|
unreachable endpoints and confirm (a) every endpoint is attempted, (b) the step
|
|
exits non-zero, (c) each endpoint's status appears in the output.
|