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
+115
View File
@@ -11,6 +11,13 @@ import {
} from "./build-kubernetes";
import { FilesystemCas } from "./cas";
import { KubernetesAuthStore } from "./kubernetes-store";
import {
MAINTENANCE_ROUTE_NAME,
MAINTENANCE_STATE_NAME,
MAINTENANCE_STATE_NAMESPACE,
MaintenanceService,
} from "./maintenance";
import { PatchStrategy, type KubernetesObject } from "@kubernetes/client-node";
import {
createKubernetesClients,
createKubernetesLeaseObjects,
@@ -112,6 +119,107 @@ const leases = new KubernetesWorkspaceLeaseProvider(
createKubernetesLeaseObjects(clients.coordination),
namespace,
);
const maintenance = new MaintenanceService(
{
async readState() {
try {
const value = (await clients.objects.read({
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: MAINTENANCE_STATE_NAME,
namespace: MAINTENANCE_STATE_NAMESPACE,
},
})) as { data?: Record<string, string> };
const hosts = JSON.parse(value.data?.hosts ?? "[]");
return Array.isArray(hosts) &&
hosts.every((host) => typeof host === "string")
? { hosts }
: { hosts: [] };
} catch (error) {
if (
error &&
typeof error === "object" &&
(("code" in error && error.code === 404) ||
("statusCode" in error && error.statusCode === 404))
)
return;
throw error;
}
},
async writeState({ hosts }) {
await clients.objects.patch(
{
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: MAINTENANCE_STATE_NAME,
namespace: MAINTENANCE_STATE_NAMESPACE,
labels: { "kuber.astrxl.dev/type": "maintenance" },
},
data: { hosts: JSON.stringify(hosts) },
} as KubernetesObject,
undefined,
undefined,
"kuber-server",
true,
PatchStrategy.ServerSideApply,
);
},
async routeExists() {
try {
await clients.objects.read({
apiVersion: "traefik.io/v1alpha1",
kind: "IngressRoute",
metadata: { name: MAINTENANCE_ROUTE_NAME, namespace: "routing" },
});
return true;
} catch (error) {
if (
error &&
typeof error === "object" &&
(("code" in error && error.code === 404) ||
("statusCode" in error && error.statusCode === 404))
)
return false;
throw error;
}
},
async apply(resource) {
await clients.objects.patch(
resource as KubernetesObject,
undefined,
undefined,
"kuber-server",
true,
PatchStrategy.ServerSideApply,
);
},
async deleteRoute() {
try {
await clients.objects.delete({
apiVersion: "traefik.io/v1alpha1",
kind: "IngressRoute",
metadata: { name: MAINTENANCE_ROUTE_NAME, namespace: "routing" },
});
} catch (error) {
if (
!(
error &&
typeof error === "object" &&
(("code" in error && error.code === 404) ||
("statusCode" in error && error.statusCode === 404))
)
)
throw error;
}
},
},
new KubernetesWorkspaceLeaseProvider(
createKubernetesLeaseObjects(clients.coordination),
MAINTENANCE_STATE_NAMESPACE,
),
);
const builds = new BuildController({
cas: new FilesystemCas(`${dataRoot}/cas`),
store: buildStore,
@@ -177,6 +285,12 @@ try {
console.error("Startup operation recovery failed", error);
}
try {
await maintenance.reconcile();
} catch (error) {
console.error("Startup maintenance reconciliation failed", error);
}
const sessionCleanupIntervalMs = Number(
process.env.KUBER_SESSION_CLEANUP_MS ?? 10 * 60 * 1000,
);
@@ -218,6 +332,7 @@ const app = createApp({
builds,
logs,
execService,
maintenance,
leases,
resolveImage: async (project, service) => {
const image = buildImageName(registry, project, service);