fix(platform-infra): stage nginx artifacts through NC01

This commit is contained in:
Codex
2026-07-13 14:19:08 +02:00
parent 635589d79e
commit df4d62b937
2 changed files with 95 additions and 11 deletions
+6
View File
@@ -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:
+89 -11
View File
@@ -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<string, NginxTarget>;
}
@@ -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<R
};
}
const prepare = await capture(config, target.route, ["sh"], prepareRemoteScript(target, nginx));
const prepare = await capture(config, target.route, ["sh"], prepareRemoteScript(target));
if (prepare.exitCode !== 0) return failedApply(id, target, "prepare", prepare, secret);
const distribution = nginx.artifacts.distribution;
const distributedImage = `${distribution.workDir}/${fileName(nginx.artifacts.image.remotePath)}`;
const distributedPlugin = `${distribution.workDir}/${fileName(nginx.artifacts.composePlugin.remotePath)}`;
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(distribution.route, nginx.artifacts.image.localPath, distributedImage),
uploadArtifact(distribution.route, nginx.artifacts.composePlugin.localPath, distributedPlugin),
uploadArtifact(target.route, secret.sourcePath, target.runtime.envPath),
];
const failedUpload = uploads.find((upload) => !upload.ok);
@@ -277,7 +305,25 @@ async function apply(config: UniDeskConfig, options: OpsApplyOptions): Promise<R
apiKey: secretSummary(target, secret),
};
}
const remote = await capture(config, target.route, ["sh"], applyRemoteScript(nginx, target, compose));
const source = await capture(config, distribution.route, ["sh"], startArtifactSourceScript(distribution));
if (source.exitCode !== 0) {
await capture(config, distribution.route, ["sh"], stopArtifactSourceScript(distribution));
return failedApply(id, target, "artifact-source", source, secret);
}
let remote: Awaited<ReturnType<typeof capture>>;
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 {