// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. k3s-deploy module for scripts/src/artifact-registry.ts. // Moved mechanically from scripts/src/artifact-registry.ts:3154-3447 for #903. import { createHash } from "node:crypto"; import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { runCommand, type CommandResult } from "../command"; import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "../config"; import { startJob } from "../jobs"; import { compareDeployJsonExecutorMirrors, deployJsonCommitImage, deployJsonDriftResult, deployJsonSourceOfTruth, hasDeployJsonExecutorContract, k3sManifestExecutorMirror, parseDeployJsonServiceContractBase64, readDeployJsonServiceContractFromFile, type DeployJsonExecutorMirror, type DeployJsonServiceContract, } from "../deploy-json-contract"; import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; import type { ArtifactConsumerSpec, ArtifactConsumerTarget, ArtifactDeployEnvironment, ArtifactRegistryOptions } from "./types"; import { d601DevFrontendAuthPatchScript, registryArtifactMissingMessage, registryArtifactProbeScript } from "./artifact-probe"; import { d601K3sGuardScript, rootExecPrelude, shellQuote } from "./bundle"; import { artifactImageRef, commandTail, deployRefFor, sourceRepoFor } from "./consumer"; import { runReadonlyStatus } from "./readonly"; import { runRemoteScript, runRemoteScriptBackground } from "./remote"; export function rollbackInfo(spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, environment: ArtifactDeployEnvironment, commit: string): Record { if (spec.kind === "compose" || spec.kind === "d601-compose") { const compose = target.compose; return { type: spec.kind === "d601-compose" ? "d601-compose-retag-recreate" : "compose-retag-recreate", composeService: compose?.serviceName, containerName: compose?.containerName, workDir: compose?.workDir, composeFile: compose?.composeFile, previousImageHint: `Use docker image ls / docker inspect to find the previous labeled ${spec.serviceId} image id; Compose volumes are unchanged.`, commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId}${environment === "prod" ? " --commit " : ""}`, }; } return { type: "d601-k3s-previous-commit", serviceId: spec.serviceId, environment, currentCommit: commit, discovery: `KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n ${target.k3s?.namespace} rollout history deployment/${target.k3s?.deploymentName} && KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n ${target.k3s?.namespace} get deployment ${target.k3s?.deploymentName} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-previous-commit}'`, commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId} --commit `, note: "Rollback is exposed as the same artifact consumer pointed at a previous commit-pinned image that still exists in D601 registry.", }; } export function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): string { const environment = options.environment ?? "prod"; if (target.k3s === undefined) throw new Error(`${spec.serviceId} missing k3s artifact consumer config for ${environment}`); const manifestText = readFileSync(rootPath(target.k3s.manifestRepoPath), "utf8"); const manifestBase64 = Buffer.from(manifestText, "utf8").toString("base64"); const sourceImage = artifactImageRef(options, spec, commit); const sourceRepo = sourceRepoFor(options, spec); const commitImage = target.targetCommitImage(commit); const k3s = target.k3s; const deploymentSpecs = [ { name: k3s.deploymentName, containerName: k3s.containerName }, ...(k3s.extraDeployments ?? []).map((deployment) => typeof deployment === "string" ? { name: deployment, containerName: k3s.containerName } : { name: deployment.name, containerName: deployment.containerName ?? k3s.containerName }), ]; const deploymentSpecsBase64 = Buffer.from(JSON.stringify(deploymentSpecs), "utf8").toString("base64"); const labels = [ `unidesk.ai/deploy-service-id=${spec.serviceId}`, `unidesk.ai/deploy-ref=${deployRefFor(options, spec)}`, `unidesk.ai/deploy-repo=${sourceRepo}`, `unidesk.ai/deploy-commit=${commit}`, `unidesk.ai/deploy-requested-commit=${commit}`, `unidesk.ai/image-source=${sourceImage}`, `unidesk.ai/deploy-environment=${environment}`, ]; return [ "set -euo pipefail", rootExecPrelude(), `registry_image=${shellQuote(sourceImage)}`, `stable_image=${shellQuote(target.targetImage)}`, `commit_image=${shellQuote(commitImage)}`, `environment=${shellQuote(environment)}`, `service_id=${shellQuote(spec.serviceId)}`, `source_repo=${shellQuote(sourceRepo)}`, `deploy_ref=${shellQuote(deployRefFor(options, spec))}`, `commit=${shellQuote(commit)}`, `dockerfile=${shellQuote(spec.dockerfile)}`, `namespace=${shellQuote(k3s.namespace)}`, `deployment=${shellQuote(k3s.deploymentName)}`, `container_name=${shellQuote(k3s.containerName)}`, `deployment_specs_b64=${shellQuote(deploymentSpecsBase64)}`, `service_name=${shellQuote(k3s.serviceName)}`, `service_port=${shellQuote(String(k3s.servicePort))}`, `health_path=${shellQuote(k3s.healthPath)}`, `apply_selector=${shellQuote(k3s.applySelector ?? "")}`, `pod_selector=${shellQuote(k3s.podLabelSelector ?? `app.kubernetes.io/name=${k3s.deploymentName}`)}`, "archive=", "manifest=", "deployment_specs=", "health_tmp=", `manifest_repo_path=${shellQuote(k3s.manifestRepoPath)}`, `manifest_b64=${shellQuote(manifestBase64)}`, "export DOCKER_CONFIG=$(mktemp -d /tmp/unidesk-artifact-docker-config.XXXXXX)", "printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"", "cleanup_artifact_cd() {", " [ -n \"${archive:-}\" ] && rm -f \"$archive\"", " [ -n \"${manifest:-}\" ] && rm -f \"$manifest\"", " [ -n \"${deployment_specs:-}\" ] && rm -f \"$deployment_specs\"", " [ -n \"${health_tmp:-}\" ] && rm -f \"$health_tmp\"", " [ -n \"${DOCKER_CONFIG:-}\" ] && rm -rf \"$DOCKER_CONFIG\"", "}", "trap cleanup_artifact_cd EXIT", "command -v docker >/dev/null", "command -v kubectl >/dev/null", "command -v ctr >/dev/null", "test -S /run/k3s/containerd/containerd.sock", d601K3sGuardScript(), `curl -fsSI -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' ${shellQuote(`http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`)} >/dev/null`, "docker pull -q \"$registry_image\" >/dev/null", "label_commit=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}')", "label_service=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/service-id\" }}')", "label_repo=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-repo\" }}')", "label_dockerfile=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}')", "test \"$label_commit\" = \"$commit\"", "test \"$label_service\" = \"$service_id\"", "test \"$label_repo\" = \"$source_repo\"", "test \"$label_dockerfile\" = \"$dockerfile\"", "docker tag \"$registry_image\" \"$stable_image\"", "docker tag \"$registry_image\" \"$commit_image\"", "archive=$(mktemp /tmp/unidesk-artifact-k3s-image.XXXXXX.tar)", "manifest=$(mktemp /tmp/unidesk-artifact-k3s-manifest.XXXXXX.yaml)", "docker save \"$commit_image\" \"$stable_image\" -o \"$archive\"", "root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import \"$archive\" >/dev/null", "containerd_image_ref() {", " ref=\"$1\"", " if root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images inspect \"$ref\" >/dev/null 2>&1; then printf '%s\\n' \"$ref\"; return; fi", " suffix=\"${ref#*/}\"", " actual_ref=$(root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls -q | grep -F \"/$suffix\" | head -1 || true)", " test -n \"$actual_ref\"", " printf '%s\\n' \"$actual_ref\"", "}", "stable_containerd_ref=$(containerd_image_ref \"$stable_image\")", "commit_containerd_ref=$(containerd_image_ref \"$commit_image\")", "printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest\"", "deployment_specs=$(mktemp /tmp/unidesk-artifact-k3s-deployments.XXXXXX.json)", "printf '%s' \"$deployment_specs_b64\" | base64 -d > \"$deployment_specs\"", "python3 - \"$deployment_specs\" \"$manifest\" <<'PY'", "import json, sys", "specs = json.load(open(sys.argv[1], encoding='utf-8'))", "manifest = open(sys.argv[2], encoding='utf-8').read()", "missing = [item['name'] for item in specs if f\"name: {item['name']}\" not in manifest]", "if missing:", " raise SystemExit('manifest missing deployment(s): ' + ','.join(missing))", "PY", "if [ -n \"$apply_selector\" ]; then kubectl apply -f \"$manifest\" -l \"$apply_selector\"; else kubectl apply -f \"$manifest\"; fi", ...(environment === "dev" && spec.serviceId === "frontend" ? [d601DevFrontendAuthPatchScript(readConfig())] : []), "previous_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}' 2>/dev/null || true)", "python3 - \"$deployment_specs\" <<'PY' | while IFS=$'\\t' read -r rollout_deployment rollout_container; do", "import json, sys", "for item in json.load(open(sys.argv[1], encoding='utf-8')):", " print(f\"{item['name']}\\t{item['containerName']}\")", "PY", " kubectl -n \"$namespace\" set image \"deployment/$rollout_deployment\" \"$rollout_container=$commit_image\"", " kubectl -n \"$namespace\" set env \"deployment/$rollout_deployment\" \"UNIDESK_DEPLOY_SERVICE_ID=$service_id\" \"UNIDESK_DEPLOY_REF=$deploy_ref\" \"UNIDESK_DEPLOY_REPO=$source_repo\" \"UNIDESK_DEPLOY_COMMIT=$commit\" \"UNIDESK_DEPLOY_REQUESTED_COMMIT=$commit\" \"CODE_QUEUE_DEPLOY_COMMIT=$commit\" \"CODE_QUEUE_DEPLOY_REQUESTED_COMMIT=$commit\"", ` kubectl -n "$namespace" annotate "deployment/$rollout_deployment" ${labels.map(shellQuote).join(" ")} --overwrite`, " if [ -n \"$previous_commit\" ] && [ \"$previous_commit\" != \"$commit\" ]; then kubectl -n \"$namespace\" annotate \"deployment/$rollout_deployment\" \"unidesk.ai/deploy-previous-commit=$previous_commit\" --overwrite; fi", "done", "python3 - \"$deployment_specs\" <<'PY' | while IFS= read -r rollout_deployment; do", "import json, sys", "for item in json.load(open(sys.argv[1], encoding='utf-8')):", " print(item['name'])", "PY", " kubectl -n \"$namespace\" rollout status \"deployment/$rollout_deployment\" --timeout=180s", "done", "deployment_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}')", "deployment_requested_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-requested-commit}')", "test \"$deployment_commit\" = \"$commit\"", "test \"$deployment_requested_commit\" = \"$commit\"", "deployment_image=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.spec.template.spec.containers[?(@.name==\"'\"$container_name\"'\")].image}')", "test \"$deployment_image\" = \"$commit_image\"", "containerd_config_label() {", " ref=\"$1\"", " key=\"$2\"", " actual_ref=$(containerd_image_ref \"$ref\")", " target_digest=$(root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images inspect \"$actual_ref\" | python3 -c 'import re,sys; m=re.search(r\"@(sha256:[0-9a-f]+)\", sys.stdin.read()); print(m.group(1) if m else \"\")')", " test -n \"$target_digest\"", " digest=\"$target_digest\"", " config_digest=", " while [ -n \"$digest\" ]; do", " content_file=$(mktemp /tmp/unidesk-artifact-content.XXXXXX.json)", " root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io content get \"$digest\" > \"$content_file\"", " next=$(python3 - \"$content_file\" <<'PY'", "import json, sys", "obj = json.load(open(sys.argv[1], encoding='utf-8'))", "manifests = obj.get('manifests')", "if isinstance(manifests, list):", " for item in manifests:", " platform = item.get('platform') or {}", " if platform.get('architecture') in ('amd64', '') or not platform:", " print('manifest:' + str(item.get('digest') or ''))", " raise SystemExit(0)", " print('')", " raise SystemExit(0)", "config = obj.get('config') or {}", "print('config:' + str(config.get('digest') or ''))", "PY", " )", " rm -f \"$content_file\"", " case \"$next\" in", " manifest:*) digest=\"${next#manifest:}\" ;;", " config:*) config_digest=\"${next#config:}\"; break ;;", " *) digest= ;;", " esac", " done", " test -n \"$config_digest\"", " config_file=$(mktemp /tmp/unidesk-artifact-config.XXXXXX.json)", " root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io content get \"$config_digest\" > \"$config_file\"", " python3 - \"$config_file\" \"$key\" <<'PY'", "import json, sys", "cfg = json.load(open(sys.argv[1], encoding='utf-8'))", "key = sys.argv[2]", "print(str(((cfg.get('config') or {}).get('Labels') or {}).get(key) or ''))", "PY", " rm -f \"$config_file\"", "}", "image_label_commit=$(containerd_config_label \"$commit_image\" 'unidesk.ai/source-commit' 2>/tmp/unidesk-artifact-containerd-label.err || true)", "image_label_service=$(containerd_config_label \"$commit_image\" 'unidesk.ai/service-id' 2>>/tmp/unidesk-artifact-containerd-label.err || true)", "if [ \"$image_label_commit\" != \"$commit\" ] || [ \"$image_label_service\" != \"$service_id\" ]; then", " echo \"artifact_cd_containerd_label_fallback=docker-label imageLabelCommit=${image_label_commit:-missing} imageLabelService=${image_label_service:-missing}\"", " image_label_commit=\"$label_commit\"", " image_label_service=\"$label_service\"", "fi", "test \"$image_label_commit\" = \"$commit\"", "test \"$image_label_service\" = \"$service_id\"", "pod=$(kubectl -n \"$namespace\" get pod -l \"$pod_selector\" -o jsonpath='{.items[0].metadata.name}')", "test -n \"$pod\"", "pod_image=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.spec.containers[?(@.name==\"'\"$container_name\"'\")].image}')", "test \"$pod_image\" = \"$commit_image\"", "pod_image_id=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.status.containerStatuses[?(@.name==\"'\"$container_name\"'\")].imageID}')", "if [ -z \"$pod_image_id\" ]; then pod_image_id=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.status.containerStatuses[0].imageID}'); fi", "case \"$health_path\" in /*) ;; *) health_path=\"/$health_path\" ;; esac", "proxy_path=\"/api/v1/namespaces/$namespace/services/http:$service_name:$service_port/proxy$health_path\"", "health_tmp=$(mktemp /tmp/unidesk-artifact-health.XXXXXX.json)", "for attempt in $(seq 1 60); do", " if kubectl get --raw \"$proxy_path\" > \"$health_tmp\" 2>/tmp/unidesk-artifact-health.err; then", " health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_tmp\" 2>/dev/null || true)", " health_requested_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"requestedCommit\") or \"\"))' \"$health_tmp\" 2>/dev/null || true)", " health_ok=$(python3 -c 'import json,sys; print(\"true\" if json.load(open(sys.argv[1])).get(\"ok\") is True else \"false\")' \"$health_tmp\" 2>/dev/null || true)", " echo \"artifact_cd_health_probe attempt=$attempt ok=$health_ok commit=$health_commit requestedCommit=$health_requested_commit\"", " if [ \"$health_ok\" = \"true\" ] && [ \"$health_commit\" = \"$commit\" ] && [ \"$health_requested_commit\" = \"$commit\" ]; then break; fi", " else", " echo \"artifact_cd_health_probe attempt=$attempt request=failed\"", " fi", " if [ \"$attempt\" = \"60\" ]; then echo artifact_cd_health_failed >&2; cat \"$health_tmp\" >&2 || true; exit 1; fi", " sleep 2", "done", "printf 'artifact_cd_service=%s\\nartifact_cd_environment=%s\\nartifact_cd_manifest_repo_path=%s\\nartifact_cd_source_image=%s\\nartifact_cd_stable_image=%s\\nartifact_cd_runtime_image=%s\\nartifact_cd_commit=%s\\nartifact_cd_requested_commit=%s\\nartifact_cd_previous_commit=%s\\nartifact_cd_pod=%s\\nartifact_cd_pod_image=%s\\nartifact_cd_pod_image_id=%s\\nartifact_cd_image_label_commit=%s\\nartifact_cd_health_commit=%s\\nartifact_cd_health_requested_commit=%s\\n' \"$service_id\" \"$environment\" \"$manifest_repo_path\" \"$registry_image\" \"$stable_image\" \"$commit_image\" \"$commit\" \"$deployment_requested_commit\" \"$previous_commit\" \"$pod\" \"$pod_image\" \"$pod_image_id\" \"$image_label_commit\" \"$health_commit\" \"$health_requested_commit\"", ].join("\n"); } export async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget): Promise> { const environment = options.environment ?? "prod"; const commit = options.commit; if (commit === null) throw new Error("artifact-registry deploy-service requires --commit "); const health = runReadonlyStatus(options, true); if (health.ok !== true) return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health }; const sourceImage = artifactImageRef(options, spec, commit); const registryProbe = runRemoteScript(options, registryArtifactProbeScript(options, spec, commit), Math.max(options.timeoutMs, 120_000)); if (registryProbe.exitCode !== 0 || registryProbe.timedOut) { return { ok: false, supported: true, serviceId: spec.serviceId, step: "registry-artifact-check", error: registryArtifactMissingMessage(spec), sourceImage, registryProbe: commandTail(registryProbe), }; } const deployScript = d601K3sArtifactDeployScript(options, spec, target, commit); const deploy = await runRemoteScriptBackground(options, deployScript, Math.max(options.timeoutMs, 420_000), "d601-k3s-deploy"); if (deploy.exitCode !== 0 || deploy.timedOut) { return { ok: false, supported: true, serviceId: spec.serviceId, step: "d601-k3s-artifact-deploy", sourceImage, registryProbe: commandTail(registryProbe), deploy: commandTail(deploy), rollback: rollbackInfo(spec, target, environment, commit), }; } return { ok: true, supported: true, serviceId: spec.serviceId, environment, commit, providerId: options.providerId, sourceRepo: sourceRepoFor(options, spec), deployRef: deployRefFor(options, spec), sourceImage, stableImage: target.targetImage, runtimeImage: target.targetCommitImage(commit), manifestRepoPath: target.k3s?.manifestRepoPath, registryProbe: commandTail(registryProbe), deploy: commandTail(deploy), validation: { liveCommit: commit, liveRequestedCommit: commit, imageLabelCommit: commit, serviceHealthCommit: commit, serviceHealthRequestedCommit: commit, healthyOldVersionAccepted: false, }, rollback: rollbackInfo(spec, target, environment, commit), }; }