Detail doc + ledger row for f2fc57c: the internal (service-token) twin of the
on-demand world backup endpoint that the break-glass console peer will call.
4.5 KiB
Internal-face break-glass world backup endpoint (§B4 "Sync", phase 2a)
- Type: feature (addition)
- Date: 2026-07-07
- Area:
internal/api,docs/openapi.yaml— Go, oracle-verified - Commit:
f2fc57c - Task: #31 Phase B4 break-glass ops — the "Sync" operation. Per the user's
"两者都要" decision the feature is built in two halves: the felis-api endpoint
that does the real backup-Job orchestration (phase 1,
7a7c0d5) and a break-glass menu peer that calls it while the API is alive (phase 2b, follow-up). This change is phase 2a: the second, internal face of that endpoint — the door the console peer will knock on.
What it does
Adds POST /api/v1/internal/servers/{name}/backup, an internal-face twin of the
external POST /api/v1/servers/{name}/backup. The on-node break-glass console (root
on the host, holding the service token) POSTs here to snapshot a stopped world while
felis-api is alive. Same 202 backing_up / 409 not_stopped / 503
backup_unavailable / 404 / 400 bad_name surface as the external face.
Why
The console cannot render the backup Job itself: it lacks the deployment coordinates
(FELIS_IMAGE, FELIS_BACKUP_PVC) that only felis-api holds — the same reason the
endpoint exists at all (phase 1). But the external face requires a Cloudflare-Access
Principal the console does not have. The internal face authenticates with the service
token (a trusted machine caller, no Principal), so the console can reach the same
orchestration without a browser session.
Design decisions
- No owner gate on the internal face. The external handler enforces owner-or-admin from the Principal; the internal handler has none — the service token IS the authorization (the operator already has root on the node), so a server owned by someone else still backs up. This mirrors how the other internal-face handlers (op-login approve, QR poll) trust the token rather than a Principal.
- Shared
enqueueBackuptail. The RWO stopped-gate, the optional-Backuper 503, the async hand-off, and the audit+202 were refactored out ofhandleBackupNowinto a singleenqueueBackup(w, r, name, rec, actor, source)that both faces call. The two faces differ only in how the caller is authorized and in the audit actor/source — the security-critical stopped-gate is single-sourced so the faces cannot drift apart. - Audit attributed to break-glass/internal. The internal handler audits directly
via
Repo.AuditwithActor:"break-glass", Source:"internal"(thea.audithelper hardcodesSource:"external"), so a console-initiated backup is distinguishable in the audit log from an owner's self-service one. - Console does not double-audit. Unlike the halt peer — which writes the CRD directly and audits locally — the backup peer goes through the API, and the API audits at the boundary. Auditing is single-sourced there; the console will not emit its own row.
Files
| File | Change |
|---|---|
internal/api/handlers_backups.go |
+handleInternalBackup, +enqueueBackup; handleBackupNow tail now calls enqueueBackup(..., p.Email, "external") |
internal/api/api.go |
register POST /api/v1/internal/servers/{name}/backup on the internal-face route table |
docs/openapi.yaml |
document the internalBackupNow operation (x-felis-face: [internal], serviceToken security) |
internal/api/handlers_backup_now_test.go |
+TestInternalBackup — no-owner-gate, break-glass/internal audit, stopped-gate/503/404/400 |
Verification
WSL oracle (go1.26.4, authoritative for Go):
go build ./... && go vet ./... && go test ./... → ALL GREEN
TestOpenAPIMatchesServedRoutes gates the new route against docs/openapi.yaml in
both directions (served⇔documented) and passes. TestInternalBackup (5 subtests) and
the existing TestBackupNow (10) both pass — the external refactor is
behaviour-preserving (same audit actor p.Email/source external).
Self-review outcome
- ponytail (over-engineering): the internal face is not a copy of the external
handler — the shared tail (
enqueueBackup) collapses the duplication, and the two handlers hold only their distinct auth + audit-attribution. No new abstraction beyond the one shared function two callers already justify. - correctness: the no-owner-gate difference is deliberate and matches the other service-token handlers; the stopped-gate is unchanged and now single-sourced, so the external and internal faces cannot diverge on the RWO safety check.