feat(api): add admin API for the SysAdmin-set auto-update maintenance window

Two admin-tier routes read and set a single platform-wide maintenance
window for the auto-update subsystem (decision core internal/updates):

  GET /api/v1/updates/window
  PUT /api/v1/updates/window

The window is stored as JSON {"start","end"} (RFC3339, or null when
unset) under the platform_settings key "update_window", reusing the
existing GetSetting/SetSetting KV seam -- no new Repo method, no
migration. Pointer times keep "unset" (null) distinct from a real
instant on both decode and encode; a never-set and an explicitly
cleared window both read back as {null,null}.

Validation mirrors the core's fail-closed Window: a window is either
fully set (both ends, end strictly after start) or fully cleared (both
null). A half-set, inverted, or empty-interval body is 400 and is never
persisted. Reads treat only a missing key as unset (ErrNotFound -> 200
nulls); any other store error 500s rather than fail open.

This is API + PERSISTENCE ONLY. Nothing consumes the stored window yet
-- the runner, the ReleaseSource/Notifier/Applier executors, and the
scheduler CronJob remain INTEGRATION-ONLY. Setting a window changes no
behavior until those land; it is the durable input they will read.
Nothing here force-updates ("不要强制自动更新").
This commit is contained in:
flyemoji committed 2026-07-01 18:17:07 +09:00
1 parent fe2ece08cc
commit 3673af63c2
4 files changed
+392

No files matched your search

+81
View File
@@ -167,6 +167,26 @@ components:
type: string
description: Correlates the response with server logs (withRequestID middleware).
UpdateWindow:
type: object
description: >
The SysAdmin-set auto-update maintenance window (internal/api/handlers_updates.go
updateWindow). An absolute [start,end) interval during which Felis may apply a
Scheduled component's update to itself; both ends null means unset (no apply is
ever opened). Keys are always present; their values are null when unset.
required: [start, end]
properties:
start:
type: string
format: date-time
nullable: true
description: Window start (RFC3339, inclusive), or null when unset.
end:
type: string
format: date-time
nullable: true
description: Window end (RFC3339, exclusive), or null when unset.
PasskeyCredential:
type: object
description: >
@@ -1464,6 +1484,67 @@ paths:
'401':
$ref: '#/components/responses/Unauthorized'
/api/v1/updates/window:
get:
tags: [admin-updates]
operationId: getUpdateWindow
summary: Read the SysAdmin-set auto-update maintenance window (admin).
description: >-
The single platform-wide maintenance window during which Felis may apply a
Scheduled component's update to itself (decision core internal/updates). An
unset window — never set, or explicitly cleared — reads back as
{start:null,end:null}. API+persistence only: nothing consumes the window
until the INTEGRATION runner and executors are wired, so setting it changes
no behavior yet.
x-felis-face: [external]
x-felis-tier: admin
security: [{ accessJWT: [] }]
responses:
'200':
description: The current maintenance window (both ends null when unset).
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateWindow'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
put:
tags: [admin-updates]
operationId: setUpdateWindow
summary: Set or clear the SysAdmin auto-update maintenance window (admin).
description: >-
Persist the maintenance window as an absolute [start,end) interval. Both
ends must be set with end strictly after start, or both null to clear the
window to unset. A half-set (exactly one end) or inverted/empty (end not
after start) body is rejected 400, mirroring the decision core's fail-closed
Window so a malformed schedule can never be stored. No forced auto-update:
setting a window only permits an apply inside it; outside, a Scheduled
component degrades to notify.
x-felis-face: [external]
x-felis-tier: admin
security: [{ accessJWT: [] }]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateWindow'
responses:
'200':
description: The stored maintenance window (echoed back).
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateWindow'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
/api/v1/fleet:
get:
tags: [admin-servers]