// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. utils module for scripts/src/platform-infra.ts. // Moved mechanically from scripts/src/platform-infra.ts:4148-4600 for #903. import { createHash, randomBytes } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import { startJob } from "../jobs"; import type { RenderedCliResult } from "../output"; import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy"; import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service"; import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library"; import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import type { Sub2ApiConfig, Sub2ApiTargetConfig } from "./entry"; import { serviceName } from "./entry"; import { codexPoolSentinelResourceNames, imageRef, managedResourceCleanupPlan } from "./manifest"; export function validateExternalPendingScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { const expectedImage = imageRef(sub2api, target); const database = sub2api.runtime.database; const redisService = sub2api.runtime.redis.serviceName; const cleanupPlan = managedResourceCleanupPlan(sub2api, target); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT capture_json() { name="$1" shift "$@" -o json >"$tmp/$name.json" 2>"$tmp/$name.err" rc=$? printf '%s' "$rc" >"$tmp/$name.rc" } capture_json ns kubectl get namespace ${target.namespace} capture_json app kubectl -n ${target.namespace} get deployment ${serviceName} capture_json redis kubectl -n ${target.namespace} get deployment ${redisService} capture_json deployments kubectl -n ${target.namespace} get deployments capture_json services kubectl -n ${target.namespace} get service capture_json cronjobs kubectl -n ${target.namespace} get cronjob capture_json statefulsets kubectl -n ${target.namespace} get statefulsets capture_json pvc kubectl -n ${target.namespace} get pvc capture_json configmap kubectl -n ${target.namespace} get configmap sub2api-config capture_json networkpolicy kubectl -n ${target.namespace} get networkpolicy allow-all python3 - "$tmp" <<'PY' import json import os import sys tmp = sys.argv[1] def rc(name): try: return int(open(os.path.join(tmp, f"{name}.rc"), encoding="utf-8").read() or "1") except FileNotFoundError: return 1 def load(name): path = os.path.join(tmp, f"{name}.json") if not os.path.exists(path): return None try: return json.load(open(path, encoding="utf-8")) except json.JSONDecodeError: return None def items(name): data = load(name) if not isinstance(data, dict): return [] return data.get("items") or [] def text(name, limit=2000): path = os.path.join(tmp, name) try: return open(path, encoding="utf-8", errors="replace").read()[-limit:] except FileNotFoundError: return "" def is_allow_all_network_policy(item): if not isinstance(item, dict): return False spec = item.get("spec") or {} return ( item.get("metadata", {}).get("name") == "allow-all" and spec.get("podSelector") == {} and set(spec.get("policyTypes") or []) == {"Ingress", "Egress"} and spec.get("ingress") == [{}] and spec.get("egress") == [{}] ) def deployment_summary(item): spec = (item or {}).get("spec") or {} status = (item or {}).get("status") or {} containers = ((spec.get("template") or {}).get("spec") or {}).get("containers") or [] volumes = ((spec.get("template") or {}).get("spec") or {}).get("volumes") or [] desired = spec.get("replicas", 1) available = status.get("availableReplicas", 0) return { "exists": isinstance(item, dict), "desired": desired, "availableReplicas": available, "readyReplicas": status.get("readyReplicas", 0), "ready": available >= desired, "images": [container.get("image") for container in containers], "volumes": volumes, } services = items("services") deployments = items("deployments") cronjobs = items("cronjobs") statefulsets = items("statefulsets") pvcs = items("pvc") configmap = load("configmap") configmap_data = (configmap or {}).get("data") or {} cleanup_plan = json.loads(${JSON.stringify(JSON.stringify(cleanupPlan))}) external_db_state = cleanup_plan["externalDbState"] redis_persistent_state = cleanup_plan["redisPersistentState"] public_exposure_state = cleanup_plan["publicExposure"] egress_proxy_state = cleanup_plan["egressProxy"] app = load("app") redis = load("redis") app_summary = deployment_summary(app) redis_summary = deployment_summary(redis) network_policy_obj = load("networkpolicy") network_policy_ok = rc("networkpolicy") == 0 and is_allow_all_network_policy(network_policy_obj) service_names = sorted(item.get("metadata", {}).get("name") for item in services) local_postgres_present = ( any(item.get("metadata", {}).get("name") == external_db_state["postgresServiceName"] for item in services) or any(item.get("metadata", {}).get("name") == external_db_state["postgresStatefulSetName"] for item in statefulsets) or any(item.get("metadata", {}).get("name") in [external_db_state["postgresPvcName"], external_db_state["appDataPvcName"]] for item in pvcs) ) redis_pvc_present = any(item.get("metadata", {}).get("name") == redis_persistent_state["pvcName"] for item in pvcs) public_exposure_deployment_present = any(item.get("metadata", {}).get("name") == public_exposure_state["deploymentName"] for item in deployments) egress_proxy_deployment_present = any(item.get("metadata", {}).get("name") == egress_proxy_state["deploymentName"] for item in deployments) egress_proxy_service_present = egress_proxy_state["serviceName"] in service_names sentinel_cronjob_present = any(item.get("metadata", {}).get("name") == "${codexPoolSentinelResourceNames().cronJobName}" for item in cronjobs) standby_disabled_ok = ( not public_exposure_deployment_present and not egress_proxy_deployment_present and not egress_proxy_service_present and not sentinel_cronjob_present ) redis_ephemeral = any(volume.get("emptyDir") == {} for volume in redis_summary["volumes"]) configmap_aligned = ( configmap_data.get("DATABASE_HOST") == "${database.host}" and configmap_data.get("DATABASE_PORT") == "${database.port}" and configmap_data.get("DATABASE_USER") == "${database.user}" and configmap_data.get("DATABASE_DBNAME") == "${database.dbName}" and configmap_data.get("DATABASE_SSLMODE") == "${database.sslMode}" and configmap_data.get("REDIS_HOST") == "${redisService}" ) app_scaled_to_zero = app_summary["exists"] and app_summary["desired"] == ${target.appReplicas} and ${target.appReplicas} == 0 image_aligned = "${expectedImage}" in app_summary["images"] redis_ready = redis_summary["exists"] and redis_summary["desired"] == ${target.redisReplicas} and redis_ephemeral and (redis_summary["ready"] or ${target.redisReplicas} == 0) payload = { "ok": rc("ns") == 0 and network_policy_ok and app_scaled_to_zero and image_aligned and redis_ready and configmap_aligned and not local_postgres_present and not redis_pvc_present and standby_disabled_ok and "${serviceName}" in service_names and "${redisService}" in service_names, "target": "${target.id}", "namespace": "${target.namespace}", "status": "pending-external-db", "databaseMode": "${target.databaseMode}", "redisMode": "${target.redisMode}", "expectedRedisReplicas": ${target.redisReplicas}, "externalDatabase": { "sourceRef": "${database.sourceRef}", "host": "${database.host}", "port": ${database.port}, "user": "${database.user}", "dbName": "${database.dbName}", "sslMode": "${database.sslMode}", "secretName": "${database.secretName}", "passwordKey": "${database.passwordKey}", "pendingAllowed": ${database.pendingAllowed ? "True" : "False"}, "passwordPrinted": False, }, "checks": { "namespaceExists": rc("ns") == 0, "allowAllNetworkPolicy": {"ok": network_policy_ok, "stderr": text("networkpolicy.err")}, "appScaledToZero": {"ok": app_scaled_to_zero, "summary": app_summary}, "imageAligned": {"ok": image_aligned, "desiredImage": "${expectedImage}", "runningImages": app_summary["images"]}, "redisReadyEphemeral": {"ok": redis_ready, "summary": redis_summary, "redisPvcPresent": redis_pvc_present, "readinessRequired": ${target.redisReplicas} > 0}, "serviceBoundary": {"serviceNames": service_names, "servicePresent": "${serviceName}" in service_names, "redisServicePresent": "${redisService}" in service_names}, "externalDbOnly": {"ok": not local_postgres_present, "localPostgresPresent": local_postgres_present}, "standbyDisabled": { "ok": standby_disabled_ok, "publicExposureDeploymentPresent": public_exposure_deployment_present, "egressProxyDeploymentPresent": egress_proxy_deployment_present, "egressProxyServicePresent": egress_proxy_service_present, "sentinelCronJobPresent": sentinel_cronjob_present, }, "configMapAligned": {"ok": configmap_aligned, "databaseHost": configmap_data.get("DATABASE_HOST"), "redisHost": configmap_data.get("REDIS_HOST")}, }, "next": { "status": "bun scripts/cli.ts platform-infra sub2api status --target ${target.id}", "activateAfterDbReady": "update config/platform-infra/sub2api.yaml target ${target.id} appReplicas and redisReplicas to 1 after ${database.sourceRef}, ${database.secretName}/${database.passwordKey}, and runtime images are ready, then apply --target ${target.id} --confirm", }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } export function boolField(value: Record | null, key: string, defaultValue: boolean): boolean { if (value === null) return defaultValue; const field = value[key]; return typeof field === "boolean" ? field : defaultValue; } export function asRecordOrNull(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; }