feat: complete phase2 baseline cdr quality rbac

This commit is contained in:
hectorzhao
2026-06-24 18:40:21 +08:00
parent 7057fd3c42
commit a86de6545f
63 changed files with 7853 additions and 3678 deletions
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env node
import crypto from 'node:crypto';
import { PrismaClient } from '@prisma/client';
const args = new Set(process.argv.slice(2));
const apply = args.has('--apply');
const json = args.has('--json');
const failOnBlockers = args.has('--fail-on-blockers');
if (args.has('--help') || args.has('-h')) {
console.log(`Usage: node scripts/phase2-gateway-migration.mjs [--apply] [--json] [--fail-on-blockers]
Default mode is dry-run. It audits legacy customer gateway policies and prints
which gateways can be safely migrated to the phase-2 single-line-group model.
Safe automatic migration requires:
- exactly one enabled legacy customer_gateway_policy per customer gateway;
- policy calleeMode = ANY;
- policy callerMode = ANY, or callerMode = PREFIX with callerValue.
All other cases are reported as blockers for manual review.`);
process.exit(0);
}
if (!process.env.DATABASE_URL) {
console.error('DATABASE_URL is required. Do not put credentials in command history; load it from the controlled secret environment.');
process.exit(2);
}
const prisma = new PrismaClient();
function id(prefix) {
return `${prefix}_${crypto.randomUUID().replaceAll('-', '').slice(0, 32 - prefix.length - 1)}`;
}
function blocker(gateway, code, message, policy = null) {
return {
gatewayId: gateway.id,
gatewayName: gateway.name,
customerId: gateway.customerId,
code,
message,
policyId: policy?.id ?? null,
policyName: policy?.name ?? null
};
}
function plannedChange(gateway, policy) {
const callerMode = policy.callerMode === 'PREFIX' ? 'PREFIXES' : 'ANY';
return {
gatewayId: gateway.id,
gatewayName: gateway.name,
customerId: gateway.customerId,
sourceIp: gateway.sourceIp,
lineGroupId: policy.lineGroupId,
callerMatchMode: callerMode,
callerPrefix: policy.callerMode === 'PREFIX' ? policy.callerValue : null,
calleeMatchMode: 'ANY',
policyId: policy.id,
policyName: policy.name
};
}
function analyzeGateway(gateway) {
const policies = gateway.policies;
if (policies.length === 0) {
return {
plan: null,
blocker: blocker(gateway, 'NO_ENABLED_POLICY', 'Gateway has no enabled legacy policy; choose a line group manually before switching hot path.')
};
}
if (policies.length > 1) {
return {
plan: null,
blocker: blocker(gateway, 'MULTIPLE_ENABLED_POLICIES', 'Gateway has multiple enabled legacy policies; split into multiple customer gateways or choose one rule manually.')
};
}
const [policy] = policies;
if (gateway.lineGroupId && gateway.lineGroupId !== policy.lineGroupId) {
return {
plan: null,
blocker: blocker(gateway, 'LINE_GROUP_CONFLICT', 'Gateway already has a phase-2 lineGroupId different from the legacy policy.', policy)
};
}
if (policy.calleeMode !== 'ANY') {
return {
plan: null,
blocker: blocker(gateway, 'CALLEE_RULE_REQUIRES_BUSINESS_PREFIX', 'Legacy callee matching cannot be inferred as a business prefix safely.', policy)
};
}
if (policy.callerMode === 'EQUALS') {
return {
plan: null,
blocker: blocker(gateway, 'CALLER_EQUALS_REQUIRES_MANUAL_PREFIX', 'Phase-2 supports caller ANY or custom prefixes; exact caller match must be reviewed manually.', policy)
};
}
if (policy.callerMode === 'PREFIX' && !policy.callerValue) {
return {
plan: null,
blocker: blocker(gateway, 'CALLER_PREFIX_EMPTY', 'Legacy caller prefix policy has an empty callerValue.', policy)
};
}
if (policy.callerMode !== 'ANY' && policy.callerMode !== 'PREFIX') {
return {
plan: null,
blocker: blocker(gateway, 'CALLER_RULE_UNSUPPORTED', `Unsupported legacy callerMode: ${policy.callerMode}`, policy)
};
}
return { plan: plannedChange(gateway, policy), blocker: null };
}
async function loadGateways() {
return prisma.customerGateway.findMany({
where: { deletedAt: null },
orderBy: [{ id: 'asc' }],
include: {
ips: { where: { deletedAt: null }, select: { sourceIp: true, status: true } },
callerPrefixes: { select: { prefix: true } },
policies: {
where: { deletedAt: null, status: 'ENABLED' },
orderBy: [{ priority: 'asc' }]
}
}
});
}
async function applyPlan(plans) {
if (plans.length === 0) {
return { updatedGateways: 0, insertedIps: 0, insertedCallerPrefixes: 0, outboxEvents: 0 };
}
return prisma.$transaction(async (tx) => {
let insertedIps = 0;
let insertedCallerPrefixes = 0;
for (const plan of plans) {
await tx.customerGateway.update({
where: { id: plan.gatewayId },
data: {
lineGroupId: plan.lineGroupId,
callerMatchMode: plan.callerMatchMode,
calleeMatchMode: 'ANY',
updatedBy: 'phase2-migration',
version: { increment: 1 }
}
});
if (plan.sourceIp) {
const result = await tx.customerGatewayIp.createMany({
data: [
{
id: id('cgip'),
gatewayId: plan.gatewayId,
sourceIp: plan.sourceIp,
status: 'ENABLED',
createdBy: 'phase2-migration',
updatedBy: 'phase2-migration'
}
],
skipDuplicates: true
});
insertedIps += result.count;
}
if (plan.callerPrefix) {
const result = await tx.customerGatewayCallerPrefix.createMany({
data: [
{
id: id('cgcp'),
gatewayId: plan.gatewayId,
prefix: plan.callerPrefix,
priority: 100,
createdBy: 'phase2-migration'
}
],
skipDuplicates: true
});
insertedCallerPrefixes += result.count;
}
}
await tx.outboxEvent.create({
data: {
id: id('out'),
aggregateType: 'customer_gateway_config',
aggregateId: 'phase2-gateway-migration',
eventType: 'phase2.gateway_migration.applied',
payload: {
gatewayIds: plans.map((plan) => plan.gatewayId),
safeGatewayCount: plans.length
}
}
});
return {
updatedGateways: plans.length,
insertedIps,
insertedCallerPrefixes,
outboxEvents: 1
};
});
}
function printHuman(summary) {
console.log(`Phase-2 gateway migration ${apply ? 'APPLY' : 'DRY-RUN'}`);
console.log(`Total gateways: ${summary.totalGateways}`);
console.log(`Safe to migrate: ${summary.safe.length}`);
console.log(`Already compatible: ${summary.alreadyCompatible.length}`);
console.log(`Manual blockers: ${summary.blockers.length}`);
if (summary.safe.length > 0) {
console.log('\nSafe plans:');
for (const plan of summary.safe) {
console.log(`- ${plan.gatewayId} ${plan.gatewayName}: lineGroup=${plan.lineGroupId}, caller=${plan.callerMatchMode}${plan.callerPrefix ? `:${plan.callerPrefix}` : ''}, callee=ANY`);
}
}
if (summary.blockers.length > 0) {
console.log('\nManual blockers:');
for (const item of summary.blockers) {
console.log(`- ${item.gatewayId} ${item.gatewayName}: ${item.code} - ${item.message}`);
}
}
if (summary.applyResult) {
console.log('\nApplied:');
console.log(`- updated gateways: ${summary.applyResult.updatedGateways}`);
console.log(`- inserted IP rows: ${summary.applyResult.insertedIps}`);
console.log(`- inserted caller prefixes: ${summary.applyResult.insertedCallerPrefixes}`);
console.log(`- outbox events: ${summary.applyResult.outboxEvents}`);
}
}
try {
const gateways = await loadGateways();
const safe = [];
const blockers = [];
const alreadyCompatible = [];
for (const gateway of gateways) {
const analysis = analyzeGateway(gateway);
if (analysis.blocker) {
blockers.push(analysis.blocker);
continue;
}
const plan = analysis.plan;
const hasIp = !plan.sourceIp || gateway.ips.some((item) => item.sourceIp === plan.sourceIp);
const hasCallerPrefix = !plan.callerPrefix || gateway.callerPrefixes.some((item) => item.prefix === plan.callerPrefix);
const compatible =
gateway.lineGroupId === plan.lineGroupId &&
gateway.callerMatchMode === plan.callerMatchMode &&
gateway.calleeMatchMode === 'ANY' &&
hasIp &&
hasCallerPrefix;
if (compatible) {
alreadyCompatible.push(plan);
} else {
safe.push(plan);
}
}
const summary = {
mode: apply ? 'apply' : 'dry-run',
totalGateways: gateways.length,
safe,
alreadyCompatible,
blockers,
applyResult: apply ? await applyPlan(safe) : null
};
if (json) {
console.log(JSON.stringify(summary, null, 2));
} else {
printHuman(summary);
}
if (failOnBlockers && blockers.length > 0) {
process.exitCode = 3;
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
} finally {
await prisma.$disconnect();
}