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
+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 });
});
});