feat: add support for replica ranges and HorizontalPodAutoscaler integration

This commit is contained in:
2026-09-03 12:02:22 +07:00 Unverified
parent 4ce8ed3e40
commit 8e9d207915
10 changed files with 700 additions and 39 deletions
+115 -1
View File
@@ -20,7 +20,12 @@ function object(
},
): KubernetesObject {
return {
apiVersion: kind === "Deployment" ? "apps/v1" : "v1",
apiVersion:
kind === "Deployment"
? "apps/v1"
: kind === "HorizontalPodAutoscaler"
? "autoscaling/v2"
: "v1",
kind,
metadata: { name, namespace: workspace.project, uid, labels },
};
@@ -222,6 +227,115 @@ describe("server management service", () => {
);
});
test("stop deletes matching HPAs before scaling selected deployments", async () => {
const calls: string[] = [];
const service = createManagementService(
dependencies({
listDeployments: async () =>
["api", "worker", "batch"].map(
(name) =>
object("Deployment", name, `${name}-uid`) as V1Deployment,
),
listProjectResources: async () => [
object("HorizontalPodAutoscaler", "api", "api-hpa-uid", undefined),
object("HorizontalPodAutoscaler", "worker", "worker-hpa-uid", undefined),
object("HorizontalPodAutoscaler", "batch", "batch-hpa-uid", undefined),
],
scaleDeployment: async (_project, name, replicas) => {
calls.push(`scale:${name}:${replicas}`);
},
deleteResource: async (item) => {
calls.push(`delete:${item.kind}:${item.name}`);
},
}),
);
expect(await service.stop(workspace, ["api", "worker"])).toEqual([
"api",
"worker",
]);
expect(calls).toEqual([
"delete:HorizontalPodAutoscaler:api",
"delete:HorizontalPodAutoscaler:worker",
"scale:api:0",
"scale:worker:0",
]);
});
test("stop deletes HPAs for all deployments and leaves unrelated HPAs alone", async () => {
const calls: string[] = [];
const service = createManagementService(
dependencies({
listDeployments: async () =>
["api", "worker"].map(
(name) =>
object("Deployment", name, `${name}-uid`) as V1Deployment,
),
listProjectResources: async () => [
object("HorizontalPodAutoscaler", "api", "api-hpa-uid", undefined),
object("HorizontalPodAutoscaler", "worker", "worker-hpa-uid", undefined),
object("HorizontalPodAutoscaler", "legacy", "legacy-hpa-uid", undefined),
],
scaleDeployment: async (_project, name, replicas) => {
calls.push(`scale:${name}:${replicas}`);
},
deleteResource: async (item) => {
calls.push(`delete:${item.kind}:${item.name}`);
},
}),
);
expect(await service.stop(workspace)).toEqual(["api", "worker"]);
expect(calls).toEqual([
"delete:HorizontalPodAutoscaler:api",
"delete:HorizontalPodAutoscaler:worker",
"scale:api:0",
"scale:worker:0",
]);
});
test("refuses to delete an HPA not owned by the workspace", async () => {
const service = createManagementService(
dependencies({
listDeployments: async () =>
[object("Deployment", "api", "api-uid") as V1Deployment],
listProjectResources: async () => [
object("HorizontalPodAutoscaler", "api", "api-hpa-uid", {
"app.kubernetes.io/managed-by": "kuber",
[WORKSPACE_UID_LABEL]: "someone-else",
}),
],
}),
);
await expect(service.stop(workspace, ["api"])).rejects.toThrow(
"is not owned by workspace",
);
});
test("stop scales deployments to zero when there are no HPAs", async () => {
const calls: string[] = [];
const service = createManagementService(
dependencies({
listDeployments: async () =>
["api", "worker"].map(
(name) =>
object("Deployment", name, `${name}-uid`) as V1Deployment,
),
listProjectResources: async () => [
object("ConfigMap", "env", "env-uid"),
object("Service", "api", "api-svc-uid"),
],
scaleDeployment: async (_project, name, replicas) => {
calls.push(`scale:${name}:${replicas}`);
},
}),
);
expect(await service.stop(workspace)).toEqual(["api", "worker"]);
expect(calls).toEqual(["scale:api:0", "scale:worker:0"]);
});
test("protects reserved namespaces before consulting Kubernetes", async () => {
let read = false;
const service = createManagementService(