103 lines
3.4 KiB
JavaScript
103 lines
3.4 KiB
JavaScript
function clampInteger(value, fallback, min, max) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return fallback;
|
|
return Math.min(max, Math.max(min, Math.floor(number)));
|
|
}
|
|
|
|
function normalizeList(value) {
|
|
const items = Array.isArray(value)
|
|
? value
|
|
: String(value ?? '').split(',');
|
|
return [...new Set(items.map((item) => String(item).trim()).filter(Boolean))];
|
|
}
|
|
|
|
export function normalizeExecutorAdmissionConfig(input = {}) {
|
|
return {
|
|
tenantAllowlist: normalizeList(input.tenantAllowlist),
|
|
userAllowlist: normalizeList(input.userAllowlist),
|
|
workspaceKindAllowlist: normalizeList(
|
|
input.workspaceKindAllowlist ?? ['workspace-alias'],
|
|
),
|
|
maxGlobalActive: clampInteger(input.maxGlobalActive, 20, 1, 10_000),
|
|
maxTenantActive: clampInteger(input.maxTenantActive, 5, 1, 1_000),
|
|
maxUserActive: clampInteger(input.maxUserActive, 2, 1, 100),
|
|
maxTenantPerMinute: clampInteger(input.maxTenantPerMinute, 30, 1, 100_000),
|
|
maxUserPerMinute: clampInteger(input.maxUserPerMinute, 10, 1, 10_000),
|
|
};
|
|
}
|
|
|
|
export function createExecutorAdmissionPolicy({
|
|
store,
|
|
config = {},
|
|
nowMs = Date.now,
|
|
} = {}) {
|
|
if (typeof store?.getAdmissionStats !== 'function') {
|
|
throw new Error('Executor admission policy requires store.getAdmissionStats');
|
|
}
|
|
const normalized = normalizeExecutorAdmissionConfig(config);
|
|
|
|
function denied(reason, details = {}) {
|
|
return {
|
|
version: 'executor-admission-v1',
|
|
allowed: false,
|
|
reason,
|
|
details,
|
|
};
|
|
}
|
|
|
|
return {
|
|
config: normalized,
|
|
async evaluate(request) {
|
|
const tenantId = request.subject?.tenantId;
|
|
const userId = request.subject?.userId;
|
|
const workspaceKind = request.task?.workspaceRef?.kind;
|
|
if (normalized.tenantAllowlist.length && !normalized.tenantAllowlist.includes(tenantId)) {
|
|
return denied('executor_tenant_not_allowed');
|
|
}
|
|
if (normalized.userAllowlist.length && !normalized.userAllowlist.includes(userId)) {
|
|
return denied('executor_user_not_allowed');
|
|
}
|
|
if (!normalized.workspaceKindAllowlist.includes(workspaceKind)) {
|
|
return denied('executor_workspace_kind_not_allowed', { workspaceKind });
|
|
}
|
|
const stats = await store.getAdmissionStats({
|
|
tenantId,
|
|
userId,
|
|
now: nowMs(),
|
|
windowMs: 60_000,
|
|
});
|
|
const checks = [
|
|
['globalActive', normalized.maxGlobalActive, 'executor_global_concurrency_limited'],
|
|
['tenantActive', normalized.maxTenantActive, 'executor_tenant_concurrency_limited'],
|
|
['userActive', normalized.maxUserActive, 'executor_user_concurrency_limited'],
|
|
['tenantRecent', normalized.maxTenantPerMinute, 'executor_tenant_rate_limited'],
|
|
['userRecent', normalized.maxUserPerMinute, 'executor_user_rate_limited'],
|
|
];
|
|
for (const [field, limit, reason] of checks) {
|
|
if (stats[field] >= limit) return denied(reason, { current: stats[field], limit });
|
|
}
|
|
return {
|
|
version: 'executor-admission-v1',
|
|
allowed: true,
|
|
reason: 'executor_admitted',
|
|
details: {
|
|
globalActive: stats.globalActive,
|
|
tenantActive: stats.tenantActive,
|
|
userActive: stats.userActive,
|
|
},
|
|
};
|
|
},
|
|
status() {
|
|
return {
|
|
version: 'executor-admission-policy-v1',
|
|
...normalized,
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
export const executorAdmissionPolicyInternals = {
|
|
clampInteger,
|
|
normalizeList,
|
|
};
|