mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
322 lines
8.6 KiB
JavaScript
322 lines
8.6 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createClusterControlAdmissionPipeline,
|
|
} = require('@qinglong/cluster-control/admission');
|
|
const {
|
|
createClusterControlRouteRegistry,
|
|
} = require('@qinglong/cluster-control/routes');
|
|
const {
|
|
CLUSTER_CONTROL_RUN_ATTEMPT_LOG_READ_ROUTE,
|
|
createClusterControlRunAttemptLogReadRoute,
|
|
} = require('../dist/run/runAttemptLogReadRoute.js');
|
|
|
|
const PRINCIPAL = Object.freeze({
|
|
subject: Object.freeze({ type: 'user', id: 'usr_viewer' }),
|
|
authenticationId: 'session:viewer',
|
|
authenticatedAtMs: 9_000,
|
|
expiresAtMs: 11_000,
|
|
assurance: 'single_factor',
|
|
});
|
|
|
|
function run(overrides = {}) {
|
|
return {
|
|
id: 'run_123',
|
|
projectId: 'prj_default',
|
|
taskId: 'task_1',
|
|
taskRevision: 'revision_1',
|
|
triggerType: 'task_start',
|
|
executionOrigin: 'manual',
|
|
executionOwner: 'runtime',
|
|
status: 'running',
|
|
version: 2,
|
|
eventSequence: 2,
|
|
priority: 0,
|
|
createdAtMs: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function attempt(overrides = {}) {
|
|
return {
|
|
id: 'attempt_123',
|
|
runId: 'run_123',
|
|
attempt: 1,
|
|
status: 'running',
|
|
executorType: 'remote_worker',
|
|
logArtifactId: `wlog-${'a'.repeat(30)}`,
|
|
callbackSequence: 0,
|
|
createdAtMs: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function metadata(query = {}) {
|
|
return Object.freeze({
|
|
requestId: 'request-log-read',
|
|
method: 'GET',
|
|
path: '/api/v3/projects/prj_default/runs/run_123/attempts/attempt_123/log',
|
|
query: Object.freeze(query),
|
|
headers: Object.freeze({ authorization: 'Bearer opaque' }),
|
|
signal: new AbortController().signal,
|
|
});
|
|
}
|
|
|
|
function pipeline(options = {}) {
|
|
const events = options.events ?? [];
|
|
const repository = options.repository ?? {
|
|
async findRunById() {
|
|
events.push('run');
|
|
return run();
|
|
},
|
|
async findAttemptById() {
|
|
events.push('attempt');
|
|
return attempt();
|
|
},
|
|
};
|
|
const reader = options.reader ?? {
|
|
async read(identity, range) {
|
|
events.push(`storage:${range.offset}:${range.length}`);
|
|
return {
|
|
status: 'available',
|
|
content: Buffer.from('cluster-log'),
|
|
start: range.offset,
|
|
endExclusive: range.offset + 11,
|
|
totalBytes: range.offset + 20,
|
|
nextOffset: range.offset + 11,
|
|
truncation: { truncated: false },
|
|
};
|
|
},
|
|
};
|
|
return createClusterControlAdmissionPipeline({
|
|
routes: createClusterControlRouteRegistry([
|
|
createClusterControlRunAttemptLogReadRoute(repository, reader),
|
|
]),
|
|
authenticator: {
|
|
authenticate() {
|
|
events.push('authenticate');
|
|
return PRINCIPAL;
|
|
},
|
|
},
|
|
policy: {
|
|
authorize(request) {
|
|
events.push(`authorize:${request.permission}`);
|
|
return options.effect
|
|
? { effect: options.effect, reasons: ['masked'], fence: null }
|
|
: {
|
|
effect: 'allow',
|
|
reasons: ['role_grant'],
|
|
fence: { projectVersion: 2, bindingVersion: 3 },
|
|
};
|
|
},
|
|
},
|
|
audit: {
|
|
record(record) {
|
|
events.push(`audit:${record.outcome}`);
|
|
},
|
|
},
|
|
now: () => 10_000,
|
|
});
|
|
}
|
|
|
|
async function invoke(value, query = {}) {
|
|
const prepared = await value.prepare(metadata(query));
|
|
return prepared.handle(null);
|
|
}
|
|
|
|
test('publishes the immutable Artifact-scoped route contract', () => {
|
|
assert.deepEqual(CLUSTER_CONTROL_RUN_ATTEMPT_LOG_READ_ROUTE, {
|
|
method: 'GET',
|
|
path: '/api/v3/projects/{projectId}/runs/{runId}/attempts/{attemptId}/log',
|
|
operationId: 'run.log.read',
|
|
permission: 'artifact.read',
|
|
projectParameter: 'projectId',
|
|
allowedQuery: ['length', 'offset'],
|
|
});
|
|
});
|
|
|
|
test('authorizes and audits before metadata and one bounded range read', async () => {
|
|
const events = [];
|
|
const result = await invoke(pipeline({ events }), {
|
|
offset: ['4'],
|
|
length: ['16'],
|
|
});
|
|
assert.equal(result.statusCode, 200);
|
|
assert.equal(result.body.schema, 'qinglong/run-attempt-log-read-result@v1');
|
|
assert.equal(
|
|
Buffer.from(result.body.content, 'base64').toString(),
|
|
'cluster-log',
|
|
);
|
|
assert.deepEqual(result.body.range, {
|
|
start: 4,
|
|
endExclusive: 15,
|
|
totalBytes: 24,
|
|
nextOffset: 15,
|
|
});
|
|
assert.deepEqual(events, [
|
|
'authenticate',
|
|
'authorize:artifact.read',
|
|
'audit:allowed',
|
|
'run',
|
|
'attempt',
|
|
'storage:4:16',
|
|
]);
|
|
});
|
|
|
|
test('uses the Cluster default window and rejects unbounded query values', async () => {
|
|
const events = [];
|
|
assert.equal((await invoke(pipeline({ events }))).statusCode, 200);
|
|
assert.equal(events.at(-1), `storage:0:${64 * 1024}`);
|
|
for (const query of [
|
|
{ offset: ['-1'] },
|
|
{ offset: ['04'] },
|
|
{ length: ['0'] },
|
|
{ length: [String(256 * 1024 + 1)] },
|
|
{ length: ['1', '2'] },
|
|
]) {
|
|
await assert.rejects(
|
|
pipeline().prepare(metadata(query)),
|
|
(error) =>
|
|
error.statusCode === 400 && error.code === 'invalid_route_query',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('masks deny and approval without reading Run or object storage', async () => {
|
|
for (const effect of ['deny', 'require_approval']) {
|
|
let touched = false;
|
|
await assert.rejects(
|
|
pipeline({
|
|
effect,
|
|
repository: {
|
|
async findRunById() {
|
|
touched = true;
|
|
return run();
|
|
},
|
|
async findAttemptById() {
|
|
touched = true;
|
|
return attempt();
|
|
},
|
|
},
|
|
reader: {
|
|
async read() {
|
|
touched = true;
|
|
return { status: 'missing' };
|
|
},
|
|
},
|
|
}).prepare(metadata()),
|
|
(error) =>
|
|
error.statusCode === 404 && error.code === 'artifact_not_found',
|
|
);
|
|
assert.equal(touched, false);
|
|
}
|
|
});
|
|
|
|
test('returns pending during upload and fails closed without an object reader', async () => {
|
|
const pending = pipeline({
|
|
reader: {
|
|
async read() {
|
|
return { status: 'missing' };
|
|
},
|
|
},
|
|
});
|
|
assert.equal((await invoke(pending)).statusCode, 202);
|
|
|
|
const unavailableRoute = createClusterControlRunAttemptLogReadRoute({
|
|
async findRunById() {
|
|
return run();
|
|
},
|
|
async findAttemptById() {
|
|
return attempt();
|
|
},
|
|
});
|
|
const prepared = await createClusterControlAdmissionPipeline({
|
|
routes: createClusterControlRouteRegistry([unavailableRoute]),
|
|
authenticator: { authenticate: () => PRINCIPAL },
|
|
policy: {
|
|
authorize: () => ({
|
|
effect: 'allow',
|
|
reasons: ['role_grant'],
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
}),
|
|
},
|
|
audit: { record() {} },
|
|
now: () => 10_000,
|
|
}).prepare(metadata());
|
|
assert.deepEqual(await prepared.handle(null), {
|
|
statusCode: 503,
|
|
body: { code: 'artifact_unavailable' },
|
|
});
|
|
});
|
|
|
|
test('maps an injected durable retention state to 410 without object access', async () => {
|
|
const {
|
|
createRunAttemptLogRetirementRecord,
|
|
} = require('@qinglong/runtime-core/run-attempt-log-retention');
|
|
let reads = 0;
|
|
const route = createClusterControlRunAttemptLogReadRoute(
|
|
{
|
|
async findRunById() {
|
|
return run({ status: 'succeeded', finishedAtMs: 10 });
|
|
},
|
|
async findAttemptById() {
|
|
return attempt({ status: 'succeeded', finishedAtMs: 10 });
|
|
},
|
|
},
|
|
{
|
|
async read() {
|
|
reads += 1;
|
|
return { status: 'missing' };
|
|
},
|
|
},
|
|
{
|
|
async inspect() {
|
|
return {
|
|
status: 'retired',
|
|
record: createRunAttemptLogRetirementRecord({
|
|
projectId: 'prj_default',
|
|
runId: 'run_123',
|
|
attemptId: 'attempt_123',
|
|
logArtifactId: `wlog-${'a'.repeat(30)}`,
|
|
executorType: 'remote_worker',
|
|
finishedAtMs: 10,
|
|
eligibleAtMs: 20,
|
|
retiredAtMs: 30,
|
|
disposition: 'deleted',
|
|
byteLength: 42,
|
|
truncation: { truncated: 'unknown' },
|
|
}),
|
|
};
|
|
},
|
|
},
|
|
);
|
|
const prepared = await createClusterControlAdmissionPipeline({
|
|
routes: createClusterControlRouteRegistry([route]),
|
|
authenticator: { authenticate: () => PRINCIPAL },
|
|
policy: {
|
|
authorize: () => ({
|
|
effect: 'allow',
|
|
reasons: ['role_grant'],
|
|
fence: { projectVersion: 1, bindingVersion: 1 },
|
|
}),
|
|
},
|
|
audit: { record() {} },
|
|
now: () => 10_000,
|
|
}).prepare(metadata());
|
|
assert.deepEqual(await prepared.handle(null), {
|
|
statusCode: 410,
|
|
body: {
|
|
schema: 'qinglong/run-attempt-log-read-result@v1',
|
|
status: 'retired',
|
|
projectId: 'prj_default',
|
|
runId: 'run_123',
|
|
attemptId: 'attempt_123',
|
|
retiredAtMs: 30,
|
|
byteLength: 42,
|
|
truncation: { truncated: 'unknown' },
|
|
},
|
|
});
|
|
assert.equal(reads, 0);
|
|
});
|