fix: protect runner cleanup artifacts
This commit is contained in:
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { startManagerServer } from "../../mgr/server.js";
|
||||
import { MemoryAgentRunStore } from "../../mgr/store.js";
|
||||
import { ManagerClient } from "../../mgr/client.js";
|
||||
import { enforceRunnerRetentionBeforeCreate } from "../../mgr/runner-retention.js";
|
||||
import { renderRunnerJobDryRun } from "../../runner/k8s-job.js";
|
||||
import type { JsonRecord, RunRecord } from "../../common/types.js";
|
||||
import { assertNoSecretLeak, createRunWithCommand, loadArtificerImageRef, type SelfTestCase } from "../harness.js";
|
||||
@@ -44,7 +45,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
});
|
||||
assert.equal(rendered.dryRun, true);
|
||||
assert.equal(rendered.mutation, false);
|
||||
assert.equal(((rendered.retention as JsonRecord).ttlSecondsAfterFinished), 86_400);
|
||||
assert.equal(((rendered.retention as JsonRecord).ttlSecondsAfterFinished), 604_800);
|
||||
assert.equal((rendered.jobIdentity as { serviceAccountName?: string }).serviceAccountName, "agentrun-v01-runner");
|
||||
assertWorkReadySummary(rendered.workReady as JsonRecord);
|
||||
assertRunnerJobUsesWritableCodexHome(rendered.manifest as JsonRecord, context.codexHome, "codex-0", "/var/run/agentrun/secrets/codex-0");
|
||||
@@ -214,7 +215,7 @@ process.exit(1);
|
||||
assert.equal((((created as JsonRecord).envImage as JsonRecord).digestPinned), true);
|
||||
assert.equal(((((created as JsonRecord).envImage as JsonRecord).imageRef as JsonRecord).kind), "env-image-dockerfile");
|
||||
assertWorkReadySummary((created as JsonRecord).workReady as JsonRecord);
|
||||
assert.equal(((created as JsonRecord).retention as JsonRecord).ttlSecondsAfterFinished, 86_400);
|
||||
assert.equal(((created as JsonRecord).retention as JsonRecord).ttlSecondsAfterFinished, 604_800);
|
||||
assert.deepEqual((((created as JsonRecord).transientEnv as JsonRecord).names) as string[], ["HWLAB_API_KEY", "HWLAB_RUNTIME_API_URL", "HWLAB_RUNTIME_WEB_URL", "HWLAB_RUNTIME_NAMESPACE", "HWLAB_RUNTIME_LANE", "HWLAB_RUNTIME_ENDPOINT_SOURCE", "HWLAB_RUNTIME_ENDPOINT_LOCKED", "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME", "UNIDESK_MAIN_SERVER_IP"]);
|
||||
const transientEnvSecret = (created as JsonRecord).transientEnvSecret as JsonRecord;
|
||||
assert.match(String(transientEnvSecret.name), /^agentrun-v01-runner-env-[a-f0-9]{20}$/u);
|
||||
@@ -229,7 +230,7 @@ process.exit(1);
|
||||
const ownerPatch = JSON.parse(await readFile(patchedTransientEnvSecret, "utf8")) as JsonRecord;
|
||||
assert.deepEqual((ownerPatch.args as string[]).slice(0, 3), ["patch", "secret", String(transientEnvSecret.name)]);
|
||||
const manifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord;
|
||||
assert.equal((manifest.spec as JsonRecord).ttlSecondsAfterFinished, 86_400);
|
||||
assert.equal((manifest.spec as JsonRecord).ttlSecondsAfterFinished, 604_800);
|
||||
assertRunnerJobUsesG14EgressProxy(manifest);
|
||||
assertRunnerJobUsesBoundedGitTransport({ manifest, gitTransport: (created as JsonRecord).gitTransport } as JsonRecord);
|
||||
assertRunnerJobUsesTransientEnvSecret(manifest, "HWLAB_API_KEY", String(transientEnvSecret.name));
|
||||
@@ -377,6 +378,63 @@ process.exit(1);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => serverWithRetention.server.close(() => resolve()));
|
||||
}
|
||||
const protectedStore = new MemoryAgentRunStore();
|
||||
const protectedServer = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store: protectedStore });
|
||||
const protectedKubectl = path.join(context.tmp, "fake-kubectl-retention-protected.js");
|
||||
const protectedDeleted = path.join(context.tmp, "retention-protected-deleted.json");
|
||||
await writeFile(protectedDeleted, "[]\n");
|
||||
try {
|
||||
const protectedClient = new ManagerClient(protectedServer.baseUrl);
|
||||
const protectedItem = await createRunWithCommand(protectedClient, context, "protected active runner", "selftest-runner-retention-protected", 15_000);
|
||||
protectedStore.claimRun(protectedItem.runId, "runner-protected", -60_000);
|
||||
await writeFile(protectedKubectl, `#!/usr/bin/env bun
|
||||
const args = Bun.argv.slice(2);
|
||||
const deletedPath = ${JSON.stringify(protectedDeleted)};
|
||||
async function readDeleted() { try { return JSON.parse(await Bun.file(deletedPath).text()); } catch { return []; } }
|
||||
async function writeDeleted(items) { await Bun.write(deletedPath, JSON.stringify(items, null, 2) + "\\n"); }
|
||||
if (args[0] === "get" && args[1] === "jobs") {
|
||||
console.log(JSON.stringify({ apiVersion: "v1", kind: "List", items: [{
|
||||
apiVersion: "batch/v1",
|
||||
kind: "Job",
|
||||
metadata: {
|
||||
name: "agentrun-v02-runner-protected",
|
||||
namespace: "agentrun-v02",
|
||||
creationTimestamp: "2026-01-01T00:00:00Z",
|
||||
labels: { "app.kubernetes.io/part-of": "agentrun", "app.kubernetes.io/name": "agentrun-runner", "app.kubernetes.io/component": "runner", "agentrun.pikastech.local/lane": "v0.2", "job-name": "agentrun-v02-runner-protected" },
|
||||
annotations: { "agentrun.pikastech.local/run-id": ${JSON.stringify(protectedItem.runId)}, "agentrun.pikastech.local/command-id": ${JSON.stringify(protectedItem.commandId)}, "agentrun.pikastech.local/runner-id": "runner-protected" },
|
||||
},
|
||||
status: {},
|
||||
}] }));
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === "get" && args[1] === "pods") { console.log(JSON.stringify({ apiVersion: "v1", kind: "List", items: [] })); process.exit(0); }
|
||||
if (args[0] === "delete") { const deleted = await readDeleted(); deleted.push({ resource: args[1], name: args[2], args }); await writeDeleted(deleted); console.log(String(args[1]) + "/" + String(args[2]) + " deleted"); process.exit(0); }
|
||||
console.error("unsupported fake protected kubectl args: " + args.join(" "));
|
||||
process.exit(1);
|
||||
`, "utf8");
|
||||
await chmod(protectedKubectl, 0o755);
|
||||
await assert.rejects(
|
||||
() => enforceRunnerRetentionBeforeCreate({
|
||||
store: protectedStore,
|
||||
incomingRunnerCount: 1,
|
||||
options: {
|
||||
namespace: "agentrun-v02",
|
||||
maxRunners: 1,
|
||||
cleanupOrder: "oldest-inactive-last-active-first",
|
||||
activeHeartbeatMaxAgeMs: 0,
|
||||
matchLabels: { "app.kubernetes.io/part-of": "agentrun", "app.kubernetes.io/name": "agentrun-runner", "app.kubernetes.io/component": "runner" },
|
||||
jobNamePrefixes: ["agentrun-v02-runner"],
|
||||
ageBasedCleanup: { enabled: false, maxAgeHours: null },
|
||||
kubectlCommand: protectedKubectl,
|
||||
},
|
||||
}),
|
||||
(error) => error instanceof Error && error.message.includes("runner retention cannot safely make room"),
|
||||
);
|
||||
const protectedDeletedItems = JSON.parse(await readFile(protectedDeleted, "utf8")) as JsonRecord[];
|
||||
assert.equal(protectedDeletedItems.length, 0);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => protectedServer.server.close(() => resolve()));
|
||||
}
|
||||
const sessionRunRecord: RunRecord = {
|
||||
id: "run-selftest-session-pvc",
|
||||
tenantId: "unidesk",
|
||||
@@ -419,7 +477,7 @@ process.exit(1);
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01");
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/sessions");
|
||||
assert.equal(envMap.get("AGENTRUN_CODEX_ROLLOUT_SUBDIR"), "sessions");
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-codex-shell-sandbox-env", "runner-k8s-job-g14-egress-proxy-env", "runner-k8s-job-bounded-git-transport-env", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-transient-env-secretref", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-tool-credential-volume", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-job-pre-create-retention", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-codex-shell-sandbox-env", "runner-k8s-job-g14-egress-proxy-env", "runner-k8s-job-bounded-git-transport-env", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-transient-env-secretref", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-tool-credential-volume", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-job-pre-create-retention", "runner-job-pre-create-retention-protects-stale-active", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user