// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. scripts module for scripts/src/deploy.ts. // Moved mechanically from scripts/src/deploy.ts:1519-2218 for #903. import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { runCommand } from "../command"; import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "../config"; import { ensureGithubSshIdentityForProvider } from "../deploy-ssh-identity"; import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "../artifact-registry"; import { startJob } from "../jobs"; import { coreInternalFetch } from "../microservices"; import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "../code-queue-source-guard"; import { k3sGuardShellLines, defaultNativeKubeconfig } from "../k3s-target-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; import { compareDeployJsonExecutorMirrors, deployJsonCommitImage, deployJsonDriftResult, deployJsonSourceOfTruth, encodeDeployJsonServiceContract, hasDeployJsonExecutorContract, k3sManifestExecutorMirror, parseDeployJsonServiceContract, type DeployJsonExecutorMirror, type DeployJsonServiceContract, } from "../deploy-json-contract"; import type { DeployManifestService } from "./types"; import { k3sTargetGuardScript, isUnideskRepo, providerSourceRepoUrl, safeId, shellQuote } from "./options"; import { buildBaseImageFallbacks, buildCachePrelude, buildImageTag, devK3sPrepullImages, directBuildContextOverride, directComposeEnvFile, directComposeFile, directDockerfileOverride, k8sManifestPath, sourceBuildContext, sourceDockerfilePath, sourceFallbackWorktree, sourceProxyPrelude, sourceWorkDir, targetIsMain, targetRepoDir, targetWorkDir } from "./paths"; import { isDevK3sDeployService, isDirectComposeDeployMode } from "./service-plan"; import { k8sKubeconfig, nativeK3sCtrAddress, nativeK3sImage, nativeK3sInstallVersion, providerGatewayWsEgressProxyUrl } from "./types"; export function syncDevFrontendAuthScript(config: UniDeskConfig): string { const sshClientToken = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_TOKEN"); if (sshClientToken === null) throw new Error(`UNIDESK_SSH_CLIENT_TOKEN must be present in ${rootPath(".state", "docker-compose.env")} before deploying dev frontend`); const sshClientRouteAllowlist = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST") ?? "G14,G14:*,D601,D601:*"; const data = { AUTH_USERNAME: Buffer.from(config.auth.username, "utf8").toString("base64"), AUTH_PASSWORD: Buffer.from(config.auth.password, "utf8").toString("base64"), SESSION_SECRET: Buffer.from(config.auth.sessionSecret, "utf8").toString("base64"), UNIDESK_SSH_CLIENT_TOKEN: Buffer.from(sshClientToken, "utf8").toString("base64"), }; const runtimeConfig = { SESSION_TTL_SECONDS: String(config.auth.sessionTtlSeconds), UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST: sshClientRouteAllowlist, }; return [ "set -euo pipefail", k3sTargetGuardScript(), `secret_patch=${shellQuote(JSON.stringify({ data }))}`, `config_patch=${shellQuote(JSON.stringify({ data: runtimeConfig }))}`, `kubectl -n unidesk-dev patch secret unidesk-dev-runtime-secrets --type merge -p "$secret_patch"`, `kubectl -n unidesk-dev patch configmap unidesk-dev-runtime-config --type merge -p "$config_patch"`, "echo dev_frontend_auth_synced=ok", ].join("\n"); } export function prepareSourceScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, exportDir: string): string { if (targetIsMain(service) && isUnideskRepo(desired.repo)) { return [ "set -euo pipefail", `repo=${shellQuote(repoRoot)}`, `commit=${shellQuote(desired.commitId)}`, `export_dir=${shellQuote(exportDir)}`, "mkdir -p \"$(dirname \"$export_dir\")\"", "git -C \"$repo\" fetch --no-tags origin \"$commit\" || git -C \"$repo\" fetch --no-tags --prune origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*'", "resolved=$(git -C \"$repo\" rev-parse --verify \"$commit^{commit}\")", "rm -rf \"$export_dir\"", "mkdir -p \"$export_dir\"", "git -C \"$repo\" archive --format=tar \"$resolved\" | tar -xf - -C \"$export_dir\"", "printf 'resolved_commit=%s\\nexport_dir=%s\\nsource_repo=%s\\n' \"$resolved\" \"$export_dir\" \"$repo\"", ].join("\n"); } const fallbackWorktree = sourceFallbackWorktree(service); return [ "set -euo pipefail", sourceProxyPrelude(service), `repo=${shellQuote(targetRepoDir(service))}`, `repo_url=${shellQuote(providerSourceRepoUrl(desired.repo))}`, `commit=${shellQuote(desired.commitId)}`, `export_dir=${shellQuote(exportDir)}`, `fallback_worktree=${shellQuote(fallbackWorktree)}`, "mkdir -p \"$(dirname \"$repo\")\" \"$(dirname \"$export_dir\")\"", "source_repo=\"\"", "source_mode=\"remote-fetch\"", "source_log=$(mktemp /tmp/unidesk-deploy-source.XXXXXX.log)", "if { [ -d \"$repo/.git\" ] || { rm -rf \"$repo\" && git clone --no-checkout \"$repo_url\" \"$repo\"; }; } >\"$source_log\" 2>&1; then", " cd \"$repo\"", " if { git remote set-url origin \"$repo_url\" && { git fetch --no-tags origin \"$commit\" || git fetch --no-tags --prune origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*'; } && git rev-parse --verify \"$commit^{commit}\" > /tmp/unidesk-deploy-resolved-commit; } >>\"$source_log\" 2>&1; then", " resolved=$(cat /tmp/unidesk-deploy-resolved-commit)", " source_repo=\"$repo\"", " fi", "fi", "if [ -z \"$source_repo\" ]; then", " echo target_source_remote_prepare=failed", " tail -80 \"$source_log\" || true", " if [ -n \"$fallback_worktree\" ] && git -C \"$fallback_worktree\" rev-parse --is-inside-work-tree >/dev/null 2>&1 && git -C \"$fallback_worktree\" rev-parse --verify \"$commit^{commit}\" > /tmp/unidesk-deploy-resolved-commit 2>/dev/null; then", " resolved=$(cat /tmp/unidesk-deploy-resolved-commit)", " source_repo=$(git -C \"$fallback_worktree\" rev-parse --show-toplevel)", " source_mode=\"provider-worktree\"", " printf 'target_source_fallback_worktree=%s\\n' \"$fallback_worktree\"", " else", " cat \"$source_log\" >&2 || true", " exit 128", " fi", "fi", "rm -rf \"$export_dir\"", "mkdir -p \"$export_dir\"", "git -C \"$source_repo\" archive --format=tar \"$resolved\" | tar -xf - -C \"$export_dir\"", "rm -f \"$source_log\" /tmp/unidesk-deploy-resolved-commit", "printf 'resolved_commit=%s\\nexport_dir=%s\\nsource_repo=%s\\nsource_mode=%s\\n' \"$resolved\" \"$export_dir\" \"$source_repo\" \"$source_mode\"", ].filter((line) => line.length > 0).join("\n"); } export function syncSourceScript(service: UniDeskMicroserviceConfig, exportDir: string): string { const workDir = sourceWorkDir(service); const buildContext = sourceBuildContext(service); const dockerfilePath = sourceDockerfilePath(service); const overlayCommands = service.id === "claudeqq" ? claudeqqDeployAssetOverlayCommands() : []; return [ "set -euo pipefail", `export_dir=${shellQuote(exportDir)}`, `work_dir=${shellQuote(workDir)}`, `build_context=${shellQuote(buildContext)}`, `dockerfile_path=${shellQuote(dockerfilePath)}`, "mkdir -p \"$work_dir\"", [ "rsync -a --delete", "--exclude '.git/'", "--exclude '.state/'", "--exclude 'logs/'", "--exclude '**/node_modules/'", "--exclude '**/dist/'", "\"$export_dir/\"", "\"$work_dir/\"", ].join(" "), ...overlayCommands, "test -f \"$build_context/$dockerfile_path\"", "printf 'synced deploy worktree to %s\\nbuild_context=%s\\n' \"$work_dir\" \"$build_context\"", ].join("\n"); } export function codeQueueSourcePreflightScript(service: UniDeskMicroserviceConfig): string { if (service.id !== "code-queue") return ""; const workDir = sourceWorkDir(service); return [ "set -euo pipefail", `guard_root=${shellQuote(workDir)}`, "if [ -f \"$guard_root/scripts/code-queue-source-guard.ts\" ] && command -v bun >/dev/null 2>&1; then", " bun \"$guard_root/scripts/code-queue-source-guard.ts\" --root \"$guard_root\"", "else", " python3 - \"$guard_root\" <<'PY'", "import json", "import os", "import re", "import sys", "", `SOURCE_SUBDIR = ${JSON.stringify(codeQueueSourceSubdir)}`, "EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json']", "", "def rel(root, path):", " return os.path.relpath(path, root).replace(os.sep, '/')", "", "def strip_comments(text):", " text = re.sub(r'/\\*[\\s\\S]*?\\*/', '', text)", " return re.sub(r'(^|[^:])//.*$', r'\\1', text, flags=re.MULTILINE)", "", "def specifiers(text):", " clean = strip_comments(text)", " values = set()", " patterns = [", " re.compile(r'\\b(?:import|export)\\s+(?:type\\s+)?(?:[\\s\\S]*?\\s+from\\s+)?[\"\\'](\\.{1,2}/[^\"\\']+)[\"\\']'),", " re.compile(r'\\bimport\\s*\\(\\s*[\"\\'](\\.{1,2}/[^\"\\']+)[\"\\']\\s*\\)'),", " ]", " for pattern in patterns:", " values.update(match.group(1) for match in pattern.finditer(clean))", " return sorted(values)", "", "def candidates(importer, specifier):", " base = os.path.abspath(os.path.join(os.path.dirname(importer), specifier))", " if any(base.endswith(extension) for extension in EXTENSIONS):", " return [base]", " return [base + extension for extension in EXTENSIONS] + [os.path.join(base, 'index' + extension) for extension in EXTENSIONS]", "", "root = os.path.abspath(sys.argv[1])", "source_root = os.path.join(root, SOURCE_SUBDIR)", "if not os.path.isdir(source_root):", " print(json.dumps({'ok': False, 'guard': 'code-queue-hostpath-source-imports', 'root': root, 'sourceRoot': source_root, 'checkedFiles': 0, 'checkedImports': 0, 'missing': [], 'degradedReason': 'source-root-missing', 'message': 'Code Queue source root is missing: ' + SOURCE_SUBDIR}, ensure_ascii=False))", " raise SystemExit(1)", "files = []", "for current_root, dirs, names in os.walk(source_root):", " dirs[:] = [name for name in dirs if name not in ('node_modules', 'dist', '.git', '.state')]", " for name in names:", " if name.endswith('.ts') or name.endswith('.tsx'):", " files.append(os.path.join(current_root, name))", "files.sort()", "missing = []", "checked_imports = 0", "for path in files:", " with open(path, encoding='utf-8') as handle:", " text = handle.read()", " for specifier in specifiers(text):", " checked_imports += 1", " expected = candidates(path, specifier)", " if any(os.path.isfile(candidate) for candidate in expected):", " continue", " missing.append({'importer': rel(root, path), 'specifier': specifier, 'expected': [rel(root, candidate) for candidate in expected]})", "result = {'ok': not missing, 'guard': 'code-queue-hostpath-source-imports', 'root': root, 'sourceRoot': source_root, 'checkedFiles': len(files), 'checkedImports': checked_imports, 'missing': missing, 'degradedReason': 'none' if not missing else 'missing-relative-import-target', 'message': 'Code Queue hostPath source import preflight passed (%d files, %d relative imports).' % (len(files), checked_imports) if not missing else 'Code Queue hostPath source import preflight failed: %d relative import target(s) are missing.' % len(missing)}", "print(json.dumps(result, ensure_ascii=False))", "raise SystemExit(0 if result['ok'] else 1)", "PY", "fi", ].join("\n"); } export function claudeqqDeployAssetOverlayCommands(): string[] { const assets = [ { relativePath: "$build_context/$dockerfile_path", sourcePath: rootPath("src/components/microservices/claudeqq/Dockerfile"), label: "Dockerfile", }, { relativePath: "$build_context/unidesk-adapter.cjs", sourcePath: rootPath("src/components/microservices/claudeqq/adapter.js"), label: "unidesk-adapter.cjs", }, ]; return assets.flatMap((asset) => { if (!existsSync(asset.sourcePath)) throw new Error(`claudeqq deploy asset missing: ${asset.sourcePath}`); const encoded = Buffer.from(readFileSync(asset.sourcePath, "utf8"), "utf8").toString("base64"); return [ `mkdir -p "$(dirname "${asset.relativePath}")"`, `printf %s ${shellQuote(encoded)} | base64 -d > "${asset.relativePath}"`, `printf 'synced_claudeqq_deploy_asset=%s\\n' ${shellQuote(asset.label)}`, ]; }); } export function syncK8sControlManifestsScript(service: UniDeskMicroserviceConfig): string { if (isDevK3sDeployService(service)) { const manifest = k8sManifestPath(service); return [ "set -euo pipefail", `target_root=${shellQuote(targetWorkDir(service))}`, `relative_path=${shellQuote(manifest)}`, "target_file=\"$target_root/$relative_path\"", "test -f \"$target_file\"", "printf 'target_k3s_control_manifest=%s\\n' \"$target_file\"", ].join("\n"); } if (service.deployment.mode !== "k3sctl-managed" || isUnideskRepo(service.repository.url)) return ""; const manifests = service.repository.composeFile.endsWith(".k8s.yaml") ? [service.repository.composeFile] : [service.repository.composeFile, k8sManifestPath(service)]; const commands = [ "set -euo pipefail", `target_root=${shellQuote(targetWorkDir(service))}`, "mkdir -p \"$target_root\"", ]; for (const relativePath of manifests) { const absolutePath = rootPath(relativePath); if (!existsSync(absolutePath)) throw new Error(`${service.id} k3s control manifest missing: ${relativePath}`); const encoded = Buffer.from(readFileSync(absolutePath, "utf8"), "utf8").toString("base64"); commands.push( `relative_path=${shellQuote(relativePath)}`, `target_file="$target_root/$relative_path"`, "mkdir -p \"$(dirname \"$target_file\")\"", `printf %s ${shellQuote(encoded)} | base64 -d > "$target_file"`, "printf 'synced_k3s_control_manifest=%s\\n' \"$target_file\"", ); } return commands.join("\n"); } export function buildImageScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string { const image = buildImageTag(service); const buildContext = sourceBuildContext(service); const dockerfilePath = sourceDockerfilePath(service); const dockerfile = `${buildContext}/${dockerfilePath}`; const labelArgs = [ "--label", `unidesk.ai/service-id=${service.id}`, "--label", `unidesk.ai/source-repo=${desired.repo}`, "--label", `unidesk.ai/source-commit=${resolvedCommit}`, "--label", `unidesk.ai/dockerfile=${dockerfilePath}`, ]; const commonArgs = [ "--progress=plain", ...labelArgs, "-t", image, "-f", dockerfile, buildContext, ]; const proxyBuildArgs = targetIsMain(service) ? [] : [ "--network", "host", "--build-arg", `HTTP_PROXY=${providerGatewayWsEgressProxyUrl}`, "--build-arg", `HTTPS_PROXY=${providerGatewayWsEgressProxyUrl}`, "--build-arg", `ALL_PROXY=${providerGatewayWsEgressProxyUrl}`, "--build-arg", "NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal", ]; return [ "set -euo pipefail", sourceProxyPrelude(service), `image=${shellQuote(image)}`, `dockerfile=${shellQuote(dockerfile)}`, "if ! docker buildx version >/dev/null 2>&1; then", " if ! docker buildx inspect default >/dev/null 2>&1; then echo target_build_builder=missing >&2; exit 1; fi", "fi", ...devK3sPrepullImages(service).flatMap((imageName) => [ `if ! docker image inspect ${shellQuote(imageName)} >/dev/null 2>&1; then`, ` docker pull ${shellQuote(imageName)}`, "fi", ]), "builder_args=()", "if docker buildx inspect --builder default >/dev/null 2>&1; then builder_args=(--builder default); echo target_build_builder=default; else echo target_build_builder=implicit; fi", "docker buildx inspect \"${builder_args[@]}\" --bootstrap || true", "echo target_build_builder_cleanup=not-required", ...buildCachePrelude("$dockerfile", buildBaseImageFallbacks(service)), `docker buildx build "\${builder_args[@]}" --load "\${cache_args[@]}" "\${base_args[@]}" ${[...proxyBuildArgs, ...commonArgs].map(shellQuote).join(" ")}`, "docker image inspect \"$image\" --format 'image_id={{.Id}} labels={{json .Config.Labels}}'", ].filter((line) => line.length > 0).join("\n"); } export function patchDevK3sManifestScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string { const manifest = `${targetWorkDir(service)}/${k8sManifestPath(service)}`; const image = buildImageTag(service); const serviceId = service.id; const deploymentName = service.repository.composeService; return [ "set -euo pipefail", `manifest=${shellQuote(manifest)}`, `service_id=${shellQuote(serviceId)}`, `deployment_name=${shellQuote(deploymentName)}`, `image=${shellQuote(image)}`, `repo=${shellQuote(desired.repo)}`, `commit=${shellQuote(resolvedCommit)}`, `requested_commit=${shellQuote(desired.commitId)}`, `python3 - "$manifest" "$service_id" "$deployment_name" "$image" "$repo" "$commit" "$requested_commit" <<'PY'`, "import re", "import sys", "path, service_id, deployment_name, image, repo, commit, requested_commit = sys.argv[1:]", "text = open(path, encoding='utf-8').read()", "segments = re.split(r'(?m)^---\\s*$', text)", "kept = []", "for segment in segments:", " if not segment.strip():", " continue", " haystack = '\\n' + segment + '\\n'", " if f'unidesk.ai/deploy-service-id: {service_id}' in segment or f'\\n name: {deployment_name}\\n' in haystack:", " kept.append(segment)", "if not kept:", " raise SystemExit(f'dev service {service_id}/{deployment_name} not found in {path}')", "patched = []", "for segment in kept:", " segment = segment.replace('unidesk.ai/image-source: deploy-env-commit', 'unidesk.ai/image-source: deploy-env-commit')", " segment = re.sub(r'image: unidesk-[^\\n]+:dev-placeholder', f'image: {image}', segment)", " segment = re.sub(r'value: replace-with-deploy-env-commit', f'value: {commit}', segment)", " segment = segment.replace('value: https://github.com/pikasTech/unidesk', f'value: {repo}')", " patched.append(segment.strip() + '\\n')", "out = '---\\n'.join(patched)", "open(path, 'w', encoding='utf-8').write(out)", "print(f'dev_k3s_manifest_patched={path}')", "print(f'dev_k3s_manifest_service={service_id}')", "print(f'dev_k3s_manifest_deployment={deployment_name}')", "print(f'dev_k3s_manifest_image={image}')", "print(f'dev_k3s_manifest_commit={commit}')", "PY", ].join("\n"); } export function directComposeResolveScript(service: UniDeskMicroserviceConfig): string { const projectHint = targetIsMain(service) ? "unidesk" : ""; return [ `work_dir=${shellQuote(targetWorkDir(service))}`, `compose_file=${shellQuote(directComposeFile(service))}`, `compose_env_file=${shellQuote(directComposeEnvFile(service))}`, `compose_service=${shellQuote(service.repository.composeService)}`, `container=${shellQuote(service.repository.containerName)}`, `project_hint=${shellQuote(projectHint)}`, `build_context_override=${shellQuote(directBuildContextOverride(service))}`, `build_dockerfile_override=${shellQuote(directDockerfileOverride(service))}`, "compose_env_args=()", "if [ -n \"$compose_env_file\" ]; then compose_env_args=(--env-file \"$compose_env_file\"); fi", "running_project=$(docker inspect -f '{{ index .Config.Labels \"com.docker.compose.project\" }}' \"$container\" 2>/dev/null || true)", "running_service=$(docker inspect -f '{{ index .Config.Labels \"com.docker.compose.service\" }}' \"$container\" 2>/dev/null || true)", "running_image=$(docker inspect -f '{{.Config.Image}}' \"$container\" 2>/dev/null || true)", "if [ \"$running_project\" = '' ]; then running_project=''; fi", "if [ \"$running_service\" = '' ]; then running_service=''; fi", "project=\"$running_project\"", "if [ -z \"$project\" ]; then project=\"$project_hint\"; fi", "if [ -z \"$project\" ]; then project=$(basename \"$work_dir\"); fi", "config_json=$(docker compose \"${compose_env_args[@]}\" -f \"$compose_file\" -p \"$project\" config --format json)", "compose_image=$(printf '%s' \"$config_json\" | python3 -c 'import json,sys; svc=sys.argv[1]; d=json.load(sys.stdin); print(((d.get(\"services\") or {}).get(svc) or {}).get(\"image\") or \"\")' \"$compose_service\")", "image=\"$compose_image\"", "if [ -z \"$image\" ]; then image=\"$running_image\"; fi", "if [ -z \"$image\" ]; then image=\"${project}-${compose_service}\"; fi", "build_context=$(printf '%s' \"$config_json\" | python3 -c 'import json,sys; svc=sys.argv[1]; b=(((json.load(sys.stdin).get(\"services\") or {}).get(svc) or {}).get(\"build\") or {}); print((b if isinstance(b,str) else b.get(\"context\")) or \".\")' \"$compose_service\")", "build_dockerfile=$(printf '%s' \"$config_json\" | python3 -c 'import json,sys; svc=sys.argv[1]; b=(((json.load(sys.stdin).get(\"services\") or {}).get(svc) or {}).get(\"build\") or {}); print(\"Dockerfile\" if isinstance(b,str) else (b.get(\"dockerfile\") or \"Dockerfile\"))' \"$compose_service\")", "build_target=$(printf '%s' \"$config_json\" | python3 -c 'import json,sys; svc=sys.argv[1]; b=(((json.load(sys.stdin).get(\"services\") or {}).get(svc) or {}).get(\"build\") or {}); print(\"\" if isinstance(b,str) else (b.get(\"target\") or \"\"))' \"$compose_service\")", "build_args_file=$(mktemp /tmp/unidesk-compose-build-args.XXXXXX)", "printf '%s' \"$config_json\" | python3 -c 'import json,sys; svc=sys.argv[1]; b=(((json.load(sys.stdin).get(\"services\") or {}).get(svc) or {}).get(\"build\") or {}); args={} if isinstance(b,str) else (b.get(\"args\") or {}); items=args.items() if isinstance(args,dict) else [(str(x).split(\"=\",1)[0], str(x).split(\"=\",1)[1] if \"=\" in str(x) else \"\") for x in args]; [print(str(k)+\"=\"+(\"\" if v is None else str(v))) for k,v in items]' \"$compose_service\" > \"$build_args_file\"", "if [ -n \"$build_context_override\" ]; then build_context=\"$build_context_override\"; fi", "if [ -n \"$build_dockerfile_override\" ]; then build_dockerfile=\"$build_dockerfile_override\"; fi", "case \"$build_context\" in /*) ;; *) build_context=$(cd \"$(dirname \"$compose_file\")\" && cd \"$build_context\" && pwd) ;; esac", "case \"$build_dockerfile\" in /*) dockerfile_abs=\"$build_dockerfile\" ;; *) dockerfile_abs=\"$build_context/$build_dockerfile\" ;; esac", "test -f \"$dockerfile_abs\"", "printf 'compose_project=%s\\ncompose_service=%s\\ncompose_image=%s\\nbuild_context=%s\\nbuild_dockerfile=%s\\nbuild_target=%s\\n' \"$project\" \"$compose_service\" \"$image\" \"$build_context\" \"$dockerfile_abs\" \"$build_target\"", ].join("\n"); } export function buildDirectImageScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string { const proxyBuildArgs = targetIsMain(service) ? [] : [ "--network", "host", "--build-arg", `HTTP_PROXY=${providerGatewayWsEgressProxyUrl}`, "--build-arg", `HTTPS_PROXY=${providerGatewayWsEgressProxyUrl}`, "--build-arg", `ALL_PROXY=${providerGatewayWsEgressProxyUrl}`, "--build-arg", "NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal", ]; const labelArgs = [ "--label", `unidesk.ai/service-id=${service.id}`, "--label", `unidesk.ai/source-repo=${desired.repo}`, "--label", `unidesk.ai/source-commit=${resolvedCommit}`, "--label", `unidesk.ai/dockerfile=${service.repository.dockerfile}`, ]; return [ "set -euo pipefail", sourceProxyPrelude(service), directComposeResolveScript(service), "builder_args=()", "if docker buildx inspect --builder default >/dev/null 2>&1; then builder_args=(--builder default); echo target_build_builder=default; else echo target_build_builder=implicit; fi", "docker buildx inspect \"${builder_args[@]}\" --bootstrap || true", ...buildCachePrelude("$dockerfile_abs", buildBaseImageFallbacks(service)), "compose_build_args=()", "while IFS= read -r item; do [ -n \"$item\" ] && compose_build_args+=(--build-arg \"$item\"); done < \"$build_args_file\"", "target_args=()", "if [ -n \"$build_target\" ]; then target_args=(--target \"$build_target\"); fi", `docker buildx build "\${builder_args[@]}" --load "\${cache_args[@]}" "\${base_args[@]}" ${[...proxyBuildArgs, ...labelArgs].map(shellQuote).join(" ")} "\${compose_build_args[@]}" "\${target_args[@]}" --progress=plain -t "$image" -f "$dockerfile_abs" "$build_context"`, "docker image inspect \"$image\" --format 'image_id={{.Id}} labels={{json .Config.Labels}}'", ].filter((line) => line.length > 0).join("\n"); } export function imageLabelVerifyScript(service: UniDeskMicroserviceConfig, expectedCommit: string): string { return [ "set -euo pipefail", `container=${shellQuote(service.repository.containerName)}`, `expected=${shellQuote(expectedCommit)}`, "cid=$(docker ps -q -f name=\"^/${container}$\" | head -1)", "test -n \"$cid\"", "image_id=$(docker inspect -f '{{.Image}}' \"$cid\")", "actual=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")", "test \"$actual\" = \"$expected\"", "printf 'container=%s image_id=%s deploy_commit=%s\\n' \"$container\" \"$image_id\" \"$actual\"", ].join("\n"); } export function directDeployEnvPrefix(service: UniDeskMicroserviceConfig): string { return `UNIDESK_${service.id.replace(/[^A-Za-z0-9]+/gu, "_").replace(/^_+|_+$/gu, "").toUpperCase()}_DEPLOY`; } export function directComposeDeployEnv(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): Record { if (!isDirectComposeDeployMode(service)) return {}; const prefix = directDeployEnvPrefix(service); return { [`${prefix}_SERVICE_ID`]: service.id, [`${prefix}_REPO`]: desired.repo, [`${prefix}_COMMIT`]: resolvedCommit, [`${prefix}_REQUESTED_COMMIT`]: desired.commitId, }; } export function composeEnvStampLines(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string[] { const deployEnv = directComposeDeployEnv(service, desired, resolvedCommit); const entries = Object.entries(deployEnv); if (entries.length === 0) return []; const encoded = Buffer.from(JSON.stringify(deployEnv), "utf8").toString("base64"); return [ `deploy_env_b64=${shellQuote(encoded)}`, "if [ -n \"$compose_env_file\" ]; then", " python3 - \"$compose_env_file\" \"$deploy_env_b64\" <<'PY'", "import base64, json, re, sys", "path = sys.argv[1]", "updates = json.loads(base64.b64decode(sys.argv[2]).decode('utf-8'))", "def env_value(value):", " text = str(value)", " return text if re.match(r'^[A-Za-z0-9_./:@-]+$', text) else json.dumps(text)", "try:", " lines = open(path, encoding='utf-8').read().splitlines()", "except FileNotFoundError:", " lines = []", "seen = set()", "out = []", "for line in lines:", " if '=' not in line or line.startswith('#'):", " out.append(line)", " continue", " key = line.split('=', 1)[0]", " if key in updates:", " out.append(f'{key}={env_value(updates[key])}')", " seen.add(key)", " else:", " out.append(line)", "for key, value in updates.items():", " if key not in seen:", " out.append(f'{key}={env_value(value)}')", "with open(path, 'w', encoding='utf-8') as handle:", " handle.write('\\n'.join(out) + '\\n')", "PY", " echo compose_env_deploy_stamp=updated", "fi", ]; } export function composeDeployScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string { return [ "set -euo pipefail", directComposeResolveScript(service), ...composeEnvStampLines(service, desired, resolvedCommit), "docker compose \"${compose_env_args[@]}\" -f \"$compose_file\" -p \"$project\" up -d --no-build --no-deps --force-recreate \"$compose_service\"", "ready=0", "for attempt in $(seq 1 90); do", " cid=$(docker ps -q -f name=\"^/${container}$\" | head -1)", " if [ -n \"$cid\" ]; then", " health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \"$cid\" 2>/dev/null || true)", " echo compose_container_probe container=$container attempt=$attempt health=$health", " if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi", " else", " echo compose_container_probe container=$container attempt=$attempt cid=missing", " fi", " sleep 1", "done", "test \"$ready\" = \"1\"", ].join("\n"); } export function writeComposeEnvFallbackPath(): string { return rootPath(".state", "docker-compose.env"); } export function rootAccessPrelude(): string[] { return [ "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 'native_k3s_root_access=missing' >&2", " return 1", "}", "root_shell() {", " script=\"$1\"", " if [ \"$(id -u)\" = \"0\" ]; then bash -lc \"$script\"; return; fi", " if sudo -n true >/dev/null 2>&1; then sudo -n bash -lc \"$script\"; return; fi", " if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- bash -lc \"$script\"; return; fi", " echo 'native_k3s_root_access=missing' >&2", " return 1", "}", ]; } export function ensureNativeK3sScript(): string { const installCommand = [ "INSTALL_K3S_SKIP_DOWNLOAD=true", `INSTALL_K3S_VERSION=${nativeK3sInstallVersion}`, "INSTALL_K3S_EXEC=\"server --disable traefik --disable servicelb --disable metrics-server --node-name D601 --node-label unidesk.ai/node-id=D601 --node-label unidesk.ai/provider-id=D601 --tls-san 127.0.0.1 --tls-san host.docker.internal --write-kubeconfig-mode 644\"", "sh /tmp/unidesk-install-k3s.sh", ].join(" "); return [ "set -euo pipefail", ...rootAccessPrelude(), `native_k3s_image=${shellQuote(nativeK3sImage)}`, `native_ctr_address=${shellQuote(nativeK3sCtrAddress)}`, `build_proxy=${shellQuote(providerGatewayWsEgressProxyUrl)}`, "configure_provider_deploy_proxy() {", " export DOCKER_CONFIG=/tmp/unidesk-docker-config-clean", " mkdir -p \"$DOCKER_CONFIG\"", " [ -f \"$DOCKER_CONFIG/config.json\" ] || printf '{}' > \"$DOCKER_CONFIG/config.json\"", " export HTTP_PROXY=\"$build_proxy\" HTTPS_PROXY=\"$build_proxy\" ALL_PROXY=\"$build_proxy\"", " export NO_PROXY=\"localhost,127.0.0.1,::1,host.docker.internal\"", " if ! curl -fsSI --max-time 20 -x \"$build_proxy\" https://github.com >/dev/null; then", " echo native_k3s_provider_egress_proxy_unavailable=$build_proxy >&2", " exit 1", " fi", " echo native_k3s_provider_egress_proxy=provider-gateway-ws-egress:$build_proxy", "}", "install_native_k3s_binaries() {", " missing=0", " for binary in k3s ctr containerd containerd-shim-runc-v2 crictl runc cni flannel bridge host-local loopback portmap bandwidth firewall; do", " command -v \"$binary\" >/dev/null 2>&1 || missing=1", " done", " if [ \"$missing\" = \"0\" ]; then return; fi", " docker image inspect \"$native_k3s_image\" >/dev/null 2>&1 || docker pull \"$native_k3s_image\"", " tmp_dir=$(mktemp -d)", " tmp_container=$(docker create \"$native_k3s_image\" sh -lc true)", " docker cp \"$tmp_container:/bin/.\" \"$tmp_dir/bin\"", " docker rm \"$tmp_container\" >/dev/null", " for binary in k3s ctr containerd containerd-shim-runc-v2 crictl runc cni flannel bridge host-local loopback portmap bandwidth firewall; do", " if [ -e \"$tmp_dir/bin/$binary\" ]; then", " root_exec install -m 755 \"$tmp_dir/bin/$binary\" \"/usr/local/bin/$binary\"", " echo native_k3s_binary_installed=$binary", " fi", " done", " rm -rf \"$tmp_dir\"", "}", "unmount_invalid_wsl_kubelet_mounts() {", " if awk 'NF != 6 && $0 ~ /\\/Docker\\/host/ {found=1} END {exit found ? 0 : 1}' /proc/mounts; then", " root_exec umount /Docker/host >/dev/null 2>&1 || root_exec umount -l /Docker/host >/dev/null 2>&1 || true", " echo native_k3s_unmounted_invalid_mount=/Docker/host", " fi", " awk 'NF != 6 {print \"native_k3s_invalid_mount_line=\" NR \":\" $0; bad=1} END {exit bad}' /proc/mounts", "}", "install_system_images_from_legacy_k3s() {", " legacy_container=$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /^rancher\\/k3s:/ {print $1; exit}')", " if [ -n \"$legacy_container\" ]; then", " if docker exec \"$legacy_container\" ctr -n k8s.io images export /tmp/unidesk-k3s-system-images.tar docker.io/rancher/local-path-provisioner:v0.0.32 docker.io/rancher/mirrored-coredns-coredns:1.12.3 >/dev/null 2>&1; then", " docker cp \"$legacy_container:/tmp/unidesk-k3s-system-images.tar\" /tmp/unidesk-k3s-system-images.tar >/dev/null", " root_exec ctr --address \"$native_ctr_address\" -n k8s.io images import /tmp/unidesk-k3s-system-images.tar >/dev/null", " echo native_k3s_imported_legacy_system_images=$legacy_container", " fi", " fi", "}", "install_pause_image() {", " pause_image=rancher/mirrored-pause:3.6", " pause_entrypoint=$(docker image inspect \"$pause_image\" --format '{{json .Config.Entrypoint}}' 2>/dev/null || true)", " if ! printf '%s' \"$pause_entrypoint\" | grep -q '\"/pause\"'; then", " docker image rm \"$pause_image\" >/dev/null 2>&1 || true", " docker pull --platform linux/amd64 \"$pause_image\"", " pause_entrypoint=$(docker image inspect \"$pause_image\" --format '{{json .Config.Entrypoint}}' 2>/dev/null || true)", " fi", " if ! printf '%s' \"$pause_entrypoint\" | grep -q '\"/pause\"'; then", " echo native_k3s_pause_image_invalid_entrypoint=$pause_entrypoint >&2", " exit 1", " fi", " docker save \"$pause_image\" -o /tmp/unidesk-k3s-pause.tar", " root_exec ctr --address \"$native_ctr_address\" -n k8s.io images rm docker.io/rancher/mirrored-pause:3.6 >/dev/null 2>&1 || true", " root_exec ctr --address \"$native_ctr_address\" -n k8s.io images import /tmp/unidesk-k3s-pause.tar >/dev/null", " root_exec ctr --address \"$native_ctr_address\" -n k8s.io images ls | grep -F 'docker.io/rancher/mirrored-pause:3.6' >/dev/null", " echo native_k3s_imported_pause_image=rancher/mirrored-pause:3.6", "}", "configure_provider_deploy_proxy", "install_native_k3s_binaries", "unmount_invalid_wsl_kubelet_mounts", "if ! systemctl cat k3s.service >/dev/null 2>&1; then", " curl -fsSL --max-time 60 https://get.k3s.io -o /tmp/unidesk-install-k3s.sh", ` root_shell ${shellQuote(installCommand)}`, "fi", "if ! systemctl is-active --quiet k3s; then", " root_exec systemctl enable --now k3s", "fi", "for attempt in $(seq 1 60); do", ` if KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes >/dev/null 2>&1; then break; fi`, " sleep 2", "done", k3sTargetGuardScript(), `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes -l unidesk.ai/node-id=D601 --no-headers | grep -q .`, `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl wait --for=condition=Ready node -l unidesk.ai/node-id=D601 --timeout=180s`, "install_system_images_from_legacy_k3s", "install_pause_image", `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n kube-system delete pod -l k8s-app=kube-dns --ignore-not-found >/dev/null 2>&1 || true`, `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n kube-system delete pod -l app=local-path-provisioner --ignore-not-found >/dev/null 2>&1 || true`, "legacy_k3s_containers=$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /^rancher\\/k3s:/ {print $1}')", "for container in $legacy_k3s_containers; do", " docker update --restart=no \"$container\" >/dev/null 2>&1 || true", " docker stop \"$container\" >/dev/null", " echo stopped_containerized_k3s=$container", "done", `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes -o wide`, "printf 'native_k3s=ready kubeconfig=%s\\n' /etc/rancher/k3s/k3s.yaml", ].join("\n"); } export function importK3sImageScript(service: UniDeskMicroserviceConfig): string { const image = buildImageTag(service); const archive = `/tmp/unidesk-${safeId(service.id)}-k3s-image.tar`; const manifest = `${targetWorkDir(service)}/${k8sManifestPath(service)}`; const codeQueueManifestPreflight = service.id === "code-queue" ? [ "if [ -f \"$manifest\" ]; then", " bad_manifest_images=$(python3 - \"$manifest\" \"$image\" <<'PY'", "import re, sys", "path, expected = sys.argv[1], sys.argv[2]", "required = {'code-queue','code-queue-read','code-queue-write','d601-provider-egress-proxy','d601-tcp-egress-gateway'}", "text = open(path, encoding='utf-8').read()", "bad = []", "seen = set()", "for doc in re.split(r'(?m)^---\\s*$', text):", " if not re.search(r'(?m)^kind:\\s*Deployment\\s*$', doc):", " continue", " match = re.search(r'(?ms)^metadata:\\s*$.*?^ name:\\s*([A-Za-z0-9_.-]+)\\s*$', doc)", " name = match.group(1) if match else ''", " if name not in required:", " continue", " seen.add(name)", " images = re.findall(r'(?m)^\\s*image:\\s*\"?([^\"\\s#]+)\"?', doc)", " if not images or any(image != expected for image in images):", " found = ','.join(images) if images else ''", " bad.append(f'{name}:{found}')", "missing = sorted(required - seen)", "bad.extend(f'{name}:' for name in missing)", "print('\\n'.join(bad))", "PY", " )", " if [ -n \"$bad_manifest_images\" ]; then", " printf 'code_queue_manifest_image_preflight_failed image=%s\\n%s\\n' \"$image\" \"$bad_manifest_images\" >&2", " exit 1", " fi", " echo code_queue_manifest_image_preflight=ok image=$image", "fi", ] : []; return [ "set -euo pipefail", ...rootAccessPrelude(), `image=${shellQuote(image)}`, `archive=${shellQuote(archive)}`, `manifest=${shellQuote(manifest)}`, "docker image inspect \"$image\" >/dev/null", ...codeQueueManifestPreflight, "rm -f \"$archive\"", "docker save \"$image\" -o \"$archive\"", `root_exec ctr --address ${shellQuote(nativeK3sCtrAddress)} -n k8s.io images import "$archive"`, "rm -f \"$archive\"", `if ! root_exec ctr --address ${shellQuote(nativeK3sCtrAddress)} -n k8s.io images ls | grep -F "$image"; then`, " printf 'native_k3s_containerd_image_missing=%s\\n' \"$image\" >&2", " exit 1", "fi", "printf 'native_k3s_containerd_image_present=%s\\n' \"$image\"", ].join("\n"); }