From 41356493d648d139ff22ee74ed979cd85b969d05 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Jul 2026 15:22:46 +0200 Subject: [PATCH] fix(platform-infra): serve nginx artifacts from local HTTP --- config/platform-infra/nginx.yaml | 4 +- scripts/src/platform-infra-nginx.ts | 124 +++++++++++++++++----------- 2 files changed, 76 insertions(+), 52 deletions(-) diff --git a/config/platform-infra/nginx.yaml b/config/platform-infra/nginx.yaml index 1f1ad5fd..78fe6f4b 100644 --- a/config/platform-infra/nginx.yaml +++ b/config/platform-infra/nginx.yaml @@ -21,9 +21,7 @@ artifacts: remotePath: /usr/local/lib/docker/cli-plugins/docker-compose sha256: c0ef2e286ba1c45bf1868b70a1c71d30a802bb3996888f2f986c3166f9e45f57 distribution: - route: NC01 - imagePath: /opt/unidesk/artifacts/nginx/nginx-lb-logger-1.0.0.tar.gz - composePluginPath: /usr/libexec/docker/cli-plugins/docker-compose + mode: local-http workDir: /tmp/unidesk-nginx-artifacts bindHost: 0.0.0.0 port: 19191 diff --git a/scripts/src/platform-infra-nginx.ts b/scripts/src/platform-infra-nginx.ts index 1a642345..ee4896b2 100644 --- a/scripts/src/platform-infra-nginx.ts +++ b/scripts/src/platform-infra-nginx.ts @@ -1,11 +1,14 @@ import { createHash } from "node:crypto"; +import { spawn } from "node:child_process"; import { chmodSync, existsSync, mkdtempSync, + mkdirSync, readFileSync, rmSync, statSync, + symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -45,9 +48,7 @@ interface ImageArtifact extends Artifact { } interface ArtifactDistribution { - route: string; - imagePath: string; - composePluginPath: string; + mode: "local-http"; workDir: string; bindHost: string; port: number; @@ -163,9 +164,7 @@ function readConfig(): NginxConfig { } function validateDistribution(distribution: ArtifactDistribution): void { - simpleName(distribution.route, "artifacts.distribution.route"); - absolutePath(distribution.imagePath, "artifacts.distribution.imagePath"); - absolutePath(distribution.composePluginPath, "artifacts.distribution.composePluginPath"); + assert(distribution.mode === "local-http", "artifacts.distribution.mode must be local-http"); absolutePath(distribution.workDir, "artifacts.distribution.workDir"); assert(distribution.bindHost === "0.0.0.0", "artifacts.distribution.bindHost must be 0.0.0.0"); port(distribution.port, "artifacts.distribution.port"); @@ -324,16 +323,7 @@ async function apply(config: UniDeskConfig, options: OpsApplyOptions): Promise { const distribution = config.artifacts.distribution; const imageLink = `${distribution.workDir}/${fileName(config.artifacts.image.remotePath)}`; const pluginLink = `${distribution.workDir}/${fileName(config.artifacts.composePlugin.remotePath)}`; - return `set -eu -dir=${shQuote(distribution.workDir)} -rm -rf "$dir" -install -d -m 0755 "$dir" -[ "$(sha256sum ${shQuote(distribution.imagePath)} | awk '{print $1}')" = \ - ${shQuote(config.artifacts.image.sha256)} ] -[ "$(sha256sum ${shQuote(distribution.composePluginPath)} | awk '{print $1}')" = \ - ${shQuote(config.artifacts.composePlugin.sha256)} ] -ln -s ${shQuote(distribution.imagePath)} ${shQuote(imageLink)} -ln -s ${shQuote(distribution.composePluginPath)} ${shQuote(pluginLink)} -pid_file="$dir/server.pid" -[ ! -f "$pid_file" ] || kill "$(cat "$pid_file")" 2>/dev/null || true -nohup python3 -m http.server ${distribution.port} --bind ${shQuote(distribution.bindHost)} \ - --directory "$dir" >"$dir/server.log" 2>&1 & -printf '%s\n' "$!" > "$pid_file" -for attempt in $(seq 1 10); do - if kill -0 "$(cat "$pid_file")" 2>/dev/null && \ - curl -fsS http://127.0.0.1:${distribution.port}/ >/dev/null; then exit 0; fi - [ "$attempt" -lt 10 ] || exit 1 - sleep 1 -done -`; + stopOwnedLocalServer(distribution.workDir); + rmSync(distribution.workDir, { recursive: true, force: true }); + mkdirSync(distribution.workDir, { recursive: true, mode: 0o755 }); + symlinkSync(config.artifacts.image.localPath, imageLink); + symlinkSync(config.artifacts.composePlugin.localPath, pluginLink); + const child = spawn( + "python3", + [ + "-m", + "http.server", + String(distribution.port), + "--bind", + distribution.bindHost, + "--directory", + distribution.workDir, + ], + { detached: true, stdio: "ignore" }, + ); + child.unref(); + if (child.pid === undefined) throw new Error("failed to start local artifact HTTP server"); + writeFileSync(join(distribution.workDir, "server.pid"), `${child.pid}\n`, { mode: 0o600 }); + const expectedBytes = String(config.artifacts.image.bytes); + for (let attempt = 1; attempt <= 20; attempt += 1) { + try { + const response = await fetch( + `http://127.0.0.1:${distribution.port}/${fileName(imageLink)}`, + { method: "HEAD" }, + ); + if (response.ok && response.headers.get("content-length") === expectedBytes) { + return { pid: child.pid, workDir: distribution.workDir }; + } + } catch { + // The server may still be binding its socket. + } + await Bun.sleep(100); + } + stopLocalArtifactSource({ pid: child.pid, workDir: distribution.workDir }); + throw new Error("local artifact HTTP server did not become ready"); } -function stopArtifactSourceScript(distribution: ArtifactDistribution): string { - return `set -eu -pid_file=${shQuote(`${distribution.workDir}/server.pid`)} -[ ! -f "$pid_file" ] || kill "$(cat "$pid_file")" 2>/dev/null || true -rm -rf ${shQuote(distribution.workDir)} -`; +function stopOwnedLocalServer(workDir: string): void { + const pidPath = join(workDir, "server.pid"); + if (!existsSync(pidPath)) return; + const pid = Number(readFileSync(pidPath, "utf8").trim()); + if (Number.isInteger(pid) && pid > 1) { + try { + process.kill(pid, "SIGTERM"); + } catch { + // A previous server may already have exited. + } + } +} + +function stopLocalArtifactSource(source: { pid: number; workDir: string }): void { + try { + process.kill(source.pid, "SIGTERM"); + } catch { + // The server may already have exited. + } + rmSync(source.workDir, { recursive: true, force: true }); } function remoteJobDir(target: NginxTarget): string { @@ -562,9 +584,13 @@ trap cleanup_backends EXIT gzip -dc "$env_file.gz" > "$env_file" chmod 0600 "$env_file" rm -f "$env_file.gz" -rm -f "$image_artifact" "$plugin" -curl -fsSL --retry 2 --connect-timeout 5 --max-time 3600 \ - ${shQuote(artifactUrls.image)} -o "$image_artifact" +image_sha=$(sha256sum "$image_artifact" 2>/dev/null | awk '{print $1}' || true) +if [ "$image_sha" != ${shQuote(config.artifacts.image.sha256)} ]; then + rm -f "$image_artifact" + curl -fsSL --retry 2 --connect-timeout 5 --max-time 3600 \ + ${shQuote(artifactUrls.image)} -o "$image_artifact" +fi +rm -f "$plugin" curl -fsSL --retry 2 --connect-timeout 5 --max-time 3600 \ ${shQuote(artifactUrls.composePlugin)} -o "$plugin" [ "$(sha256sum "$image_artifact" | awk '{print $1}')" = ${shQuote(config.artifacts.image.sha256)} ]