a7711d2dc3
新增 pingora-gateway 独立二进制 crate,覆盖路由、静态资源、压缩、接流保护、TLS 直连和访问日志能力。 新增 Nginx canary、realpath canary、direct preflight、direct live、direct enable 和 rollback 脚本。 新增 Pingora 切流证据包、命令证据、manifest 验真、根目录总审计和 release readiness 聚合门禁。 完善 API release、Jenkins、systemd、health patrol、生产部署和发布包自包含校验。 更新 Pingora 试点文档、Nginx README 与 Hermes 共享记忆。
243 lines
6.8 KiB
JavaScript
243 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
const MATRIX_PATH = 'deploy/pingora/nginx-route-parity.matrix.json';
|
|
const PRODUCTION_NGINX_PATH = 'deploy/nginx/genarrative.conf';
|
|
const DEVELOPMENT_NGINX_PATH = 'deploy/nginx/genarrative-dev-http.conf';
|
|
const PINGORA_DOC_PATH =
|
|
'docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md';
|
|
const PINGORA_GATEWAY_SOURCE = 'server-rs/crates/pingora-gateway/src/main.rs';
|
|
|
|
const VALID_KINDS = new Set([
|
|
'proxy',
|
|
'static',
|
|
'redirect_permanent',
|
|
'shadow_probe',
|
|
'not_found',
|
|
]);
|
|
const VALID_PROXY_TARGETS = new Set(['api', 'spacetime']);
|
|
const VALID_STATIC_ROOTS = new Set(['web', 'acme']);
|
|
const VALID_STATIC_MODES = new Set(['exact', 'spa_fallback']);
|
|
const VALID_PROTECTION_CLASSES = new Set([
|
|
'admin_api',
|
|
'gallery_list',
|
|
'gallery_detail',
|
|
'api',
|
|
'spacetime',
|
|
]);
|
|
const REQUIRED_ROUTE_IDS = [
|
|
'acme_challenge',
|
|
'shadow_probe',
|
|
'admin_redirect',
|
|
'admin_api_proxy',
|
|
'admin_assets',
|
|
'admin_spa_fallback',
|
|
'web_assets',
|
|
'puzzle_gallery_list',
|
|
'custom_world_gallery_list',
|
|
'puzzle_gallery_detail',
|
|
'custom_world_gallery_detail',
|
|
'generic_api_proxy',
|
|
'spacetime_subscribe',
|
|
'spacetime_identity',
|
|
'v1_forbidden',
|
|
'healthz_forbidden',
|
|
'readyz_forbidden',
|
|
'generated_assets_forbidden',
|
|
'web_spa_fallback',
|
|
];
|
|
|
|
const files = {
|
|
production: readFileSync(PRODUCTION_NGINX_PATH, 'utf8'),
|
|
development: readFileSync(DEVELOPMENT_NGINX_PATH, 'utf8'),
|
|
};
|
|
const docs = readFileSync(PINGORA_DOC_PATH, 'utf8');
|
|
const pingoraGatewaySource = readFileSync(PINGORA_GATEWAY_SOURCE, 'utf8');
|
|
const matrix = JSON.parse(readFileSync(MATRIX_PATH, 'utf8'));
|
|
const failures = [];
|
|
|
|
function fail(message) {
|
|
failures.push(message);
|
|
}
|
|
|
|
function hasOwn(object, key) {
|
|
return Object.prototype.hasOwnProperty.call(object, key);
|
|
}
|
|
|
|
function requireString(value, context) {
|
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
fail(`${context} 必须是非空字符串。`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function validateExpectation(route) {
|
|
const context = `${MATRIX_PATH} route ${route.id}`;
|
|
const expect = route.expect;
|
|
if (!expect || typeof expect !== 'object' || Array.isArray(expect)) {
|
|
fail(`${context} 缺少 expect 对象。`);
|
|
return;
|
|
}
|
|
|
|
if (!VALID_KINDS.has(expect.kind)) {
|
|
fail(`${context} expect.kind 不支持: ${expect.kind}`);
|
|
return;
|
|
}
|
|
|
|
if (expect.kind === 'proxy') {
|
|
if (!VALID_PROXY_TARGETS.has(expect.target)) {
|
|
fail(`${context} proxy target 不支持: ${expect.target}`);
|
|
}
|
|
if (
|
|
hasOwn(expect, 'bodyLimit') &&
|
|
expect.bodyLimit !== null &&
|
|
expect.bodyLimit !== 'default' &&
|
|
(!Number.isInteger(expect.bodyLimit) || expect.bodyLimit < 1)
|
|
) {
|
|
fail(`${context} bodyLimit 必须是 null、default 或正整数。`);
|
|
}
|
|
if (!VALID_PROTECTION_CLASSES.has(expect.protectionClass)) {
|
|
fail(
|
|
`${context} proxy protectionClass 不支持: ${expect.protectionClass}`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (hasOwn(expect, 'protectionClass')) {
|
|
fail(`${context} 非 proxy 路由不能配置 protectionClass。`);
|
|
}
|
|
|
|
if (expect.kind === 'static') {
|
|
if (!VALID_STATIC_ROOTS.has(expect.root)) {
|
|
fail(`${context} static root 不支持: ${expect.root}`);
|
|
}
|
|
if (!VALID_STATIC_MODES.has(expect.mode)) {
|
|
fail(`${context} static mode 不支持: ${expect.mode}`);
|
|
}
|
|
}
|
|
|
|
if (
|
|
expect.kind === 'redirect_permanent' &&
|
|
!requireString(expect.location, `${context} redirect location`)
|
|
) {
|
|
fail(`${context} redirect_permanent 必须配置 location。`);
|
|
}
|
|
}
|
|
|
|
function validateNginxFragments(route) {
|
|
for (const environment of ['production', 'development']) {
|
|
const fragments = route.nginx?.[environment];
|
|
if (fragments === undefined) {
|
|
if (environment === 'production' && route.id !== 'shadow_probe') {
|
|
fail(`${MATRIX_PATH} route ${route.id} 缺少 production Nginx 片段。`);
|
|
}
|
|
continue;
|
|
}
|
|
if (!Array.isArray(fragments) || fragments.length === 0) {
|
|
fail(
|
|
`${MATRIX_PATH} route ${route.id} 的 ${environment} Nginx 片段不能为空。`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
for (const fragment of fragments) {
|
|
if (!requireString(fragment, `${route.id} ${environment} Nginx 片段`)) {
|
|
continue;
|
|
}
|
|
if (!files[environment].includes(fragment)) {
|
|
fail(
|
|
`${environment} Nginx 模板缺少 route ${route.id} 片段: ${fragment}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateDocFragments(route) {
|
|
if (!Array.isArray(route.docs) || route.docs.length === 0) {
|
|
fail(`${MATRIX_PATH} route ${route.id} 缺少 docs 片段。`);
|
|
return;
|
|
}
|
|
|
|
for (const fragment of route.docs) {
|
|
if (!requireString(fragment, `${route.id} docs 片段`)) {
|
|
continue;
|
|
}
|
|
if (!docs.includes(fragment)) {
|
|
fail(`Pingora 试点文档缺少 route ${route.id} 片段: ${fragment}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateMatrixShape() {
|
|
if (matrix.version !== 1) {
|
|
fail(`${MATRIX_PATH} version 必须为 1。`);
|
|
}
|
|
if (!Array.isArray(matrix.routes) || matrix.routes.length === 0) {
|
|
fail(`${MATRIX_PATH} routes 不能为空。`);
|
|
return;
|
|
}
|
|
|
|
const ids = new Set();
|
|
const samplePaths = new Set();
|
|
for (const route of matrix.routes) {
|
|
if (!requireString(route.id, `${MATRIX_PATH} route.id`)) {
|
|
continue;
|
|
}
|
|
if (ids.has(route.id)) {
|
|
fail(`${MATRIX_PATH} route id 重复: ${route.id}`);
|
|
}
|
|
ids.add(route.id);
|
|
|
|
if (!requireString(route.samplePath, `${route.id} samplePath`)) {
|
|
continue;
|
|
}
|
|
if (!route.samplePath.startsWith('/')) {
|
|
fail(`${MATRIX_PATH} route ${route.id} samplePath 必须以 / 开头。`);
|
|
}
|
|
if (samplePaths.has(route.samplePath)) {
|
|
fail(`${MATRIX_PATH} samplePath 重复: ${route.samplePath}`);
|
|
}
|
|
samplePaths.add(route.samplePath);
|
|
|
|
validateExpectation(route);
|
|
validateNginxFragments(route);
|
|
validateDocFragments(route);
|
|
}
|
|
|
|
for (const routeId of REQUIRED_ROUTE_IDS) {
|
|
if (!ids.has(routeId)) {
|
|
fail(`${MATRIX_PATH} 缺少必需 route id: ${routeId}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateRustTestUsesMatrix() {
|
|
for (const fragment of [
|
|
'include_str!("../../../../deploy/pingora/nginx-route-parity.matrix.json")',
|
|
'serde_json::from_str(ROUTE_PARITY_MATRIX_JSON)',
|
|
'protection_class_for_route(&route, &case.sample_path)',
|
|
'fn matches_nginx_route_parity_matrix()',
|
|
]) {
|
|
if (!pingoraGatewaySource.includes(fragment)) {
|
|
fail(`Pingora Rust 路由 parity 测试缺少矩阵接入片段: ${fragment}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
validateMatrixShape();
|
|
validateRustTestUsesMatrix();
|
|
|
|
if (failures.length > 0) {
|
|
console.error('[check:pingora-route-parity] FAILED');
|
|
for (const failure of failures) {
|
|
console.error(`- ${failure}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`[check:pingora-route-parity] OK (${matrix.routes.length} routes)`);
|