补充Pingora真实上游Docker验收
为 Docker canary 增加真实 api-server 与 SpacetimeDB 上游参数 允许 canary live 接受真实 SpacetimeDB identity GET 的 405 语义 同步运维文档记录真实上游 Docker 验收命令
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user