P5: OSS readiness — docs site, contributor onboarding, e2e harness

P5-01 — Documentation site under docs/book/ rendered with mdBook
(downloaded via Makefile, same static-binary pattern as Tailwind).
Structured chapters: getting started, concepts, operations,
security, reference. `make docs` / `make docs-watch`. Generated
output gitignored.

P5-02 — CONTRIBUTING.md rewritten from placeholder to a full
guide. CODE_OF_CONDUCT.md adapted from Contributor Covenant for a
single-maintainer project. .gitea/issue_template/{bug,feature}.md
and PULL_REQUEST_TEMPLATE.md.

P5-04 — Six README screenshots captured live from a fresh server
bootstrap (login, empty dashboard, add-host, alerts, settings,
audit log). README rewritten to centre the screenshot grid and
link out to the docs site.

P5-05 — SECURITY.md with disclosure policy (3-day ack, 30-day
default window), scope in/out, threat-model summary, operator
hardening checklist. Mirrored as a docs-site chapter.

P5-06 — End-to-end test harness. e2e/compose.e2e.yml brings up
server + sibling Linux agent (alpine + restic) + restic/rest-server.
Agent uses announce-and-approve so Playwright can drive the full
operator flow: bootstrap → login → accept pending → backup →
verify terminal status. Second spec scrapes /metrics to assert
the P6-04 endpoint surface. .gitea/workflows/e2e.yml runs on every
PR; local how-to in docs/e2e.md.
This commit is contained in:
2026-05-07 23:56:02 +01:00
parent a252b25854
commit 89537d417a
47 changed files with 2818 additions and 61 deletions
+58
View File
@@ -0,0 +1,58 @@
# Environment variables
The server reads its configuration from environment variables
(canonical) with an optional YAML overlay. Env wins over YAML so
operators can tweak a single setting without rewriting the file.
## Server
| Variable | Default | Meaning |
|---------------------------|----------------------------------|---------|
| `RM_LISTEN` | `:8080` | TCP listener for the HTTP server. |
| `RM_DATA_DIR` | `/data` | Persistent state directory (SQLite, secret key, agent assets). |
| `RM_BASE_URL` | (none) | Public URL clients use; required for OIDC redirects + cookie scope. |
| `RM_SECRET_KEY_FILE` | `${RM_DATA_DIR}/secret.key` | Path to the AEAD key file. Auto-generated on first run. |
| `RM_COOKIE_SECURE` | `true` | Set `false` only for local HTTP testing. Controls `Secure` on session cookies. |
| `RM_TRUSTED_PROXY` | (none) | Comma-separated CIDRs trusted for `X-Forwarded-*`. |
| `RM_BUNDLED_ASSETS_DIR` | `/opt/restic-manager/dist` | Read-only path with bundled agent binaries + install scripts (the docker image bakes them here). |
| `RM_METRICS_TOKEN` | (off) | When set, `GET /metrics` requires `Authorization: Bearer <token>`. |
| `RM_METRICS_TRUSTED_CIDR` | (off) | When set, `GET /metrics` restricts source IPs (comma-CIDR). |
OIDC variables (all optional; empty issuer disables OIDC):
| Variable | Meaning |
|--------------------------------|---------|
| `RM_OIDC_ISSUER` | OIDC discovery URL (e.g. `https://auth.example.com`). |
| `RM_OIDC_CLIENT_ID` | Client ID registered with the IdP. |
| `RM_OIDC_CLIENT_SECRET` | Client secret (or use `RM_OIDC_CLIENT_SECRET_FILE`). |
| `RM_OIDC_CLIENT_SECRET_FILE` | Path to a file holding the client secret. |
| `RM_OIDC_DISPLAY_NAME` | Button label on the login page (e.g. "Authelia"). |
| `RM_OIDC_ROLE_CLAIM` | Token claim that carries roles (default `groups`). |
| `RM_OIDC_ROLE_MAPPING` | `idp-group=role` entries, comma-separated (e.g. `rm-admin=admin,rm-ops=operator`). |
| `RM_OIDC_REDIRECT_URL` | Override for the redirect URL; defaults to `${RM_BASE_URL}/auth/oidc/callback`. |
## Agent
| Variable | Default | Meaning |
|----------------------|---------|---------|
| `RM_AGENT_CONFIG` | `/etc/restic-manager/agent.yaml` (Linux) | Config file path. |
The agent's other settings live in the YAML file (server URL,
bearer token, optional cert pin). The install script writes that
file for you at enrolment.
## Build-time
The Makefile threads `-ldflags` from `git describe` into the
`internal/version` package so `--version` and the dashboard
footer show the right values:
```
-X gitea.dcglab.co.uk/steve/restic-manager/internal/version.Version=$(VERSION)
-X gitea.dcglab.co.uk/steve/restic-manager/internal/version.Commit=$(COMMIT)
```
If you build with `go build` directly (no Makefile), `Version`
falls back to `dev` and the agent-update comparison falls back
to "always equal". Source-build deployments can still run; they
just don't participate in the self-update flow.
+82
View File
@@ -0,0 +1,82 @@
# HTTP endpoints
A non-exhaustive map of the surfaces the control plane exposes.
All `/api/*` routes return JSON; all other paths render HTML
(server-rendered with HTMX in the loop).
The canonical wiring lives at
[`internal/server/http/server.go`](https://gitea.dcglab.co.uk/steve/restic-manager/src/branch/main/internal/server/http/server.go);
when in doubt, read the routes block there.
## Public (no auth)
| Method | Path | Purpose |
|--------|----------------------------|---------|
| GET | `/healthz` | Liveness probe. Returns 204. |
| POST | `/api/auth/login` | Local-user login. JSON body: `{username, password}`. |
| POST | `/api/auth/logout` | Invalidate the session cookie. |
| POST | `/api/bootstrap` | First-run admin creation. Accepts the token printed at first start. |
| POST | `/api/agents/enroll` | Token-based agent enrolment. |
| POST | `/api/agents/announce` | Announce-and-approve agent enrolment. |
| GET | `/agent/binary?os=&arch=` | Serves the agent binary for the install scripts. |
| GET | `/install/*` | Serves the Linux + Windows install scripts and the systemd unit. |
| GET | `/api/version` | Build version + commit JSON. |
| GET | `/metrics` | Prometheus exposition (only when opted-in via `RM_METRICS_TOKEN` / `RM_METRICS_TRUSTED_CIDR`). |
| GET | `/login`, `/setup`, `/bootstrap` | UI pages. |
## Authenticated (any role)
| Method | Path | Purpose |
|--------|------------------------------------------|---------|
| GET | `/` | Dashboard. |
| GET | `/hosts/{id}` | Host detail. |
| GET | `/hosts/{id}/repo` | Repo tab. |
| GET | `/hosts/{id}/jobs` | Jobs tab. |
| GET | `/hosts/{id}/sources` | Source groups list. |
| GET | `/hosts/{id}/schedules` | Schedules list. |
| GET | `/jobs/{id}` | Live job log. |
| GET | `/api/hosts`, `/api/fleet/summary` | JSON list + summary. |
| GET | `/api/jobs/{id}/stream` | WebSocket subscription to a job's live log. |
| GET | `/api/jobs/{id}/log.{txt,ndjson}` | Persisted log download. |
## Operator role and above
| Method | Path | Purpose |
|--------|---------------------------------------|---------|
| POST | `/hosts/{id}/run-backup` | Run-now (HTMX form-post). |
| POST | `/hosts/{id}/sources/{gid}/run-now` | Per-source-group run-now. |
| POST | `/hosts/{id}/repo/{prune,check,unlock,reinit,probe}` | Maintenance actions. |
| POST | `/api/hosts/{id}/snapshots/diff` | Snapshot-diff job. |
| POST | `/hosts/{id}/restore` | Restore wizard submit. |
| POST | `/api/jobs/{id}/cancel` | Cancel a running job. |
| POST | `/hosts/{id}/tags` | Update host tags. |
| POST | `/hosts/{id}/sources` and friends | Source-group CRUD. |
| POST | `/hosts/{id}/schedules` and friends | Schedule CRUD. |
| POST | `/hosts/{id}/repo/credentials`, `/admin-credentials` | Credential update. |
## Admin role only
| Method | Path | Purpose |
|--------|---------------------------------------|---------|
| POST | `/hosts/new` | Mint enrolment token (Add host). |
| POST | `/hosts/{id}/delete` | Delete + cascade. |
| POST | `/hosts/{id}/update` | Dispatch a single agent update. |
| GET/POST | `/settings/users/...` | User management. |
| POST | `/settings/notifications/...` | Notification channel CRUD + test fire. |
| POST | `/settings/fleet-update/...` | Fleet-update worker. |
## WebSocket
| Path | Who connects | Auth |
|--------------------------------|--------------|------|
| `/ws/agent` | Agent | Bearer token issued at enrolment. |
| `/ws/agent/pending` | Agent (announce flow) | Pending-id query param. |
| `/api/jobs/{id}/stream` | Browser | Session cookie. |
## RBAC enforcement
Routes are grouped into chi route-groups by required role
(`viewer < operator < admin`); the `requireRole` middleware in
`internal/server/http/middleware.go` is the bouncer. Sessions
re-validate `disabled_at` on every request, so a disabled user's
cookie stops working immediately.