feat(platform-infra): add YAML-controlled nginx deployment

This commit is contained in:
Codex
2026-07-13 14:04:47 +02:00
parent 21567bafbf
commit 635589d79e
6 changed files with 812 additions and 1 deletions
+667
View File
@@ -0,0 +1,667 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync, statSync } from "node:fs";
import { isAbsolute, join } from "node:path";
import type { UniDeskConfig } from "./config";
import { repoRoot, rootPath } from "./config";
import { startJob } from "./jobs";
import {
capture,
compactCapture,
fingerprintValues,
parseEnvFile,
parseJsonOutput,
parseOpsApplyOptions,
parseOpsCommonOptions,
readYamlRecord,
shQuote,
type OpsApplyOptions,
type OpsCommonOptions,
} from "./platform-infra-ops-library";
const configPath = rootPath("config", "platform-infra", "nginx.yaml");
const configLabel = "config/platform-infra/nginx.yaml";
interface Artifact {
localPath: string;
remotePath: string;
sha256: string;
}
interface ImageArtifact extends Artifact {
buildTarget: string;
image: string;
imageId: string;
bytes: number;
}
interface Route {
listenPort: number;
upstreams: Array<{ host: string; port: number }>;
}
interface Backend {
id: string;
bindHost: string;
port: number;
unit: string;
}
interface NginxTarget {
route: string;
runtime: {
workDir: string;
composePath: string;
envPath: string;
projectName: string;
containerName: string;
networkMode: "host";
restart: string;
resources: { cpus: string; memory: string; pidsLimit: number };
logDir: string;
routes: Route[];
environment: Record<string, string>;
};
apiKey: {
sourceRef: string;
sourceKey: string;
targetKey: string;
requestHeader: string;
};
validation: {
requestCount: number;
cleanupAfterSuccess: boolean;
backendScriptPath: string;
backends: Backend[];
};
}
interface NginxConfig {
version: number;
kind: "platform-infra-nginx";
metadata: { name: string };
defaults: { target: string };
artifacts: { image: ImageArtifact; composePlugin: Artifact };
targets: Record<string, NginxTarget>;
}
interface SecretMaterial {
sourcePath: string;
value: string;
fingerprint: string;
}
export function nginxHelp(): Record<string, unknown> {
return {
command: "platform-infra nginx plan|apply|status",
output: "json",
usage: [
"bun scripts/cli.ts platform-infra nginx plan [--target PK01]",
"bun scripts/cli.ts platform-infra nginx apply [--target PK01] --dry-run",
"bun scripts/cli.ts platform-infra nginx apply [--target PK01] --confirm",
"bun scripts/cli.ts platform-infra nginx status [--target PK01] [--full|--raw]",
],
configTruth: configLabel,
secretPolicy: "API key values are never printed; output contains presence and fingerprint only.",
};
}
export async function runPlatformInfraNginxCommand(
config: UniDeskConfig,
args: string[],
): Promise<Record<string, unknown>> {
const [action = "plan"] = args;
if (action === "plan") return plan(parseOpsCommonOptions(args.slice(1)));
if (action === "apply") return await apply(config, parseOpsApplyOptions(args.slice(1)));
if (action === "status") return await status(config, parseOpsCommonOptions(args.slice(1)));
return { ok: false, error: "unsupported-platform-infra-nginx-command", args, help: nginxHelp() };
}
function readConfig(): NginxConfig {
const config = readYamlRecord<NginxConfig>(configPath, "platform-infra-nginx");
assert(config.version === 1, `${configLabel}.version must be 1`);
assert(config.metadata.name === "nginx", `${configLabel}.metadata.name must be nginx`);
assert(Object.keys(config.targets).length > 0, `${configLabel}.targets must not be empty`);
validateArtifact(config.artifacts.image, "artifacts.image");
validateArtifact(config.artifacts.composePlugin, "artifacts.composePlugin");
assert(Number.isInteger(config.artifacts.image.bytes), `${configLabel}.artifacts.image.bytes must be an integer`);
assert(config.artifacts.image.bytes > 0, `${configLabel}.artifacts.image.bytes must be positive`);
assert(/^sha256:[a-f0-9]{64}$/u.test(config.artifacts.image.imageId), "artifacts.image.imageId is invalid");
for (const [targetId, target] of Object.entries(config.targets)) validateTarget(targetId, target);
return config;
}
function validateArtifact(artifact: Artifact, path: string): void {
absolutePath(artifact.localPath, `${path}.localPath`);
absolutePath(artifact.remotePath, `${path}.remotePath`);
assert(/^[a-f0-9]{64}$/u.test(artifact.sha256), `${configLabel}.${path}.sha256 is invalid`);
}
function validateTarget(id: string, target: NginxTarget): void {
simpleName(id, `targets.${id}`);
simpleName(target.route, `targets.${id}.route`);
const runtime = target.runtime;
for (const [key, value] of [
["workDir", runtime.workDir],
["composePath", runtime.composePath],
["envPath", runtime.envPath],
["logDir", runtime.logDir],
["backendScriptPath", target.validation.backendScriptPath],
]) absolutePath(value, `targets.${id}.${key}`);
assert(runtime.networkMode === "host", `targets.${id}.runtime.networkMode must be host`);
simpleName(runtime.projectName, `targets.${id}.runtime.projectName`);
simpleName(runtime.containerName, `targets.${id}.runtime.containerName`);
assert(runtime.routes.length > 0, `targets.${id}.runtime.routes must not be empty`);
const listenPorts = new Set<number>();
for (const route of runtime.routes) {
port(route.listenPort, `targets.${id}.runtime.routes.listenPort`);
assert(!listenPorts.has(route.listenPort), `targets.${id}.runtime.routes contains a duplicate listenPort`);
listenPorts.add(route.listenPort);
assert(route.upstreams.length > 0, `targets.${id}.runtime.routes.upstreams must not be empty`);
for (const upstream of route.upstreams) {
assert(/^[A-Za-z0-9_.-]+$/u.test(upstream.host), `targets.${id} upstream host is invalid`);
port(upstream.port, `targets.${id} upstream port`);
}
}
assert(target.validation.requestCount === 4, `targets.${id}.validation.requestCount must be 4`);
assert(target.validation.cleanupAfterSuccess, `targets.${id}.validation.cleanupAfterSuccess must be true`);
assert(target.validation.backends.length === 2, `targets.${id}.validation.backends must contain two backends`);
for (const backend of target.validation.backends) {
simpleName(backend.id, `targets.${id}.validation.backends.id`);
assert(backend.bindHost === "127.0.0.1", `targets.${id} validation backend must bind to 127.0.0.1`);
port(backend.port, `targets.${id} validation backend port`);
assert(/^[A-Za-z0-9_.@-]+\.service$/u.test(backend.unit), `targets.${id} backend unit is invalid`);
}
for (const key of Object.keys(runtime.environment)) {
assert(/^(NGINX|LOGGER)_[A-Z0-9_]+$/u.test(key), `targets.${id}.runtime.environment key ${key} is invalid`);
}
assert(target.apiKey.sourceKey === "LOGGER_API_KEY", `targets.${id}.apiKey.sourceKey must be LOGGER_API_KEY`);
assert(target.apiKey.targetKey === "LOGGER_API_KEY", `targets.${id}.apiKey.targetKey must be LOGGER_API_KEY`);
assert(target.apiKey.requestHeader === "X-API-Key", `targets.${id}.apiKey.requestHeader must be X-API-Key`);
}
function resolveTarget(config: NginxConfig, requested: string | null): { id: string; target: NginxTarget } {
const id = requested ?? config.defaults.target;
const target = config.targets[id];
if (target === undefined) throw new Error(`unknown nginx target: ${id}`);
return { id, target };
}
function plan(options: OpsCommonOptions): Record<string, unknown> {
const config = readConfig();
const { id, target } = resolveTarget(config, options.targetId);
const secret = readSecret(target);
const image = localArtifactSummary(config.artifacts.image);
const composePlugin = localArtifactSummary(config.artifacts.composePlugin);
const compose = renderCompose(config, target);
return {
ok: image.valid && composePlugin.valid,
action: "platform-infra-nginx-plan",
mutation: false,
target: targetSummary(id, target),
artifacts: { image, composePlugin },
compose: {
sha256: sha256(compose),
bytes: Buffer.byteLength(compose),
routes: renderRoutes(target.runtime.routes),
},
apiKey: secretSummary(target, secret),
validation: validationSummary(target),
next: {
dryRun: `bun scripts/cli.ts platform-infra nginx apply --target ${id} --dry-run`,
apply: `bun scripts/cli.ts platform-infra nginx apply --target ${id} --confirm`,
status: `bun scripts/cli.ts platform-infra nginx status --target ${id}`,
},
};
}
async function apply(config: UniDeskConfig, options: OpsApplyOptions): Promise<Record<string, unknown>> {
const nginx = readConfig();
const { id, target } = resolveTarget(nginx, options.targetId);
const secret = readSecret(target);
const image = localArtifactSummary(nginx.artifacts.image);
const composePlugin = localArtifactSummary(nginx.artifacts.composePlugin);
if (!image.valid || !composePlugin.valid) {
return { ok: false, action: "platform-infra-nginx-apply", mode: "artifact-invalid", image, composePlugin };
}
if (options.confirm && !options.wait) {
const job = startJob(
`platform_infra_nginx_apply_${id.toLowerCase()}`,
["bun", "scripts/cli.ts", "platform-infra", "nginx", "apply", "--target", id, "--confirm", "--wait"],
`Deploy YAML-controlled Nginx to ${id}, run four serial requests, and remove host test backends`,
);
return {
ok: true,
action: "platform-infra-nginx-apply",
mode: "async-job",
mutation: true,
target: targetSummary(id, target),
job,
statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
};
}
const compose = renderCompose(nginx, target);
if (options.dryRun) {
return {
ok: true,
action: "platform-infra-nginx-apply",
mode: "dry-run",
mutation: false,
target: targetSummary(id, target),
compose: {
sha256: sha256(compose),
bytes: Buffer.byteLength(compose),
routes: renderRoutes(target.runtime.routes),
},
apiKey: secretSummary(target, secret),
artifacts: { image, composePlugin },
validation: validationSummary(target),
};
}
const prepare = await capture(config, target.route, ["sh"], prepareRemoteScript(target, nginx));
if (prepare.exitCode !== 0) return failedApply(id, target, "prepare", prepare, secret);
const uploads = [
uploadArtifact(target.route, nginx.artifacts.image.localPath, nginx.artifacts.image.remotePath),
uploadArtifact(target.route, nginx.artifacts.composePlugin.localPath, nginx.artifacts.composePlugin.remotePath),
uploadArtifact(target.route, secret.sourcePath, target.runtime.envPath),
];
const failedUpload = uploads.find((upload) => !upload.ok);
if (failedUpload !== undefined) {
return {
ok: false,
action: "platform-infra-nginx-apply",
mode: "confirmed",
stage: "upload",
target: targetSummary(id, target),
uploads,
apiKey: secretSummary(target, secret),
};
}
const remote = await capture(config, target.route, ["sh"], applyRemoteScript(nginx, target, compose));
const parsed = parseJsonOutput(remote.stdout);
return {
ok: remote.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-nginx-apply",
mode: "confirmed",
mutation: true,
target: targetSummary(id, target),
uploads,
apiKey: secretSummary(target, secret),
validation: parsed ?? compactCapture(remote, { full: true }),
remote: compactCapture(remote, { full: options.full || remote.exitCode !== 0 }),
next: { status: `bun scripts/cli.ts platform-infra nginx status --target ${id}` },
};
}
async function status(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
const nginx = readConfig();
const { id, target } = resolveTarget(nginx, options.targetId);
const secret = readSecret(target);
const remote = await capture(config, target.route, ["sh"], statusRemoteScript(nginx, target));
const parsed = parseJsonOutput(remote.stdout);
return {
ok: remote.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-nginx-status",
target: targetSummary(id, target),
apiKey: secretSummary(target, secret),
summary: parsed,
remote: compactCapture(remote, { full: options.full || remote.exitCode !== 0 }),
...(options.raw ? { raw: remote } : {}),
};
}
function prepareRemoteScript(target: NginxTarget, config: NginxConfig): string {
return `set -eu
install -d -m 0755 ${shQuote(target.runtime.workDir)} ${shQuote(target.runtime.logDir)}
install -d -m 0755 ${shQuote(parentPath(config.artifacts.image.remotePath))}
install -d -m 0755 ${shQuote(parentPath(config.artifacts.composePlugin.remotePath))}
`;
}
function applyRemoteScript(config: NginxConfig, target: NginxTarget, compose: string): string {
const backendScript = renderBackendScript();
const unitFiles = target.validation.backends.map((backend) => ({
path: `/etc/systemd/system/${backend.unit}`,
content: renderBackendUnit(target.validation.backendScriptPath, backend),
}));
const firstRoute = target.runtime.routes[0]!;
const expected = target.validation.backends.map((backend) => backend.id).join(",");
const actualExpected = Array.from(
{ length: target.validation.requestCount },
(_, index) => target.validation.backends[index % target.validation.backends.length]!.id,
).join(",");
const unitNames = target.validation.backends.map((backend) => shQuote(backend.unit)).join(" ");
const unitPaths = target.validation.backends
.map((backend) => shQuote(`/etc/systemd/system/${backend.unit}`))
.join(" ");
const unitWrites = unitFiles
.map((unit) => {
const encoded = shQuote(Buffer.from(unit.content).toString("base64"));
return `printf '%s' ${encoded} | base64 -d > ${shQuote(unit.path)}`;
})
.join("\n");
const adminBase = [
"http://127.0.0.1:",
target.runtime.environment.NGINX_ADMIN_PORT,
target.runtime.environment.LOGGER_ADMIN_PREFIX,
].join("");
const validationFormat = [
'{"ok":true,"requests":',
String(target.validation.requestCount),
',"backendSequence":"%s","loggedSequence":"%s",',
'"unauthorizedStatus":%s,"activeTestBackends":%s,"logCleared":true}\\n',
].join("");
assert(expected.length > 0, "validation backends are missing");
return `set -eu
image_artifact=${shQuote(config.artifacts.image.remotePath)}
plugin=${shQuote(config.artifacts.composePlugin.remotePath)}
env_file=${shQuote(target.runtime.envPath)}
compose_file=${shQuote(target.runtime.composePath)}
log_file=${shQuote(`${target.runtime.logDir}/requests.jsonl`)}
backend_script=${shQuote(target.validation.backendScriptPath)}
cleanup_backends() {
systemctl stop ${unitNames} >/dev/null 2>&1 || true
systemctl disable ${unitNames} >/dev/null 2>&1 || true
rm -f ${unitPaths} "$backend_script"
systemctl daemon-reload >/dev/null 2>&1 || true
}
trap cleanup_backends EXIT
[ "$(sha256sum "$image_artifact" | awk '{print $1}')" = ${shQuote(config.artifacts.image.sha256)} ]
[ "$(sha256sum "$plugin" | awk '{print $1}')" = ${shQuote(config.artifacts.composePlugin.sha256)} ]
chmod 0755 "$plugin"
docker compose version >/dev/null
chmod 0600 "$env_file"
printf '%s' ${shQuote(Buffer.from(compose).toString("base64"))} | base64 -d > "$compose_file"
printf '%s' ${shQuote(Buffer.from(backendScript).toString("base64"))} | base64 -d > "$backend_script"
chmod 0755 "$backend_script"
${unitWrites}
systemctl daemon-reload
systemctl enable --now ${unitNames} >/dev/null
for backend_port in ${target.validation.backends.map((backend) => backend.port).join(" ")}; do
for attempt in $(seq 1 10); do
if curl -fsS http://127.0.0.1:"$backend_port"/ >/dev/null; then break; fi
[ "$attempt" -lt 10 ] || exit 1
sleep 1
done
done
docker load -i "$image_artifact" >/dev/null
loaded_image_id=$(docker image inspect ${shQuote(config.artifacts.image.image)} --format '{{.Id}}')
[ "$loaded_image_id" = ${shQuote(config.artifacts.image.imageId)} ]
docker compose -f "$compose_file" -p ${shQuote(target.runtime.projectName)} up -d --force-recreate
for attempt in $(seq 1 20); do
if wget -qO- ${adminBase}/health >/dev/null; then break; fi
[ "$attempt" -lt 20 ] || exit 1
sleep 1
done
actual=
for sequence in $(seq 1 ${target.validation.requestCount}); do
backend=$(curl -fsS -D - -o /dev/null http://127.0.0.1:${firstRoute.listenPort}/validation?sequence="$sequence" \
| awk 'BEGIN { IGNORECASE=1 } /^X-Backend:/ { gsub("\\r", "", $2); print $2; exit }')
actual="\${actual}\${actual:+,}\${backend}"
done
[ "$actual" = ${shQuote(actualExpected)} ]
set -a
. "$env_file"
set +a
logger_curl() {
printf 'header = "${target.apiKey.requestHeader}: %s"\n' "$LOGGER_API_KEY" | curl --config - "$@"
}
unauthorized=$(curl -sS -o /dev/null -w '%{http_code}' \
${adminBase}/logs/info)
[ "$unauthorized" = 401 ]
logger_curl -fsS -o /tmp/nginx-logger-info.json \
${adminBase}/logs/info
logger_curl -fsS -o /tmp/nginx-logger-tail.jsonl \
'${adminBase}/logs/tail?lines=${target.validation.requestCount}'
logger_curl -fsS -o /tmp/nginx-logger-download.jsonl \
${adminBase}/logs/download
[ -s /tmp/nginx-logger-download.jsonl ]
logged=$(python3 -c 'import json,sys; print(",".join(json.loads(x)["backend"] for x in open(sys.argv[1])))' \
/tmp/nginx-logger-tail.jsonl)
[ "$logged" = ${shQuote(actualExpected)} ]
logger_curl -fsS -X POST -o /tmp/nginx-logger-clear.json \
${adminBase}/logs/clear
[ ! -s "$log_file" ]
cleanup_backends
trap - EXIT
active_backends=0
for unit in ${unitNames}; do
systemctl is-active --quiet "$unit" && active_backends=$((active_backends + 1)) || true
done
printf ${shQuote(validationFormat)} \
"$actual" "$logged" "$unauthorized" "$active_backends"
`;
}
function statusRemoteScript(config: NginxConfig, target: NginxTarget): string {
const adminPort = target.runtime.environment.NGINX_ADMIN_PORT;
const prefix = target.runtime.environment.LOGGER_ADMIN_PREFIX;
const statusFormat = [
'{"ok":%s,"running":%s,"imageId":"%s","expectedImageId":"%s",',
'"healthStatus":%s,"logBytes":%s,"activeTestBackends":%s}\\n',
].join("");
return `set -eu
container=${shQuote(target.runtime.containerName)}
running=false
if [ "$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null || true)" = true ]; then running=true; fi
image_id=$(docker inspect -f '{{.Image}}' "$container" 2>/dev/null || true)
health_code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:${adminPort}${prefix}/health 2>/dev/null || true)
log_bytes=$(wc -c < ${shQuote(`${target.runtime.logDir}/requests.jsonl`)} 2>/dev/null || printf 0)
active_backends=0
for unit in ${target.validation.backends.map((backend) => shQuote(backend.unit)).join(" ")}; do
systemctl is-active --quiet "$unit" && active_backends=$((active_backends + 1)) || true
done
ok=false
[ "$running" = true ] && [ "$image_id" = ${shQuote(config.artifacts.image.imageId)} ] && \
[ "$health_code" = 200 ] && [ "$active_backends" -eq 0 ] && ok=true
printf ${shQuote(statusFormat)} \
"$ok" "$running" "$image_id" ${shQuote(config.artifacts.image.imageId)} \
"\${health_code:-0}" "\${log_bytes:-0}" "$active_backends"
`;
}
function renderCompose(config: NginxConfig, target: NginxTarget): string {
const environment = {
...target.runtime.environment,
NGINX_ROUTES: renderRoutes(target.runtime.routes),
};
const envLines = Object.entries(environment)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => ` ${key}: ${yamlString(value.replaceAll("$", () => "$$"))}`)
.join("\n");
return `name: ${target.runtime.projectName}
services:
nginx:
image: ${config.artifacts.image.image}
container_name: ${target.runtime.containerName}
restart: ${target.runtime.restart}
network_mode: ${target.runtime.networkMode}
mem_limit: ${target.runtime.resources.memory}
cpus: ${yamlString(target.runtime.resources.cpus)}
pids_limit: ${target.runtime.resources.pidsLimit}
env_file:
- ${target.runtime.envPath}
environment:
${envLines}
volumes:
- ${target.runtime.logDir}:/var/log/logger
logging:
driver: json-file
options:
max-size: 5m
max-file: "2"
`;
}
function renderBackendScript(): string {
return `#!/usr/bin/env python3
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
host, port, backend = sys.argv[1], int(sys.argv[2]), sys.argv[3]
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
body = ('{"backend":"%s"}\\n' % backend).encode("ascii")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("X-Backend", backend)
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
return
HTTPServer((host, port), Handler).serve_forever()
`;
}
function renderBackendUnit(scriptPath: string, backend: Backend): string {
return `[Unit]
Description=Nginx validation backend ${backend.id}
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 ${scriptPath} ${backend.bindHost} ${backend.port} ${backend.id}
Restart=on-failure
MemoryMax=32M
CPUQuota=10%
[Install]
WantedBy=multi-user.target
`;
}
function readSecret(target: NginxTarget): SecretMaterial {
const sourcePath = isAbsolute(target.apiKey.sourceRef)
? target.apiKey.sourceRef
: join(repoRoot, target.apiKey.sourceRef);
assert(existsSync(sourcePath), `${target.apiKey.sourceRef} is missing`);
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
const value = values[target.apiKey.sourceKey];
assert(
typeof value === "string" && value.length >= 16,
`${target.apiKey.sourceRef} is missing ${target.apiKey.sourceKey}`,
);
return {
sourcePath,
value,
fingerprint: fingerprintValues({ [target.apiKey.sourceKey]: value }, [target.apiKey.sourceKey]),
};
}
function localArtifactSummary(artifact: Artifact & { bytes?: number }): Record<string, unknown> & { valid: boolean } {
const present = existsSync(artifact.localPath);
const bytes = present ? statSync(artifact.localPath).size : 0;
const digest = present ? sha256(readFileSync(artifact.localPath)) : null;
const valid = present && digest === artifact.sha256 && (artifact.bytes === undefined || bytes === artifact.bytes);
return { present, bytes, sha256: digest, expectedSha256: artifact.sha256, valid };
}
function uploadArtifact(route: string, localPath: string, remotePath: string): Record<string, unknown> {
const result = Bun.spawnSync(["trans", route, "upload", localPath, remotePath], {
stdout: "pipe",
stderr: "pipe",
});
const stdout = new TextDecoder().decode(result.stdout);
const parsed = parseJsonOutput(stdout);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
localPath,
remotePath,
bytes: parsed?.bytes ?? 0,
sha256: parsed?.sha256 ?? null,
verified: parsed?.verified === true,
stderrBytes: result.stderr.length,
};
}
function targetSummary(id: string, target: NginxTarget): Record<string, unknown> {
return {
id,
route: target.route,
runtime: "host-docker-compose",
container: target.runtime.containerName,
resources: target.runtime.resources,
routes: target.runtime.routes,
adminPort: target.runtime.environment.NGINX_ADMIN_PORT,
};
}
function validationSummary(target: NginxTarget): Record<string, unknown> {
return {
requestCount: target.validation.requestCount,
concurrency: 1,
backends: "host-python-systemd-temporary",
cleanupAfterSuccess: target.validation.cleanupAfterSuccess,
};
}
function secretSummary(target: NginxTarget, secret: SecretMaterial): Record<string, unknown> {
return {
sourceRef: target.apiKey.sourceRef,
sourceKey: target.apiKey.sourceKey,
targetKey: target.apiKey.targetKey,
present: true,
fingerprint: secret.fingerprint,
valuesPrinted: false,
};
}
function failedApply(
id: string,
target: NginxTarget,
stage: string,
remote: Awaited<ReturnType<typeof capture>>,
secret: SecretMaterial,
): Record<string, unknown> {
return {
ok: false,
action: "platform-infra-nginx-apply",
mode: "confirmed",
stage,
target: targetSummary(id, target),
apiKey: secretSummary(target, secret),
remote: compactCapture(remote, { full: true }),
};
}
function renderRoutes(routes: Route[]): string {
return routes
.map((route) => {
const upstreams = route.upstreams.map((upstream) => `${upstream.host}:${upstream.port}`).join(",");
return `${route.listenPort}=${upstreams}`;
})
.join(";");
}
function sha256(value: string | Buffer): string {
return createHash("sha256").update(value).digest("hex");
}
function yamlString(value: string): string {
return JSON.stringify(value);
}
function parentPath(path: string): string {
return path.slice(0, path.lastIndexOf("/")) || "/";
}
function absolutePath(value: string, path: string): void {
assert(typeof value === "string" && isAbsolute(value), `${configLabel}.${path} must be an absolute path`);
}
function simpleName(value: string, path: string): void {
assert(typeof value === "string" && /^[A-Za-z0-9._-]+$/u.test(value), `${configLabel}.${path} is invalid`);
}
function port(value: number, path: string): void {
assert(Number.isInteger(value) && value >= 1 && value <= 65535, `${configLabel}.${path} must be a valid port`);
}
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
+22 -1
View File
@@ -391,7 +391,24 @@ export interface ManagedResourceCleanupPlan {
export function platformInfraHelp(): unknown {
const target = sub2ApiHelpTargetSummary();
return {
command: "platform-infra sub2api|sub2rank|langbot|n8n|webterm|wechat-archive|observability|secret-plane|kafka|gitea|pipelines-as-code ...",
command: [
"platform-infra",
[
"sub2api",
"sub2rank",
"nginx",
"langbot",
"n8n",
"webterm",
"wechat-archive",
"observability",
"secret-plane",
"kafka",
"gitea",
"pipelines-as-code",
].join("|"),
"...",
].join(" "),
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]",
@@ -413,6 +430,10 @@ export function platformInfraHelp(): unknown {
"bun scripts/cli.ts platform-infra sub2rank validate [--target NC01] [--full|--raw]",
"bun scripts/cli.ts platform-infra egress-proxy traffic --target D601 --sample-seconds 15",
"bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark --targets D601,D518 --profile no-mirror-600m --dry-run",
"bun scripts/cli.ts platform-infra nginx plan [--target PK01]",
"bun scripts/cli.ts platform-infra nginx apply [--target PK01] --dry-run",
"bun scripts/cli.ts platform-infra nginx apply [--target PK01] --confirm",
"bun scripts/cli.ts platform-infra nginx status [--target PK01] [--full|--raw]",
"bun scripts/cli.ts platform-infra langbot plan [--target G14]",
"bun scripts/cli.ts platform-infra langbot apply [--target G14] --confirm",
"bun scripts/cli.ts platform-infra langbot status [--target G14] [--full|--raw]",
+4
View File
@@ -50,6 +50,10 @@ export async function runPlatformInfraCommand(config: UniDeskConfig, args: strin
const { runLangBotCommand } = await import("../platform-infra-langbot");
return await runLangBotCommand(config, args.slice(1));
}
if (target === "nginx") {
const { runPlatformInfraNginxCommand } = await import("../platform-infra-nginx");
return await runPlatformInfraNginxCommand(config, args.slice(1));
}
if (target === "n8n") {
const { runN8nCommand } = await import("../platform-infra-n8n");
return await runN8nCommand(config, args.slice(1));