mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:58:17 +08:00
274 lines
8.1 KiB
JavaScript
274 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('node:fs');
|
|
|
|
const {
|
|
createPostgresDatabaseOpener,
|
|
} = require('../packages/ql3-cluster-postgres/dist/entrypoints/runtime.js');
|
|
const {
|
|
PostgresPluginPackageLifecyclePlanReader,
|
|
} = require('../packages/ql3-cluster-postgres/dist/entrypoints/packageManager.js');
|
|
const {
|
|
createClusterPluginPackageLifecycleManagementService,
|
|
} = require('../packages/ql3-cluster-admin/dist/plugin-package/lifecycle/pluginPackageLifecycleManagement.js');
|
|
const {
|
|
runClusterPluginPackageLifecycleExecution,
|
|
runClusterPluginPackageLifecyclePlan,
|
|
} = require('../packages/ql3-cluster-admin/dist/plugin-package/lifecycle/pluginPackageLifecycleExecutor.js');
|
|
|
|
const PHASES = new Set(['plan', 'propose', 'decide', 'execute']);
|
|
const REQUESTER = Object.freeze({
|
|
type: 'user',
|
|
id: 'ha-lifecycle-owner',
|
|
});
|
|
const REVIEWER = Object.freeze({
|
|
type: 'user',
|
|
id: 'ha-lifecycle-reviewer',
|
|
});
|
|
|
|
function requiredEnvironment(name) {
|
|
const value = process.env[name];
|
|
if (!value) throw new Error(`${name} is required`);
|
|
return value;
|
|
}
|
|
|
|
function positiveInteger(value, label) {
|
|
if (!/^[1-9]\d*$/.test(value ?? '')) {
|
|
throw new Error(`${label} is invalid`);
|
|
}
|
|
const parsed = Number(value);
|
|
if (!Number.isSafeInteger(parsed)) {
|
|
throw new Error(`${label} is invalid`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function databaseOpener(role, connectionString, applicationName) {
|
|
return createPostgresDatabaseOpener({
|
|
role,
|
|
connection: {
|
|
connectionString,
|
|
tls: { mode: 'disable' },
|
|
},
|
|
pool: {
|
|
applicationName,
|
|
maxConnections: 2,
|
|
connectionTimeoutMs: 2_000,
|
|
},
|
|
onPoolError() {},
|
|
});
|
|
}
|
|
|
|
function identifiers(ordinal) {
|
|
const suffix = String(ordinal).padStart(2, '0');
|
|
return Object.freeze({
|
|
approvalRequestId: `approval-managed-lifecycle-${ordinal}`,
|
|
approvalAuditEventId:
|
|
`34000000-0000-4000-8000-0000000001${suffix}`,
|
|
decisionId: `decision-managed-lifecycle-${ordinal}`,
|
|
decisionAuditEventId:
|
|
`35000000-0000-4000-8000-0000000001${suffix}`,
|
|
consumptionId: `consume-managed-lifecycle-${ordinal}`,
|
|
dispatchId: `dispatch-managed-lifecycle-${ordinal}`,
|
|
executionAuditEventId:
|
|
`36000000-0000-4000-8000-0000000001${suffix}`,
|
|
});
|
|
}
|
|
|
|
async function withDatabase(opener, callback) {
|
|
const database = await opener();
|
|
let failure;
|
|
try {
|
|
return await callback(database);
|
|
} catch (error) {
|
|
failure = error;
|
|
throw error;
|
|
} finally {
|
|
try {
|
|
await database.close();
|
|
} catch (closeError) {
|
|
if (failure !== undefined) {
|
|
throw new AggregateError(
|
|
[failure, closeError],
|
|
'lifecycle crash actor failed and PostgreSQL did not close',
|
|
);
|
|
}
|
|
throw closeError;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadPlan(managerOpener, actionRef) {
|
|
return withDatabase(managerOpener, async (database) => {
|
|
const plan = await new PostgresPluginPackageLifecyclePlanReader(
|
|
database.pool,
|
|
).findByActionRef(actionRef);
|
|
if (!plan) throw new Error('durable lifecycle plan is absent');
|
|
return plan;
|
|
});
|
|
}
|
|
|
|
function principal(subject, authenticationId, plan) {
|
|
return Object.freeze({
|
|
subject,
|
|
authenticationId,
|
|
authenticatedAtMs: plan.plannedAtMs,
|
|
expiresAtMs: plan.expiresAtMs,
|
|
assurance: 'multi_factor',
|
|
});
|
|
}
|
|
|
|
async function runPhase(options) {
|
|
const ids = identifiers(options.ordinal);
|
|
const executorOpener = databaseOpener(
|
|
'package-executor',
|
|
options.executorUrl,
|
|
`ql3-ha-lifecycle-crash-${options.action}-${options.phase}-executor`,
|
|
);
|
|
const managerOpener = databaseOpener(
|
|
'package-manager',
|
|
options.managerUrl,
|
|
`ql3-ha-lifecycle-crash-${options.action}-${options.phase}-manager`,
|
|
);
|
|
if (options.phase === 'plan') {
|
|
const result = await runClusterPluginPackageLifecyclePlan({
|
|
openDatabase: executorOpener,
|
|
actionRef: options.actionRef,
|
|
action: options.action,
|
|
projectId: options.projectId,
|
|
packageName: options.packageName,
|
|
requestedBy: REQUESTER,
|
|
confirmAuthorization() {},
|
|
});
|
|
return Object.freeze({
|
|
status: result.status,
|
|
plan: result.plan,
|
|
});
|
|
}
|
|
const plan = await loadPlan(managerOpener, options.actionRef);
|
|
if (options.phase === 'propose') {
|
|
return withDatabase(managerOpener, async (database) => {
|
|
const service = createClusterPluginPackageLifecycleManagementService({
|
|
pool: database.pool,
|
|
now: () => plan.plannedAtMs + 1,
|
|
});
|
|
const result = await service.propose({
|
|
actionRef: options.actionRef,
|
|
approvalRequestId: ids.approvalRequestId,
|
|
approvalAuditEventId: ids.approvalAuditEventId,
|
|
principal: principal(
|
|
REQUESTER,
|
|
`managed-lifecycle-owner-auth-${options.ordinal}`,
|
|
plan,
|
|
),
|
|
});
|
|
return Object.freeze({
|
|
status: result.approvalStatus,
|
|
approvalRequest: result.approvalRequest,
|
|
});
|
|
});
|
|
}
|
|
if (options.phase === 'decide') {
|
|
return withDatabase(managerOpener, async (database) => {
|
|
const service = createClusterPluginPackageLifecycleManagementService({
|
|
pool: database.pool,
|
|
now: () => plan.plannedAtMs + 2,
|
|
});
|
|
const result = await service.decide({
|
|
actionRef: options.actionRef,
|
|
approvalRequestId: ids.approvalRequestId,
|
|
expectedVersion: 1,
|
|
decisionId: ids.decisionId,
|
|
auditEventId: ids.decisionAuditEventId,
|
|
decision: 'approved',
|
|
reasonCode: 'reviewed',
|
|
principal: principal(
|
|
REVIEWER,
|
|
`managed-lifecycle-reviewer-auth-${options.ordinal}`,
|
|
plan,
|
|
),
|
|
});
|
|
return Object.freeze({
|
|
status: result.status,
|
|
approvalRequest: result.request,
|
|
});
|
|
});
|
|
}
|
|
const result = await runClusterPluginPackageLifecycleExecution({
|
|
openDatabase: executorOpener,
|
|
actionRef: options.actionRef,
|
|
approvalRequestId: ids.approvalRequestId,
|
|
consumptionId: ids.consumptionId,
|
|
dispatchId: ids.dispatchId,
|
|
auditEventId: ids.executionAuditEventId,
|
|
confirmAuthorization() {},
|
|
});
|
|
return Object.freeze({
|
|
status: result.status,
|
|
receipt: result.receipt,
|
|
});
|
|
}
|
|
|
|
function writeMarker(markerPath, options, status) {
|
|
const descriptor = fs.openSync(markerPath, 'wx', 0o600);
|
|
try {
|
|
fs.writeSync(
|
|
descriptor,
|
|
JSON.stringify({
|
|
schema:
|
|
'qinglong/postgresql-plugin-package-lifecycle-process-crash@v1',
|
|
action: options.action,
|
|
phase: options.phase,
|
|
status,
|
|
pid: process.pid,
|
|
}),
|
|
);
|
|
fs.fsyncSync(descriptor);
|
|
} finally {
|
|
fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const phase = process.argv[2];
|
|
if (!PHASES.has(phase)) {
|
|
throw new Error('phase must be plan, propose, decide or execute');
|
|
}
|
|
const action = requiredEnvironment('QL3_LIFECYCLE_CRASH_ACTION');
|
|
if (action !== 'disable' && action !== 'enable') {
|
|
throw new Error('QL3_LIFECYCLE_CRASH_ACTION is invalid');
|
|
}
|
|
const options = Object.freeze({
|
|
phase,
|
|
action,
|
|
ordinal: positiveInteger(
|
|
requiredEnvironment('QL3_LIFECYCLE_CRASH_ORDINAL'),
|
|
'QL3_LIFECYCLE_CRASH_ORDINAL',
|
|
),
|
|
actionRef: requiredEnvironment('QL3_LIFECYCLE_CRASH_ACTION_REF'),
|
|
projectId: requiredEnvironment('QL3_LIFECYCLE_CRASH_PROJECT_ID'),
|
|
packageName: requiredEnvironment('QL3_LIFECYCLE_CRASH_PACKAGE_NAME'),
|
|
managerUrl: requiredEnvironment('QL3_LIFECYCLE_CRASH_MANAGER_URL'),
|
|
executorUrl: requiredEnvironment('QL3_LIFECYCLE_CRASH_EXECUTOR_URL'),
|
|
});
|
|
const result = await runPhase(options);
|
|
if (process.env.QL3_LIFECYCLE_CRASH_KILL_AFTER_DURABLE === 'true') {
|
|
const markerPath = requiredEnvironment(
|
|
'QL3_LIFECYCLE_CRASH_MARKER_PATH',
|
|
);
|
|
writeMarker(markerPath, options, result.status);
|
|
process.kill(process.pid, 'SIGKILL');
|
|
throw new Error('SIGKILL did not terminate lifecycle crash actor');
|
|
}
|
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
process.stderr.write(
|
|
`ql3 lifecycle crash actor failed: ${
|
|
error instanceof Error ? error.stack ?? error.message : String(error)
|
|
}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|