fix(platform-infra): serve nginx artifacts from local HTTP

This commit is contained in:
Codex
2026-07-13 15:22:46 +02:00
parent 700794e4a9
commit 41356493d6
2 changed files with 76 additions and 52 deletions
+1 -3
View File
@@ -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
+75 -49
View File
@@ -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<R
apiKey: secretSummary(target, secret),
};
}
const source = await capture(
config,
distribution.route,
["sh"],
startArtifactSourceScript(nginx),
);
if (source.exitCode !== 0) {
await capture(config, distribution.route, ["sh"], stopArtifactSourceScript(distribution));
return failedApply(id, target, "artifact-source", source, secret);
}
const source = await startLocalArtifactSource(nginx);
let outcome: RemoteJobOutcome;
try {
const remoteScript = applyRemoteScript(nginx, target, compose, {
@@ -349,7 +339,7 @@ async function apply(config: UniDeskConfig, options: OpsApplyOptions): Promise<R
if (start.exitCode !== 0) return failedApply(id, target, "remote-job-start", start, secret);
outcome = await waitRemoteJob(config, target);
} finally {
await capture(config, distribution.route, ["sh"], stopArtifactSourceScript(distribution));
stopLocalArtifactSource(source);
}
const parsed = parseJsonOutput(outcome.stdout);
return {
@@ -416,40 +406,72 @@ install -d -m 0755 ${shQuote(target.runtime.workDir)} ${shQuote(target.runtime.l
`;
}
function startArtifactSourceScript(config: NginxConfig): string {
async function startLocalArtifactSource(
config: NginxConfig,
): Promise<{ pid: number; workDir: string }> {
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)} ]