mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
311 lines
8.6 KiB
JavaScript
311 lines
8.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
|
|
const {
|
|
createModelProviderCredentialTransitionCommand,
|
|
MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
|
} = require('@qinglong/ai/model-provider-credential-catalog');
|
|
const {
|
|
modelProviderCredentialAdministrationOperationId,
|
|
} = require('@qinglong/ai/model-provider-credential-administration');
|
|
const {
|
|
createModelProviderCredentialTestPlan,
|
|
normalizeModelProviderCredentialTestAllowlist,
|
|
resolveModelProviderCredentialTestEndpoint,
|
|
} = require('@qinglong/ai/model-provider-credential-test-connection');
|
|
const {
|
|
MODEL_PROVIDER_CREDENTIAL_TEST_PLAN_OPERATION_ID,
|
|
PostgresModelProviderCredentialTestPlanRepository,
|
|
} = require('@qinglong/ai/postgres-model-provider-credential-test-connection');
|
|
const {
|
|
MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
|
} = require('@qinglong/ai/provider-credential');
|
|
const {
|
|
PostgresModelProviderCredentialRepository,
|
|
} = require('@qinglong/ai/postgres-model-provider-credential-storage');
|
|
const {
|
|
createPostgresDatabaseOpener,
|
|
loadPostgresCertificateAuthorityFile,
|
|
} = require('@qinglong/cluster-postgres/ai-credential-manager');
|
|
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
|
|
|
const UUID_V4_PATTERN =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
|
|
function fail(message) {
|
|
throw new Error(`provider credential test live actor: ${message}`);
|
|
}
|
|
|
|
function requiredEnvironment(name) {
|
|
const value = process.env[name];
|
|
if (!value) fail(`${name} is required`);
|
|
return value;
|
|
}
|
|
|
|
function exact(value, keys) {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value) &&
|
|
JSON.stringify(Object.keys(value).sort()) ===
|
|
JSON.stringify([...keys].sort())
|
|
);
|
|
}
|
|
|
|
function identifier(value, name) {
|
|
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
|
|
fail(`${name} is invalid`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function uuid(value, name) {
|
|
if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) {
|
|
fail(`${name} is invalid`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function timestamp(value, name) {
|
|
if (!Number.isSafeInteger(value) || value < 0) fail(`${name} is invalid`);
|
|
return value;
|
|
}
|
|
|
|
function readCommand() {
|
|
const commandFile = requiredEnvironment('QL3_LIVE_ACTOR_COMMAND_FILE');
|
|
const stat = fs.lstatSync(commandFile);
|
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) {
|
|
fail('command file is invalid');
|
|
}
|
|
return JSON.parse(fs.readFileSync(commandFile, 'utf8'));
|
|
}
|
|
|
|
function connection() {
|
|
const ca = loadPostgresCertificateAuthorityFile(
|
|
requiredEnvironment('QL3_LIVE_POSTGRES_CA_FILE'),
|
|
);
|
|
return createPostgresDatabaseOpener({
|
|
role: 'ai-credential-manager',
|
|
connection: {
|
|
host: requiredEnvironment('QL3_LIVE_POSTGRES_HOST'),
|
|
port: Number(requiredEnvironment('QL3_LIVE_POSTGRES_PORT')),
|
|
database: requiredEnvironment('QL3_LIVE_POSTGRES_DATABASE'),
|
|
user: requiredEnvironment('QL3_LIVE_POSTGRES_USER'),
|
|
password: requiredEnvironment('QL3_LIVE_POSTGRES_PASSWORD'),
|
|
applicationName: 'ql3-provider-credential-test-live-actor',
|
|
tls: {
|
|
mode: 'verify-full',
|
|
servername: requiredEnvironment('QL3_LIVE_POSTGRES_TLS_SERVERNAME'),
|
|
ca,
|
|
},
|
|
},
|
|
pool: {
|
|
max: 1,
|
|
idleTimeoutMs: 1_000,
|
|
connectionTimeoutMs: 5_000,
|
|
},
|
|
onPoolError() {},
|
|
});
|
|
}
|
|
|
|
function subject(command) {
|
|
return Object.freeze({
|
|
type: 'user',
|
|
id: identifier(command.actorId, 'actorId'),
|
|
});
|
|
}
|
|
|
|
function fence() {
|
|
return Object.freeze({ projectVersion: 1, bindingVersion: 1 });
|
|
}
|
|
|
|
async function bind(database, command) {
|
|
if (
|
|
!exact(command, [
|
|
'action',
|
|
'actorId',
|
|
'mutationId',
|
|
'occurredAtMs',
|
|
'projectId',
|
|
'provider',
|
|
'requestId',
|
|
'schemaVersion',
|
|
'secretName',
|
|
]) ||
|
|
command.schemaVersion !== 1 ||
|
|
command.action !== 'bind'
|
|
) {
|
|
fail('bind command is invalid');
|
|
}
|
|
const actor = subject(command);
|
|
const projectId = identifier(command.projectId, 'projectId');
|
|
const provider = identifier(command.provider, 'provider');
|
|
const mutationId = uuid(command.mutationId, 'mutationId');
|
|
const occurredAtMs = timestamp(command.occurredAtMs, 'occurredAtMs');
|
|
const binding = Object.freeze({
|
|
schema: MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
|
projectId,
|
|
provider,
|
|
revision: 'live-material-v1',
|
|
secretRef: createSecretRef({
|
|
projectId,
|
|
name: identifier(command.secretName, 'secretName'),
|
|
}),
|
|
scheme: 'bearer',
|
|
});
|
|
const catalogCommand = createModelProviderCredentialTransitionCommand({
|
|
schema: MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
|
mutationId,
|
|
projectId,
|
|
provider,
|
|
expectedGeneration: 0,
|
|
action: 'bind',
|
|
binding,
|
|
changedBy: actor,
|
|
});
|
|
const result = await new PostgresModelProviderCredentialRepository(
|
|
database.pool,
|
|
).commitAuthorized({
|
|
command: catalogCommand,
|
|
actor,
|
|
fence: fence(),
|
|
audit: {
|
|
eventId: mutationId,
|
|
requestId: identifier(command.requestId, 'requestId'),
|
|
operationId: modelProviderCredentialAdministrationOperationId('bind'),
|
|
projectId,
|
|
subject: actor,
|
|
authenticationId: `live-bind-${mutationId}`,
|
|
outcome: 'allowed',
|
|
reasons: ['project_owner'],
|
|
fence: fence(),
|
|
occurredAtMs,
|
|
},
|
|
});
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
action: 'bind',
|
|
status: result.status,
|
|
projectId,
|
|
provider,
|
|
generation: result.transition.generation,
|
|
revision: result.transition.binding?.revision ?? null,
|
|
});
|
|
}
|
|
|
|
async function plan(database, command) {
|
|
if (
|
|
!exact(command, [
|
|
'action',
|
|
'actorId',
|
|
'allowlist',
|
|
'expiresAtMs',
|
|
'occurredAtMs',
|
|
'projectId',
|
|
'provider',
|
|
'requestId',
|
|
'schemaVersion',
|
|
'testId',
|
|
]) ||
|
|
command.schemaVersion !== 1 ||
|
|
command.action !== 'plan'
|
|
) {
|
|
fail('plan command is invalid');
|
|
}
|
|
const actor = subject(command);
|
|
const projectId = identifier(command.projectId, 'projectId');
|
|
const provider = identifier(command.provider, 'provider');
|
|
const testId = uuid(command.testId, 'testId');
|
|
const occurredAtMs = timestamp(command.occurredAtMs, 'occurredAtMs');
|
|
const allowlist = normalizeModelProviderCredentialTestAllowlist(
|
|
command.allowlist,
|
|
);
|
|
const value = createModelProviderCredentialTestPlan({
|
|
testId,
|
|
requestId: identifier(command.requestId, 'requestId'),
|
|
projectId,
|
|
provider,
|
|
endpoint: resolveModelProviderCredentialTestEndpoint(allowlist, provider),
|
|
requestedBy: actor,
|
|
fence: fence(),
|
|
plannedAtMs: occurredAtMs,
|
|
expiresAtMs: timestamp(command.expiresAtMs, 'expiresAtMs'),
|
|
});
|
|
const result = await new PostgresModelProviderCredentialTestPlanRepository(
|
|
database.pool,
|
|
{ quotaWindowMs: 60_000, quotaLimit: 32 },
|
|
).createAuthorized({
|
|
plan: value,
|
|
audit: {
|
|
eventId: testId,
|
|
requestId: value.requestId,
|
|
operationId: MODEL_PROVIDER_CREDENTIAL_TEST_PLAN_OPERATION_ID,
|
|
projectId,
|
|
subject: actor,
|
|
authenticationId: `live-plan-${testId}`,
|
|
outcome: 'allowed',
|
|
reasons: ['project_owner'],
|
|
fence: fence(),
|
|
occurredAtMs,
|
|
},
|
|
});
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
action: 'plan',
|
|
status: result.status,
|
|
testId,
|
|
projectId,
|
|
provider,
|
|
planDigest: result.plan.planDigest,
|
|
endpointConfigDigest: result.plan.endpoint.configDigest,
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const command = readCommand();
|
|
const openDatabase = connection();
|
|
const database = await openDatabase();
|
|
try {
|
|
const result =
|
|
command.action === 'bind'
|
|
? await bind(database, command)
|
|
: command.action === 'plan'
|
|
? await plan(database, command)
|
|
: fail('action is unsupported');
|
|
const serialized = JSON.stringify(result);
|
|
fs.writeFileSync('/dev/termination-log', serialized, {
|
|
encoding: 'utf8',
|
|
mode: 0o600,
|
|
});
|
|
process.stdout.write(`${serialized}\n`);
|
|
} finally {
|
|
await database.close();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
const failure = JSON.stringify({
|
|
schemaVersion: 1,
|
|
event: 'live_actor_failed',
|
|
name: error instanceof Error ? error.name : 'Error',
|
|
message:
|
|
error instanceof Error
|
|
? error.message.slice(0, 1_024)
|
|
: 'unknown actor failure',
|
|
});
|
|
process.stderr.write(`${failure}\n`);
|
|
try {
|
|
fs.writeFileSync('/dev/termination-log', failure, {
|
|
encoding: 'utf8',
|
|
mode: 0o600,
|
|
});
|
|
} catch {
|
|
// Kubernetes falls back to the bounded stderr line on actor failure.
|
|
}
|
|
process.exitCode = 1;
|
|
});
|