fix: 锁定 runner reconcile YAML 边界

This commit is contained in:
root
2026-07-11 04:51:35 +02:00
parent 5632dbc257
commit b2c0c194c2
2 changed files with 32 additions and 9 deletions
+15 -2
View File
@@ -908,8 +908,21 @@ async function route({ method, url, body, store, sourceCommit, authSummary, runn
}
if (method === "POST" && path === "/api/v1/reconciler/runner-jobs") {
const record = body === null ? {} : asRecord(body, "runnerReconciler");
const limit = Math.max(1, Math.min(numberField(record, "limit", runnerReconcilerOptions?.batchSize ?? 20), 500));
const namespace = optionalString(record.namespace) ?? runnerReconcilerOptions?.namespace ?? process.env.AGENTRUN_RUNTIME_NAMESPACE ?? "agentrun-v01";
const requestedLimit = Math.max(1, Math.min(numberField(record, "limit", runnerReconcilerOptions.batchSize), 500));
const limit = Math.min(requestedLimit, runnerReconcilerOptions.batchSize);
const requestedNamespace = optionalString(record.namespace);
if (requestedNamespace !== null && requestedNamespace !== runnerReconcilerOptions.namespace) {
throw new AgentRunError("tenant-policy-denied", "runner reconciler namespace is fixed by manager runtime configuration", {
httpStatus: 403,
details: {
reason: "runner-reconciler-namespace-override-denied",
configuredNamespace: runnerReconcilerOptions.namespace,
requestedNamespace,
valuesPrinted: false,
},
});
}
const namespace = runnerReconcilerOptions.namespace;
return await reconcileRunnerJobsOnce({
store,
namespace,
@@ -168,7 +168,7 @@ const selfTest: SelfTestCase = async (context) => {
"periodic-reconciler-protects-unsafe-runners",
"periodic-reconciler-cas-conflict-fails-closed",
"periodic-reconciler-namespace-fence-prevents-duplicate-terminalization",
"controlled-reconcile-api-exposes-bounded-retention-result",
"controlled-reconcile-api-enforces-yaml-batch-and-namespace",
"automatic-reconciler-reports-next-schedule",
],
};
@@ -609,15 +609,15 @@ async function assertConcurrentPeriodicReconcileIsSerialized(input: { fixturePat
}
async function assertControlledReconcileApiExposesRetention(input: { fixturePath: string; statePath: string; kubectlCommand: string }): Promise<void> {
const fact = pendingFact(350, new Date(Date.now() - 60 * 60_000).toISOString());
const facts = [350, 351].map((index) => pendingFact(index, new Date(Date.now() - 60 * 60_000).toISOString()));
await writeFixture(input, {
jobs: [runnerJob(fact, 350)],
pods: [runnerPod(fact, 350, "pending")],
events: [failedMountEvent(fact, 350)],
jobs: facts.map((fact, index) => runnerJob(fact, index + 350)),
pods: facts.map((fact, index) => runnerPod(fact, index + 350, "pending")),
events: facts.map((fact, index) => failedMountEvent(fact, index + 350)),
bumpJobResourceVersionAfterPodDelete: true,
bumpedJobResourceVersion: "2",
});
const store = new RetentionFactStore([fact]);
const store = new RetentionFactStore(facts);
const retention = retentionOptions(input.kubectlCommand, 20);
const server = await startManagerServer({
port: 0,
@@ -644,8 +644,10 @@ async function assertControlledReconcileApiExposesRetention(input: { fixturePath
},
});
try {
const summary = await new ManagerClient(server.baseUrl).post("/api/v1/reconciler/runner-jobs", { limit: 1, namespace: "agentrun-nc01-v02" }) as JsonRecord;
const client = new ManagerClient(server.baseUrl);
const summary = await client.post("/api/v1/reconciler/runner-jobs", { limit: 500 }) as JsonRecord;
const cleanup = summary.stalePendingRetention as JsonRecord;
assert.equal(summary.limit, 1);
assert.equal(cleanup.enabled, true);
assert.equal(cleanup.selectedRunnerCount, 1);
assert.equal(cleanup.terminalizedRunCount, 1);
@@ -654,6 +656,14 @@ async function assertControlledReconcileApiExposesRetention(input: { fixturePath
assert.equal(cleanup.valuesPrinted, false);
assert.equal(summary.nextReconcileAt, null);
assert.equal(JSON.stringify(summary).includes("abc123-secret-value"), false);
assert.equal(store.getRun((facts[1] as RetentionFact).run.id).status, "pending");
await assert.rejects(
() => client.post("/api/v1/reconciler/runner-jobs", { limit: 1, namespace: "another-runtime" }),
(error) => error instanceof AgentRunError
&& error.failureKind === "tenant-policy-denied"
&& ((((error.details?.error as JsonRecord | undefined)?.details) as JsonRecord | undefined)?.reason) === "runner-reconciler-namespace-override-denied",
);
assert.equal(store.getRun((facts[1] as RetentionFact).run.id).status, "pending");
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}