// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. bundle module for scripts/src/artifact-registry.ts. // Moved mechanically from scripts/src/artifact-registry.ts:979-1080 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 { ArtifactRegistryOptions, RenderedBundle, RenderedFile } from "./types"; export function sha256(text: string): string { return createHash("sha256").update(text).digest("hex"); } export function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } export function d601K3sGuardScript(): string { return d601K3sGuardShellLines().join("\n"); } export function base64(value: string): string { return Buffer.from(value, "utf8").toString("base64"); } export function safeName(value: string): string { return value.replace(/[^A-Za-z0-9_.-]/gu, "-"); } export function rootExecPrelude(): 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 'artifact_registry_root_access=missing' >&2", " return 1", "}", ].join("\n"); } export function file(path: string, content: string, mode = "0644"): RenderedFile { return { path, mode, sha256: sha256(content), content }; } export function registryConfig(): string { return `version: 0.1 log: fields: service: unidesk-artifact-registry storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: false http: addr: :5000 headers: X-Content-Type-Options: [nosniff] health: storagedriver: enabled: true interval: 10s threshold: 3 `; } export function composeFile(options: ArtifactRegistryOptions): string { return `name: ${options.composeProject} services: ${options.serviceName}: image: ${options.image} container_name: ${options.containerName} restart: unless-stopped ports: - "${options.host}:${options.port}:5000" volumes: - "${options.baseDir}/config.yml:/etc/docker/registry/config.yml:ro" - "${options.storageDir}:/var/lib/registry" labels: unidesk.ai/service-id: artifact-registry unidesk.ai/managed-by: host-systemd-docker-compose unidesk.ai/provider-id: ${options.providerId} `; } export function systemdUnit(options: ArtifactRegistryOptions): string { return `[Unit] Description=UniDesk D601 Artifact Registry (CNCF Distribution) Documentation=https://github.com/pikasTech/unidesk/blob/master/docs/reference/artifact-registry.md After=network-online.target Wants=network-online.target ConditionPathExists=/var/run/docker.sock [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=${options.baseDir} ExecStartPre=/bin/mkdir -p ${options.baseDir} ${options.storageDir} ExecStartPre=/bin/sh -lc 'docker info >/dev/null' ExecStart=/bin/sh -lc 'docker compose -p ${options.composeProject} -f ${options.baseDir}/compose.yml up -d --remove-orphans' ExecStop=/bin/sh -lc 'docker compose -p ${options.composeProject} -f ${options.baseDir}/compose.yml down' TimeoutStartSec=120 TimeoutStopSec=120 [Install] WantedBy=multi-user.target `; } export function renderBundle(options: ArtifactRegistryOptions): RenderedBundle { const paths = { unit: `/etc/systemd/system/${options.unitName}`, compose: `${options.baseDir}/compose.yml`, config: `${options.baseDir}/config.yml`, storage: options.storageDir, baseDir: options.baseDir, }; return { paths, files: [ file(paths.config, registryConfig()), file(paths.compose, composeFile(options)), file(paths.unit, systemdUnit(options)), ], }; }