fix(platform-infra): serve nginx artifacts from local HTTP
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)} ]
|
||||
|
||||
Reference in New Issue
Block a user