feat: add maintenance toggle command

This commit is contained in:
2026-09-18 15:21:28 +00:00 Unverified
parent e3df2f4d76
commit dcdacd5c04
12 changed files with 712 additions and 7 deletions
+66 -2
View File
@@ -72,6 +72,11 @@ import {
safeLog,
type ProcessLogEntry,
} from "../lib/request-log";
import {
MaintenanceBusyError,
normalizeMaintenanceHost,
type MaintenanceService,
} from "./maintenance";
const API_PREFIX = "/api/v2";
const RUNTIME_SESSION_MS = 24 * 60 * 60 * 1000;
@@ -103,6 +108,7 @@ export type AppOptions = {
builds?: BuildController;
logs?: LogService;
execService?: ExecService;
maintenance?: MaintenanceService;
resolveImage?: (
project: string,
service: string,
@@ -1792,6 +1798,57 @@ export function createApp(
return response(adopted);
}
match = new RegExp(`^${API_PREFIX}/maintenance/([^/]+)$`).exec(path);
if (match) {
const host = pathPart(match[1]!);
try {
normalizeMaintenanceHost(host);
} catch (error) {
throw new HttpError(
400,
"Invalid hostname",
"MAINTENANCE_HOST_INVALID",
error instanceof Error ? error.message : "Invalid hostname",
);
}
if (!options.maintenance)
throw new HttpError(
503,
"Service unavailable",
"MAINTENANCE_UNAVAILABLE",
"Maintenance service is not configured",
);
if (identity.apiKey?.workspace) {
await audit(identity, request, "authorization.workspace", "denied", {
workspace: identity.apiKey.workspace,
});
throw new HttpError(
403,
"Forbidden",
"MAINTENANCE_GLOBAL_SCOPE_REQUIRED",
"Maintenance requires an unscoped API key or an operator session",
);
}
if (request.method === "GET") {
await requireCapability(identity, request, "kubernetes:write");
return response(await options.maintenance.status(host));
}
if (request.method === "POST") {
await requireCapability(identity, request, "kubernetes:write");
const body = await readJson(request);
if (typeof body.enabled !== "boolean")
throw new HttpError(
400,
"Invalid request",
"MAINTENANCE_ENABLED_REQUIRED",
"enabled must be a boolean",
);
const result = await options.maintenance.set(host, body.enabled);
await audit(identity, request, "maintenance.toggle", "success", result);
return response(result);
}
}
match = new RegExp(`^${API_PREFIX}/workspaces/([^/]+)(?:/(.*))?$`).exec(
path,
);
@@ -2326,9 +2383,16 @@ export function createApp(
error instanceof WorkspaceConflictError ||
error instanceof OperationConflictError ||
error instanceof BuildConflictError ||
error instanceof WorkspaceAdoptionError
error instanceof WorkspaceAdoptionError ||
error instanceof MaintenanceBusyError
)
return new HttpError(409, "Conflict", error.code, error.message);
return new HttpError(
409,
"Conflict",
error.code,
error.message,
error instanceof MaintenanceBusyError ? { "retry-after": "1" } : undefined,
);
if (
error instanceof WorkspaceValidationError ||
error instanceof OperationValidationError ||