// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. actions module for scripts/src/platform-infra.ts. // Moved mechanically from scripts/src/platform-infra.ts:1588-1852 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 { Sub2ApiAccountLocalProxyConfig, Sub2ApiEgressProxyConfig, Sub2ApiPublicExposureConfig } from "./entry"; import type { ApplyOptions, DisclosureOptions, TargetOptions } from "./options"; import { applyScript, dryRunScript } from "./apply-script"; import { readSub2ApiConfig } from "./config"; import { configPath, fieldManager, manifestPath, serviceName } from "./entry"; import { hostDockerApplyScript, hostDockerDryRunScript, hostDockerPlanSummary, hostDockerStatusScript } from "./host-docker"; import { imageRef, isExternalTarget, isHostDockerTarget, managedResourceCleanupPlan, manifest, resolveTarget, targetDatabase, targetDependencyImages, targetHasSentinel, targetImage } from "./manifest"; import { applyPk01PublicExposure } from "./pk01-public-exposure"; import { policyChecks } from "./policy"; import { prepareAccountLocalProxySecret, prepareEgressProxySecret, prepareExternalActiveSecret, preparePublicExposureSecret } from "./secrets-and-egress"; import { statusScript } from "./status-script"; import { boolField } from "./utils"; export function publicExposureSummary(exposure: Sub2ApiPublicExposureConfig): Record { return { enabled: exposure.enabled, mode: exposure.mode, publicBaseUrl: exposure.publicBaseUrl, dns: exposure.dns, local: exposure.local, frpc: exposure.frpc === null ? null : { deploymentName: exposure.frpc.deploymentName, secretName: exposure.frpc.secretName, image: exposure.frpc.image, serverAddr: exposure.frpc.serverAddr, serverPort: exposure.frpc.serverPort, proxyName: exposure.frpc.proxyName, remotePort: exposure.frpc.remotePort, localIP: exposure.frpc.localIP, localPort: exposure.frpc.localPort, tokenSourceRef: exposure.frpc.tokenSourceRef, valuesPrinted: false, }, pk01: { route: exposure.pk01.route, mode: "caddy-edge", caddyBinaryPath: exposure.pk01.caddyBinaryPath, caddyDownloadProxyUrl: exposure.pk01.caddyDownloadProxyUrl, caddyConfigPath: exposure.pk01.caddyConfigPath, caddyServiceName: exposure.pk01.caddyServiceName, pikanodeHttpHostPort: exposure.pk01.pikanodeHttpHostPort, }, }; } export function egressProxySummary(proxy: Sub2ApiEgressProxyConfig): Record { return { enabled: proxy.enabled, mode: proxy.sourceType === "master-shadowsocks" ? "master-shadowsocks-http-proxy" : "master-vpn-subscription-http-proxy", deploymentName: proxy.deploymentName, serviceName: proxy.serviceName, secretName: proxy.secretName, image: proxy.image, imagePullPolicy: proxy.imagePullPolicy, listenPort: proxy.listenPort, hostNetwork: proxy.hostNetwork, sourceConfigRef: proxy.sourceConfigRef, sourceFingerprint: proxy.sourceFingerprint, sourceRef: proxy.sourceRef, sourceType: proxy.sourceType, preferredOutbound: proxy.preferredOutbound, masterShadowsocks: proxy.masterShadowsocks === null ? null : { serverHost: proxy.masterShadowsocks.serverHost, serverPort: proxy.masterShadowsocks.serverPort, method: proxy.masterShadowsocks.method, }, applyToSub2Api: proxy.applyToSub2Api, applyToSentinel: proxy.applyToSentinel, healthProbeUrl: proxy.healthProbeUrl, noProxy: proxy.noProxy, valuesPrinted: false, }; } export function accountLocalProxySummary(proxy: Sub2ApiAccountLocalProxyConfig): Record { return { enabled: proxy.enabled, mode: "sub2api-pod-loopback-account-proxy", containerName: proxy.containerName, secretName: proxy.secretName, image: proxy.image, imagePullPolicy: proxy.imagePullPolicy, listen: `${proxy.listenHost}:${proxy.listenPort}`, sourceConfigRef: proxy.sourceConfigRef, sourceFingerprint: proxy.sourceFingerprint, sourceRef: proxy.sourceRef, sourceType: proxy.sourceType, healthProbeUrl: proxy.healthProbeUrl, valuesPrinted: false, }; } export function plan(options: TargetOptions): Record { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, options.targetId); const yaml = manifest(sub2api, target); const policy = policyChecks(sub2api, yaml, target); const database = targetDatabase(sub2api, target); const hostDocker = isHostDockerTarget(target) ? hostDockerPlanSummary(sub2api, target) : null; return { ok: policy.every((check) => check.ok), action: "platform-infra-sub2api-plan", target: { id: target.id, route: target.route, namespace: target.namespace, runtimeMode: target.runtimeMode, role: target.role, manifestPath, configPath, fieldManager, serviceDns: isHostDockerTarget(target) ? `host-docker:127.0.0.1:${target.hostDocker?.appPort ?? 0}` : `${serviceName}.${target.namespace}.svc.cluster.local:8080`, }, config: { path: configPath, version: sub2api.version, kind: sub2api.kind, metadata: sub2api.metadata, image: imageRef(sub2api, target), pullPolicy: targetImage(sub2api, target).pullPolicy, dependencyImages: targetDependencyImages(sub2api, target), security: sub2api.security, target: { runtimeMode: target.runtimeMode, databaseMode: target.databaseMode, redisMode: target.redisMode, appReplicas: target.appReplicas, redisReplicas: target.redisReplicas, hostDocker, publicExposure: target.publicExposure === null ? null : publicExposureSummary(target.publicExposure), egressProxy: target.egressProxy === null ? null : egressProxySummary(target.egressProxy), accountLocalProxy: target.accountLocalProxy === null ? null : accountLocalProxySummary(target.accountLocalProxy), }, externalDatabase: isExternalTarget(target) ? { status: target.databaseMode === "external-active" ? "external-db-active" : "pending-external-db", sourceRef: sub2api.runtime.database.sourceRef, sourceKeys: sub2api.runtime.database.sourceKeys, secretName: sub2api.runtime.database.secretName, passwordKey: sub2api.runtime.database.passwordKey, host: database.host, port: database.port, user: database.user, dbName: database.dbName, sslMode: database.sslMode, pendingAllowed: database.pendingAllowed, } : null, }, decision: { owner: "UniDesk", namespace: target.namespace, reason: target.databaseMode === "external-pending" ? `${target.id} is a standby Sub2API platform-infra target prepared through YAML; it stays scaled to zero until this target is promoted in YAML.` : isHostDockerTarget(target) ? `${target.id} is activated as a PK01 host-Docker Sub2API target against PK01 local PostgreSQL without k3s or sentinel.` : target.databaseMode === "external-active" ? `${target.id} is activated as a platform-infra Sub2API target against the external PK01 PostgreSQL runtime while keeping app state outside the k3s node.` : `${target.id} is a bundled active Sub2API platform-infra target.`, exposure: target.publicExposure?.enabled ? isHostDockerTarget(target) ? `Public HTTPS ${target.publicExposure.publicBaseUrl} through PK01 Caddy to PK01 local Docker; no D601 egress, no k3s Ingress/NodePort/LoadBalancer.` : `Public HTTPS ${target.publicExposure.publicBaseUrl} through PK01 Caddy and ${target.id} frpc; no master server forwarding and no Kubernetes Ingress/NodePort/LoadBalancer.` : "ClusterIP only; no public ingress or node-level exposure.", resourcePolicy: isHostDockerTarget(target) ? "PK01 host-Docker compose is controlled by YAML; no Kubernetes resources are rendered." : "No Kubernetes CPU/memory requests or limits, matching issue #220.", imageVersionControl: "Sub2API image repository/tag/pullPolicy are controlled by config/platform-infra/sub2api.yaml in the UniDesk repository.", urlAllowlistControl: "Sub2API upstream URL validation options are controlled by config/platform-infra/sub2api.yaml and rendered to SECURITY_URL_ALLOWLIST_* env vars.", networkPolicy: isHostDockerTarget(target) ? "Not applicable for PK01 host-Docker deployment." : "NetworkPolicy/allow-all is rendered with the deployment so kube-router cannot silently default-deny Sub2API cross-pod traffic.", publicExposure: target.publicExposure?.enabled ? { mode: target.publicExposure.mode === "pk01-local" ? "pk01-caddy-local-docker" : "pk01-caddy-frp-direct", dataPath: target.publicExposure.mode === "pk01-local" ? `client -> PK01 Caddy -> ${target.publicExposure.local?.upstreamHost}:${target.publicExposure.local?.upstreamPort} -> PK01 Docker Sub2API` : `client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API`, pikanodeRole: `pikapython.com upstream only; ${target.publicExposure.dns.hostname} does not pass through pikanode Express`, publicBaseUrl: target.publicExposure.publicBaseUrl, hostname: target.publicExposure.dns.hostname, expectedA: target.publicExposure.dns.expectedA, } : null, egressProxy: target.egressProxy?.enabled ? { mode: `${target.id} in-cluster HTTP proxy client to ${target.egressProxy.sourceType}`, service: `${target.egressProxy.serviceName}.${target.namespace}.svc.cluster.local:${target.egressProxy.listenPort}`, hostNetwork: target.egressProxy.hostNetwork, sourceRef: target.egressProxy.sourceRef, sourceType: target.egressProxy.sourceType, applyToSub2Api: target.egressProxy.applyToSub2Api, applyToSentinel: target.egressProxy.applyToSentinel, valuesPrinted: false, } : null, accountLocalProxy: target.accountLocalProxy?.enabled ? { mode: `${target.id} Sub2API pod loopback proxy for manually configured account proxy URLs`, listen: `${target.accountLocalProxy.listenHost}:${target.accountLocalProxy.listenPort}`, sourceRef: target.accountLocalProxy.sourceRef, sourceType: target.accountLocalProxy.sourceType, valuesPrinted: false, } : null, dataStores: isExternalTarget(target) ? [ isHostDockerTarget(target) ? `PK01 local PostgreSQL through ${database.host}:${database.port}` : target.databaseMode === "external-active" ? "External PostgreSQL active from platform-db/PK01" : "External PostgreSQL pending from platform-db/Pika01", target.redisReplicas === 0 ? `${target.id} local Redis 8 ephemeral cache, scaled to zero until activation` : `${target.id} local Redis 8 ephemeral cache`, ] : ["PostgreSQL 18", "Redis 8"], appPoolCaps: { databaseMaxOpenConns: 10, databaseMaxIdleConns: 2, redisPoolSize: 32, redisMinIdleConns: 2, }, standbyActivation: target.databaseMode === "external-pending" ? { target: target.id, currentMode: "predeployed-standby", enableByYaml: `change target ${target.id} databaseMode to external-active and set appReplicas/redisReplicas to 1, then apply --target ${target.id} --confirm`, sharedExternalDb: `${sub2api.runtime.database.host}:${sub2api.runtime.database.port}/${sub2api.runtime.database.dbName}`, sentinelEnabled: targetHasSentinel(sub2api, target), } : null, cleanup: managedResourceCleanupPlan(sub2api, target), }, policy, next: { dryRun: `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --dry-run`, apply: target.databaseMode === "external-pending" && target.appReplicas === 0 ? `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm # predeploy only; app replicas=0 until DB is ready` : `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm`, status: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`, validate: `bun scripts/cli.ts platform-infra sub2api validate --target ${target.id}`, }, }; } export async function apply(config: UniDeskConfig, options: ApplyOptions): Promise> { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, options.targetId); const yaml = manifest(sub2api, target); const policy = policyChecks(sub2api, yaml, target); if (!policy.every((check) => check.ok)) { return { ok: false, action: "platform-infra-sub2api-apply", mode: "policy-blocked", target: target.id, policy, }; } if (options.confirm && !options.wait) { const job = startJob( `platform_infra_sub2api_apply_${target.id.toLowerCase()}`, ["bun", "scripts/cli.ts", "platform-infra", "sub2api", "apply", "--target", target.id, "--confirm", "--wait"], `Apply ${target.id} ${isHostDockerTarget(target) ? "host-Docker" : "k3s"} platform-infra Sub2API desired state through the controlled UniDesk CLI`, ); return { ok: true, action: "platform-infra-sub2api-apply", mode: "async-job", target: { id: target.id, route: target.route, namespace: target.namespace, }, job, statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, next: { status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, rollout: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`, validate: `bun scripts/cli.ts platform-infra sub2api validate --target ${target.id}`, }, }; } if (options.dryRun) { const accountLocalProxySecretMaterial = prepareAccountLocalProxySecret(sub2api, target); const result = await capture( config, target.route, ["sh"], isHostDockerTarget(target) ? hostDockerDryRunScript(sub2api, target) : dryRunScript(yaml, target, accountLocalProxySecretMaterial), ); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-apply", mode: "dry-run", target: { id: target.id, route: target.route, namespace: target.namespace, }, policy, remote: parsed ?? compactCapture(result, { full: true }), }; } const secretMaterial = target.databaseMode === "external-active" ? prepareExternalActiveSecret(sub2api) : null; const publicExposureSecretMaterial = preparePublicExposureSecret(sub2api, target); const egressProxySecretMaterial = prepareEgressProxySecret(sub2api, target); const accountLocalProxySecretMaterial = prepareAccountLocalProxySecret(sub2api, target); const result = await capture( config, target.route, ["sh"], isHostDockerTarget(target) ? hostDockerApplyScript(sub2api, target, secretMaterial) : applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial, accountLocalProxySecretMaterial), ); const parsed = parseJsonOutput(result.stdout); const pk01Exposure = target.publicExposure === null ? null : await applyPk01PublicExposure(config, target); return { ok: result.exitCode === 0 && boolField(parsed, "ok", false) && (pk01Exposure === null || pk01Exposure.ok === true), action: "platform-infra-sub2api-apply", mode: "confirmed", target: { id: target.id, route: target.route, namespace: target.namespace, }, policy, remote: parsed ?? compactCapture(result, { full: true }), pk01Exposure, next: { status: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`, validate: `bun scripts/cli.ts platform-infra sub2api validate --target ${target.id}`, }, }; } export async function status(config: UniDeskConfig, options: DisclosureOptions): Promise> { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, options.targetId); const result = await capture(config, target.route, ["sh"], isHostDockerTarget(target) ? hostDockerStatusScript(sub2api, target) : statusScript(sub2api, target)); const parsed = parseJsonOutput(result.stdout); if (options.raw) { return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-status", target: { id: target.id, route: target.route, namespace: target.namespace, }, remote: compactCapture(result, { full: true }), parsed, }; } return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-status", target: { id: target.id, route: target.route, namespace: target.namespace, }, summary: parsed, remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }), }; }