fix(platform-infra): stage nginx artifacts through NC01
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user