178 lines
9.7 KiB
TypeScript
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",
|
|
];
|
|
}
|