mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
243 lines
7.1 KiB
TypeScript
243 lines
7.1 KiB
TypeScript
import path from 'path';
|
|
import type {
|
|
ExecutionNumericLimit,
|
|
ExecutionResourcePolicy,
|
|
ExecutionSpec,
|
|
} from './execution';
|
|
import { InvalidExecutionSpecError } from './executorErrors';
|
|
|
|
export const MAX_EXECUTION_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000;
|
|
export const MAX_TERMINATION_GRACE_MS = 5 * 60 * 1000;
|
|
export const MAX_EXECUTION_ARGUMENTS = 4096;
|
|
export const MAX_EXECUTION_COMMAND_BYTES = 128 * 1024;
|
|
|
|
function invalid(message: string): never {
|
|
throw new InvalidExecutionSpecError(message);
|
|
}
|
|
|
|
function assertNonEmptyIdentifier(value: string, name: string): void {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 1 ||
|
|
value.length > 255 ||
|
|
/[\u0000-\u001f\u007f]/.test(value)
|
|
) {
|
|
invalid(
|
|
`${name} must be between 1 and 255 characters and contain no control characters`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertSafeDuration(
|
|
value: number,
|
|
name: string,
|
|
maximum: number,
|
|
allowZero: boolean,
|
|
): void {
|
|
if (
|
|
!Number.isSafeInteger(value) ||
|
|
value < (allowZero ? 0 : 1) ||
|
|
value > maximum
|
|
) {
|
|
invalid(
|
|
`${name} must be a safe integer between ${
|
|
allowZero ? 0 : 1
|
|
} and ${maximum}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertEnforcement(
|
|
value: unknown,
|
|
name: string,
|
|
): asserts value is 'required' | 'best_effort' {
|
|
if (value !== 'required' && value !== 'best_effort') {
|
|
invalid(`${name} enforcement is invalid`);
|
|
}
|
|
}
|
|
|
|
function assertNumericLimit(
|
|
value: ExecutionNumericLimit | undefined,
|
|
name: string,
|
|
): void {
|
|
if (value === undefined) return;
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
invalid(`${name} must be an object`);
|
|
}
|
|
if (!Number.isSafeInteger(value.value) || value.value < 1) {
|
|
invalid(`${name} must be a positive safe integer`);
|
|
}
|
|
assertEnforcement(value.enforcement, name);
|
|
}
|
|
|
|
function assertResourcePolicy(
|
|
policy: ExecutionResourcePolicy | undefined,
|
|
): void {
|
|
if (policy === undefined) return;
|
|
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
|
|
invalid('resourcePolicy must be an object');
|
|
}
|
|
assertNumericLimit(policy.memoryBytes, 'memoryBytes');
|
|
assertNumericLimit(policy.cpuMillisPerSecond, 'cpuMillisPerSecond');
|
|
if (policy.filesystemIsolation !== undefined) {
|
|
assertEnforcement(policy.filesystemIsolation, 'filesystemIsolation');
|
|
}
|
|
if (policy.networkIsolation !== undefined) {
|
|
assertEnforcement(policy.networkIsolation, 'networkIsolation');
|
|
}
|
|
}
|
|
|
|
function commandSize(spec: ExecutionSpec): number {
|
|
if (spec.command.kind === 'shell') {
|
|
return Buffer.byteLength(spec.command.command, 'utf8');
|
|
}
|
|
return (
|
|
Buffer.byteLength(spec.command.file, 'utf8') +
|
|
spec.command.args.reduce(
|
|
(total, argument) => total + Buffer.byteLength(argument, 'utf8'),
|
|
0,
|
|
)
|
|
);
|
|
}
|
|
|
|
export function assertExecutionSpec(spec: ExecutionSpec): void {
|
|
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
invalid('ExecutionSpec must be an object');
|
|
}
|
|
assertNonEmptyIdentifier(spec.runId, 'runId');
|
|
assertNonEmptyIdentifier(spec.attemptId, 'attemptId');
|
|
assertNonEmptyIdentifier(spec.projectId, 'projectId');
|
|
assertNonEmptyIdentifier(spec.taskId, 'taskId');
|
|
assertNonEmptyIdentifier(spec.taskRevision, 'taskRevision');
|
|
assertSafeDuration(
|
|
spec.terminationGraceMs,
|
|
'terminationGraceMs',
|
|
MAX_TERMINATION_GRACE_MS,
|
|
true,
|
|
);
|
|
if (spec.timeoutMs !== undefined) {
|
|
assertSafeDuration(
|
|
spec.timeoutMs,
|
|
'timeoutMs',
|
|
MAX_EXECUTION_TIMEOUT_MS,
|
|
false,
|
|
);
|
|
}
|
|
if (
|
|
spec.environmentPolicy !== 'inherit' &&
|
|
spec.environmentPolicy !== 'isolated'
|
|
) {
|
|
invalid('environmentPolicy is invalid');
|
|
}
|
|
if (
|
|
spec.workingDirectory !== undefined &&
|
|
(typeof spec.workingDirectory !== 'string' ||
|
|
spec.workingDirectory.includes('\0') ||
|
|
!path.isAbsolute(spec.workingDirectory))
|
|
) {
|
|
invalid('workingDirectory must be an absolute path containing no NUL');
|
|
}
|
|
if (!spec.command || typeof spec.command !== 'object') {
|
|
invalid('command must be an object');
|
|
}
|
|
if (spec.command.kind === 'argv') {
|
|
if (
|
|
typeof spec.command.file !== 'string' ||
|
|
!spec.command.file ||
|
|
spec.command.file.includes('\0')
|
|
) {
|
|
invalid('argv command file must be non-empty and contain no NUL');
|
|
}
|
|
if (!Array.isArray(spec.command.args)) {
|
|
invalid('argv command args must be an array');
|
|
}
|
|
if (spec.command.args.length > MAX_EXECUTION_ARGUMENTS) {
|
|
invalid('argv command has too many arguments');
|
|
}
|
|
if (
|
|
spec.command.args.some(
|
|
(argument) => typeof argument !== 'string' || argument.includes('\0'),
|
|
)
|
|
) {
|
|
invalid('argv command arguments must be strings containing no NUL');
|
|
}
|
|
} else if (spec.command.kind === 'shell') {
|
|
if (
|
|
typeof spec.command.command !== 'string' ||
|
|
!spec.command.command ||
|
|
spec.command.command.includes('\0')
|
|
) {
|
|
invalid('shell command must be non-empty and contain no NUL');
|
|
}
|
|
if (
|
|
spec.command.shell !== undefined &&
|
|
(typeof spec.command.shell !== 'string' ||
|
|
!path.isAbsolute(spec.command.shell) ||
|
|
spec.command.shell.includes('\0'))
|
|
) {
|
|
invalid('shell must be an absolute path containing no NUL');
|
|
}
|
|
} else {
|
|
invalid('command kind is invalid');
|
|
}
|
|
if (commandSize(spec) > MAX_EXECUTION_COMMAND_BYTES) {
|
|
invalid('execution command is too large');
|
|
}
|
|
assertResourcePolicy(spec.resourcePolicy);
|
|
}
|
|
|
|
export function cloneExecutionSpec(spec: ExecutionSpec): ExecutionSpec {
|
|
assertExecutionSpec(spec);
|
|
return {
|
|
runId: spec.runId,
|
|
attemptId: spec.attemptId,
|
|
projectId: spec.projectId,
|
|
taskId: spec.taskId,
|
|
taskRevision: spec.taskRevision,
|
|
command:
|
|
spec.command.kind === 'argv'
|
|
? {
|
|
kind: 'argv',
|
|
file: spec.command.file,
|
|
args: [...spec.command.args],
|
|
}
|
|
: {
|
|
kind: 'shell',
|
|
command: spec.command.command,
|
|
...(spec.command.shell === undefined
|
|
? {}
|
|
: { shell: spec.command.shell }),
|
|
},
|
|
...(spec.workingDirectory === undefined
|
|
? {}
|
|
: { workingDirectory: spec.workingDirectory }),
|
|
environmentPolicy: spec.environmentPolicy,
|
|
...(spec.timeoutMs === undefined ? {} : { timeoutMs: spec.timeoutMs }),
|
|
terminationGraceMs: spec.terminationGraceMs,
|
|
...(spec.resourcePolicy === undefined
|
|
? {}
|
|
: {
|
|
resourcePolicy: {
|
|
...(spec.resourcePolicy.memoryBytes === undefined
|
|
? {}
|
|
: { memoryBytes: { ...spec.resourcePolicy.memoryBytes } }),
|
|
...(spec.resourcePolicy.cpuMillisPerSecond === undefined
|
|
? {}
|
|
: {
|
|
cpuMillisPerSecond: {
|
|
...spec.resourcePolicy.cpuMillisPerSecond,
|
|
},
|
|
}),
|
|
...(spec.resourcePolicy.filesystemIsolation === undefined
|
|
? {}
|
|
: {
|
|
filesystemIsolation: spec.resourcePolicy.filesystemIsolation,
|
|
}),
|
|
...(spec.resourcePolicy.networkIsolation === undefined
|
|
? {}
|
|
: { networkIsolation: spec.resourcePolicy.networkIsolation }),
|
|
},
|
|
}),
|
|
};
|
|
}
|