mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): isolate plugin secret action execution
This commit is contained in:
@@ -74,6 +74,10 @@ export interface ApprovedActionDispatchBatchSummary {
|
||||
readonly nextCursor?: Readonly<ApprovedActionExecutionCursor>;
|
||||
}
|
||||
|
||||
export interface ApprovedActionDispatchByIdOptions {
|
||||
readonly dispatchId: string;
|
||||
}
|
||||
|
||||
interface MutableSummary {
|
||||
scanned: number;
|
||||
claimed: number;
|
||||
@@ -262,6 +266,54 @@ export class ApprovedActionDispatcher {
|
||||
return Object.freeze({ ...summary });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute one durable dispatch without scanning the shared due queue.
|
||||
*
|
||||
* This is the entry point for an action-scoped executor (for example a
|
||||
* Kubernetes Job with an exact Secret projection). The handler check happens
|
||||
* before the claim so a narrowly configured executor cannot lease and block
|
||||
* an action outside its authority.
|
||||
*/
|
||||
async dispatchById(
|
||||
options: Readonly<ApprovedActionDispatchByIdOptions>,
|
||||
): Promise<Readonly<ApprovedActionDispatchBatchSummary>> {
|
||||
if (
|
||||
!options ||
|
||||
typeof options !== 'object' ||
|
||||
Array.isArray(options) ||
|
||||
!exactKeys(options, ['dispatchId'])
|
||||
) {
|
||||
throw new TypeError('Approved Action exact dispatch is invalid');
|
||||
}
|
||||
const dispatchId = identifier(options.dispatchId, 'dispatch id');
|
||||
const summary: MutableSummary = {
|
||||
scanned: 0,
|
||||
claimed: 0,
|
||||
started: 0,
|
||||
succeeded: 0,
|
||||
failed: 0,
|
||||
blocked: 0,
|
||||
retrying: 0,
|
||||
deferred: 0,
|
||||
recoveryRequired: 0,
|
||||
alreadyTerminal: 0,
|
||||
unavailable: 0,
|
||||
truncated: false,
|
||||
};
|
||||
const snapshot = await this.#find(dispatchId);
|
||||
if (!snapshot) {
|
||||
summary.unavailable = 1;
|
||||
return Object.freeze({ ...summary });
|
||||
}
|
||||
summary.scanned = 1;
|
||||
if (!this.#handlers.has(snapshot.dispatch.action.actionType)) {
|
||||
summary.unavailable = 1;
|
||||
return Object.freeze({ ...summary });
|
||||
}
|
||||
await this.#dispatchOne(dispatchId, summary);
|
||||
return Object.freeze({ ...summary });
|
||||
}
|
||||
|
||||
async #dispatchOne(
|
||||
dispatchId: string,
|
||||
summary: MutableSummary,
|
||||
|
||||
@@ -302,3 +302,43 @@ test('does not claim an action without a matching handler', async () => {
|
||||
assert.equal(repository.execution.status, 'pending');
|
||||
assert.equal(repository.startCalls, 0);
|
||||
});
|
||||
|
||||
test('dispatches only the requested durable action without a queue scan', async () => {
|
||||
const repository = new InMemoryExecutionRepository(dispatch());
|
||||
repository.listDueExecutions = async () => {
|
||||
throw new Error('exact dispatch must not scan');
|
||||
};
|
||||
const summary = await createDispatcher(repository, {
|
||||
actionType: 'plugin_package.install',
|
||||
async inspect(value) {
|
||||
return { status: 'ready', actionDigest: value.action.actionDigest };
|
||||
},
|
||||
async execute() {
|
||||
return {
|
||||
outcome: 'succeeded',
|
||||
resultCode: 'package_admitted',
|
||||
resultDigest: RESULT_DIGEST,
|
||||
};
|
||||
},
|
||||
}).dispatchById({ dispatchId: 'dispatch-dispatcher-v1' });
|
||||
assert.equal(summary.scanned, 1);
|
||||
assert.equal(summary.claimed, 1);
|
||||
assert.equal(summary.succeeded, 1);
|
||||
assert.equal(summary.truncated, false);
|
||||
});
|
||||
|
||||
test('exact dispatch does not claim an action outside configured authority', async () => {
|
||||
const repository = new InMemoryExecutionRepository(dispatch());
|
||||
const dispatcher = new ApprovedActionDispatcher(repository, [], {
|
||||
owner: 'dispatcher_instance_1',
|
||||
clock: () => 100,
|
||||
createId: () => 'dispatcher-exact-id',
|
||||
});
|
||||
const summary = await dispatcher.dispatchById({
|
||||
dispatchId: 'dispatch-dispatcher-v1',
|
||||
});
|
||||
assert.equal(summary.scanned, 1);
|
||||
assert.equal(summary.claimed, 0);
|
||||
assert.equal(summary.unavailable, 1);
|
||||
assert.equal(repository.execution.status, 'pending');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user