Files
pikasTech-unidesk/scripts/src/artifact-registry/artifact-probe.ts
T
2026-06-25 16:16:25 +00:00

178 lines
9.7 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. artifact-probe module for scripts/src/artifact-registry.ts.
// Moved mechanically from scripts/src/artifact-registry.ts:2391-2578 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, ArtifactRegistryOptions } from "./types";
import { base64, d601K3sGuardScript, shellQuote } from "./bundle";
import { artifactImageRef, deployRefFor, sourceRepoFor } from "./consumer";
export function registryArtifactProbeScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
const sourceImage = artifactImageRef(options, spec, commit);
return [
"set -euo pipefail",
`registry_image=${shellQuote(sourceImage)}`,
`manifest_url=${shellQuote(`http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`)}`,
"headers=$(mktemp /tmp/unidesk-artifact-manifest.XXXXXX.headers)",
"trap 'rm -f \"$headers\"' EXIT",
"curl -fsSI -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -D \"$headers\" -o /dev/null \"$manifest_url\"",
"manifest_digest=$(awk 'BEGIN{IGNORECASE=1} /^Docker-Content-Digest:/ {gsub(/\\r/, \"\", $2); print $2; exit}' \"$headers\")",
"printf 'registry_image=%s\\nmanifest_url=%s\\nmanifest_digest=%s\\n' \"$registry_image\" \"$manifest_url\" \"$manifest_digest\"",
].join("\n");
}
export function registryArtifactMissingMessage(spec: ArtifactConsumerSpec): string {
return `${spec.serviceId} image artifact is missing from D601 registry; run CI artifact publication first`;
}
export function verifyLocalArtifactLabels(
localLoadedImage: string,
spec: ArtifactConsumerSpec,
commit: string,
sourceRepo: string,
): Record<string, unknown> | null {
const inspectPulled = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{json .Config.Labels}}"], repoRoot);
const labelCommit = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/source-commit\" }}"], repoRoot);
const labelService = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/service-id\" }}"], repoRoot);
const labelRepo = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/source-repo\" }}"], repoRoot);
const labelDockerfile = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}"], repoRoot);
const observed = {
commit: labelCommit.stdout.trim(),
serviceId: labelService.stdout.trim(),
sourceRepo: labelRepo.stdout.trim(),
dockerfile: labelDockerfile.stdout.trim(),
labels: inspectPulled.stdout.trim(),
};
const ok = observed.commit === commit
&& observed.serviceId === spec.serviceId
&& observed.sourceRepo === sourceRepo
&& observed.dockerfile === spec.dockerfile;
if (ok) return null;
return {
ok: false,
step: "image-label-verify",
expected: { commit, serviceId: spec.serviceId, sourceRepo, dockerfile: spec.dockerfile },
observed,
};
}
export function d601DevFrontendAuthPatchScript(config: UniDeskConfig): string {
const sshClientToken = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_TOKEN");
if (sshClientToken === null) throw new Error("UNIDESK_SSH_CLIENT_TOKEN must be present in .state/docker-compose.env before deploying dev frontend");
const sshClientRouteAllowlist = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST") ?? "G14,G14:*,D601,D601:*";
const secretData = {
AUTH_USERNAME: base64(config.auth.username),
AUTH_PASSWORD: base64(config.auth.password),
SESSION_SECRET: base64(config.auth.sessionSecret),
UNIDESK_SSH_CLIENT_TOKEN: base64(sshClientToken),
};
const configData = {
SESSION_TTL_SECONDS: String(config.auth.sessionTtlSeconds),
UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST: sshClientRouteAllowlist,
};
return [
d601K3sGuardScript(),
`secret_patch=${shellQuote(JSON.stringify({ data: secretData }))}`,
`config_patch=${shellQuote(JSON.stringify({ data: configData }))}`,
"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 artifact_cd_dev_frontend_auth_synced=ok",
].join("\n");
}
export function composeArtifactEnvValues(spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, options: ArtifactRegistryOptions, commit: string): Record<string, string> {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const prefix = target.compose.deployEnvPrefix;
return {
[`${prefix}_SERVICE_ID`]: spec.serviceId,
[`${prefix}_REF`]: deployRefFor(options, spec),
[`${prefix}_REPO`]: sourceRepoFor(options, spec),
[`${prefix}_COMMIT`]: commit,
[`${prefix}_REQUESTED_COMMIT`]: commit,
};
}
export function syntheticComposeHealthDeployScript(compose: NonNullable<ArtifactConsumerTarget["compose"]>, commit: string, sourceRepo: string): string[] {
if (compose.syntheticHealthDeployProof === undefined) return [];
const prefix = compose.deployEnvPrefix;
const envKeys = [
"UNIDESK_DEPLOY_SERVICE_ID",
"UNIDESK_DEPLOY_REF",
"UNIDESK_DEPLOY_REPO",
"UNIDESK_DEPLOY_COMMIT",
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
`${prefix}_SERVICE_ID`,
`${prefix}_REF`,
`${prefix}_REPO`,
`${prefix}_COMMIT`,
`${prefix}_REQUESTED_COMMIT`,
];
const envKeysJson = JSON.stringify(envKeys);
return [
"runtime_metadata_json=$(docker inspect \"$cid\" --format '{{json .Config.Env}}')",
"container_label_commit=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-commit\" }}' \"$cid\")",
"container_label_requested_commit=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-requested-commit\" }}' \"$cid\")",
"container_label_repo=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-repo\" }}' \"$cid\")",
"container_label_ref=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-ref\" }}' \"$cid\")",
`python3 - "$health_json" "$runtime_metadata_json" ${shellQuote(envKeysJson)} ${shellQuote(prefix)} ${shellQuote(commit)} ${shellQuote(sourceRepo)} "$actual_commit" "$actual_service" "$actual_repo" "$container_label_commit" "$container_label_requested_commit" "$container_label_repo" "$container_label_ref" <<'PY'`,
"import json, sys",
"path, env_json, keys_json, prefix, expected_commit, expected_repo, image_commit, image_service, image_repo, label_commit, label_requested_commit, label_repo, label_ref = sys.argv[1:]",
"body = json.load(open(path, encoding='utf-8'))",
"env_items = json.loads(env_json)",
"allow = set(json.loads(keys_json))",
"env = {}",
"for item in env_items:",
" if not isinstance(item, str) or '=' not in item:",
" continue",
" key, value = item.split('=', 1)",
" if key in allow:",
" env[key] = value",
"deploy = body.get('deploy') if isinstance(body, dict) and isinstance(body.get('deploy'), dict) else {}",
"synthetic = {",
" 'serviceId': deploy.get('serviceId') or env.get('UNIDESK_DEPLOY_SERVICE_ID') or env.get(prefix + '_SERVICE_ID') or image_service,",
" 'ref': deploy.get('ref') or env.get('UNIDESK_DEPLOY_REF') or env.get(prefix + '_REF') or label_ref,",
" 'repo': deploy.get('repo') or env.get('UNIDESK_DEPLOY_REPO') or env.get(prefix + '_REPO') or label_repo or image_repo,",
" 'commit': deploy.get('commit') or env.get('UNIDESK_DEPLOY_COMMIT') or env.get(prefix + '_COMMIT') or label_commit or image_commit,",
" 'requestedCommit': deploy.get('requestedCommit') or env.get('UNIDESK_DEPLOY_REQUESTED_COMMIT') or env.get(prefix + '_REQUESTED_COMMIT') or label_requested_commit,",
"}",
"body['deploy'] = synthetic",
"body['deployProof'] = {",
" 'kind': 'compose-container-runtime-metadata',",
" 'sources': ['service-health', 'container-env', 'container-labels', 'image-labels'],",
" 'sourceDirectoryUsed': False,",
" 'envKeysUsed': sorted([key for key in env if key.startswith('UNIDESK_DEPLOY_') or key.startswith(prefix + '_')]),",
"}",
"if synthetic.get('commit') != expected_commit or synthetic.get('requestedCommit') != expected_commit:",
" raise SystemExit('synthetic deploy commit mismatch: ' + json.dumps(synthetic, sort_keys=True))",
"if synthetic.get('repo') != expected_repo:",
" raise SystemExit('synthetic deploy repo mismatch: ' + json.dumps(synthetic, sort_keys=True))",
"if synthetic.get('serviceId') != image_service:",
" raise SystemExit('synthetic deploy service mismatch: ' + json.dumps(synthetic, sort_keys=True))",
"with open(path, 'w', encoding='utf-8') as handle:",
" json.dump(body, handle, separators=(',', ':'))",
" handle.write('\\n')",
"print('synthetic_health_deploy_proof=compose-container-runtime-metadata sourceDirectoryUsed=false')",
"PY",
];
}