diff --git a/src/mgr/kubernetes-runner-job.ts b/src/mgr/kubernetes-runner-job.ts index b6f4e6c..1882374 100644 --- a/src/mgr/kubernetes-runner-job.ts +++ b/src/mgr/kubernetes-runner-job.ts @@ -48,6 +48,7 @@ export interface RunnerJobDefaults { envIdentity?: string; artifactCatalogFile?: string; serviceAccountName?: string; + nodeSelector?: Record; jobNamePrefix?: string; lane?: string; runnerIdleTimeoutMs?: number; @@ -142,6 +143,7 @@ async function createKubernetesRunnerJobUnderFence(options: { store: AgentRunSto sourceCommit, envImage, serviceAccountName: serviceAccountName ?? null, + nodeSelector: options.defaults.nodeSelector ?? null, attemptId: optionalString(options.input.attemptId) ?? null, runnerId: optionalString(options.input.runnerId) ?? null, runnerJobId: runnerJobId ?? null, @@ -242,6 +244,7 @@ async function createKubernetesRunnerJobUnderFence(options: { store: AgentRunSto ...(backendRetryInitialBackoffMs !== undefined ? { backendRetryInitialBackoffMs } : {}), ...(backendRetryMaxBackoffMs !== undefined ? { backendRetryMaxBackoffMs } : {}), ...(serviceAccountName ? { serviceAccountName } : {}), + ...(options.defaults.nodeSelector ? { nodeSelector: options.defaults.nodeSelector } : {}), ...(jobNamePrefix ? { jobNamePrefix } : {}), ...(lane ? { lane } : {}), ...(sessionPvc ? { sessionPvc } : {}), @@ -307,6 +310,7 @@ async function createKubernetesRunnerJobUnderFence(options: { store: AgentRunSto image, sourceCommit, placement: "kubernetes-job", + nodeSelector: options.defaults.nodeSelector ?? null, logPath: `kubectl -n ${render.namespace} logs job/${render.jobName}`, }, envImage, diff --git a/src/mgr/server.ts b/src/mgr/server.ts index f70bbe7..909a49b 100644 --- a/src/mgr/server.ts +++ b/src/mgr/server.ts @@ -62,6 +62,11 @@ function runnerJobDefaultsForRequest(defaults: ManagerServerOptions["runnerJobDe ...optionalStringRecord("envIdentity", defaults?.envIdentity ?? process.env.AGENTRUN_ENV_IDENTITY), ...optionalStringRecord("artifactCatalogFile", defaults?.artifactCatalogFile ?? process.env.AGENTRUN_ARTIFACT_CATALOG_FILE), serviceAccountName, + ...(defaults?.nodeSelector + ? { nodeSelector: defaults.nodeSelector } + : process.env.AGENTRUN_RUNNER_NODE_SELECTOR_JSON + ? { nodeSelector: jsonRecordEnv("AGENTRUN_RUNNER_NODE_SELECTOR_JSON", process.env.AGENTRUN_RUNNER_NODE_SELECTOR_JSON) } + : {}), jobNamePrefix, lane, ...(defaults?.runnerIdleTimeoutMs !== undefined ? { runnerIdleTimeoutMs: defaults.runnerIdleTimeoutMs } : optionalPositiveIntegerRecord("runnerIdleTimeoutMs", process.env.AGENTRUN_RUNNER_IDLE_TIMEOUT_MS)), diff --git a/src/runner/k8s-job.ts b/src/runner/k8s-job.ts index 9398720..594c7ca 100644 --- a/src/runner/k8s-job.ts +++ b/src/runner/k8s-job.ts @@ -53,6 +53,7 @@ export interface RunnerJobRenderOptions { runnerId?: string; sourceCommit?: string; serviceAccountName?: string; + nodeSelector?: Record; jobNamePrefix?: string; lane?: string; imagePullPolicy?: string; @@ -231,6 +232,7 @@ export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { mani }, spec: { serviceAccountName, + ...(options.nodeSelector && Object.keys(options.nodeSelector).length > 0 ? { nodeSelector: options.nodeSelector } : {}), automountServiceAccountToken: false, restartPolicy: "Never", containers: [ diff --git a/src/selftest/cases/20-runner-k8s-job.ts b/src/selftest/cases/20-runner-k8s-job.ts index 2a4002c..cfa0fc0 100644 --- a/src/selftest/cases/20-runner-k8s-job.ts +++ b/src/selftest/cases/20-runner-k8s-job.ts @@ -45,12 +45,14 @@ const selfTest: SelfTestCase = async (context) => { image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111", attemptId: "attempt_selftest", sourceCommit: "self-test", + nodeSelector: { "unidesk.ai/node-id": "NC01" }, transientEnv: [{ name: "HWLAB_API_KEY", value: "hwl_live_selftest", sensitive: true }], }); assert.equal(rendered.dryRun, true); assert.equal(rendered.mutation, false); assert.equal(((rendered.retention as JsonRecord).ttlSecondsAfterFinished), 604_800); assert.equal((rendered.jobIdentity as { serviceAccountName?: string }).serviceAccountName, "agentrun-v01-runner"); + assert.deepEqual(((((rendered.manifest as JsonRecord).spec as JsonRecord).template as JsonRecord).spec as JsonRecord).nodeSelector, { "unidesk.ai/node-id": "NC01" }); assertWorkReadySummary(rendered.workReady as JsonRecord); assertRunnerJobUsesWritableCodexHome(rendered.manifest as JsonRecord, context.codexHome, "codex-0", "/var/run/agentrun/secrets/codex-0"); assertRunnerJobUsesToolCredential(rendered, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN");