补充Pingora真实上游Docker验收

为 Docker canary 增加真实 api-server 与 SpacetimeDB 上游参数

允许 canary live 接受真实 SpacetimeDB identity GET 的 405 语义

同步运维文档记录真实上游 Docker 验收命令
This commit is contained in:
2026-06-17 20:44:44 +08:00
parent a7711d2dc3
commit 9eb4937afb
3 changed files with 171 additions and 36 deletions
File diff suppressed because one or more lines are too long
+169 -34
View File
@@ -72,10 +72,35 @@ async function main() {
const accessLogFile = path.join(logsRoot, 'pingora.access.log');
await mkdir(logsRoot, { recursive: true });
await chmod(logsRoot, 0o777);
await prepareStaticRoots(webRoot, acmeRoot);
const api = await startApiMock();
const spacetime = await startSpacetimeMock();
const gatewayWebRoot = config.webRoot || webRoot;
const gatewayAcmeRoot = config.acmeRoot || acmeRoot;
if (!config.webRoot) {
await prepareStaticWebRoot(webRoot);
}
if (!config.acmeRoot) {
await prepareAcmeRoot(acmeRoot);
}
const realUpstreams = config.realUpstreams || config.apiUpstream || config.spacetimeUpstream;
if (realUpstreams && (!config.apiUpstream || !config.spacetimeUpstream)) {
throw new Error(
'真实上游模式必须同时提供 --api-upstream 与 --spacetime-upstream。',
);
}
const api = realUpstreams
? { upstream: config.apiUpstream, state: null }
: await startApiMock();
const spacetime = realUpstreams
? { upstream: config.spacetimeUpstream, state: null }
: await startSpacetimeMock();
if (realUpstreams) {
console.log(
`[pingora-canary-docker] 使用真实上游 api=${api.upstream} spacetime=${spacetime.upstream} webRoot=${gatewayWebRoot}`,
);
await assertRealUpstreamsReady(api.upstream, spacetime.upstream);
}
const pingoraPort = await getFreePort();
const nginxPort = await getFreePort();
const realpathNginxPort = await getFreePort();
@@ -97,10 +122,10 @@ async function main() {
env: {
...smokeEnv(),
GENARRATIVE_PINGORA_GATEWAY_LISTEN: `0.0.0.0:${pingoraPort}`,
GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM: `127.0.0.1:${api.port}`,
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM: `127.0.0.1:${spacetime.port}`,
GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT: webRoot,
GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT: acmeRoot,
GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM: api.upstream,
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM: spacetime.upstream,
GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT: gatewayWebRoot,
GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT: gatewayAcmeRoot,
GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE: path.join(
tempRoot,
'maintenance',
@@ -280,27 +305,29 @@ async function main() {
'/assets/app.js',
]);
ensure(
api.state.requests.some(
(request) => request.url === '/api/creation-entry/config',
),
'Docker Nginx canary 未把 API 代表路径交给 mock api-server',
);
ensure(
spacetime.state.requests.some((request) => request.url === '/v1/identity'),
'Docker Nginx canary 未把 SpacetimeDB identity 代表路径交给 mock SpacetimeDB',
);
ensure(
api.state.requests.filter(
(request) => request.url === '/api/creation-entry/config',
).length >= 2,
'Docker Nginx realpath canary 未把真实 API 代表路径交给 mock api-server',
);
ensure(
spacetime.state.requests.filter((request) => request.url === '/v1/identity')
.length >= 2,
'Docker Nginx realpath canary 未把真实 SpacetimeDB identity 路径交给 mock SpacetimeDB',
);
if (!realUpstreams) {
ensure(
api.state.requests.some(
(request) => request.url === '/api/creation-entry/config',
),
'Docker Nginx canary 未把 API 代表路径交给 mock api-server',
);
ensure(
spacetime.state.requests.some((request) => request.url === '/v1/identity'),
'Docker Nginx canary 未把 SpacetimeDB identity 代表路径交给 mock SpacetimeDB',
);
ensure(
api.state.requests.filter(
(request) => request.url === '/api/creation-entry/config',
).length >= 2,
'Docker Nginx realpath canary 未把真实 API 代表路径交给 mock api-server',
);
ensure(
spacetime.state.requests.filter((request) => request.url === '/v1/identity')
.length >= 2,
'Docker Nginx realpath canary 未把真实 SpacetimeDB identity 路径交给 mock SpacetimeDB',
);
}
}
function parseArgs(argv) {
@@ -310,6 +337,23 @@ function parseArgs(argv) {
requireDocker: false,
skipBuild: false,
verbose: false,
realUpstreams: false,
apiUpstream: normalizeOptionalHostPort(
process.env.GENARRATIVE_PINGORA_CANARY_REAL_API_UPSTREAM,
'GENARRATIVE_PINGORA_CANARY_REAL_API_UPSTREAM',
),
spacetimeUpstream: normalizeOptionalHostPort(
process.env.GENARRATIVE_PINGORA_CANARY_REAL_SPACETIME_UPSTREAM,
'GENARRATIVE_PINGORA_CANARY_REAL_SPACETIME_UPSTREAM',
),
webRoot: normalizeOptionalDirectory(
process.env.GENARRATIVE_PINGORA_CANARY_REAL_WEB_ROOT,
'GENARRATIVE_PINGORA_CANARY_REAL_WEB_ROOT',
),
acmeRoot: normalizeOptionalDirectory(
process.env.GENARRATIVE_PINGORA_CANARY_REAL_ACME_ROOT,
'GENARRATIVE_PINGORA_CANARY_REAL_ACME_ROOT',
),
};
for (let index = 0; index < argv.length; index += 1) {
@@ -325,10 +369,21 @@ Options:
--pull Pull the image when it is not present locally.
--require-docker Treat missing Docker/image/host-network as failure instead of skip.
--skip-build Reuse an existing pingora-gateway debug binary.
--real-upstreams Require real api-server and SpacetimeDB upstream arguments.
--api-upstream <host:port>
Use a real api-server upstream instead of the built-in mock.
--spacetime-upstream <host:port>
Use a real SpacetimeDB upstream instead of the built-in mock.
--web-root <path> Serve an existing web root, for example dist/.
--acme-root <path> Serve an existing ACME root; temp root is used by default.
--verbose Print Pingora and Docker Nginx process logs.
Environment aliases:
GENARRATIVE_PINGORA_CANARY_DOCKER_IMAGE
GENARRATIVE_PINGORA_CANARY_REAL_API_UPSTREAM
GENARRATIVE_PINGORA_CANARY_REAL_SPACETIME_UPSTREAM
GENARRATIVE_PINGORA_CANARY_REAL_WEB_ROOT
GENARRATIVE_PINGORA_CANARY_REAL_ACME_ROOT
GENARRATIVE_PINGORA_GATEWAY_BINARY
默认不会拉取镜像;本机或 CI 要做强验收时建议:
@@ -348,6 +403,24 @@ Environment aliases:
case '--skip-build':
result.skipBuild = true;
break;
case '--real-upstreams':
result.realUpstreams = true;
break;
case '--api-upstream':
result.apiUpstream = normalizeHostPort(requireValue(argv, ++index, arg), arg);
break;
case '--spacetime-upstream':
result.spacetimeUpstream = normalizeHostPort(
requireValue(argv, ++index, arg),
arg,
);
break;
case '--web-root':
result.webRoot = normalizeDirectory(requireValue(argv, ++index, arg), arg);
break;
case '--acme-root':
result.acmeRoot = normalizeDirectory(requireValue(argv, ++index, arg), arg);
break;
case '--verbose':
result.verbose = true;
break;
@@ -367,6 +440,56 @@ function requireValue(argv, index, flag) {
return value;
}
function normalizeOptionalHostPort(value, label) {
if (!value) {
return '';
}
return normalizeHostPort(value, label);
}
function normalizeHostPort(value, label) {
validateNoControlCharacters(value, label);
const raw = String(value || '').trim();
if (!raw) {
throw new Error(`${label} 不能为空。`);
}
if (raw.includes('://') || /[\s/?#@]/u.test(raw)) {
throw new Error(`${label} 必须是 host:port,不能包含 scheme、路径、查询、片段或空白字符。`);
}
try {
const parsed = new URL(`http://${raw}`);
const port = Number.parseInt(parsed.port, 10);
if (!parsed.hostname || !Number.isInteger(port) || port <= 0 || port > 65535) {
throw new Error('invalid host:port');
}
} catch {
throw new Error(`${label} 必须是合法 host:port。`);
}
return raw;
}
function normalizeOptionalDirectory(value, label) {
if (!value) {
return '';
}
return normalizeDirectory(value, label);
}
function normalizeDirectory(value, label) {
validateNoControlCharacters(value, label);
const resolved = path.resolve(repoRoot, String(value || '').trim());
if (!existsSync(resolved)) {
throw new Error(`${label} 不存在:${resolved}`);
}
return resolved;
}
function validateNoControlCharacters(value, label) {
if (/[\0\r\n]/u.test(String(value ?? ''))) {
throw new Error(`${label} 不能包含换行或 NUL 字符。`);
}
}
function ensureDockerReady() {
const result = spawnSync(
'docker',
@@ -425,12 +548,9 @@ function skipOrFail(message) {
return false;
}
async function prepareStaticRoots(webRoot, acmeRoot) {
async function prepareStaticWebRoot(webRoot) {
await mkdir(path.join(webRoot, 'admin', 'assets'), { recursive: true });
await mkdir(path.join(webRoot, 'assets'), { recursive: true });
await mkdir(path.join(acmeRoot, '.well-known', 'acme-challenge'), {
recursive: true,
});
await writeFile(path.join(webRoot, 'index.html'), '<main>site-shell</main>');
await writeFile(
@@ -445,6 +565,12 @@ async function prepareStaticRoots(webRoot, acmeRoot) {
path.join(webRoot, 'admin', 'assets', 'admin.js'),
'console.log("admin asset");',
);
}
async function prepareAcmeRoot(acmeRoot) {
await mkdir(path.join(acmeRoot, '.well-known', 'acme-challenge'), {
recursive: true,
});
await writeFile(
path.join(acmeRoot, '.well-known', 'acme-challenge', 'token'),
'acme-token',
@@ -501,7 +627,7 @@ async function startApiMock() {
});
const port = await listen(server);
return { port, state };
return { upstream: `127.0.0.1:${port}`, state };
}
async function startSpacetimeMock() {
@@ -542,7 +668,16 @@ async function startSpacetimeMock() {
});
const port = await listen(server);
return { port, state };
return { upstream: `127.0.0.1:${port}`, state };
}
async function assertRealUpstreamsReady(apiUpstream, spacetimeUpstream) {
await waitForHttp(`http://${apiUpstream}/healthz`, 200, {
label: '等待真实 api-server 就绪',
});
await waitForHttp(`http://${spacetimeUpstream}/v1/ping`, 200, {
label: '等待真实 SpacetimeDB 就绪',
});
}
async function renderNginxConfig({
+1 -1
View File
@@ -242,7 +242,7 @@ async function main() {
{
name: 'spacetime-identity',
path: '/v1/identity',
expectedStatuses: [200, 401, 403, 404, 503],
expectedStatuses: [200, 401, 403, 404, 405, 503],
},
{
name: 'web-assets',