fix: 锁定 runner reconcile YAML 边界
This commit is contained in:
+15
-2
@@ -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()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user