diff --git a/config/platform-infra/nginx.yaml b/config/platform-infra/nginx.yaml index 794462c2..026b94d5 100644 --- a/config/platform-infra/nginx.yaml +++ b/config/platform-infra/nginx.yaml @@ -20,6 +20,12 @@ artifacts: localPath: /usr/libexec/docker/cli-plugins/docker-compose remotePath: /usr/local/lib/docker/cli-plugins/docker-compose sha256: c0ef2e286ba1c45bf1868b70a1c71d30a802bb3996888f2f986c3166f9e45f57 + distribution: + route: NC01 + workDir: /tmp/unidesk-nginx-artifacts + bindHost: 0.0.0.0 + port: 19191 + publicBaseUrl: http://152.53.229.148:19191 targets: PK01: diff --git a/scripts/src/platform-infra-nginx.ts b/scripts/src/platform-infra-nginx.ts index c9dca3dc..84df3b8b 100644 --- a/scripts/src/platform-infra-nginx.ts +++ b/scripts/src/platform-infra-nginx.ts @@ -34,6 +34,14 @@ interface ImageArtifact extends Artifact { bytes: number; } +interface ArtifactDistribution { + route: string; + workDir: string; + bindHost: string; + port: number; + publicBaseUrl: string; +} + interface Route { listenPort: number; upstreams: Array<{ host: string; port: number }>; @@ -80,7 +88,11 @@ interface NginxConfig { kind: "platform-infra-nginx"; metadata: { name: string }; defaults: { target: string }; - artifacts: { image: ImageArtifact; composePlugin: Artifact }; + artifacts: { + image: ImageArtifact; + composePlugin: Artifact; + distribution: ArtifactDistribution; + }; targets: Record; } @@ -123,6 +135,7 @@ function readConfig(): NginxConfig { 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"); + validateDistribution(config.artifacts.distribution); 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"); @@ -130,6 +143,18 @@ function readConfig(): NginxConfig { return config; } +function validateDistribution(distribution: ArtifactDistribution): void { + simpleName(distribution.route, "artifacts.distribution.route"); + 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"); + const expectedSuffix = `:${distribution.port}`; + assert( + distribution.publicBaseUrl.startsWith("http://") && distribution.publicBaseUrl.endsWith(expectedSuffix), + "artifacts.distribution.publicBaseUrl must be HTTP and end with the declared port", + ); +} + function validateArtifact(artifact: Artifact, path: string): void { absolutePath(artifact.localPath, `${path}.localPath`); absolutePath(artifact.remotePath, `${path}.remotePath`); @@ -258,11 +283,14 @@ async function apply(config: UniDeskConfig, options: OpsApplyOptions): Promise !upload.ok); @@ -277,7 +305,25 @@ async function apply(config: UniDeskConfig, options: OpsApplyOptions): Promise>; + try { + remote = await capture( + config, + target.route, + ["sh"], + applyRemoteScript(nginx, target, compose, { + image: `${distribution.publicBaseUrl}/${fileName(distributedImage)}`, + composePlugin: `${distribution.publicBaseUrl}/${fileName(distributedPlugin)}`, + }), + ); + } finally { + await capture(config, distribution.route, ["sh"], stopArtifactSourceScript(distribution)); + } const parsed = parseJsonOutput(remote.stdout); return { ok: remote.exitCode === 0 && parsed?.ok === true, @@ -310,15 +356,43 @@ async function status(config: UniDeskConfig, options: OpsCommonOptions): Promise }; } -function prepareRemoteScript(target: NginxTarget, config: NginxConfig): string { +function prepareRemoteScript(target: NginxTarget): 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 { +function startArtifactSourceScript(distribution: ArtifactDistribution): string { + return `set -eu +dir=${shQuote(distribution.workDir)} +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 +`; +} + +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 applyRemoteScript( + config: NginxConfig, + target: NginxTarget, + compose: string, + artifactUrls: { image: string; composePlugin: string }, +): string { const backendScript = renderBackendScript(); const unitFiles = target.validation.backends.map((backend) => ({ path: `/etc/systemd/system/${backend.unit}`, @@ -366,6 +440,10 @@ cleanup_backends() { systemctl daemon-reload >/dev/null 2>&1 || true } trap cleanup_backends EXIT +curl -fsSL --retry 2 --connect-timeout 5 --max-time 120 \ + ${shQuote(artifactUrls.image)} -o "$image_artifact" +curl -fsSL --retry 2 --connect-timeout 5 --max-time 120 \ + ${shQuote(artifactUrls.composePlugin)} -o "$plugin" [ "$(sha256sum "$image_artifact" | awk '{print $1}')" = ${shQuote(config.artifacts.image.sha256)} ] [ "$(sha256sum "$plugin" | awk '{print $1}')" = ${shQuote(config.artifacts.composePlugin.sha256)} ] chmod 0755 "$plugin" @@ -646,8 +724,8 @@ function yamlString(value: string): string { return JSON.stringify(value); } -function parentPath(path: string): string { - return path.slice(0, path.lastIndexOf("/")) || "/"; +function fileName(path: string): string { + return path.slice(path.lastIndexOf("/") + 1); } function absolutePath(value: string, path: string): void {