fix: prewarm d601 dev k3s images
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ function help(): unknown {
|
||||
{ command: "decision list [--type ...] [--status ...] [--level ...] [--linked-goal-id id] [--limit N]", description: "List Decision Center records through the user-service proxy." },
|
||||
{ command: "decision show <id>", description: "Show one Decision Center record." },
|
||||
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest; --env uses fixed environment refs for dry-run planning in Phase 0." },
|
||||
{ command: "dev-env validate [--manifest path] [--kubectl-dry-run]", description: "Validate the D601 unidesk-dev namespace/database foundation manifest and production DB URL guardrails without applying resources." },
|
||||
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
|
||||
{ command: "schedule list|get|runs|run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N." },
|
||||
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
|
||||
{ command: "codex deploy <commitId> [--provider-id D601] [--timeout-ms N]", description: "Compatibility wrapper for deploy apply --service code-queue with a temporary repo+commit manifest." },
|
||||
|
||||
+181
-7
@@ -1,10 +1,17 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { runCommand } from "./command";
|
||||
import { repoRoot, rootPath } from "./config";
|
||||
import { startJob } from "./jobs";
|
||||
|
||||
const defaultManifest = "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml";
|
||||
const devNamespace = "unidesk-dev";
|
||||
const prodNamespace = "unidesk";
|
||||
const defaultProviderId = "D601";
|
||||
const defaultProxyUrl = "http://127.0.0.1:18789";
|
||||
const defaultPrewarmImages = [
|
||||
"postgres:16-alpine",
|
||||
"rancher/mirrored-library-busybox:1.36.1",
|
||||
];
|
||||
const foundationRequiredKinds = new Set([
|
||||
"Namespace/unidesk-dev",
|
||||
"Secret/unidesk-dev-runtime-secrets",
|
||||
@@ -30,17 +37,36 @@ interface ManifestDocument {
|
||||
namespace: string | null;
|
||||
}
|
||||
|
||||
interface DevEnvOptions {
|
||||
interface ValidateOptions {
|
||||
manifestPath: string;
|
||||
kubectlDryRun: boolean;
|
||||
}
|
||||
|
||||
interface PrewarmImagesOptions {
|
||||
providerId: string;
|
||||
images: string[];
|
||||
proxyUrl: string;
|
||||
pullMissing: boolean;
|
||||
pullTimeoutMs: number;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
function isHelpArg(arg: string | undefined): boolean {
|
||||
return arg === "help" || arg === "--help" || arg === "-h";
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): DevEnvOptions {
|
||||
const options: DevEnvOptions = { manifestPath: defaultManifest, kubectlDryRun: false };
|
||||
function positiveInteger(value: string | undefined, option: string): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${option} must be a positive integer`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function rejectUnsafeToken(value: string, option: string): void {
|
||||
if (/[\s\x00-\x1f\x7f]/u.test(value)) throw new Error(`${option} must not contain whitespace or control characters`);
|
||||
}
|
||||
|
||||
function parseValidateOptions(args: string[]): ValidateOptions {
|
||||
const options: ValidateOptions = { manifestPath: defaultManifest, kubectlDryRun: false };
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--manifest") {
|
||||
@@ -57,6 +83,51 @@ function parseOptions(args: string[]): DevEnvOptions {
|
||||
return options;
|
||||
}
|
||||
|
||||
function parsePrewarmImagesOptions(args: string[]): PrewarmImagesOptions {
|
||||
const images: string[] = [];
|
||||
const options: PrewarmImagesOptions = {
|
||||
providerId: defaultProviderId,
|
||||
images,
|
||||
proxyUrl: defaultProxyUrl,
|
||||
pullMissing: true,
|
||||
pullTimeoutMs: 300_000,
|
||||
dryRun: false,
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--provider-id") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.length === 0) throw new Error("--provider-id requires a value");
|
||||
rejectUnsafeToken(value, "--provider-id");
|
||||
options.providerId = value;
|
||||
index += 1;
|
||||
} else if (arg === "--image") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.length === 0) throw new Error("--image requires a value");
|
||||
rejectUnsafeToken(value, "--image");
|
||||
images.push(value);
|
||||
index += 1;
|
||||
} else if (arg === "--proxy-url") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.length === 0) throw new Error("--proxy-url requires a value");
|
||||
rejectUnsafeToken(value, "--proxy-url");
|
||||
options.proxyUrl = value;
|
||||
index += 1;
|
||||
} else if (arg === "--pull-timeout-ms") {
|
||||
options.pullTimeoutMs = positiveInteger(args[index + 1], "--pull-timeout-ms");
|
||||
index += 1;
|
||||
} else if (arg === "--no-pull") {
|
||||
options.pullMissing = false;
|
||||
} else if (arg === "--dry-run") {
|
||||
options.dryRun = true;
|
||||
} else if (!isHelpArg(arg)) {
|
||||
throw new Error(`unknown dev-env prewarm-images option: ${arg}`);
|
||||
}
|
||||
}
|
||||
if (options.images.length === 0) options.images = [...defaultPrewarmImages];
|
||||
return options;
|
||||
}
|
||||
|
||||
function scalarAfter(text: string, key: string): string | null {
|
||||
const match = text.match(new RegExp(`^\\s*${key}:\\s*"?([^"\\n#]+)"?\\s*(?:#.*)?$`, "mu"));
|
||||
return match?.[1]?.trim() ?? null;
|
||||
@@ -135,17 +206,92 @@ function kubectlDryRun(manifestPath: string): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function prewarmImagesScript(options: PrewarmImagesOptions): string {
|
||||
const imageArray = options.images.map(shellQuote).join(" ");
|
||||
const pullTimeoutSeconds = Math.max(1, Math.ceil(options.pullTimeoutMs / 1000));
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
"root_exec() {",
|
||||
" if [ \"$(id -u)\" = \"0\" ]; then \"$@\"; return; fi",
|
||||
" if sudo -n true >/dev/null 2>&1; then sudo -n \"$@\"; return; fi",
|
||||
" if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- \"$@\"; return; fi",
|
||||
" echo 'dev_env_native_k3s_root_access=missing' >&2",
|
||||
" return 1",
|
||||
"}",
|
||||
"normalize_image() {",
|
||||
" image=\"$1\"",
|
||||
" case \"$image\" in",
|
||||
" *@*) printf '%s\\n' \"$image\" ;;",
|
||||
" docker.io/*|ghcr.io/*|gcr.io/*|quay.io/*|cgr.dev/*|registry.*/*|localhost/*|*.*/*) printf '%s\\n' \"$image\" ;;",
|
||||
" */*) printf 'docker.io/%s\\n' \"$image\" ;;",
|
||||
" *) printf 'docker.io/library/%s\\n' \"$image\" ;;",
|
||||
" esac",
|
||||
"}",
|
||||
`images=(${imageArray})`,
|
||||
`proxy_url=${shellQuote(options.proxyUrl)}`,
|
||||
`pull_missing=${options.pullMissing ? "1" : "0"}`,
|
||||
`pull_timeout_seconds=${pullTimeoutSeconds}`,
|
||||
"kubeconfig=/etc/rancher/k3s/k3s.yaml",
|
||||
"ctr_address=/run/k3s/containerd/containerd.sock",
|
||||
"export DOCKER_CONFIG=/tmp/unidesk-dev-env-docker-config",
|
||||
"mkdir -p \"$DOCKER_CONFIG\"",
|
||||
"printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
|
||||
"printf 'dev_env_k3s_context='",
|
||||
"KUBECONFIG=\"$kubeconfig\" kubectl config current-context",
|
||||
"printf 'dev_env_k3s_nodes='",
|
||||
"KUBECONFIG=\"$kubeconfig\" kubectl get nodes -o name | tr '\\n' ' '",
|
||||
"printf '\\n'",
|
||||
"for image in \"${images[@]}\"; do",
|
||||
" if docker image inspect \"$image\" >/dev/null 2>&1; then",
|
||||
" echo dev_env_image_cached=$image",
|
||||
" elif [ \"$pull_missing\" = \"1\" ]; then",
|
||||
" echo dev_env_image_pull=$image",
|
||||
" timeout \"$pull_timeout_seconds\" env HTTP_PROXY=\"$proxy_url\" HTTPS_PROXY=\"$proxy_url\" ALL_PROXY=\"$proxy_url\" NO_PROXY=\"localhost,127.0.0.1,::1,host.docker.internal\" docker pull --platform linux/amd64 \"$image\"",
|
||||
" else",
|
||||
" echo dev_env_image_missing=$image >&2",
|
||||
" exit 1",
|
||||
" fi",
|
||||
"done",
|
||||
"archive=$(mktemp /tmp/unidesk-dev-env-images.XXXXXX.tar)",
|
||||
"list_file=$(mktemp /tmp/unidesk-dev-env-ctr-images.XXXXXX.txt)",
|
||||
"trap 'rm -f \"$archive\" \"$list_file\"' EXIT",
|
||||
"docker save \"${images[@]}\" -o \"$archive\"",
|
||||
"root_exec ctr --address \"$ctr_address\" -n k8s.io images import --digests --all-platforms \"$archive\"",
|
||||
"root_exec ctr --address \"$ctr_address\" -n k8s.io images ls > \"$list_file\"",
|
||||
"missing=0",
|
||||
"for image in \"${images[@]}\"; do",
|
||||
" needle=$(normalize_image \"$image\")",
|
||||
" if grep -F \"$needle\" \"$list_file\" >/dev/null || grep -F \"$image\" \"$list_file\" >/dev/null; then",
|
||||
" echo dev_env_containerd_image_ready=$image",
|
||||
" else",
|
||||
" echo dev_env_containerd_image_missing=$image needle=$needle >&2",
|
||||
" missing=1",
|
||||
" fi",
|
||||
"done",
|
||||
"test \"$missing\" = \"0\"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function devEnvHelp(): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
command: "dev-env validate",
|
||||
usage: "bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]",
|
||||
command: "dev-env",
|
||||
usage: [
|
||||
"bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]",
|
||||
"bun scripts/cli.ts dev-env prewarm-images [--image image] [--provider-id D601] [--no-pull] [--proxy-url URL] [--pull-timeout-ms N] [--dry-run]",
|
||||
],
|
||||
defaultManifest,
|
||||
defaultPrewarmImages,
|
||||
checks: [
|
||||
"all namespaced resources must target unidesk-dev",
|
||||
"required foundation resources or backend-core-dev/frontend-dev resources must exist",
|
||||
"dev DATABASE_URL values must target postgres-dev/unidesk_dev and not production routes",
|
||||
"--kubectl-dry-run optionally asks kubectl to client-dry-run the manifest without applying it",
|
||||
"prewarm-images imports dev foundation images from Docker into native k3s containerd on D601",
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -153,9 +299,37 @@ function devEnvHelp(): Record<string, unknown> {
|
||||
export function runDevEnvCommand(args: string[]): unknown {
|
||||
const action = args[0];
|
||||
if (action === undefined || isHelpArg(action)) return devEnvHelp();
|
||||
if (action !== "validate") throw new Error("dev-env usage: bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]");
|
||||
if (action === "prewarm-images") {
|
||||
const options = parsePrewarmImagesOptions(args.slice(1));
|
||||
const script = prewarmImagesScript(options);
|
||||
const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
providerId: options.providerId,
|
||||
images: options.images,
|
||||
proxyUrl: options.proxyUrl,
|
||||
pullMissing: options.pullMissing,
|
||||
pullTimeoutMs: options.pullTimeoutMs,
|
||||
command,
|
||||
};
|
||||
}
|
||||
const job = startJob("dev_env_prewarm_images", command, `Prewarm ${options.images.length} dev foundation image(s) into ${options.providerId} native k3s containerd`);
|
||||
return {
|
||||
ok: true,
|
||||
providerId: options.providerId,
|
||||
images: options.images,
|
||||
proxyUrl: options.proxyUrl,
|
||||
pullMissing: options.pullMissing,
|
||||
pullTimeoutMs: options.pullTimeoutMs,
|
||||
job,
|
||||
statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
|
||||
};
|
||||
}
|
||||
if (action !== "validate") throw new Error("dev-env usage: bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run] OR dev-env prewarm-images");
|
||||
|
||||
const options = parseOptions(args.slice(1));
|
||||
const options = parseValidateOptions(args.slice(1));
|
||||
const manifestPath = rootPath(options.manifestPath);
|
||||
const manifestText = readFileSync(manifestPath, "utf8");
|
||||
const docs = parseManifestDocuments(manifestText);
|
||||
|
||||
Reference in New Issue
Block a user