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
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, spyOn, test } from "bun:test";
import { runMaintenance } from "../../command/maintenance";
import type { ApiRequestInit } from "../../lib/api";
describe("maintenance command", () => {
test("first call reads status and asks for confirmation without mutation", async () => {
const calls: string[] = [];
const output = spyOn(console, "log").mockImplementation(() => {});
try {
await runMaintenance(
"a.example.com",
async <T>(path: string) => {
calls.push(path);
return { host: "a.example.com", enabled: false, hosts: [] } as T;
},
async () => false,
);
expect(calls).toEqual(["/maintenance/a.example.com"]);
expect(output).toHaveBeenCalledWith(
expect.stringContaining("currently \x1b[32mOFF\x1b[0m"),
);
} finally {
output.mockRestore();
}
});
test("confirmed call posts an idempotent desired state and prints result", async () => {
const calls: Array<{
path: string;
init?: { method?: string; json?: unknown };
}> = [];
const output = spyOn(console, "log").mockImplementation(() => {});
try {
await runMaintenance(
"a.example.com",
async <T>(path: string, init?: ApiRequestInit) => {
calls.push({ path, init });
return (
calls.length === 1
? { host: "a.example.com", enabled: false, hosts: [] }
: {
host: "a.example.com",
enabled: true,
hosts: ["a.example.com"],
}
) as T;
},
async () => true,
);
expect(calls).toEqual([
{ path: "/maintenance/a.example.com", init: undefined },
{
path: "/maintenance/a.example.com",
init: { method: "POST", json: { enabled: true } },
},
]);
expect(output).toHaveBeenCalledWith(
"a.example.com maintenance is now \x1b[31mON\x1b[0m",
);
} finally {
output.mockRestore();
}
});
});
+223
View File
@@ -0,0 +1,223 @@
import { describe, expect, test } from "bun:test";
import {
MaintenanceService,
maintenanceMiddleware,
maintenanceRoute,
normalizeMaintenanceHost,
type MaintenancePersistence,
} from "../../server/maintenance";
import { createApp } from "../../server/app";
import { hashToken, MemoryAuthStore } from "../../server/auth";
class MemoryPersistence implements MaintenancePersistence {
state: { hosts: string[] } | undefined;
resources: Record<string, unknown>[] = [];
deleted = 0;
routePresent = false;
failApply = false;
failWrite = false;
async readState() {
return this.state;
}
async writeState(value: { hosts: string[] }) {
if (this.failWrite) throw new Error("state write failed");
this.state = value;
}
async routeExists() {
return this.routePresent;
}
async apply(resource: Record<string, unknown>) {
if (this.failApply) throw new Error("route apply failed");
this.resources.push(resource);
if (resource.kind === "IngressRoute") this.routePresent = true;
}
async deleteRoute() {
this.deleted += 1;
this.routePresent = false;
}
}
const lease = { acquire: async () => ({ release: async () => {} }) };
describe("maintenance override", () => {
test("normalizes only DNS hostnames", () => {
expect(normalizeMaintenanceHost(" Sub.Domain.COM. ")).toBe(
"sub.domain.com",
);
for (const host of [
"http://example.com",
"example.com:443",
"127.0.0.1",
"[::1]",
"*.example.com",
"example",
"a..com",
])
expect(() => normalizeMaintenanceHost(host)).toThrow();
});
test("enables, deduplicates, and disables the last host", async () => {
const persistence = new MemoryPersistence();
const service = new MaintenanceService(persistence, lease);
expect((await service.status("a.example.com")).enabled).toBe(false);
expect((await service.toggle("A.example.com")).hosts).toEqual([
"a.example.com",
]);
persistence.state = {
hosts: ["a.example.com", "b.example.com", "A.example.com"],
};
expect((await service.toggle("a.example.com")).hosts).toEqual([
"b.example.com",
]);
expect((await service.toggle("b.example.com")).hosts).toEqual([]);
expect(persistence.deleted).toBe(2);
expect(persistence.state).toEqual({ hosts: [] });
});
test("renders the single shared error route and rewrite middleware", () => {
expect(maintenanceMiddleware()).toMatchObject({
metadata: { name: "maintenance-override", namespace: "routing" },
spec: {
replacePathRegex: { regex: "^/.*", replacement: "/__error/1001" },
},
});
expect(maintenanceRoute(["a.example.com", "b.example.com"])).toMatchObject({
metadata: { name: "maintenance-override", namespace: "routing" },
spec: {
routes: [
{
match: "Host(`a.example.com`) || Host(`b.example.com`)",
priority: 1_000_000,
services: [
{
name: "error-page",
namespace: "routing",
port: 3000,
scheme: "http",
},
],
},
],
},
});
});
test("maps an unavailable global lease to a retryable API conflict", async () => {
const persistence = new MemoryPersistence();
const store = new MemoryAuthStore();
await store.putUser({ username: "operator", passwordHash: "hash", roles: ["operator"] });
await store.putSession({ tokenHash: hashToken("operator-token"), username: "operator", roles: ["operator"], expiresAt: "2099-01-01T00:00:00.000Z" });
const app = createApp({
store,
maintenance: new MaintenanceService(persistence, { acquire: async () => undefined }),
});
const result = await app(new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
method: "POST",
headers: { authorization: "Bearer operator-token", "content-type": "application/json" },
body: JSON.stringify({ enabled: true }),
}));
expect(result.status).toBe(409);
expect(result.headers.get("retry-after")).toBe("1");
expect(await result.json()).toMatchObject({ code: "MAINTENANCE_BUSY" });
expect(persistence.state).toBeUndefined();
});
test("recovers the persisted desired state after route or state failures", async () => {
const persistence = new MemoryPersistence();
const service = new MaintenanceService(persistence, lease);
persistence.failApply = true;
await expect(service.set("a.example.com", true)).rejects.toThrow("route apply failed");
expect(persistence.state).toBeUndefined();
persistence.failApply = false;
expect(await service.status("a.example.com")).toMatchObject({ enabled: false });
persistence.failWrite = true;
await expect(service.set("a.example.com", true)).rejects.toThrow("state write failed");
expect(persistence.routePresent).toBe(true);
persistence.failWrite = false;
expect(await service.status("a.example.com")).toMatchObject({ enabled: false });
expect(persistence.routePresent).toBe(false);
expect(await service.set("a.example.com", true)).toMatchObject({ enabled: true });
persistence.routePresent = false;
expect(await service.status("a.example.com")).toMatchObject({ enabled: true });
expect(persistence.routePresent).toBe(true);
});
test("requires kubernetes write without workspace or trust context", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "viewer",
passwordHash: "hash",
roles: ["viewer"],
});
await store.putUser({
username: "operator",
passwordHash: "hash",
roles: ["operator"],
});
for (const token of ["viewer-token", "operator-token"])
await store.putSession({
tokenHash: hashToken(token),
username: token.startsWith("viewer") ? "viewer" : "operator",
roles: token.startsWith("viewer") ? ["viewer"] : ["operator"],
expiresAt: "2099-01-01T00:00:00.000Z",
});
await store.createApiKey({
id: "maintenance-scoped-key",
tokenHash: hashToken("scoped-key"),
username: "operator",
capabilities: ["kubernetes:write"],
workspace: "shop",
expiresAt: "2099-01-01T00:00:00.000Z",
});
await store.createApiKey({
id: "maintenance-global-key",
tokenHash: hashToken("global-key"),
username: "operator",
capabilities: ["kubernetes:write"],
expiresAt: "2099-01-01T00:00:00.000Z",
});
const persistence = new MemoryPersistence();
const app = createApp({
store,
maintenance: new MaintenanceService(persistence, lease),
});
const viewer = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer viewer-token" },
}),
);
expect(viewer.status).toBe(403);
const scoped = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer scoped-key" },
}),
);
expect(scoped.status).toBe(403);
expect(await scoped.json()).toMatchObject({ code: "MAINTENANCE_GLOBAL_SCOPE_REQUIRED" });
const global = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer global-key" },
}),
);
expect(global.status).toBe(200);
const status = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer operator-token" },
}),
);
expect(await status.json()).toMatchObject({ enabled: false });
const toggle = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
method: "POST",
headers: {
authorization: "Bearer operator-token",
"content-type": "application/json",
},
body: JSON.stringify({ enabled: true }),
}),
);
expect(await toggle.json()).toMatchObject({ enabled: true });
});
});