feat(api): add internal-face break-glass world backup endpoint (§B4 Sync)
Add POST /api/v1/internal/servers/{name}/backup so the on-node break-glass
console can snapshot a stopped world while felis-api is alive. It goes through
the API (not direct-to-CRD like halt) because rendering the backup Job needs
deployment coordinates (FELIS_IMAGE, FELIS_BACKUP_PVC) only felis-api holds.
Service-token auth (no Principal); the middleware IS the authorization, since
the operator already has root on the node. Refactor the RWO stopped-gate,
optional-Backuper 503, async hand-off and audit+202 into a shared enqueueBackup
tail so the external (owner/admin) and internal (break-glass) faces cannot
diverge on the security-critical stopped-gate. The internal audit is attributed
to break-glass/internal so a console-initiated backup is distinguishable from an
owner self-service one.
This commit is contained in:
4 files changed
+170
-1
No files matched your search
@@ -1017,6 +1017,54 @@ paths:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Error' }
|
||||
|
||||
/api/v1/internal/servers/{name}/backup:
|
||||
post:
|
||||
tags: [account-internal]
|
||||
operationId: internalBackupNow
|
||||
summary: Break-glass on-demand world backup (service token; server must be stopped).
|
||||
description: >-
|
||||
The break-glass console (root on the node, holding the service token) POSTs
|
||||
here to snapshot a stopped world while the API is alive — it goes through the
|
||||
API rather than direct-to-CRD because rendering the backup Job needs
|
||||
deployment coordinates only felis-api holds. Same RWO stopped-gate and async
|
||||
202 as the external backupNow; there is no Principal (trusted machine caller),
|
||||
and the action is audited to "break-glass".
|
||||
x-felis-face: [internal]
|
||||
x-felis-tier: service
|
||||
security: [{ serviceToken: [] }]
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'202':
|
||||
description: Backup started.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [name, status]
|
||||
properties:
|
||||
name: { type: string }
|
||||
status: { type: string, const: backing_up }
|
||||
'400':
|
||||
description: Invalid server name (bad_name).
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Error' }
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
description: Unknown server.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Error' }
|
||||
'409':
|
||||
description: Server is not stopped (its world PVC is still mounted).
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Error' }
|
||||
'503':
|
||||
$ref: '#/components/responses/ServiceUnavailable'
|
||||
|
||||
# ----------------------------------------------------- external: servers ---
|
||||
/api/v1/servers/{name}/wake:
|
||||
post:
|
||||
|
||||
Reference in new issue
Block a user