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"; const serviceName = "sub2api"; const fieldManager = "unidesk-platform-infra"; const manifestPath = rootPath("src", "components", "platform-infra", "sub2api", "sub2api.k8s.yaml"); const configPath = rootPath("config", "platform-infra", "sub2api.yaml"); const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml"); const sub2apiCaddyManagedMarker = "sub2api"; const requiredSecretKeys = ["POSTGRES_PASSWORD", "ADMIN_PASSWORD", "JWT_SECRET", "TOTP_ENCRYPTION_KEY"] as const; type DatabaseMode = "bundled" | "external-pending" | "external-active"; type RedisMode = "bundled-persistent" | "local-ephemeral"; interface Sub2ApiConfig { version: number; kind: string; metadata: { id: string; owner: string; relatedIssues: number[]; }; defaults: Sub2ApiDefaults; image: { repository: string; tag: string; pullPolicy: "Always" | "IfNotPresent" | "Never"; }; dependencyImages: { postgres: string; redis: string; }; security: { urlAllowlist: { enabled: boolean; allowInsecureHttp: boolean; allowPrivateHosts: boolean; upstreamHosts: string[]; }; }; targets: Sub2ApiTargetConfig[]; runtime: { database: ExternalDatabaseConfig; secrets: RuntimeSecretsConfig; redis: RuntimeRedisConfig; appData: { mode: "persistent-pvc" | "empty-dir"; }; sentinel: { mode: "singleton"; enabledOnTargets: string[]; }; }; } interface Sub2ApiDefaults { targetId: string; cleanup: { externalDbState: { postgresStatefulSetName: string; postgresServiceName: string; postgresPvcName: string; appDataPvcName: string; }; redisPersistentState: { pvcName: string; }; publicExposure: { deploymentName: string; configMapName: string; secretName: string; }; egressProxy: { deploymentName: string; serviceName: string; secretName: string; }; }; } interface Sub2ApiTargetConfig { id: string; route: string; namespace: string; role: string; enabled: boolean; databaseMode: DatabaseMode; redisMode: RedisMode; appReplicas: number; redisReplicas: number; image: Partial; dependencyImages: Partial; publicExposure: Sub2ApiPublicExposureConfig | null; egressProxy: Sub2ApiEgressProxyConfig | null; } interface Sub2ApiPublicExposureConfig { enabled: boolean; publicBaseUrl: string; dns: { hostname: string; expectedA: string; resolvers: string[]; }; frpc: { deploymentName: string; secretName: string; secretKey: string; image: string; serverAddr: string; serverPort: number; proxyName: string; remotePort: number; localIP: string; localPort: number; tokenSourceRef: string; tokenSourceKey: string; }; pk01: { route: string; caddyBinaryPath: string; caddyDownloadUrl: string; caddyDownloadProxyUrl: string | null; caddyConfigPath: string; caddyServiceName: string; caddyStorageDir: string; caddyEmail: string; pikanodeRoot: string; pikanodeContainerName: string; pikanodeImage: string; pikanodeHttpHostPort: number; responseHeaderTimeoutSeconds: number; }; } interface Sub2ApiEgressProxyConfig { enabled: boolean; deploymentName: string; serviceName: string; secretName: string; secretKey: string; image: string; imagePullPolicy: "Always" | "IfNotPresent" | "Never"; listenPort: number; sourceRef: string; sourceKey: string; sourceType: "subscription-url"; preferredOutbound: "vless-reality" | "hysteria2"; noProxy: string[]; applyToSub2Api: boolean; applyToSentinel: boolean; healthProbeUrl: string; } interface ExternalDatabaseConfig { mode: "external"; sourceRef: string; sourceKeys: { user: string; password: string; dbName: string; }; secretName: string; passwordKey: string; host: string; port: number; user: string; dbName: string; sslMode: string; pendingAllowed: boolean; } interface RuntimeSecretsConfig { root: string; appSourceRef: string; } interface RuntimeRedisConfig { serviceName: string; persistence: boolean; } interface ExternalActiveSecretMaterial { sourceRef: string; sourcePath: string; appSourceRef: string; appSourcePath: string; action: "create" | "update" | "none"; fingerprint: string; values: Record; } interface PublicExposureSecretMaterial { sourceRef: string; sourcePath: string; secretName: string; secretKey: string; fingerprint: string; frpcToml: string; valuesPrinted: false; } interface EgressProxySecretMaterial { sourceRef: string; sourcePath: string; secretName: string; secretKey: string; fingerprint: string; subscriptionBytes: number; selectedOutbound: string; configJson: string; proxyUrl: string; noProxy: string; valuesPrinted: false; } interface ManagedResourceCleanupPlan { externalDbState: { enabled: boolean; postgresStatefulSetName: string; postgresServiceName: string; postgresPvcName: string; appDataPvcName: string; }; redisPersistentState: { enabled: boolean; pvcName: string; }; publicExposure: { enabled: boolean; deploymentName: string; configMapName: string; secretName: string; }; egressProxy: { enabled: boolean; deploymentName: string; serviceName: string; secretName: string; }; sentinel: { enabled: boolean; cronJobName: string; configMapName: string; credentialsSecretName: string; stateConfigMapName: string; serviceAccountName: string; roleName: string; roleBindingName: string; }; } export function platformInfraHelp(): unknown { const target = sub2ApiHelpTargetSummary(); return { command: "platform-infra sub2api|langbot|n8n|wechat-archive ...", output: "json", usage: [ "bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]", "bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --dry-run", "bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --confirm", "bun scripts/cli.ts platform-infra sub2api status [--target G14|D601] [--full|--raw]", "bun scripts/cli.ts platform-infra sub2api validate [--target G14|D601] [--full|--raw]", "bun scripts/cli.ts platform-infra sub2api codex-pool plan", "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm", "bun scripts/cli.ts platform-infra sub2api codex-pool validate", "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id ", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm", "bun scripts/cli.ts platform-infra langbot plan [--target G14]", "bun scripts/cli.ts platform-infra langbot apply [--target G14] --confirm", "bun scripts/cli.ts platform-infra langbot status [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra langbot logs [--target G14] [--component app|plugin-runtime|frpc|all]", "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm", "bun scripts/cli.ts platform-infra langbot query --path /api/v1/platform/bots", "bun scripts/cli.ts platform-infra n8n plan [--target G14]", "bun scripts/cli.ts platform-infra n8n apply [--target G14] --confirm", "bun scripts/cli.ts platform-infra n8n status [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra n8n logs [--target G14] [--component app|frpc|all]", "bun scripts/cli.ts platform-infra n8n validate [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra wechat-archive plan [--target G14]", "bun scripts/cli.ts platform-infra wechat-archive apply [--target G14] --confirm", "bun scripts/cli.ts platform-infra wechat-archive status [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra wechat-archive validate [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra wechat-archive pull --remote-path /UniDesk/WeChatArchive/...", "bun scripts/cli.ts platform-infra wechat-archive wcf-host-prepare --confirm", "bun scripts/cli.ts platform-infra wechat-archive wcf-host-start --confirm", "bun scripts/cli.ts platform-infra wechat-archive collector-image-build --confirm", "bun scripts/cli.ts platform-infra wechat-archive collector-apply --confirm", "bun scripts/cli.ts platform-infra wechat-archive collector-status --full", ], description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n and WeChat archive workflows. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.", target, codexPool: { usage: [ "bun scripts/cli.ts platform-infra sub2api codex-pool plan", "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm", "bun scripts/cli.ts platform-infra sub2api codex-pool validate", "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id ", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status", ], module: "scripts/src/platform-infra-sub2api-codex.ts", }, }; } function sub2ApiHelpTargetSummary(): Record { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, sub2api.defaults.targetId); return { default: sub2api.defaults.targetId, namespace: target.namespace, service: serviceName, serviceDns: `${serviceName}.${target.namespace}.svc.cluster.local:8080`, exposure: target.publicExposure?.enabled === true ? "yaml-controlled-public-exposure" : "k3s-cluster-internal-only", resourceLimits: "unset-by-policy", versionConfigPath: configPath, }; } export async function runPlatformInfraCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [target, action] = args; if (target === "langbot") { const { runLangBotCommand } = await import("./platform-infra-langbot"); return await runLangBotCommand(config, args.slice(1)); } if (target === "n8n") { const { runN8nCommand } = await import("./platform-infra-n8n"); return await runN8nCommand(config, args.slice(1)); } if (target === "wechat-archive") { const { runWechatArchiveCommand } = await import("./platform-infra-wechat-archive"); return await runWechatArchiveCommand(config, args.slice(1)); } if (target !== "sub2api") return unsupported(args); if (action === "plan" || action === undefined) return plan(parseTargetOptions(args.slice(2))); if (action === "apply") return await apply(config, parseApplyOptions(args.slice(2))); if (action === "status") return await status(config, parseDisclosureOptions(args.slice(2))); if (action === "validate") return await validate(config, parseDisclosureOptions(args.slice(2))); if (action === "codex-pool") { const { runCodexPoolCommand } = await import("./platform-infra-sub2api-codex"); return await runCodexPoolCommand(config, args.slice(2)); } return unsupported(args); } interface ApplyOptions { targetId: string; dryRun: boolean; confirm: boolean; wait: boolean; } interface DisclosureOptions { targetId: string; full: boolean; raw: boolean; } interface TargetOptions { targetId: string; } interface PolicyCheck { name: string; ok: boolean; detail: string; } function unsupported(args: string[]): Record { return { ok: false, error: "unsupported-platform-infra-command", args, help: platformInfraHelp(), }; } function parseApplyOptions(args: string[]): ApplyOptions { const target = parseTargetOptions(args); validateOptions(args, new Set(["--dry-run", "--confirm", "--wait", "--target"])); if (args.includes("--dry-run") && args.includes("--confirm")) throw new Error("apply accepts only one of --dry-run or --confirm"); return { targetId: target.targetId, dryRun: args.includes("--dry-run") || !args.includes("--confirm"), confirm: args.includes("--confirm"), wait: args.includes("--wait"), }; } function parseDisclosureOptions(args: string[]): DisclosureOptions { const target = parseTargetOptions(args); validateOptions(args, new Set(["--full", "--raw", "--target"])); const raw = args.includes("--raw"); return { targetId: target.targetId, full: raw || args.includes("--full"), raw }; } function parseTargetOptions(args: string[]): TargetOptions { let targetId: string | null = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); targetId = value; index += 1; } else if (arg.startsWith("--target=")) { targetId = arg.slice("--target=".length); } } const resolvedTargetId = targetId ?? defaultSub2ApiTargetId(); if (!/^[A-Za-z0-9._-]+$/u.test(resolvedTargetId)) throw new Error("--target must be a simple target id"); return { targetId: resolvedTargetId }; } function defaultSub2ApiTargetId(): string { return readSub2ApiConfig().defaults.targetId; } function validateOptions(args: string[], booleanOptions: Set): void { for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--target") { index += 1; continue; } if (arg.startsWith("--target=") && booleanOptions.has("--target")) continue; if (booleanOptions.has(arg)) continue; throw new Error(`unsupported option: ${arg}`); } } function readSub2ApiConfig(): Sub2ApiConfig { const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${configPath} must contain a YAML object`); const root = parsed as Record; const version = integerField(root, "version", ""); if (version !== 1) throw new Error(`${configPath}.version must be 1`); const kind = stringField(root, "kind", ""); if (kind !== "platform-infra-sub2api") throw new Error(`${configPath}.kind must be platform-infra-sub2api`); const metadata = objectField(root, "metadata", ""); const relatedIssues = integerArrayField(metadata, "relatedIssues", "metadata"); const defaults = parseDefaults(root); const image = (parsed as { image?: unknown }).image; if (typeof image !== "object" || image === null || Array.isArray(image)) throw new Error(`${configPath}.image must be an object`); const record = image as Record; const repository = stringField(record, "repository", "image"); const tag = stringField(record, "tag", "image"); const pullPolicy = stringField(record, "pullPolicy", "image"); if (pullPolicy !== "Always" && pullPolicy !== "IfNotPresent" && pullPolicy !== "Never") throw new Error(`${configPath}.image.pullPolicy must be Always, IfNotPresent, or Never`); if (!isImageRepository(repository)) throw new Error(`${configPath}.image.repository has an unsupported format`); if (!/^[A-Za-z0-9._-]+$/u.test(tag)) throw new Error(`${configPath}.image.tag has an unsupported format`); const dependencyImages = parseDependencyImages(root); const security = objectField(parsed as Record, "security", ""); const urlAllowlist = objectField(security, "urlAllowlist", "security"); const enabled = booleanField(urlAllowlist, "enabled", "security.urlAllowlist"); const allowInsecureHttp = booleanField(urlAllowlist, "allowInsecureHttp", "security.urlAllowlist"); const allowPrivateHosts = booleanField(urlAllowlist, "allowPrivateHosts", "security.urlAllowlist"); const upstreamHosts = stringArrayField(urlAllowlist, "upstreamHosts", "security.urlAllowlist"); const targets = parseTargets(root, defaults.targetId); const runtime = parseRuntime(root); return { version, kind, metadata: { id: stringField(metadata, "id", "metadata"), owner: stringField(metadata, "owner", "metadata"), relatedIssues, }, defaults, image: { repository, tag, pullPolicy }, dependencyImages, security: { urlAllowlist: { enabled, allowInsecureHttp, allowPrivateHosts, upstreamHosts, }, }, targets, runtime, }; } function parseDefaults(root: Record): Sub2ApiDefaults { const defaults = objectField(root, "defaults", ""); const targetId = stringField(defaults, "targetId", "defaults"); if (!/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error(`${configPath}.defaults.targetId must be a simple target id`); const cleanup = objectField(defaults, "cleanup", "defaults"); const externalDbState = objectField(cleanup, "externalDbState", "defaults.cleanup"); const redisPersistentState = objectField(cleanup, "redisPersistentState", "defaults.cleanup"); const publicExposure = objectField(cleanup, "publicExposure", "defaults.cleanup"); const egressProxy = objectField(cleanup, "egressProxy", "defaults.cleanup"); const parsed: Sub2ApiDefaults = { targetId, cleanup: { externalDbState: { postgresStatefulSetName: stringField(externalDbState, "postgresStatefulSetName", "defaults.cleanup.externalDbState"), postgresServiceName: stringField(externalDbState, "postgresServiceName", "defaults.cleanup.externalDbState"), postgresPvcName: stringField(externalDbState, "postgresPvcName", "defaults.cleanup.externalDbState"), appDataPvcName: stringField(externalDbState, "appDataPvcName", "defaults.cleanup.externalDbState"), }, redisPersistentState: { pvcName: stringField(redisPersistentState, "pvcName", "defaults.cleanup.redisPersistentState"), }, publicExposure: { deploymentName: stringField(publicExposure, "deploymentName", "defaults.cleanup.publicExposure"), configMapName: stringField(publicExposure, "configMapName", "defaults.cleanup.publicExposure"), secretName: stringField(publicExposure, "secretName", "defaults.cleanup.publicExposure"), }, egressProxy: { deploymentName: stringField(egressProxy, "deploymentName", "defaults.cleanup.egressProxy"), serviceName: stringField(egressProxy, "serviceName", "defaults.cleanup.egressProxy"), secretName: stringField(egressProxy, "secretName", "defaults.cleanup.egressProxy"), }, }, }; const resourceNames = { ...parsed.cleanup.externalDbState, redisPersistentPvcName: parsed.cleanup.redisPersistentState.pvcName, publicExposureDeploymentName: parsed.cleanup.publicExposure.deploymentName, publicExposureConfigMapName: parsed.cleanup.publicExposure.configMapName, publicExposureSecretName: parsed.cleanup.publicExposure.secretName, egressProxyDeploymentName: parsed.cleanup.egressProxy.deploymentName, egressProxyServiceName: parsed.cleanup.egressProxy.serviceName, egressProxySecretName: parsed.cleanup.egressProxy.secretName, }; for (const [key, value] of Object.entries(resourceNames)) { validateKubernetesResourceName(value, `defaults.cleanup.${key}`); } return parsed; } function parseDependencyImages(root: Record): Sub2ApiConfig["dependencyImages"] { const value = root.dependencyImages; if (value === undefined) throw new Error(`${configPath}.dependencyImages must be an object`); if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.dependencyImages must be an object`); const record = value as Record; const postgres = stringField(record, "postgres", "dependencyImages"); const redis = stringField(record, "redis", "dependencyImages"); if (!isImageReference(postgres)) throw new Error(`${configPath}.dependencyImages.postgres has an unsupported format`); if (!isImageReference(redis)) throw new Error(`${configPath}.dependencyImages.redis has an unsupported format`); return { postgres, redis }; } function parseTargets(root: Record, defaultTargetId: string): Sub2ApiTargetConfig[] { const value = root.targets; if (value === undefined) throw new Error(`${configPath}.targets must be an array`); if (!Array.isArray(value)) throw new Error(`${configPath}.targets must be an array`); const targets = value.map((item, index) => { if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error(`${configPath}.targets[${index}] must be an object`); const record = item as Record; const path = `targets[${index}]`; const id = stringField(record, "id", path); const route = stringField(record, "route", path); const targetNamespace = stringField(record, "namespace", path); const role = stringField(record, "role", path); const enabled = booleanField(record, "enabled", path); const databaseMode = enumField(record, "databaseMode", path, ["bundled", "external-pending", "external-active"] as const); const redisMode = enumField(record, "redisMode", path, ["bundled-persistent", "local-ephemeral"] as const); const appReplicas = integerField(record, "appReplicas", path); const redisReplicas = integerField(record, "redisReplicas", path); const image = targetImageOverride(record, path); const dependencyImages = targetDependencyImageOverride(record, path); const publicExposure = parsePublicExposureConfig(record.publicExposure, path); const egressProxy = parseEgressProxyConfig(record.egressProxy, path); if (!/^[A-Za-z0-9._-]+$/u.test(id)) throw new Error(`${configPath}.${path}.id must be a simple target id`); if (!/^[A-Za-z0-9:_./-]+$/u.test(route)) throw new Error(`${configPath}.${path}.route has an unsupported format`); if (!isKubernetesName(targetNamespace)) throw new Error(`${configPath}.${path}.namespace must be a Kubernetes namespace name`); if (appReplicas < 0) throw new Error(`${configPath}.${path}.appReplicas must be >= 0`); if (redisReplicas < 0) throw new Error(`${configPath}.${path}.redisReplicas must be >= 0`); return { id, route, namespace: targetNamespace, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, image, dependencyImages, publicExposure, egressProxy }; }); const ids = new Set(); for (const target of targets) { if (ids.has(target.id)) throw new Error(`${configPath}.targets contains duplicate id ${target.id}`); ids.add(target.id); } if (!ids.has(defaultTargetId)) throw new Error(`${configPath}.targets must include defaults.targetId ${defaultTargetId}`); return targets; } function targetImageOverride(record: Record, path: string): Partial { if (record.image === undefined) return {}; const image = objectField(record, "image", path); const override: Partial = {}; if (image.repository !== undefined) { const repository = stringField(image, "repository", `${path}.image`); if (!isImageRepository(repository)) throw new Error(`${configPath}.${path}.image.repository has an unsupported format`); override.repository = repository; } if (image.tag !== undefined) { const tag = stringField(image, "tag", `${path}.image`); if (!/^[A-Za-z0-9._-]+$/u.test(tag)) throw new Error(`${configPath}.${path}.image.tag has an unsupported format`); override.tag = tag; } if (image.pullPolicy !== undefined) { override.pullPolicy = enumField(image, "pullPolicy", `${path}.image`, ["Always", "IfNotPresent", "Never"] as const); } return override; } function targetDependencyImageOverride(record: Record, path: string): Partial { if (record.dependencyImages === undefined) return {}; const dependencyImages = objectField(record, "dependencyImages", path); const override: Partial = {}; if (dependencyImages.postgres !== undefined) { const postgres = stringField(dependencyImages, "postgres", `${path}.dependencyImages`); if (!isImageReference(postgres)) throw new Error(`${configPath}.${path}.dependencyImages.postgres has an unsupported format`); override.postgres = postgres; } if (dependencyImages.redis !== undefined) { const redis = stringField(dependencyImages, "redis", `${path}.dependencyImages`); if (!isImageReference(redis)) throw new Error(`${configPath}.${path}.dependencyImages.redis has an unsupported format`); override.redis = redis; } return override; } function parsePublicExposureConfig(value: unknown, path: string): Sub2ApiPublicExposureConfig | null { if (value === undefined || value === null) return null; if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.publicExposure must be an object`); const record = value as Record; const enabled = booleanField(record, "enabled", `${path}.publicExposure`); const publicBaseUrl = stringField(record, "publicBaseUrl", `${path}.publicExposure`); const dnsRaw = objectField(record, "dns", `${path}.publicExposure`); const frpcRaw = objectField(record, "frpc", `${path}.publicExposure`); const pk01Raw = objectField(record, "pk01", `${path}.publicExposure`); const hostname = stringField(dnsRaw, "hostname", `${path}.publicExposure.dns`); const expectedA = stringField(dnsRaw, "expectedA", `${path}.publicExposure.dns`); const resolvers = stringArrayField(dnsRaw, "resolvers", `${path}.publicExposure.dns`); const exposure: Sub2ApiPublicExposureConfig = { enabled, publicBaseUrl: normalizePublicBaseUrl(publicBaseUrl, `${path}.publicExposure.publicBaseUrl`), dns: { hostname, expectedA, resolvers, }, frpc: { deploymentName: stringField(frpcRaw, "deploymentName", `${path}.publicExposure.frpc`), secretName: stringField(frpcRaw, "secretName", `${path}.publicExposure.frpc`), secretKey: stringField(frpcRaw, "secretKey", `${path}.publicExposure.frpc`), image: stringField(frpcRaw, "image", `${path}.publicExposure.frpc`), serverAddr: stringField(frpcRaw, "serverAddr", `${path}.publicExposure.frpc`), serverPort: integerField(frpcRaw, "serverPort", `${path}.publicExposure.frpc`), proxyName: stringField(frpcRaw, "proxyName", `${path}.publicExposure.frpc`), remotePort: integerField(frpcRaw, "remotePort", `${path}.publicExposure.frpc`), localIP: stringField(frpcRaw, "localIP", `${path}.publicExposure.frpc`), localPort: integerField(frpcRaw, "localPort", `${path}.publicExposure.frpc`), tokenSourceRef: stringField(frpcRaw, "tokenSourceRef", `${path}.publicExposure.frpc`), tokenSourceKey: stringField(frpcRaw, "tokenSourceKey", `${path}.publicExposure.frpc`), }, pk01: { route: stringField(pk01Raw, "route", `${path}.publicExposure.pk01`), caddyBinaryPath: stringField(pk01Raw, "caddyBinaryPath", `${path}.publicExposure.pk01`), caddyDownloadUrl: stringField(pk01Raw, "caddyDownloadUrl", `${path}.publicExposure.pk01`), caddyDownloadProxyUrl: pk01Raw.caddyDownloadProxyUrl === undefined ? null : stringField(pk01Raw, "caddyDownloadProxyUrl", `${path}.publicExposure.pk01`), caddyConfigPath: stringField(pk01Raw, "caddyConfigPath", `${path}.publicExposure.pk01`), caddyServiceName: stringField(pk01Raw, "caddyServiceName", `${path}.publicExposure.pk01`), caddyStorageDir: stringField(pk01Raw, "caddyStorageDir", `${path}.publicExposure.pk01`), caddyEmail: stringField(pk01Raw, "caddyEmail", `${path}.publicExposure.pk01`), pikanodeRoot: stringField(pk01Raw, "pikanodeRoot", `${path}.publicExposure.pk01`), pikanodeContainerName: stringField(pk01Raw, "pikanodeContainerName", `${path}.publicExposure.pk01`), pikanodeImage: stringField(pk01Raw, "pikanodeImage", `${path}.publicExposure.pk01`), pikanodeHttpHostPort: integerField(pk01Raw, "pikanodeHttpHostPort", `${path}.publicExposure.pk01`), responseHeaderTimeoutSeconds: integerField(pk01Raw, "responseHeaderTimeoutSeconds", `${path}.publicExposure.pk01`), }, }; validatePublicExposureConfig(exposure, path); return exposure; } function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgressProxyConfig | null { if (value === undefined || value === null) return null; if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.egressProxy must be an object`); const record = value as Record; const sourceType = enumField(record, "sourceType", `${path}.egressProxy`, ["subscription-url"] as const); const preferredOutbound = enumField(record, "preferredOutbound", `${path}.egressProxy`, ["vless-reality", "hysteria2"] as const); const imagePullPolicy = enumField(record, "imagePullPolicy", `${path}.egressProxy`, ["Always", "IfNotPresent", "Never"] as const); const noProxy = stringArrayField(record, "noProxy", `${path}.egressProxy`); const proxy: Sub2ApiEgressProxyConfig = { enabled: booleanField(record, "enabled", `${path}.egressProxy`), deploymentName: stringField(record, "deploymentName", `${path}.egressProxy`), serviceName: stringField(record, "serviceName", `${path}.egressProxy`), secretName: stringField(record, "secretName", `${path}.egressProxy`), secretKey: stringField(record, "secretKey", `${path}.egressProxy`), image: stringField(record, "image", `${path}.egressProxy`), imagePullPolicy, listenPort: integerField(record, "listenPort", `${path}.egressProxy`), sourceRef: stringField(record, "sourceRef", `${path}.egressProxy`), sourceKey: stringField(record, "sourceKey", `${path}.egressProxy`), sourceType, preferredOutbound, noProxy, applyToSub2Api: booleanField(record, "applyToSub2Api", `${path}.egressProxy`), applyToSentinel: booleanField(record, "applyToSentinel", `${path}.egressProxy`), healthProbeUrl: stringField(record, "healthProbeUrl", `${path}.egressProxy`), }; validateEgressProxyConfig(proxy, path); return proxy; } function parseRuntime(root: Record): Sub2ApiConfig["runtime"] { const value = root.runtime; if (value === undefined) throw new Error(`${configPath}.runtime must be an object`); if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.runtime must be an object`); const runtime = value as Record; const database = objectField(runtime, "database", "runtime"); const databaseMode = enumField(database, "mode", "runtime.database", ["external"] as const); const sourceRef = stringField(database, "sourceRef", "runtime.database"); const sourceKeysRaw = objectField(database, "sourceKeys", "runtime.database"); const sourceKeys = { user: stringField(sourceKeysRaw, "user", "runtime.database.sourceKeys"), password: stringField(sourceKeysRaw, "password", "runtime.database.sourceKeys"), dbName: stringField(sourceKeysRaw, "dbName", "runtime.database.sourceKeys"), }; const secret = stringField(database, "secretName", "runtime.database"); const passwordKey = stringField(database, "passwordKey", "runtime.database"); const host = stringField(database, "host", "runtime.database"); const port = integerField(database, "port", "runtime.database"); const user = stringField(database, "user", "runtime.database"); const dbName = stringField(database, "dbName", "runtime.database"); const sslMode = stringField(database, "sslMode", "runtime.database"); const pendingAllowed = booleanField(database, "pendingAllowed", "runtime.database"); if (!isKubernetesName(secret)) throw new Error(`${configPath}.runtime.database.secretName must be a Kubernetes Secret name`); if (!/^[A-Za-z0-9_./-]+$/u.test(sourceRef)) throw new Error(`${configPath}.runtime.database.sourceRef has an unsupported format`); for (const [key, value] of Object.entries(sourceKeys)) { if (!/^[A-Z0-9_]+$/u.test(value)) throw new Error(`${configPath}.runtime.database.sourceKeys.${key} must be an env key`); } if (!/^[A-Z0-9_]+$/u.test(passwordKey)) throw new Error(`${configPath}.runtime.database.passwordKey must be an env key`); if (!/^[A-Za-z0-9._-]+$/u.test(host)) throw new Error(`${configPath}.runtime.database.host has an unsupported format`); if (!/^[A-Za-z0-9_][-A-Za-z0-9_]*$/u.test(user)) throw new Error(`${configPath}.runtime.database.user has an unsupported format`); if (!/^[A-Za-z0-9_][-A-Za-z0-9_]*$/u.test(dbName)) throw new Error(`${configPath}.runtime.database.dbName has an unsupported format`); if (!/^[A-Za-z0-9_-]+$/u.test(sslMode)) throw new Error(`${configPath}.runtime.database.sslMode has an unsupported format`); if (port < 1 || port > 65535) throw new Error(`${configPath}.runtime.database.port must be a TCP port`); if (databaseMode !== "external") throw new Error(`${configPath}.runtime.database.mode must be external`); const redis = objectField(runtime, "redis", "runtime"); const redisServiceName = stringField(redis, "serviceName", "runtime.redis"); const redisPersistence = booleanField(redis, "persistence", "runtime.redis"); if (!isKubernetesName(redisServiceName)) throw new Error(`${configPath}.runtime.redis.serviceName must be a Kubernetes Service name`); const secretsRaw = objectField(runtime, "secrets", "runtime"); const secretRoot = stringField(secretsRaw, "root", "runtime.secrets"); const appSourceRef = stringField(secretsRaw, "appSourceRef", "runtime.secrets"); if (!/^[A-Za-z0-9_./-]+$/u.test(secretRoot)) throw new Error(`${configPath}.runtime.secrets.root has an unsupported format`); if (!/^[A-Za-z0-9_./-]+$/u.test(appSourceRef)) throw new Error(`${configPath}.runtime.secrets.appSourceRef has an unsupported format`); const appData = objectField(runtime, "appData", "runtime"); const appDataMode = enumField(appData, "mode", "runtime.appData", ["persistent-pvc", "empty-dir"] as const); const sentinel = objectField(runtime, "sentinel", "runtime"); const sentinelMode = enumField(sentinel, "mode", "runtime.sentinel", ["singleton"] as const); const enabledOnTargets = stringArrayField(sentinel, "enabledOnTargets", "runtime.sentinel"); if (sentinelMode !== "singleton") throw new Error(`${configPath}.runtime.sentinel.mode must be singleton`); return { database: { mode: "external", sourceRef, sourceKeys, secretName: secret, passwordKey, host, port, user, dbName, sslMode, pendingAllowed }, secrets: { root: secretRoot, appSourceRef }, redis: { serviceName: redisServiceName, persistence: redisPersistence }, appData: { mode: appDataMode }, sentinel: { mode: "singleton", enabledOnTargets }, }; } function stringField(obj: Record, key: string, path: string): string { const value = obj[key]; if (typeof value !== "string" || value.length === 0) throw new Error(`${fieldLabel(path, key)} must be a non-empty string`); return value; } function objectField(obj: Record, key: string, path: string): Record { const value = obj[key]; if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${fieldLabel(path, key)} must be an object`); return value as Record; } function booleanField(obj: Record, key: string, path: string): boolean { return yamlBooleanField(obj, key, configPath, path); } function stringArrayField(obj: Record, key: string, path: string): string[] { const value = obj[key]; if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim().length > 0)) { throw new Error(`${fieldLabel(path, key)} must be an array of non-empty strings`); } return value.map((item) => item.trim()); } function enumField(obj: Record, key: string, path: string, values: T): T[number] { const value = stringField(obj, key, path); if (!(values as readonly string[]).includes(value)) throw new Error(`${fieldLabel(path, key)} must be one of ${values.join(", ")}`); return value as T[number]; } function integerField(obj: Record, key: string, path: string): number { return yamlIntegerField(obj, key, configPath, path); } function integerArrayField(obj: Record, key: string, path: string): number[] { const value = obj[key]; if (!Array.isArray(value) || !value.every((item) => typeof item === "number" && Number.isInteger(item))) { throw new Error(`${fieldLabel(path, key)} must be an array of integers`); } return value; } function fieldLabel(path: string, key: string): string { return yamlFieldLabel(configPath, path, key); } function isKubernetesName(value: string): boolean { return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value); } function isImageRepository(value: string): boolean { return /^[A-Za-z0-9._:/-]+$/u.test(value) && !value.includes("..") && !value.includes("@") && !value.endsWith(":"); } function isImageReference(value: string): boolean { return /^[A-Za-z0-9._:/-]+$/u.test(value) && value.includes(":") && !value.includes(".."); } function validatePublicExposureConfig(config: Sub2ApiPublicExposureConfig, path: string): void { const url = new URL(config.publicBaseUrl); if (url.protocol !== "https:") throw new Error(`${configPath}.${path}.publicExposure.publicBaseUrl must use https://`); if (url.hostname !== config.dns.hostname) throw new Error(`${configPath}.${path}.publicExposure.publicBaseUrl hostname must match publicExposure.dns.hostname`); validateHostname(config.dns.hostname, `${path}.publicExposure.dns.hostname`); if (!/^[0-9.]+$/u.test(config.dns.expectedA)) throw new Error(`${configPath}.${path}.publicExposure.dns.expectedA must be an IPv4 address`); for (const resolver of config.dns.resolvers) { if (!/^[0-9.]+$/u.test(resolver)) throw new Error(`${configPath}.${path}.publicExposure.dns.resolvers entries must be IPv4 resolvers`); } validateKubernetesResourceName(config.frpc.deploymentName, `${path}.publicExposure.frpc.deploymentName`); validateKubernetesResourceName(config.frpc.secretName, `${path}.publicExposure.frpc.secretName`); if (config.frpc.secretKey !== "frpc.toml") throw new Error(`${configPath}.${path}.publicExposure.frpc.secretKey must be frpc.toml`); if (!isImageReference(config.frpc.image)) throw new Error(`${configPath}.${path}.publicExposure.frpc.image has an unsupported format`); validateHostOrIp(config.frpc.serverAddr, `${path}.publicExposure.frpc.serverAddr`); validatePort(config.frpc.serverPort, `${path}.publicExposure.frpc.serverPort`); validateProxyName(config.frpc.proxyName, `${path}.publicExposure.frpc.proxyName`); validatePort(config.frpc.remotePort, `${path}.publicExposure.frpc.remotePort`); validateHostOrIp(config.frpc.localIP, `${path}.publicExposure.frpc.localIP`); validatePort(config.frpc.localPort, `${path}.publicExposure.frpc.localPort`); if (!/^[A-Za-z0-9_./-]+$/u.test(config.frpc.tokenSourceRef)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceRef has an unsupported format`); if (!/^[A-Z0-9_]+$/u.test(config.frpc.tokenSourceKey)) throw new Error(`${configPath}.${path}.publicExposure.frpc.tokenSourceKey must be an env key`); if (!/^[A-Za-z0-9:_./-]+$/u.test(config.pk01.route)) throw new Error(`${configPath}.${path}.publicExposure.pk01.route has an unsupported format`); if (!config.pk01.caddyBinaryPath.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyBinaryPath must be absolute`); if (!/^https:\/\//u.test(config.pk01.caddyDownloadUrl)) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyDownloadUrl must use https://`); if (config.pk01.caddyDownloadProxyUrl !== null) validateProxyUrl(config.pk01.caddyDownloadProxyUrl, `${path}.publicExposure.pk01.caddyDownloadProxyUrl`); if (!config.pk01.caddyConfigPath.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyConfigPath must be absolute`); validateProxyName(config.pk01.caddyServiceName, `${path}.publicExposure.pk01.caddyServiceName`); if (!config.pk01.caddyStorageDir.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyStorageDir must be absolute`); if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/u.test(config.pk01.caddyEmail)) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyEmail must be an email address`); if (!config.pk01.pikanodeRoot.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.pikanodeRoot must be absolute`); validateProxyName(config.pk01.pikanodeContainerName, `${path}.publicExposure.pk01.pikanodeContainerName`); if (!isImageRepository(config.pk01.pikanodeImage) && !isImageReference(config.pk01.pikanodeImage)) throw new Error(`${configPath}.${path}.publicExposure.pk01.pikanodeImage has an unsupported format`); validatePort(config.pk01.pikanodeHttpHostPort, `${path}.publicExposure.pk01.pikanodeHttpHostPort`); if (config.pk01.responseHeaderTimeoutSeconds < 1) throw new Error(`${configPath}.${path}.publicExposure.pk01.responseHeaderTimeoutSeconds must be >= 1`); } function validateEgressProxyConfig(config: Sub2ApiEgressProxyConfig, path: string): void { validateKubernetesResourceName(config.deploymentName, `${path}.egressProxy.deploymentName`); validateKubernetesResourceName(config.serviceName, `${path}.egressProxy.serviceName`); validateKubernetesResourceName(config.secretName, `${path}.egressProxy.secretName`); if (config.secretKey !== "config.json") throw new Error(`${configPath}.${path}.egressProxy.secretKey must be config.json`); if (!isImageReference(config.image)) throw new Error(`${configPath}.${path}.egressProxy.image has an unsupported format`); validatePort(config.listenPort, `${path}.egressProxy.listenPort`); if (!/^[A-Za-z0-9_./-]+$/u.test(config.sourceRef)) throw new Error(`${configPath}.${path}.egressProxy.sourceRef has an unsupported format`); if (!/^[A-Z0-9_]+$/u.test(config.sourceKey)) throw new Error(`${configPath}.${path}.egressProxy.sourceKey must be an env key`); for (const entry of config.noProxy) { if (!/^[A-Za-z0-9.:_/*-]+$/u.test(entry)) throw new Error(`${configPath}.${path}.egressProxy.noProxy contains an unsupported entry`); } const url = new URL(config.healthProbeUrl); if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path}.egressProxy.healthProbeUrl must use http:// or https://`); } function normalizePublicBaseUrl(value: string, path: string): string { let parsed: URL; try { parsed = new URL(value); } catch { throw new Error(`${configPath}.${path} must be a valid URL`); } parsed.pathname = parsed.pathname.replace(/\/+$/u, ""); if (parsed.search || parsed.hash) throw new Error(`${configPath}.${path} must not include query or hash`); return parsed.toString().replace(/\/+$/u, ""); } function validateHostname(value: string, path: string): void { if (!/^[A-Za-z0-9.-]+$/u.test(value) || !value.includes(".")) throw new Error(`${configPath}.${path} must be a hostname`); } function validateKubernetesResourceName(value: string, path: string): void { if (!isKubernetesName(value)) throw new Error(`${configPath}.${path} must be a Kubernetes resource name`); } function validateHostOrIp(value: string, path: string): void { if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error(`${configPath}.${path} has an unsupported format`); } function validatePort(value: number, path: string): void { if (value < 1 || value > 65535) throw new Error(`${configPath}.${path} must be a TCP port`); } function validateProxyName(value: string, path: string): void { if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${configPath}.${path} has an unsupported format`); } function validateProxyUrl(value: string, path: string): void { let url: URL; try { url = new URL(value); } catch { throw new Error(`${configPath}.${path} must be a valid proxy URL`); } if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path} must use http:// or https://`); if (url.username || url.password || url.search || url.hash) throw new Error(`${configPath}.${path} must not include credentials, query, or hash`); } function resolveTarget(sub2api: Sub2ApiConfig, targetId: string): Sub2ApiTargetConfig { const target = sub2api.targets.find((item) => item.id.toLowerCase() === targetId.toLowerCase()); if (target === undefined) { const known = sub2api.targets.map((item) => item.id).join(", "); throw new Error(`unknown Sub2API target ${targetId}; known targets: ${known}`); } if (!target.enabled) throw new Error(`Sub2API target ${target.id} is disabled in ${configPath}`); return target; } function targetImage(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): Sub2ApiConfig["image"] { return { repository: target.image.repository ?? sub2api.image.repository, tag: target.image.tag ?? sub2api.image.tag, pullPolicy: target.image.pullPolicy ?? sub2api.image.pullPolicy, }; } function targetDependencyImages(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): Sub2ApiConfig["dependencyImages"] { return { postgres: target.dependencyImages.postgres ?? sub2api.dependencyImages.postgres, redis: target.dependencyImages.redis ?? sub2api.dependencyImages.redis, }; } function imageRef(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { const image = targetImage(sub2api, target); return `${image.repository}:${image.tag}`; } function isExternalTarget(target: Sub2ApiTargetConfig): boolean { return target.databaseMode === "external-pending" || target.databaseMode === "external-active"; } function manifest(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { if (isExternalTarget(target)) return externalPendingManifest(sub2api, target); const template = readFileSync(manifestPath, "utf8"); const urlAllowlist = sub2api.security.urlAllowlist; const configHash = configHashFor(sub2api, target); const image = targetImage(sub2api, target); const dependencyImages = targetDependencyImages(sub2api, target); return template .replaceAll("__SUB2API_IMAGE__", imageRef(sub2api, target)) .replaceAll("__SUB2API_IMAGE_PULL_POLICY__", image.pullPolicy) .replaceAll("postgres:18-alpine", dependencyImages.postgres) .replaceAll("redis:8-alpine", dependencyImages.redis) .replaceAll("__SUB2API_CONFIG_HASH__", configHash) .replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ENABLED__", String(urlAllowlist.enabled)) .replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP__", String(urlAllowlist.allowInsecureHttp)) .replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS__", String(urlAllowlist.allowPrivateHosts)) .replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS__", urlAllowlist.upstreamHosts.join(",")); } function configHashFor(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { return createHash("sha256") .update(JSON.stringify({ image: targetImage(sub2api, target), dependencyImages: targetDependencyImages(sub2api, target), security: sub2api.security, target, runtime: sub2api.runtime, })) .digest("hex") .slice(0, 16); } function targetHasSentinel(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): boolean { return sub2api.runtime.sentinel.enabledOnTargets.some((id) => id.toLowerCase() === target.id.toLowerCase()); } function managedResourceCleanupPlan(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): ManagedResourceCleanupPlan { const sentinel = codexPoolSentinelResourceNames(); return { externalDbState: { enabled: isExternalTarget(target), ...sub2api.defaults.cleanup.externalDbState, }, redisPersistentState: { enabled: target.redisMode === "local-ephemeral", ...sub2api.defaults.cleanup.redisPersistentState, }, publicExposure: { enabled: target.publicExposure?.enabled === true, deploymentName: target.publicExposure?.frpc.deploymentName ?? sub2api.defaults.cleanup.publicExposure.deploymentName, configMapName: sub2api.defaults.cleanup.publicExposure.configMapName, secretName: target.publicExposure?.frpc.secretName ?? sub2api.defaults.cleanup.publicExposure.secretName, }, egressProxy: { enabled: target.egressProxy?.enabled === true, deploymentName: target.egressProxy?.deploymentName ?? sub2api.defaults.cleanup.egressProxy.deploymentName, serviceName: target.egressProxy?.serviceName ?? sub2api.defaults.cleanup.egressProxy.serviceName, secretName: target.egressProxy?.secretName ?? sub2api.defaults.cleanup.egressProxy.secretName, }, sentinel: { ...sentinel, enabled: targetHasSentinel(sub2api, target), }, }; } function codexPoolSentinelResourceNames(): ManagedResourceCleanupPlan["sentinel"] { const defaults: ManagedResourceCleanupPlan["sentinel"] = { enabled: false, cronJobName: "sub2api-account-sentinel", configMapName: "sub2api-account-sentinel-config", credentialsSecretName: "sub2api-account-sentinel-profiles", stateConfigMapName: "sub2api-account-sentinel-state", serviceAccountName: "sub2api-account-sentinel", roleName: "sub2api-account-sentinel", roleBindingName: "sub2api-account-sentinel", }; if (!existsSync(codexPoolConfigPath)) return defaults; const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return defaults; const sentinel = (parsed as Record).sentinel; if (typeof sentinel !== "object" || sentinel === null || Array.isArray(sentinel)) return defaults; const record = sentinel as Record; const text = (key: string, fallback: string): string => { const value = record[key]; return typeof value === "string" && value.length > 0 ? value : fallback; }; const serviceAccountName = text("serviceAccountName", defaults.serviceAccountName); return { enabled: false, cronJobName: text("cronJobName", defaults.cronJobName), configMapName: text("configMapName", defaults.configMapName), credentialsSecretName: text("credentialsSecretName", defaults.credentialsSecretName), stateConfigMapName: text("stateConfigMapName", defaults.stateConfigMapName), serviceAccountName, roleName: serviceAccountName, roleBindingName: serviceAccountName, }; } function externalPendingManifest(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { const urlAllowlist = sub2api.security.urlAllowlist; const database = sub2api.runtime.database; const databaseStatus = target.databaseMode === "external-active" ? "external-db-active" : "pending-external-db"; const redisService = sub2api.runtime.redis.serviceName; const appReplicas = target.appReplicas; const redisReplicas = target.redisReplicas; const image = targetImage(sub2api, target); const dependencyImages = targetDependencyImages(sub2api, target); const publicExposure = renderPublicExposureManifest(target); const egressProxy = renderEgressProxyManifest(target); const proxyEnv = sub2ApiProxyEnv(target); return `apiVersion: v1 kind: Namespace metadata: name: ${target.namespace} labels: app.kubernetes.io/name: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-all namespace: ${target.namespace} labels: app.kubernetes.io/name: platform-infra app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - {} egress: - {} --- apiVersion: v1 kind: ConfigMap metadata: name: sub2api-config namespace: ${target.namespace} labels: app.kubernetes.io/name: sub2api app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} data: AUTO_SETUP: "true" SERVER_HOST: "0.0.0.0" SERVER_PORT: "8080" SERVER_MODE: "release" RUN_MODE: "standard" DATABASE_HOST: "${database.host}" DATABASE_PORT: "${database.port}" DATABASE_USER: "${database.user}" DATABASE_DBNAME: "${database.dbName}" DATABASE_SSLMODE: "${database.sslMode}" DATABASE_MAX_OPEN_CONNS: "10" DATABASE_MAX_IDLE_CONNS: "2" DATABASE_CONN_MAX_LIFETIME_MINUTES: "30" DATABASE_CONN_MAX_IDLE_TIME_MINUTES: "5" REDIS_HOST: "${redisService}" REDIS_PORT: "6379" REDIS_PASSWORD: "" REDIS_DB: "0" REDIS_POOL_SIZE: "32" REDIS_MIN_IDLE_CONNS: "2" REDIS_ENABLE_TLS: "false" ADMIN_EMAIL: "admin@sub2api.platform-infra.local" JWT_EXPIRE_HOUR: "24" TZ: "Asia/Shanghai" SECURITY_URL_ALLOWLIST_ENABLED: "${urlAllowlist.enabled}" SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP: "${urlAllowlist.allowInsecureHttp}" SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS: "${urlAllowlist.allowPrivateHosts}" SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS: "${urlAllowlist.upstreamHosts.join(",")}" UPDATE_PROXY_URL: "${proxyEnv.httpProxy}" HTTP_PROXY: "${proxyEnv.httpProxy}" HTTPS_PROXY: "${proxyEnv.httpProxy}" ALL_PROXY: "${proxyEnv.httpProxy}" http_proxy: "${proxyEnv.httpProxy}" https_proxy: "${proxyEnv.httpProxy}" all_proxy: "${proxyEnv.httpProxy}" NO_PROXY: "${proxyEnv.noProxy}" no_proxy: "${proxyEnv.noProxy}" GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT: "0" GATEWAY_OPENAI_HTTP2_ENABLED: "true" GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1: "true" GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD: "2" GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS: "60" GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS: "600" GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT: "900" GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL: "10" GATEWAY_IMAGE_CONCURRENCY_ENABLED: "false" GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS: "0" GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE: "reject" GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS: "30" GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS: "100" --- apiVersion: v1 kind: Service metadata: name: ${redisService} namespace: ${target.namespace} labels: app.kubernetes.io/name: sub2api-redis app.kubernetes.io/component: redis app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} unidesk.ai/state-mode: ephemeral-cache spec: selector: app.kubernetes.io/name: sub2api-redis app.kubernetes.io/component: redis ports: - name: redis port: 6379 targetPort: redis --- apiVersion: v1 kind: Service metadata: name: sub2api namespace: ${target.namespace} labels: app.kubernetes.io/name: sub2api app.kubernetes.io/component: app app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} spec: selector: app.kubernetes.io/name: sub2api app.kubernetes.io/component: app ports: - name: http port: 8080 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: sub2api-redis namespace: ${target.namespace} labels: app.kubernetes.io/name: sub2api-redis app.kubernetes.io/component: redis app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} unidesk.ai/state-mode: ephemeral-cache spec: replicas: ${redisReplicas} strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: sub2api-redis app.kubernetes.io/component: redis template: metadata: labels: app.kubernetes.io/name: sub2api-redis app.kubernetes.io/component: redis app.kubernetes.io/part-of: platform-infra spec: securityContext: fsGroup: 999 containers: - name: redis image: ${dependencyImages.redis} imagePullPolicy: IfNotPresent command: - sh - -c args: - redis-server --save "" --appendonly no ports: - name: redis containerPort: 6379 env: - name: TZ value: Asia/Shanghai readinessProbe: exec: command: - redis-cli - ping initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 livenessProbe: exec: command: - redis-cli - ping initialDelaySeconds: 30 periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 6 volumeMounts: - name: redis-data mountPath: /data volumes: - name: redis-data emptyDir: {} --- apiVersion: apps/v1 kind: Deployment metadata: name: sub2api namespace: ${target.namespace} labels: app.kubernetes.io/name: sub2api app.kubernetes.io/component: app app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} unidesk.ai/database-status: ${databaseStatus} spec: replicas: ${appReplicas} strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: sub2api app.kubernetes.io/component: app template: metadata: annotations: unidesk.ai/sub2api-config-hash: "${configHashFor(sub2api, target)}" unidesk.ai/database-source-ref: "${database.sourceRef}" unidesk.ai/database-status: "${databaseStatus}" labels: app.kubernetes.io/name: sub2api app.kubernetes.io/component: app app.kubernetes.io/part-of: platform-infra spec: securityContext: fsGroup: 1000 initContainers: - name: wait-postgres image: ${dependencyImages.postgres} imagePullPolicy: IfNotPresent command: - sh - -c - until pg_isready -h ${database.host} -p ${database.port} -U ${database.user} -d ${database.dbName}; do sleep 2; done - name: wait-redis image: ${dependencyImages.redis} imagePullPolicy: IfNotPresent command: - sh - -c - until redis-cli -h ${redisService} ping | grep -q PONG; do sleep 2; done containers: - name: sub2api image: ${imageRef(sub2api, target)} imagePullPolicy: ${image.pullPolicy} ports: - name: http containerPort: 8080 envFrom: - configMapRef: name: sub2api-config env: - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: ${database.secretName} key: ${database.passwordKey} - name: ADMIN_PASSWORD valueFrom: secretKeyRef: name: ${database.secretName} key: ADMIN_PASSWORD - name: JWT_SECRET valueFrom: secretKeyRef: name: ${database.secretName} key: JWT_SECRET - name: TOTP_ENCRYPTION_KEY valueFrom: secretKeyRef: name: ${database.secretName} key: TOTP_ENCRYPTION_KEY readinessProbe: httpGet: path: /health port: http initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 30 periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 6 startupProbe: httpGet: path: /health port: http periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 30 volumeMounts: - name: sub2api-data mountPath: /app/data volumes: - name: sub2api-data emptyDir: {} ${publicExposure} ${egressProxy} `; } function sub2ApiProxyEnv(target: Sub2ApiTargetConfig): { httpProxy: string; noProxy: string } { const proxy = target.egressProxy; if (proxy === null || !proxy.enabled || !proxy.applyToSub2Api) return { httpProxy: "", noProxy: "" }; return { httpProxy: `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`, noProxy: proxy.noProxy.join(","), }; } function renderPublicExposureManifest(target: Sub2ApiTargetConfig): string { const exposure = target.publicExposure; if (exposure === null || !exposure.enabled) return ""; return `--- apiVersion: apps/v1 kind: Deployment metadata: name: ${exposure.frpc.deploymentName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${exposure.frpc.deploymentName} app.kubernetes.io/component: tunnel app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} unidesk.ai/public-hostname: ${exposure.dns.hostname} spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: ${exposure.frpc.deploymentName} app.kubernetes.io/component: tunnel template: metadata: labels: app.kubernetes.io/name: ${exposure.frpc.deploymentName} app.kubernetes.io/component: tunnel app.kubernetes.io/part-of: platform-infra annotations: unidesk.ai/public-base-url: "${exposure.publicBaseUrl}" unidesk.ai/frp-server: "${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}" unidesk.ai/frp-remote-port: "${exposure.frpc.remotePort}" spec: containers: - name: frpc image: ${exposure.frpc.image} imagePullPolicy: IfNotPresent args: - -c - /etc/frp/frpc.toml volumeMounts: - name: frpc-config mountPath: /etc/frp/frpc.toml subPath: ${exposure.frpc.secretKey} readOnly: true volumes: - name: frpc-config secret: secretName: ${exposure.frpc.secretName} `; } function renderEgressProxyManifest(target: Sub2ApiTargetConfig): string { const proxy = target.egressProxy; if (proxy === null || !proxy.enabled) return ""; return `--- apiVersion: v1 kind: Service metadata: name: ${proxy.serviceName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${proxy.deploymentName} app.kubernetes.io/component: egress-proxy app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} unidesk.ai/proxy-source: master-vpn-subscription spec: selector: app.kubernetes.io/name: ${proxy.deploymentName} app.kubernetes.io/component: egress-proxy ports: - name: http port: ${proxy.listenPort} targetPort: proxy --- apiVersion: apps/v1 kind: Deployment metadata: name: ${proxy.deploymentName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${proxy.deploymentName} app.kubernetes.io/component: egress-proxy app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} unidesk.ai/proxy-source: master-vpn-subscription spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: ${proxy.deploymentName} app.kubernetes.io/component: egress-proxy template: metadata: labels: app.kubernetes.io/name: ${proxy.deploymentName} app.kubernetes.io/component: egress-proxy app.kubernetes.io/part-of: platform-infra annotations: unidesk.ai/proxy-source-ref: "${proxy.sourceRef}" unidesk.ai/proxy-preferred-outbound: "${proxy.preferredOutbound}" spec: containers: - name: proxy image: ${proxy.image} imagePullPolicy: ${proxy.imagePullPolicy} args: - run - -c - /etc/sing-box/${proxy.secretKey} ports: - name: proxy containerPort: ${proxy.listenPort} readinessProbe: tcpSocket: port: proxy initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 livenessProbe: tcpSocket: port: proxy initialDelaySeconds: 30 periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 6 volumeMounts: - name: proxy-config mountPath: /etc/sing-box/${proxy.secretKey} subPath: ${proxy.secretKey} readOnly: true volumes: - name: proxy-config secret: secretName: ${proxy.secretName} `; } function publicExposureSummary(exposure: Sub2ApiPublicExposureConfig): Record { return { enabled: exposure.enabled, publicBaseUrl: exposure.publicBaseUrl, dns: exposure.dns, frpc: { 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, }, }; } function egressProxySummary(proxy: Sub2ApiEgressProxyConfig): Record { return { enabled: proxy.enabled, mode: "master-vpn-subscription-http-proxy", deploymentName: proxy.deploymentName, serviceName: proxy.serviceName, secretName: proxy.secretName, image: proxy.image, imagePullPolicy: proxy.imagePullPolicy, listenPort: proxy.listenPort, sourceRef: proxy.sourceRef, sourceType: proxy.sourceType, preferredOutbound: proxy.preferredOutbound, applyToSub2Api: proxy.applyToSub2Api, applyToSentinel: proxy.applyToSentinel, healthProbeUrl: proxy.healthProbeUrl, noProxy: proxy.noProxy, valuesPrinted: false, }; } 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); return { ok: policy.every((check) => check.ok), action: "platform-infra-sub2api-plan", target: { id: target.id, route: target.route, namespace: target.namespace, role: target.role, manifestPath, configPath, fieldManager, serviceDns: `${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: { databaseMode: target.databaseMode, redisMode: target.redisMode, appReplicas: target.appReplicas, redisReplicas: target.redisReplicas, publicExposure: target.publicExposure === null ? null : publicExposureSummary(target.publicExposure), egressProxy: target.egressProxy === null ? null : egressProxySummary(target.egressProxy), }, 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: sub2api.runtime.database.host, port: sub2api.runtime.database.port, user: sub2api.runtime.database.user, dbName: sub2api.runtime.database.dbName, sslMode: sub2api.runtime.database.sslMode, pendingAllowed: sub2api.runtime.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.` : 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 ? `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: "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: "NetworkPolicy/allow-all is rendered with the deployment so kube-router cannot silently default-deny Sub2API cross-pod traffic.", publicExposure: target.publicExposure?.enabled ? { mode: "pk01-caddy-frp-direct", dataPath: `client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API`, pikanodeRole: "pikapython.com upstream only; api.pikapython.com 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 the master VPN subscription`, service: `${target.egressProxy.serviceName}.${target.namespace}.svc.cluster.local:${target.egressProxy.listenPort}`, sourceRef: target.egressProxy.sourceRef, applyToSub2Api: target.egressProxy.applyToSub2Api, applyToSentinel: target.egressProxy.applyToSentinel, valuesPrinted: false, } : null, dataStores: isExternalTarget(target) ? [ 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}`, }, }; } 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} k3s platform-infra Sub2API manifests 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 result = await capture(config, target.route, ["script"], dryRunScript(yaml, target)); 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 result = await capture(config, target.route, ["script"], applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial)); const parsed = parseJsonOutput(result.stdout); const pk01Exposure = publicExposureSecretMaterial === 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}`, }, }; } async function status(config: UniDeskConfig, options: DisclosureOptions): Promise> { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, options.targetId); const result = await capture(config, target.route, ["script"], 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 }), }; } async function validate(config: UniDeskConfig, options: DisclosureOptions): Promise> { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, options.targetId); const script = target.databaseMode === "external-pending" ? validateExternalPendingScript(sub2api, target) : target.databaseMode === "external-active" ? validateExternalActiveScript(sub2api, target) : validateScript(sub2api, target); const result = await capture(config, target.route, ["script"], script); const parsed = parseJsonOutput(result.stdout); if (options.raw) { return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-validate", 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-validate", target: { id: target.id, route: target.route, namespace: target.namespace, }, summary: parsed, remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }), }; } function policyChecks(sub2api: Sub2ApiConfig, yaml: string, target: Sub2ApiTargetConfig): PolicyCheck[] { const cleanup = sub2api.defaults.cleanup; const redisService = sub2api.runtime.redis.serviceName; const checks: PolicyCheck[] = [ { name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: "Sub2API must not be exposed through Kubernetes Ingress.", }, { name: "no-nodeport-or-loadbalancer", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Services must remain ClusterIP/internal-only.", }, { name: "no-host-network", ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml), detail: "Pods must not join the host network.", }, { name: "no-host-port", ok: !/^\s*hostPort:\s*[0-9]+\s*$/mu.test(yaml), detail: "Pods must not expose host ports.", }, { name: "no-cpu-memory-resources", ok: !/^\s*(cpu|memory):\s*/mu.test(yaml), detail: "Issue #220 requires no Kubernetes CPU/memory requests or limits.", }, { name: "no-resource-quota-or-limit-range", ok: !/^\s*kind:\s*(ResourceQuota|LimitRange)\s*$/mu.test(yaml), detail: "The platform-infra namespace must not receive quota/default limit objects for this deployment.", }, { name: "expected-namespace", ok: new RegExp(`^\\s*name:\\s*${escapeRegExp(target.namespace)}\\s*$`, "mu").test(yaml), detail: `Manifest declares namespace ${target.namespace}.`, }, { name: "allow-all-network-policy", ok: hasAllowAllNetworkPolicy(yaml, target.namespace), detail: `Manifest must include NetworkPolicy/allow-all in ${target.namespace} to keep kube-router from blocking Sub2API cross-pod traffic.`, }, ]; if (isExternalTarget(target)) { checks.push( { name: "external-db-no-local-postgres", ok: !/^\s*kind:\s*StatefulSet\s*$/mu.test(yaml) && !new RegExp(`\\b${escapeRegExp(cleanup.externalDbState.postgresStatefulSetName)}\\b`, "u").test(yaml), detail: "External DB targets must not deploy a local PostgreSQL StatefulSet, Service, or PVC.", }); } if (target.databaseMode === "external-pending") { checks.push( { name: "pending-db-replicas-match-yaml", ok: hasDeploymentReplicas(yaml, serviceName, target.appReplicas) && hasDeploymentReplicas(yaml, redisService, target.redisReplicas), detail: `External-pending predeployment renders YAML-declared app/Redis replicas ${target.appReplicas}/${target.redisReplicas}.`, }, ); } else if (target.databaseMode === "external-active") { checks.push({ name: "external-active-replicas-match-yaml", ok: hasDeploymentReplicas(yaml, serviceName, target.appReplicas) && hasDeploymentReplicas(yaml, redisService, target.redisReplicas), detail: `External-active targets render YAML-declared app/Redis replicas ${target.appReplicas}/${target.redisReplicas} against the external PostgreSQL runtime.`, }); } else { checks.push({ name: "bundled-db-present", ok: /^\s*kind:\s*StatefulSet\s*$/mu.test(yaml) && new RegExp(`\\b${escapeRegExp(cleanup.externalDbState.postgresStatefulSetName)}\\b`, "u").test(yaml), detail: "Bundled active targets render the local PostgreSQL StatefulSet.", }); } if (target.redisMode === "local-ephemeral") { checks.push({ name: "local-redis-ephemeral", ok: !new RegExp(`\\b${escapeRegExp(cleanup.redisPersistentState.pvcName)}\\b`, "u").test(yaml) && /^\s*emptyDir:\s*\{\}\s*$/mu.test(yaml), detail: "Local Redis is an ephemeral cache and must not allocate persistent Redis storage.", }); } if (target.egressProxy?.enabled) { checks.push({ name: "egress-proxy-yaml-controlled", ok: hasDeploymentReplicas(yaml, target.egressProxy.deploymentName, 1) && new RegExp(`^\\s*name:\\s*${escapeRegExp(target.egressProxy.serviceName)}\\s*$`, "mu").test(yaml), detail: "Egress HTTP proxy client must be rendered as a YAML-controlled platform-infra Deployment and ClusterIP Service.", }); } return checks; } function hasAllowAllNetworkPolicy(yaml: string, namespaceName: string): boolean { return yaml.split(/^---\s*$/mu).some((document) => { return /^\s*kind:\s*NetworkPolicy\s*$/mu.test(document) && /^\s*name:\s*allow-all\s*$/mu.test(document) && new RegExp(`^\\s*namespace:\\s*${escapeRegExp(namespaceName)}\\s*$`, "mu").test(document) && /^\s*podSelector:\s*\{\}\s*$/mu.test(document) && /^\s*-\s*Ingress\s*$/mu.test(document) && /^\s*-\s*Egress\s*$/mu.test(document) && /^\s*ingress:\s*\n\s*-\s*\{\}\s*$/mu.test(document) && /^\s*egress:\s*\n\s*-\s*\{\}\s*$/mu.test(document); }); } function hasDeploymentReplicas(yaml: string, name: string, replicas: number): boolean { return yaml.split(/^---\s*$/mu).some((document) => { return /^\s*kind:\s*Deployment\s*$/mu.test(document) && new RegExp(`^\\s*name:\\s*${escapeRegExp(name)}\\s*$`, "mu").test(document) && new RegExp(`^\\s*replicas:\\s*${replicas}\\s*$`, "mu").test(document); }); } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } function prepareExternalActiveSecret(sub2api: Sub2ApiConfig): ExternalActiveSecretMaterial { const database = sub2api.runtime.database; const root = secretRoot(sub2api); const dbSource = readEnvSourceFile({ root, sourceRef: database.sourceRef, missingMessage: (sourcePath) => `external-active requires ${redactRepoPath(sourcePath)}; run platform-db postgres apply/status first to materialize the PK01 DB credential source`, }); const dbValues = dbSource.values; const dbUser = requiredEnvValue(dbValues, database.sourceKeys.user, database.sourceRef); const dbPassword = requiredEnvValue(dbValues, database.sourceKeys.password, database.sourceRef); const dbName = requiredEnvValue(dbValues, database.sourceKeys.dbName, database.sourceRef); if (dbUser !== database.user) throw new Error(`${database.sourceRef}.${database.sourceKeys.user} does not match ${configPath}.runtime.database.user`); if (dbName !== database.dbName) throw new Error(`${database.sourceRef}.${database.sourceKeys.dbName} does not match ${configPath}.runtime.database.dbName`); const appSourcePath = join(root, sub2api.runtime.secrets.appSourceRef); const existedBefore = existsSync(appSourcePath); const existing = existedBefore ? parseEnvFile(readFileSync(appSourcePath, "utf8")) : {}; const next = { ...existing }; next.POSTGRES_PASSWORD = dbPassword; if (next.ADMIN_PASSWORD === undefined || next.ADMIN_PASSWORD.length === 0) next.ADMIN_PASSWORD = randomBytes(16).toString("hex"); if (next.JWT_SECRET === undefined || next.JWT_SECRET.length === 0) next.JWT_SECRET = randomBytes(32).toString("hex"); if (next.TOTP_ENCRYPTION_KEY === undefined || next.TOTP_ENCRYPTION_KEY.length === 0) next.TOTP_ENCRYPTION_KEY = randomBytes(32).toString("hex"); const values: Record = { POSTGRES_PASSWORD: next.POSTGRES_PASSWORD, ADMIN_PASSWORD: next.ADMIN_PASSWORD, JWT_SECRET: next.JWT_SECRET, TOTP_ENCRYPTION_KEY: next.TOTP_ENCRYPTION_KEY, }; const missing = requiredSecretKeys.filter((key) => values[key].length === 0); if (missing.length > 0) throw new Error(`external-active app secret source is missing ${missing.join(", ")}`); const action = !existedBefore ? "create" : requiredSecretKeys.some((key) => existing[key] !== values[key]) ? "update" : "none"; if (action !== "none") writeEnvFile(appSourcePath, { ...existing, ...values }); return { sourceRef: database.sourceRef, sourcePath: dbSource.sourcePathRedacted, appSourceRef: sub2api.runtime.secrets.appSourceRef, appSourcePath: redactRepoPath(appSourcePath), action, fingerprint: fingerprintSecretValues(values, [...requiredSecretKeys]), values, }; } function preparePublicExposureSecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): PublicExposureSecretMaterial | null { const exposure = target.publicExposure; if (exposure === null || !exposure.enabled) return null; return prepareFrpcSecret({ secretRoot: secretRoot(sub2api), exposure, sourcePathRedactor: redactRepoPath, parseEnvFile, requiredEnvValue, readTextFile: (sourcePath) => { if (!existsSync(sourcePath)) throw new Error(`publicExposure requires ${redactRepoPath(sourcePath)} with ${exposure.frpc.tokenSourceKey}; copy the PK01 frps token into the local secret source first`); return readTextFile(sourcePath); }, }); } function prepareEgressProxySecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): EgressProxySecretMaterial | null { const proxy = target.egressProxy; if (proxy === null || !proxy.enabled) return null; const source = readEnvSourceFile({ root: secretRoot(sub2api), sourceRef: proxy.sourceRef, missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the master VPN subscription source first`, }); const values = source.values; const subscriptionUrl = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef); const subscription = fetchSubscription(subscriptionUrl); const decoded = decodeSubscription(subscription.body); const nodes = decoded.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean); const selected = selectSubscriptionNode(nodes, proxy.preferredOutbound); const configJson = renderSingBoxConfig(selected, proxy); const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`; const noProxy = proxy.noProxy.join(","); return { sourceRef: proxy.sourceRef, sourcePath: source.sourcePathRedacted, secretName: proxy.secretName, secretKey: proxy.secretKey, fingerprint: fingerprintSecretValues({ subscription: subscription.body, configJson }, ["subscription", "configJson"]), subscriptionBytes: Buffer.byteLength(subscription.body, "utf8"), selectedOutbound: selected.kind, configJson, proxyUrl, noProxy, valuesPrinted: false, }; } function fetchSubscription(subscriptionUrl: string): { body: string } { const shellUrl = shQuote(subscriptionUrl); const command = `curl -fsSL --connect-timeout 10 --max-time 30 ${shellUrl}`; const proc = Bun.spawnSync(["bash", "-lc", command], { stdout: "pipe", stderr: "pipe" }); if (proc.exitCode !== 0) { const stderr = new TextDecoder().decode(proc.stderr).slice(-500); throw new Error(`failed to fetch master VPN subscription: exit=${proc.exitCode} stderr=${stderr}`); } const body = new TextDecoder().decode(proc.stdout).trim(); if (body.length === 0) throw new Error("master VPN subscription is empty"); return { body }; } function decodeSubscription(raw: string): string { if (raw.includes("://")) return raw; const normalized = raw.replace(/\s+/gu, ""); const padding = "=".repeat((4 - (normalized.length % 4)) % 4); try { return Buffer.from(`${normalized}${padding}`, "base64").toString("utf8"); } catch (error) { throw new Error(`master VPN subscription is neither URI list nor base64 URI list: ${String(error)}`); } } type SubscriptionNode = | { kind: "vless-reality"; uri: string; uuid: string; host: string; port: number; sni: string; flow: string | null; fingerprint: string | null; publicKey: string; shortId: string | null } | { kind: "hysteria2"; uri: string; password: string; host: string; port: number; sni: string | null; obfsPassword: string | null; insecure: boolean }; function selectSubscriptionNode(nodes: string[], preferred: Sub2ApiEgressProxyConfig["preferredOutbound"]): SubscriptionNode { const parsed = nodes.map(parseSubscriptionNode).filter((node): node is SubscriptionNode => node !== null); const preferredNode = parsed.find((node) => node.kind === preferred); if (preferredNode !== undefined) return preferredNode; const fallback = parsed.find((node) => node.kind === "vless-reality") ?? parsed.find((node) => node.kind === "hysteria2"); if (fallback !== undefined) return fallback; throw new Error("master VPN subscription does not contain a supported vless-reality or hysteria2 node"); } function parseSubscriptionNode(uri: string): SubscriptionNode | null { let url: URL; try { url = new URL(uri); } catch { return null; } if (url.protocol === "vless:") { const params = url.searchParams; if (params.get("security") !== "reality") return null; const publicKey = params.get("pbk"); if (publicKey === null || publicKey.length === 0) return null; return { kind: "vless-reality", uri, uuid: decodeURIComponent(url.username), host: requiredUrlHost(url, uri), port: requiredUrlPort(url, uri), sni: params.get("sni") ?? "", flow: params.get("flow"), fingerprint: params.get("fp"), publicKey, shortId: params.get("sid"), }; } if (url.protocol === "hysteria2:" || url.protocol === "hy2:") { const params = url.searchParams; return { kind: "hysteria2", uri, password: decodeURIComponent(url.username), host: requiredUrlHost(url, uri), port: requiredUrlPort(url, uri), sni: params.get("sni"), obfsPassword: params.get("obfs-password"), insecure: params.get("insecure") === "1" || params.get("insecure") === "true", }; } return null; } function requiredUrlHost(url: URL, uri: string): string { if (url.hostname.length === 0) throw new Error(`VPN subscription node is missing host: ${redactSubscriptionUri(uri)}`); return url.hostname; } function requiredUrlPort(url: URL, uri: string): number { const port = Number(url.port); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`VPN subscription node is missing a valid port: ${redactSubscriptionUri(uri)}`); return port; } function renderSingBoxConfig(node: SubscriptionNode, proxy: Sub2ApiEgressProxyConfig): string { const outbound = node.kind === "vless-reality" ? { type: "vless", tag: "master-vpn", server: node.host, server_port: node.port, uuid: node.uuid, flow: node.flow ?? undefined, tls: { enabled: true, server_name: node.sni, utls: { enabled: true, fingerprint: node.fingerprint ?? "chrome" }, reality: { enabled: true, public_key: node.publicKey, short_id: node.shortId ?? "" }, }, } : { type: "hysteria2", tag: "master-vpn", server: node.host, server_port: node.port, password: node.password, obfs: node.obfsPassword === null ? undefined : { type: "salamander", password: node.obfsPassword }, tls: { enabled: true, server_name: node.sni ?? node.host, insecure: node.insecure }, }; const config = stripUndefined({ log: { level: "info", timestamp: true }, inbounds: [ { type: "mixed", tag: "mixed-in", listen: "0.0.0.0", listen_port: proxy.listenPort, }, ], outbounds: [ outbound, { type: "direct", tag: "direct" }, { type: "block", tag: "block" }, ], route: { rules: [ { ip_is_private: true, outbound: "direct" }, { domain_suffix: ["cluster.local", "svc"], outbound: "direct" }, ], final: "master-vpn", }, }); return `${JSON.stringify(config, null, 2)}\n`; } function stripUndefined(value: unknown): unknown { if (Array.isArray(value)) return value.map(stripUndefined); if (value !== null && typeof value === "object") { const output: Record = {}; for (const [key, child] of Object.entries(value as Record)) { if (child !== undefined) output[key] = stripUndefined(child); } return output; } return value; } function redactSubscriptionUri(uri: string): string { return uri.replace(/\/\/[^@]+@/u, "//@").replace(/password=[^&#]+/giu, "password=").replace(/pbk=[^&#]+/giu, "pbk="); } async function applyPk01PublicExposure(config: UniDeskConfig, target: Sub2ApiTargetConfig): Promise> { const exposure = target.publicExposure; if (exposure === null || !exposure.enabled) return { ok: true, action: "not-enabled" }; const start = await startPk01PublicExposureJob(config, target, exposure); if (!start.ok || typeof start.remoteJobId !== "string") { return { ok: false, action: "platform-infra-sub2api-pk01-public-exposure", route: exposure.pk01.route, mode: "remote-job-start-failed", remoteJob: start, }; } const waited = await waitPk01PublicExposureJob(config, exposure, start.remoteJobId); const terminal = asRecordOrNull(waited.terminal); const summary = asRecordOrNull(terminal?.summary); return { ok: waited.ok === true && boolField(summary, "ok", false), action: "platform-infra-sub2api-pk01-public-exposure", route: exposure.pk01.route, mode: "remote-job", summary, remoteJob: { start, waited, }, }; } async function startPk01PublicExposureJob(config: UniDeskConfig, target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): Promise> { const jobId = `sub2api-pk01-exposure-${new Date().toISOString().replace(/[^0-9A-Za-z]/gu, "")}-${randomBytes(3).toString("hex")}`; const payload = pk01PublicExposureScript(target, exposure); const payloadB64 = Buffer.from(payload, "utf8").toString("base64"); const script = ` set -eu job_id=${shQuote(jobId)} base="$HOME/.unidesk/platform-infra/sub2api-pk01-exposure/jobs" job_dir="$base/$job_id" mkdir -p "$job_dir" payload="$job_dir/payload.sh" runner="$job_dir/runner.sh" stdout="$job_dir/stdout.log" stderr="$job_dir/stderr.log" state="$job_dir/state.json" printf '%s' '${payloadB64}' | base64 -d >"$payload" chmod 0700 "$payload" cat >"$runner" <<'RUNNER' #!/bin/sh set +e job_dir="$1" payload="$job_dir/payload.sh" stdout="$job_dir/stdout.log" stderr="$job_dir/stderr.log" state="$job_dir/state.json" write_state() { status="$1" stage="$2" exit_code="$3" python3 - "$state" "$status" "$stage" "$exit_code" <<'PY' import datetime import json import sys path, status, stage, exit_code = sys.argv[1:5] payload = { "ok": status == "succeeded", "status": status, "stage": stage, "updatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), "exitCode": None if exit_code == "" else int(exit_code), } if status in ("succeeded", "failed"): payload["finishedAt"] = payload["updatedAt"] with open(path, "w", encoding="utf-8") as handle: json.dump(payload, handle, ensure_ascii=False) PY } write_state running start "" sh "$payload" >"$stdout" 2>"$stderr" rc=$? if [ "$rc" -eq 0 ]; then write_state succeeded complete "$rc" else write_state failed complete "$rc" fi exit "$rc" RUNNER chmod 0700 "$runner" python3 - "$state" <<'PY' import datetime import json import sys payload = { "ok": False, "status": "queued", "stage": "queued", "updatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), "exitCode": None, } with open(sys.argv[1], "w", encoding="utf-8") as handle: json.dump(payload, handle, ensure_ascii=False) PY nohup "$runner" "$job_dir" >/dev/null 2>&1 & printf '%s' "$!" >"$job_dir/pid" python3 - "$job_id" "$job_dir" <<'PY' import json import os import sys job_id, job_dir = sys.argv[1:3] payload = { "ok": True, "remoteJobId": job_id, "statePath": os.path.join(job_dir, "state.json"), "stdoutPath": os.path.join(job_dir, "stdout.log"), "stderrPath": os.path.join(job_dir, "stderr.log"), "valuesPrinted": False, } print(json.dumps(payload, ensure_ascii=False, indent=2)) PY `; const result = await capture(config, exposure.pk01.route, ["script"], script); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), remoteJobId: typeof parsed?.remoteJobId === "string" ? parsed.remoteJobId : null, parsed, capture: compactCapture(result, { full: result.exitCode !== 0 }), }; } async function waitPk01PublicExposureJob(config: UniDeskConfig, exposure: Sub2ApiPublicExposureConfig, remoteJobId: string): Promise> { const observations: Array> = []; const maxPolls = 120; let lastProgressKey: string | null = null; for (let attempt = 0; attempt < maxPolls; attempt += 1) { const observation = await readPk01PublicExposureJob(config, exposure, remoteJobId); observations.push(observation.summary); const statusValue = observation.status; const stage = typeof observation.summary.stage === "string" ? observation.summary.stage : null; const progressKey = `${statusValue ?? "unknown"}:${stage ?? "unknown"}`; if (attempt === 0 || progressKey !== lastProgressKey || attempt % 6 === 0 || statusValue === "succeeded" || statusValue === "failed") { lastProgressKey = progressKey; console.error(JSON.stringify({ event: "platform-infra.sub2api.pk01-exposure.progress", at: new Date().toISOString(), remoteJobId, attempt: attempt + 1, status: statusValue, stage, exitCode: observation.summary.exitCode ?? null, })); } if (statusValue === "succeeded" || statusValue === "failed") { return { ok: statusValue === "succeeded" && boolField(asRecordOrNull(observation.summary.summary), "ok", false), attempts: attempt + 1, terminal: observation.summary, recent: observations.slice(-10), }; } await Bun.sleep(5000); } return { ok: false, attempts: maxPolls, terminal: null, recent: observations.slice(-10), error: "PK01 public exposure job did not finish before local polling budget", }; } async function readPk01PublicExposureJob(config: UniDeskConfig, exposure: Sub2ApiPublicExposureConfig, remoteJobId: string): Promise<{ status: string | null; summary: Record }> { const safeId = remoteJobId.replace(/[^A-Za-z0-9_.-]/gu, ""); const script = ` set +e job_dir="$HOME/.unidesk/platform-infra/sub2api-pk01-exposure/jobs/${safeId}" state="$job_dir/state.json" stdout="$job_dir/stdout.log" stderr="$job_dir/stderr.log" if [ -f "$state" ]; then cat "$state"; else printf '{"status":"missing","stage":"missing","exitCode":null}'; fi printf '\\n---STDOUT---\\n' if [ -f "$stdout" ]; then tail -200 "$stdout"; fi printf '\\n---STDERR---\\n' if [ -f "$stderr" ]; then tail -120 "$stderr"; fi printf '\\n---DOWNLOAD---\\n' download_part="$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64.part" download_cache="$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64" if [ -f "$download_part" ]; then stat -c 'part_bytes=%s part_mtime=%y' "$download_part"; fi if [ -f "$download_cache" ]; then stat -c 'cache_bytes=%s cache_mtime=%y' "$download_cache"; fi `; const result = await capture(config, exposure.pk01.route, ["script"], script); const [stateText, rest = ""] = result.stdout.split("\n---STDOUT---\n"); const [stdoutAndMaybeStderr = "", downloadProgress = ""] = rest.split("\n---DOWNLOAD---\n"); const [stdoutTail = "", stderrTail = ""] = stdoutAndMaybeStderr.split("\n---STDERR---\n"); const parsed = parseJsonOutput(stateText) ?? {}; const statusValue = typeof parsed.status === "string" ? parsed.status : null; const summary = parseJsonOutput(stdoutTail); return { status: statusValue, summary: { ok: result.exitCode === 0, remoteJobId, status: statusValue, stage: parsed.stage ?? null, updatedAt: parsed.updatedAt ?? null, finishedAt: parsed.finishedAt ?? null, exitCode: parsed.exitCode ?? null, summary, stdoutTail: stdoutTail.slice(-8000), stderrTail: stderrTail.slice(-4000), downloadProgress: downloadProgress.slice(-1000), capture: compactCapture(result, { full: result.exitCode !== 0 }), }, }; } function pk01PublicExposureScript(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string { const caddyfile = renderPk01Caddyfile(exposure); const serviceUnit = renderPk01CaddyService(exposure); const caddyfileB64 = Buffer.from(caddyfile, "utf8").toString("base64"); const serviceUnitB64 = Buffer.from(serviceUnit, "utf8").toString("base64"); const caddyDownloadProxyArgs = exposure.pk01.caddyDownloadProxyUrl === null ? "" : `--proxy ${shQuote(exposure.pk01.caddyDownloadProxyUrl)}`; const caddyDownloadProxyUrl = exposure.pk01.caddyDownloadProxyUrl ?? ""; return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT caddyfile="$tmp/Caddyfile" service_unit="$tmp/${exposure.pk01.caddyServiceName}.service" printf '%s' '${caddyfileB64}' | base64 -d >"$caddyfile" printf '%s' '${serviceUnitB64}' | base64 -d >"$service_unit" download_out="$tmp/download.out" download_err="$tmp/download.err" install_out="$tmp/install.out" install_err="$tmp/install.err" validate_out="$tmp/validate.out" validate_err="$tmp/validate.err" pikanode_out="$tmp/pikanode.out" pikanode_err="$tmp/pikanode.err" caddy_out="$tmp/caddy.out" caddy_err="$tmp/caddy.err" probe_out="$tmp/probe.out" probe_err="$tmp/probe.err" download_action="kept-existing" download_proxy_url=${shQuote(caddyDownloadProxyUrl)} download_proxy_mode="direct" if [ -n "$download_proxy_url" ]; then download_proxy_mode="curl-proxy"; fi download_rc=0 if ! [ -x ${shQuote(exposure.pk01.caddyBinaryPath)} ]; then cache_dir="$HOME/.cache/unidesk/platform-infra" cache_path="$cache_dir/caddy-linux-amd64" cache_part="$cache_path.part" mkdir -p "$cache_dir" if [ -x "$cache_path" ]; then download_action="used-cache" "$cache_path" version >"$download_out" 2>"$download_err" || true else download_action="downloaded" resume_args= if [ -s "$cache_part" ]; then resume_args="-C -"; fi curl -fL --connect-timeout 20 --retry 5 --retry-delay 3 --max-time 600 ${caddyDownloadProxyArgs} $resume_args ${shQuote(exposure.pk01.caddyDownloadUrl)} -o "$cache_part" >"$download_out" 2>"$download_err" download_rc=$? if [ "$download_rc" -eq 33 ]; then rm -f "$cache_part" curl -fL --connect-timeout 20 --retry 5 --retry-delay 3 --max-time 600 ${caddyDownloadProxyArgs} ${shQuote(exposure.pk01.caddyDownloadUrl)} -o "$cache_part" >>"$download_out" 2>>"$download_err" download_rc=$? fi if [ "$download_rc" -eq 0 ]; then mv "$cache_part" "$cache_path" chmod 0755 "$cache_path" fi fi if [ "$download_rc" -eq 0 ]; then sudo install -m 0755 "$cache_path" ${shQuote(exposure.pk01.caddyBinaryPath)} >>"$download_out" 2>>"$download_err" download_rc=$? fi else rm -f "$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64.part" ${shQuote(exposure.pk01.caddyBinaryPath)} version >"$download_out" 2>"$download_err" || true fi install_rc=1 if [ "$download_rc" -eq 0 ]; then sudo mkdir -p "$(dirname ${shQuote(exposure.pk01.caddyConfigPath)})" ${shQuote(exposure.pk01.caddyStorageDir)} /etc/systemd/system merged_caddyfile="$tmp/Caddyfile.merged" sudo python3 - "$caddyfile" ${shQuote(exposure.pk01.caddyConfigPath)} "$merged_caddyfile" ${shQuote(exposure.dns.hostname)} >"$install_out" 2>"$install_err" <<'PY' ${pk01CaddyMergeManagedBlocksPython()} PY install_rc=$? if [ "$install_rc" -eq 0 ]; then sudo install -m 0644 "$merged_caddyfile" ${shQuote(exposure.pk01.caddyConfigPath)} >>"$install_out" 2>>"$install_err" install_rc=$? fi if [ "$install_rc" -eq 0 ]; then sudo install -m 0644 "$service_unit" /etc/systemd/system/${exposure.pk01.caddyServiceName}.service >>"$install_out" 2>>"$install_err" install_rc=$? fi fi validate_rc=1 if [ "$install_rc" -eq 0 ]; then sudo ${shQuote(exposure.pk01.caddyBinaryPath)} validate --config ${shQuote(exposure.pk01.caddyConfigPath)} >"$validate_out" 2>"$validate_err" validate_rc=$? else : >"$validate_out" : >"$validate_err" fi pikanode_rc=1 if [ "$validate_rc" -eq 0 ]; then if docker inspect ${shQuote(exposure.pk01.pikanodeContainerName)} >/dev/null 2>&1; then docker rm -f ${shQuote(exposure.pk01.pikanodeContainerName)} >"$pikanode_out" 2>"$pikanode_err" pikanode_rc=$? else : >"$pikanode_out" : >"$pikanode_err" pikanode_rc=0 fi if [ "$pikanode_rc" -eq 0 ]; then docker run -d --name ${shQuote(exposure.pk01.pikanodeContainerName)} \\ --restart=always \\ -p 127.0.0.1:${exposure.pk01.pikanodeHttpHostPort}:8888 \\ -p 22000-22099:22000-22099 \\ -p 7500:7500 \\ -v ${shQuote(exposure.pk01.pikanodeRoot)}:/usr/src/app \\ -v /etc/letsencrypt:/etc/letsencrypt:ro \\ -w /usr/src/app \\ ${shQuote(exposure.pk01.pikanodeImage)} \\ sh init.sh >>"$pikanode_out" 2>>"$pikanode_err" pikanode_rc=$? if [ "$pikanode_rc" -ne 0 ]; then printf '%s\\n' '[rollback] restarting pikanode with previous public 80/443 mapping' >>"$pikanode_err" docker rm -f ${shQuote(exposure.pk01.pikanodeContainerName)} >>"$pikanode_out" 2>>"$pikanode_err" || true docker run -d --name ${shQuote(exposure.pk01.pikanodeContainerName)} \\ --restart=always \\ -p 80:8888 \\ -p 22000-22099:22000-22099 \\ -p 443:443 \\ -p 7500:7500 \\ -v ${shQuote(exposure.pk01.pikanodeRoot)}:/usr/src/app \\ -v /etc/letsencrypt:/etc/letsencrypt:ro \\ -w /usr/src/app \\ ${shQuote(exposure.pk01.pikanodeImage)} \\ sh init.sh >>"$pikanode_out" 2>>"$pikanode_err" || true fi fi else : >"$pikanode_out" : >"$pikanode_err" fi caddy_rc=1 if [ "$pikanode_rc" -eq 0 ]; then sudo systemctl daemon-reload >"$caddy_out" 2>"$caddy_err" sudo systemctl enable --now ${shQuote(exposure.pk01.caddyServiceName)} >>"$caddy_out" 2>>"$caddy_err" caddy_rc=$? if [ "$caddy_rc" -eq 0 ]; then sudo systemctl reload ${shQuote(exposure.pk01.caddyServiceName)} >>"$caddy_out" 2>>"$caddy_err" || sudo systemctl restart ${shQuote(exposure.pk01.caddyServiceName)} >>"$caddy_out" 2>>"$caddy_err" caddy_rc=$? fi fi probe_rc=1 if [ "$caddy_rc" -eq 0 ]; then { printf '[pikanode]\\n' curl -kfsS --max-time 10 --resolve pikapython.com:443:127.0.0.1 https://pikapython.com/ -o /dev/null -w 'status=%{http_code}\\n' printf '[api-health]\\n' curl -kfsS --max-time 10 --resolve ${exposure.dns.hostname}:443:127.0.0.1 ${exposure.publicBaseUrl}/health -o /dev/null -w 'status=%{http_code}\\n' || true } >"$probe_out" 2>"$probe_err" probe_rc=0 else : >"$probe_out" : >"$probe_err" fi python3 - "$download_rc" "$install_rc" "$validate_rc" "$pikanode_rc" "$caddy_rc" "$probe_rc" "$download_action" "$download_proxy_mode" "$download_proxy_url" "$download_out" "$download_err" "$install_out" "$install_err" "$validate_out" "$validate_err" "$pikanode_out" "$pikanode_err" "$caddy_out" "$caddy_err" "$probe_out" "$probe_err" <<'PY' import json import sys download_rc, install_rc, validate_rc, pikanode_rc, caddy_rc, probe_rc = [int(value) for value in sys.argv[1:7]] download_action = sys.argv[7] download_proxy_mode = sys.argv[8] download_proxy_url = sys.argv[9] or None paths = sys.argv[10:] def text(path, limit=4000): try: return open(path, encoding="utf-8", errors="replace").read()[-limit:] except FileNotFoundError: return "" payload = { "ok": download_rc == 0 and install_rc == 0 and validate_rc == 0 and pikanode_rc == 0 and caddy_rc == 0, "target": "${target.id}", "mode": "pk01-caddy-frp-direct", "publicBaseUrl": "${exposure.publicBaseUrl}", "hostname": "${exposure.dns.hostname}", "expectedA": "${exposure.dns.expectedA}", "dataPath": "client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API", "pikanodeRole": "pikapython.com upstream only; api.pikapython.com does not pass through pikanode Express", "caddy": { "binaryPath": "${exposure.pk01.caddyBinaryPath}", "configPath": "${exposure.pk01.caddyConfigPath}", "serviceName": "${exposure.pk01.caddyServiceName}", "downloadProxy": {"mode": download_proxy_mode, "url": download_proxy_url or None}, }, "frp": { "server": "${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}", "remotePort": ${exposure.frpc.remotePort}, "proxyName": "${exposure.frpc.proxyName}", }, "steps": { "downloadCaddy": {"exitCode": download_rc, "action": download_action, "stdout": text(paths[0]), "stderr": text(paths[1])}, "installConfig": {"exitCode": install_rc, "stdout": text(paths[2]), "stderr": text(paths[3])}, "validateCaddyConfig": {"exitCode": validate_rc, "stdout": text(paths[4]), "stderr": text(paths[5])}, "restartPikanodeLoopback": {"exitCode": pikanode_rc, "stdout": text(paths[6]), "stderr": text(paths[7])}, "startCaddy": {"exitCode": caddy_rc, "stdout": text(paths[8]), "stderr": text(paths[9])}, "localProbe": {"exitCode": probe_rc, "stdout": text(paths[10]), "stderr": text(paths[11])}, }, "valuesPrinted": False, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } function renderPk01Caddyfile(exposure: Sub2ApiPublicExposureConfig): string { const apexHost = baseDomain(exposure.dns.hostname); const apiBlock = renderCaddyManagedBlock( sub2apiCaddyManagedMarker, renderSimpleReverseProxyCaddySiteBlock({ hostname: exposure.dns.hostname, upstream: `127.0.0.1:${exposure.frpc.remotePort}`, responseHeaderTimeoutSeconds: exposure.pk01.responseHeaderTimeoutSeconds, }), ); return `{ email ${exposure.pk01.caddyEmail} storage file_system { root ${exposure.pk01.caddyStorageDir} } } ${apexHost}, www.${apexHost} { reverse_proxy 127.0.0.1:${exposure.pk01.pikanodeHttpHostPort} } ${apiBlock.trim()} `; } function renderPk01CaddyService(exposure: Sub2ApiPublicExposureConfig): string { return `[Unit] Description=UniDesk PK01 Caddy edge for PikaPython and Sub2API After=network-online.target docker.service Wants=network-online.target [Service] Type=simple ExecStart=${exposure.pk01.caddyBinaryPath} run --environ --config ${exposure.pk01.caddyConfigPath} ExecReload=${exposure.pk01.caddyBinaryPath} reload --config ${exposure.pk01.caddyConfigPath} --force Restart=always RestartSec=3 LimitNOFILE=1048576 AmbientCapabilities=CAP_NET_BIND_SERVICE NoNewPrivileges=true Environment=XDG_DATA_HOME=${exposure.pk01.caddyStorageDir} Environment=XDG_CONFIG_HOME=/etc/caddy [Install] WantedBy=multi-user.target `; } function baseDomain(hostname: string): string { const parts = hostname.split("."); return parts.length <= 2 ? hostname : parts.slice(-2).join("."); } function secretRoot(sub2api: Sub2ApiConfig): string { const root = sub2api.runtime.secrets.root; return isAbsolute(root) ? root : rootPath(root); } function writeEnvFile(path: string, values: Record): void { mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); const lines = Object.keys(values) .sort() .map((key) => `${key}=${quoteEnv(values[key])}`); writeFileSync(path, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 }); chmodSync(path, 0o600); } function quoteEnv(value: string): string { if (/^[A-Za-z0-9_./:@%?&=+-]+$/u.test(value)) return value; return `'${value.replaceAll("'", "'\"'\"'")}'`; } function dryRunScript(yaml: string, target: Sub2ApiTargetConfig): string { const encoded = Buffer.from(yaml, "utf8").toString("base64"); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT manifest="$tmp/sub2api.k8s.yaml" printf '%s' '${encoded}' | base64 -d > "$manifest" client_out="$tmp/client.out" client_err="$tmp/client.err" server_out="$tmp/server.out" server_err="$tmp/server.err" kubectl apply --dry-run=client -f "$manifest" >"$client_out" 2>"$client_err" client_rc=$? if kubectl get namespace ${target.namespace} >/dev/null 2>&1; then namespace_exists=true kubectl apply --server-side --dry-run=server --field-manager=${fieldManager} -f "$manifest" >"$server_out" 2>"$server_err" server_rc=$? else namespace_exists=false server_rc=0 printf '%s\\n' 'server dry-run skipped because namespace does not exist yet; first apply creates it before namespaced resources' >"$server_out" : >"$server_err" fi python3 - "$client_rc" "$server_rc" "$namespace_exists" "$client_out" "$client_err" "$server_out" "$server_err" <<'PY' import json import sys client_rc = int(sys.argv[1]) server_rc = int(sys.argv[2]) namespace_exists = sys.argv[3] == "true" paths = sys.argv[4:] def text(path): try: return open(path, encoding="utf-8").read() except FileNotFoundError: return "" payload = { "ok": client_rc == 0 and server_rc == 0, "target": "${target.id}", "namespace": "${target.namespace}", "namespaceExistsBeforeDryRun": namespace_exists, "clientDryRun": { "exitCode": client_rc, "stdout": text(paths[0])[-4000:], "stderr": text(paths[1])[-4000:], }, "serverDryRun": { "exitCode": server_rc, "disposition": "executed" if namespace_exists else "skipped-namespace-missing", "stdout": text(paths[2])[-4000:], "stderr": text(paths[3])[-4000:], }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } function applyScript( sub2api: Sub2ApiConfig, yaml: string, target: Sub2ApiTargetConfig, secretMaterial: ExternalActiveSecretMaterial | null, publicExposureSecretMaterial: PublicExposureSecretMaterial | null, egressProxySecretMaterial: EgressProxySecretMaterial | null, ): string { const encoded = Buffer.from(yaml, "utf8").toString("base64"); const cleanupPlan = managedResourceCleanupPlan(sub2api, target); const appSecretName = sub2api.runtime.database.secretName; const publicExposureSecretName = publicExposureSecretMaterial?.secretName ?? sub2api.defaults.cleanup.publicExposure.secretName; const publicExposureSecretKey = publicExposureSecretMaterial?.secretKey ?? "frpc.toml"; const egressProxySecretName = egressProxySecretMaterial?.secretName ?? sub2api.defaults.cleanup.egressProxy.secretName; const egressProxySecretKey = egressProxySecretMaterial?.secretKey ?? "config.json"; if (target.databaseMode === "external-active" && secretMaterial === null) throw new Error("external-active apply requires secret material"); const externalSecretFiles = secretMaterial === null ? "" : requiredSecretKeys.map((key) => { const value = Buffer.from(secretMaterial.values[key], "utf8").toString("base64"); return ` printf '%s' '${value}' | base64 -d >"$tmp/secret.${key}"`; }).join("\n"); const secretSourceSummary = secretMaterial === null ? "None" : `json.loads(${JSON.stringify(JSON.stringify({ sourceRef: secretMaterial.sourceRef, sourcePath: secretMaterial.sourcePath, appSourceRef: secretMaterial.appSourceRef, appSourcePath: secretMaterial.appSourcePath, sourceAction: secretMaterial.action, fingerprint: secretMaterial.fingerprint, valuesPrinted: false, }))})`; const exposureSecretFile = publicExposureSecretMaterial === null ? "" : ` printf '%s' '${Buffer.from(publicExposureSecretMaterial.frpcToml, "utf8").toString("base64")}' | base64 -d >"$tmp/frpc.toml"`; const exposureSecretSummary = publicExposureSecretMaterial === null ? "None" : `json.loads(${JSON.stringify(JSON.stringify({ sourceRef: publicExposureSecretMaterial.sourceRef, sourcePath: publicExposureSecretMaterial.sourcePath, secretName: publicExposureSecretMaterial.secretName, secretKey: publicExposureSecretMaterial.secretKey, fingerprint: publicExposureSecretMaterial.fingerprint, valuesPrinted: false, }))})`; const egressProxySecretFile = egressProxySecretMaterial === null ? "" : ` printf '%s' '${Buffer.from(egressProxySecretMaterial.configJson, "utf8").toString("base64")}' | base64 -d >"$tmp/egress-proxy-config.json"`; const egressProxySecretSummary = egressProxySecretMaterial === null ? "None" : `json.loads(${JSON.stringify(JSON.stringify({ sourceRef: egressProxySecretMaterial.sourceRef, sourcePath: egressProxySecretMaterial.sourcePath, secretName: egressProxySecretMaterial.secretName, secretKey: egressProxySecretMaterial.secretKey, fingerprint: egressProxySecretMaterial.fingerprint, subscriptionBytes: egressProxySecretMaterial.subscriptionBytes, selectedOutbound: egressProxySecretMaterial.selectedOutbound, proxyUrl: egressProxySecretMaterial.proxyUrl, noProxy: egressProxySecretMaterial.noProxy, valuesPrinted: false, }))})`; return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT manifest="$tmp/sub2api.k8s.yaml" printf '%s' '${encoded}' | base64 -d > "$manifest" ns_out="$tmp/ns.out" ns_err="$tmp/ns.err" secret_out="$tmp/secret.out" secret_err="$tmp/secret.err" exposure_secret_out="$tmp/exposure-secret.out" exposure_secret_err="$tmp/exposure-secret.err" egress_secret_out="$tmp/egress-secret.out" egress_secret_err="$tmp/egress-secret.err" apply_out="$tmp/apply.out" apply_err="$tmp/apply.err" cleanup_out="$tmp/cleanup.out" cleanup_err="$tmp/cleanup.err" kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$ns_out" 2>"$ns_err" ns_rc=$? secret_action="unknown" secret_rc=0 exposure_secret_action="not-enabled" exposure_secret_rc=0 egress_secret_action="not-enabled" egress_secret_rc=0 if [ "$ns_rc" -eq 0 ]; then if [ "${target.databaseMode}" = "external-pending" ]; then secret_action="external-pending-not-managed" : >"$secret_out" printf '%s\\n' 'external DB target expects its DB credential Secret from the platform DB handoff; predeploy does not create placeholder secrets' >"$secret_err" elif [ "${target.databaseMode}" = "external-active" ]; then ${externalSecretFiles} kubectl -n ${target.namespace} create secret generic ${appSecretName} \\ --from-file=POSTGRES_PASSWORD="$tmp/secret.POSTGRES_PASSWORD" \\ --from-file=ADMIN_PASSWORD="$tmp/secret.ADMIN_PASSWORD" \\ --from-file=JWT_SECRET="$tmp/secret.JWT_SECRET" \\ --from-file=TOTP_ENCRYPTION_KEY="$tmp/secret.TOTP_ENCRYPTION_KEY" \\ --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err" secret_rc=$? secret_action="external-active-synced" elif kubectl -n ${target.namespace} get secret ${appSecretName} >/dev/null 2>&1; then secret_action="kept-existing" : >"$secret_out" : >"$secret_err" else rand_hex() { bytes="$1" if command -v openssl >/dev/null 2>&1; then openssl rand -hex "$bytes" else dd if=/dev/urandom bs="$bytes" count=1 2>/dev/null | od -An -tx1 | tr -d ' \\n' fi } kubectl -n ${target.namespace} create secret generic ${appSecretName} \\ --from-literal=POSTGRES_PASSWORD="$(rand_hex 32)" \\ --from-literal=ADMIN_PASSWORD="$(rand_hex 16)" \\ --from-literal=JWT_SECRET="$(rand_hex 32)" \\ --from-literal=TOTP_ENCRYPTION_KEY="$(rand_hex 32)" \\ --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err" secret_rc=$? secret_action="created" fi if [ "${publicExposureSecretMaterial === null ? "false" : "true"}" = "true" ]; then ${exposureSecretFile} kubectl -n ${target.namespace} create secret generic ${publicExposureSecretName} \\ --from-file=${publicExposureSecretKey}="$tmp/frpc.toml" \\ --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$exposure_secret_out" 2>"$exposure_secret_err" exposure_secret_rc=$? exposure_secret_action="synced" else : >"$exposure_secret_out" : >"$exposure_secret_err" fi if [ "${egressProxySecretMaterial === null ? "false" : "true"}" = "true" ]; then ${egressProxySecretFile} kubectl -n ${target.namespace} create secret generic ${egressProxySecretName} \\ --from-file=${egressProxySecretKey}="$tmp/egress-proxy-config.json" \\ --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$egress_secret_out" 2>"$egress_secret_err" egress_secret_rc=$? egress_secret_action="synced" else : >"$egress_secret_out" : >"$egress_secret_err" fi fi apply_rc=1 if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ] && [ "$exposure_secret_rc" -eq 0 ] && [ "$egress_secret_rc" -eq 0 ]; then kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err" apply_rc=$? else : >"$apply_out" printf '%s\\n' 'skipped because namespace, app secret, public exposure secret, or egress proxy secret step failed' >"$apply_err" fi cleanup_rc=0 if [ "$apply_rc" -eq 0 ]; then python3 - "$cleanup_out" "$cleanup_err" <<'PY' import json import subprocess import sys import time out_path, err_path = sys.argv[1:3] namespace = ${JSON.stringify(target.namespace)} plan = json.loads(${JSON.stringify(JSON.stringify(cleanupPlan))}) def run(args): proc = subprocess.run(["kubectl", "-n", namespace, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE) return { "args": ["kubectl", "-n", namespace, *args], "exitCode": proc.returncode, "stdout": proc.stdout.decode("utf-8", errors="replace")[-2000:], "stderr": proc.stderr.decode("utf-8", errors="replace")[-2000:], } def delete(kind, name): return run(["delete", kind, name, "--ignore-not-found=true", "--wait=false"]) items = [] external_db_state = plan["externalDbState"] redis_persistent_state = plan["redisPersistentState"] if external_db_state["enabled"]: items.append({"name": "legacy-postgres-statefulset", **delete("statefulset", external_db_state["postgresStatefulSetName"])}) items.append({"name": "legacy-postgres-service", **delete("service", external_db_state["postgresServiceName"])}) items.append({"name": "legacy-postgres-pvc", **delete("pvc", external_db_state["postgresPvcName"])}) items.append({"name": "legacy-app-data-pvc", **delete("pvc", external_db_state["appDataPvcName"])}) if redis_persistent_state["enabled"]: items.append({"name": "legacy-redis-pvc", **delete("pvc", redis_persistent_state["pvcName"])}) if not plan["publicExposure"]["enabled"]: items.append({"name": "disabled-public-frpc-deployment", **delete("deployment", plan["publicExposure"]["deploymentName"])}) items.append({"name": "disabled-public-frpc-configmap", **delete("configmap", plan["publicExposure"]["configMapName"])}) items.append({"name": "disabled-public-frpc-secret", **delete("secret", plan["publicExposure"]["secretName"])}) if not plan["egressProxy"]["enabled"]: items.append({"name": "disabled-egress-proxy-deployment", **delete("deployment", plan["egressProxy"]["deploymentName"])}) items.append({"name": "disabled-egress-proxy-service", **delete("service", plan["egressProxy"]["serviceName"])}) items.append({"name": "disabled-egress-proxy-secret", **delete("secret", plan["egressProxy"]["secretName"])}) if not plan["sentinel"]["enabled"]: items.append({"name": "disabled-sentinel-cronjob", **delete("cronjob", plan["sentinel"]["cronJobName"])}) items.append({"name": "disabled-sentinel-jobs", **run(["delete", "job", "-l", f"app.kubernetes.io/name={plan['sentinel']['cronJobName']}", "--ignore-not-found=true", "--wait=false"])}) items.append({"name": "disabled-sentinel-configmap", **delete("configmap", plan["sentinel"]["configMapName"])}) items.append({"name": "disabled-sentinel-credentials-secret", **delete("secret", plan["sentinel"]["credentialsSecretName"])}) items.append({"name": "disabled-sentinel-serviceaccount", **delete("serviceaccount", plan["sentinel"]["serviceAccountName"])}) items.append({"name": "disabled-sentinel-role", **delete("role", plan["sentinel"]["roleName"])}) items.append({"name": "disabled-sentinel-rolebinding", **delete("rolebinding", plan["sentinel"]["roleBindingName"])}) watch = [] if external_db_state["enabled"]: watch.extend([ ("statefulset", external_db_state["postgresStatefulSetName"]), ("service", external_db_state["postgresServiceName"]), ("pvc", external_db_state["postgresPvcName"]), ("pvc", external_db_state["appDataPvcName"]), ]) if redis_persistent_state["enabled"]: watch.append(("pvc", redis_persistent_state["pvcName"])) if not plan["publicExposure"]["enabled"]: watch.append(("deployment", plan["publicExposure"]["deploymentName"])) if not plan["egressProxy"]["enabled"]: watch.append(("deployment", plan["egressProxy"]["deploymentName"])) watch.append(("service", plan["egressProxy"]["serviceName"])) if not plan["sentinel"]["enabled"]: watch.append(("cronjob", plan["sentinel"]["cronJobName"])) deadline = time.time() + 90 remaining = [] while True: remaining = [] for kind, name in watch: result = run(["get", kind, name]) if result["exitCode"] == 0: remaining.append({"kind": kind, "name": name}) if not remaining or time.time() >= deadline: break time.sleep(3) payload = { "ok": all(item["exitCode"] == 0 for item in items) and not remaining, "plan": plan, "items": items, "remainingAfterWait": remaining, "valuesPrinted": False, } open(out_path, "w", encoding="utf-8").write(json.dumps(payload, ensure_ascii=False, indent=2)) open(err_path, "w", encoding="utf-8").write("") sys.exit(0 if payload["ok"] else 1) PY cleanup_rc=$? else : >"$cleanup_out" printf '%s\\n' 'skipped because apply failed' >"$cleanup_err" cleanup_rc=1 fi python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$apply_rc" "$cleanup_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_err" <<'PY' import json import sys ns_rc = int(sys.argv[1]) secret_rc = int(sys.argv[2]) exposure_secret_rc = int(sys.argv[3]) egress_secret_rc = int(sys.argv[4]) apply_rc = int(sys.argv[5]) cleanup_rc = int(sys.argv[6]) secret_action = sys.argv[7] exposure_secret_action = sys.argv[8] egress_secret_action = sys.argv[9] paths = sys.argv[10:] def text(path): try: return open(path, encoding="utf-8").read() except FileNotFoundError: return "" def parsed(path): try: return json.load(open(path, encoding="utf-8")) except Exception: return None payload = { "ok": ns_rc == 0 and secret_rc == 0 and exposure_secret_rc == 0 and egress_secret_rc == 0 and apply_rc == 0 and cleanup_rc == 0, "target": "${target.id}", "namespace": "${target.namespace}", "databaseMode": "${target.databaseMode}", "appReplicas": ${target.appReplicas}, "redisReplicas": ${target.redisReplicas}, "secret": { "name": "${appSecretName}", "action": secret_action, "requiredKeys": ${JSON.stringify(requiredSecretKeys)}, "managedByThisApply": ${target.databaseMode === "external-pending" ? "False" : "True"}, "externalActiveSource": ${secretSourceSummary}, "valuesPrinted": False, }, "publicExposure": ${exposureSecretSummary}, "egressProxy": ${egressProxySecretSummary}, "steps": { "namespace": {"exitCode": ns_rc, "stdout": text(paths[0])[-4000:], "stderr": text(paths[1])[-4000:]}, "secret": {"exitCode": secret_rc, "stdout": text(paths[2])[-4000:], "stderr": text(paths[3])[-4000:]}, "publicExposureSecret": {"exitCode": exposure_secret_rc, "action": exposure_secret_action, "stdout": text(paths[4])[-4000:], "stderr": text(paths[5])[-4000:]}, "egressProxySecret": {"exitCode": egress_secret_rc, "action": egress_secret_action, "stdout": text(paths[6])[-4000:], "stderr": text(paths[7])[-4000:]}, "apply": {"exitCode": apply_rc, "stdout": text(paths[8])[-8000:], "stderr": text(paths[9])[-4000:]}, "cleanup": {"exitCode": cleanup_rc, "summary": parsed(paths[10]), "stdout": text(paths[10])[-8000:], "stderr": text(paths[11])[-4000:]}, }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } function statusScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { const expectedImage = imageRef(sub2api, target); const expectedUrlAllowlist = sub2api.security.urlAllowlist; const externalPending = target.databaseMode === "external-pending"; const externalActive = target.databaseMode === "external-active"; const proxy = target.egressProxy?.enabled ? target.egressProxy : null; const expectedProxyEnv = sub2ApiProxyEnv(target); const cleanupPlan = managedResourceCleanupPlan(sub2api, target); const appSecretName = sub2api.runtime.database.secretName; const redisService = sub2api.runtime.redis.serviceName; 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 deployments kubectl -n ${target.namespace} get deployments -l app.kubernetes.io/part-of=platform-infra capture_json statefulsets kubectl -n ${target.namespace} get statefulsets -l app.kubernetes.io/part-of=platform-infra capture_json pods kubectl -n ${target.namespace} get pods -l app.kubernetes.io/part-of=platform-infra capture_json services kubectl -n ${target.namespace} get services -l app.kubernetes.io/part-of=platform-infra capture_json pvc kubectl -n ${target.namespace} get pvc -l app.kubernetes.io/part-of=platform-infra capture_json secrets kubectl -n ${target.namespace} get secret ${appSecretName} capture_json configmap kubectl -n ${target.namespace} get configmap sub2api-config capture_json networkpolicies kubectl -n ${target.namespace} get networkpolicy capture_json cronjobs kubectl -n ${target.namespace} get cronjob -l app.kubernetes.io/part-of=platform-infra capture_json ingresses kubectl -n ${target.namespace} get ingress capture_json quotas kubectl -n ${target.namespace} get resourcequota capture_json limitranges kubectl -n ${target.namespace} get limitrange pod_name="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${serviceName},app.kubernetes.io/component=app -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pod-name.err" || true)" printf '%s' "$pod_name" >"$tmp/pod-name.txt" if [ -n "$pod_name" ]; then kubectl -n ${target.namespace} exec "$pod_name" -- sh -c 'printf "SECURITY_URL_ALLOWLIST_ENABLED=%s\\n" "$SECURITY_URL_ALLOWLIST_ENABLED"; printf "SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=%s\\n" "$SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP"; printf "SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=%s\\n" "$SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS"; printf "SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=%s\\n" "$SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS"; printf "HTTP_PROXY=%s\\n" "$HTTP_PROXY"; printf "HTTPS_PROXY=%s\\n" "$HTTPS_PROXY"; printf "ALL_PROXY=%s\\n" "$ALL_PROXY"; printf "NO_PROXY=%s\\n" "$NO_PROXY"' >"$tmp/pod-env.out" 2>"$tmp/pod-env.err" printf '%s' "$?" >"$tmp/pod-env.rc" else : >"$tmp/pod-env.out" printf '%s\\n' 'sub2api app pod not found' >"$tmp/pod-env.err" printf '%s' "1" >"$tmp/pod-env.rc" fi 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): path = os.path.join(tmp, name) try: return open(path, encoding="utf-8", errors="replace").read() except FileNotFoundError: return "" def deployment_summary(item): spec = item.get("spec") or {} status = item.get("status") or {} desired = spec.get("replicas", 1) available = status.get("availableReplicas", 0) init_containers = ((spec.get("template") or {}).get("spec") or {}).get("initContainers", []) containers = ((spec.get("template") or {}).get("spec") or {}).get("containers", []) return { "name": item["metadata"]["name"], "desired": desired, "readyReplicas": status.get("readyReplicas", 0), "availableReplicas": available, "updatedReplicas": status.get("updatedReplicas", 0), "ready": available >= desired, "images": [c.get("image") for c in containers], "initImages": [c.get("image") for c in init_containers], } def statefulset_summary(item): spec = item.get("spec") or {} status = item.get("status") or {} desired = spec.get("replicas", 1) ready = status.get("readyReplicas", 0) return { "name": item["metadata"]["name"], "desired": desired, "readyReplicas": ready, "currentReplicas": status.get("currentReplicas", 0), "updatedReplicas": status.get("updatedReplicas", 0), "ready": ready >= desired, "images": [c.get("image") for c in ((spec.get("template") or {}).get("spec") or {}).get("containers", [])], } def pod_summary(item): status = item.get("status") or {} container_statuses = status.get("containerStatuses") or [] return { "name": item["metadata"]["name"], "phase": status.get("phase"), "ready": all((cs.get("ready") is True) for cs in container_statuses) if container_statuses else False, "restarts": sum(int(cs.get("restartCount") or 0) for cs in container_statuses), "nodeName": (item.get("spec") or {}).get("nodeName"), "containers": [ { "name": cs.get("name"), "ready": cs.get("ready"), "restartCount": cs.get("restartCount"), "image": cs.get("image"), "state": list((cs.get("state") or {}).keys()), "stateDetail": cs.get("state") or {}, } for cs in container_statuses ], "initContainers": [ { "name": cs.get("name"), "ready": cs.get("ready"), "restartCount": cs.get("restartCount"), "image": cs.get("image"), "state": list((cs.get("state") or {}).keys()), "stateDetail": cs.get("state") or {}, } for cs in (status.get("initContainerStatuses") or []) ], "conditions": [ { "type": condition.get("type"), "status": condition.get("status"), "reason": condition.get("reason"), "message": condition.get("message"), } for condition in status.get("conditions", []) ], } def service_summary(item): spec = item.get("spec") or {} return { "name": item["metadata"]["name"], "type": spec.get("type", "ClusterIP"), "clusterIP": spec.get("clusterIP"), "ports": [ { "name": p.get("name"), "port": p.get("port"), "targetPort": p.get("targetPort"), "nodePort": p.get("nodePort"), } for p in spec.get("ports", []) ], } def pvc_summary(item): spec = item.get("spec") or {} status = item.get("status") or {} req = (spec.get("resources") or {}).get("requests") or {} return { "name": item["metadata"]["name"], "phase": status.get("phase"), "storageClassName": spec.get("storageClassName"), "requestedStorage": req.get("storage"), } def network_policy_summary(item): spec = item.get("spec") or {} return { "name": item["metadata"]["name"], "podSelector": spec.get("podSelector"), "policyTypes": spec.get("policyTypes") or [], "ingress": spec.get("ingress") or [], "egress": spec.get("egress") or [], } def is_allow_all_network_policy(item): 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 resource_findings(kind, collection): findings = [] for item in collection: spec = item.get("spec") or {} template_spec = ((spec.get("template") or {}).get("spec") or {}) if template_spec.get("hostNetwork") is True: findings.append({"kind": kind, "name": item["metadata"]["name"], "field": "hostNetwork"}) all_containers = [(container, "containers") for container in template_spec.get("containers", [])] + [(container, "initContainers") for container in template_spec.get("initContainers", [])] for container, container_group in all_containers: resources = container.get("resources") or {} if resources.get("requests"): findings.append({"kind": kind, "name": item["metadata"]["name"], "container": container.get("name"), "containerGroup": container_group, "field": "resources.requests"}) if resources.get("limits"): findings.append({"kind": kind, "name": item["metadata"]["name"], "container": container.get("name"), "containerGroup": container_group, "field": "resources.limits"}) for port in container.get("ports", []): if "hostPort" in port: findings.append({"kind": kind, "name": item["metadata"]["name"], "container": container.get("name"), "containerGroup": container_group, "field": "hostPort", "value": port.get("hostPort")}) return findings deployments = items("deployments") statefulsets = items("statefulsets") services = items("services") pods = items("pods") pvcs = items("pvc") networkpolicies = items("networkpolicies") cronjobs = items("cronjobs") secret = load("secrets") configmap = load("configmap") configmap_data = (configmap or {}).get("data") or {} secret_keys = sorted(((secret or {}).get("data") or {}).keys()) missing_secret_keys = [key for key in ${JSON.stringify(requiredSecretKeys)} if key not in secret_keys] external_pending = ${externalPending ? "True" : "False"} external_active = ${externalActive ? "True" : "False"} expected_app_replicas = ${target.appReplicas} expected_redis_replicas = ${target.redisReplicas} 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"] service_violations = [] for svc in services: spec = svc.get("spec") or {} if spec.get("type", "ClusterIP") != "ClusterIP": service_violations.append({"name": svc["metadata"]["name"], "type": spec.get("type")}) for port in spec.get("ports", []): if "nodePort" in port: service_violations.append({"name": svc["metadata"]["name"], "nodePort": port.get("nodePort")}) resource_violations = resource_findings("Deployment", deployments) + resource_findings("StatefulSet", statefulsets) expected_image = "${expectedImage}" expected_url_allowlist = json.loads(${JSON.stringify(JSON.stringify(expectedUrlAllowlist))}) sub2api_deployment = next((deployment_summary(item) for item in deployments if item["metadata"]["name"] == "${serviceName}"), None) redis_deployment = next((deployment_summary(item) for item in deployments if item["metadata"]["name"] == "${redisService}"), None) sub2api_desired_aligned = sub2api_deployment is not None and sub2api_deployment.get("desired") == expected_app_replicas redis_desired_aligned = redis_deployment is not None and redis_deployment.get("desired") == expected_redis_replicas image_aligned = sub2api_deployment is not None and expected_image in sub2api_deployment.get("images", []) url_allowlist_runtime = { "enabled": configmap_data.get("SECURITY_URL_ALLOWLIST_ENABLED"), "allowInsecureHttp": configmap_data.get("SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP"), "allowPrivateHosts": configmap_data.get("SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS"), "upstreamHosts": configmap_data.get("SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS"), } pod_env = {} for line in text("pod-env.out").splitlines(): if "=" not in line: continue key, value = line.split("=", 1) pod_env[key] = value url_allowlist_pod_env = { "enabled": pod_env.get("SECURITY_URL_ALLOWLIST_ENABLED"), "allowInsecureHttp": pod_env.get("SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP"), "allowPrivateHosts": pod_env.get("SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS"), "upstreamHosts": pod_env.get("SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS"), } expected_url_allowlist_strings = { "enabled": str(expected_url_allowlist.get("enabled")).lower(), "allowInsecureHttp": str(expected_url_allowlist.get("allowInsecureHttp")).lower(), "allowPrivateHosts": str(expected_url_allowlist.get("allowPrivateHosts")).lower(), "upstreamHosts": ",".join(expected_url_allowlist.get("upstreamHosts") or []), } expected_proxy_env = json.loads(${JSON.stringify(JSON.stringify({ enabled: proxy !== null && proxy.applyToSub2Api, httpProxy: expectedProxyEnv.httpProxy, noProxy: expectedProxyEnv.noProxy, deploymentName: proxy?.deploymentName ?? null, serviceName: proxy?.serviceName ?? null, }))}) url_allowlist_configmap_aligned = url_allowlist_runtime == expected_url_allowlist_strings url_allowlist_pod_env_aligned = rc("pod-env") == 0 and url_allowlist_pod_env == expected_url_allowlist_strings url_allowlist_aligned = url_allowlist_configmap_aligned and (url_allowlist_pod_env_aligned or (external_pending and expected_app_replicas == 0)) proxy_env_runtime = { "HTTP_PROXY": pod_env.get("HTTP_PROXY"), "HTTPS_PROXY": pod_env.get("HTTPS_PROXY"), "ALL_PROXY": pod_env.get("ALL_PROXY"), "NO_PROXY": pod_env.get("NO_PROXY"), } if expected_proxy_env.get("enabled"): proxy_env_aligned = ( rc("pod-env") == 0 and proxy_env_runtime.get("HTTP_PROXY") == expected_proxy_env.get("httpProxy") and proxy_env_runtime.get("HTTPS_PROXY") == expected_proxy_env.get("httpProxy") and proxy_env_runtime.get("ALL_PROXY") == expected_proxy_env.get("httpProxy") and proxy_env_runtime.get("NO_PROXY") == expected_proxy_env.get("noProxy") ) else: proxy_env_aligned = True allow_all_network_policy = next((item for item in networkpolicies if item.get("metadata", {}).get("name") == "allow-all"), None) network_policy = { "requiredName": "allow-all", "exists": allow_all_network_policy is not None, "ok": allow_all_network_policy is not None and is_allow_all_network_policy(allow_all_network_policy), "policies": [network_policy_summary(item) for item in networkpolicies], } boundary = { "internalOnly": len(service_violations) == 0 and len(items("ingresses")) == 0, "serviceViolations": service_violations, "ingressCount": len(items("ingresses")), "resourceQuotaCount": len(items("quotas")), "limitRangeCount": len(items("limitranges")), "resourceViolations": resource_violations, } deployment_summaries = [deployment_summary(item) for item in deployments] statefulset_summaries = [statefulset_summary(item) for item in statefulsets] workload_ready = all(d["ready"] for d in deployment_summaries) and all(s["ready"] for s in statefulset_summaries) local_postgres_present = ( any(item.get("metadata", {}).get("name") == external_db_state["postgresStatefulSetName"] for item in statefulsets) or any(item.get("metadata", {}).get("name") == external_db_state["postgresServiceName"] for item in services) 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) sentinel_cronjob_present = any(item.get("metadata", {}).get("name") == "${codexPoolSentinelResourceNames().cronJobName}" for item in cronjobs) standby_disabled_resources_ok = not external_pending or ( not public_exposure_deployment_present and not egress_proxy_deployment_present and not sentinel_cronjob_present ) secret_ready = len(missing_secret_keys) == 0 secret_ok = secret_ready or external_pending if external_pending: state_model_ok = ( not local_postgres_present and not redis_pvc_present and sub2api_desired_aligned and redis_desired_aligned and expected_app_replicas == 0 and expected_redis_replicas == 0 and standby_disabled_resources_ok ) elif external_active: state_model_ok = ( not local_postgres_present and not redis_pvc_present and sub2api_desired_aligned and redis_desired_aligned and expected_app_replicas == 1 and expected_redis_replicas == 1 ) else: state_model_ok = local_postgres_present and sub2api_desired_aligned and redis_desired_aligned status_label = "pending-external-db" if external_pending else "external-db-active" if external_active else "active" payload = { "ok": rc("ns") == 0 and workload_ready and image_aligned and url_allowlist_aligned and proxy_env_aligned and network_policy["ok"] and boundary["internalOnly"] and len(resource_violations) == 0 and boundary["resourceQuotaCount"] == 0 and boundary["limitRangeCount"] == 0 and secret_ok and state_model_ok, "target": "${target.id}", "route": "${target.route}", "namespace": "${target.namespace}", "status": status_label, "databaseMode": "${target.databaseMode}", "redisMode": "${target.redisMode}", "expectedAppReplicas": expected_app_replicas, "expectedRedisReplicas": expected_redis_replicas, "namespaceExists": rc("ns") == 0, "deployments": deployment_summaries, "statefulsets": statefulset_summaries, "pods": [pod_summary(item) for item in pods], "services": [service_summary(item) for item in services], "pvcs": [pvc_summary(item) for item in pvcs], "networkPolicy": network_policy, "secret": { "name": "${appSecretName}", "exists": rc("secrets") == 0, "requiredKeys": ${JSON.stringify(requiredSecretKeys)}, "missingKeys": missing_secret_keys, "ready": secret_ready, "requiredForPredeployOk": not external_pending, "valuesPrinted": False, }, "stateModel": { "localPostgresPresent": local_postgres_present, "redisPvcPresent": redis_pvc_present, "sub2apiDesiredReplicasAligned": sub2api_desired_aligned, "redisDesiredReplicasAligned": redis_desired_aligned, "standbyDisabledResourcesOk": standby_disabled_resources_ok, "publicExposureDeploymentPresent": public_exposure_deployment_present, "egressProxyDeploymentPresent": egress_proxy_deployment_present, "sentinelCronJobPresent": sentinel_cronjob_present, "ok": state_model_ok, }, "imageControl": { "desiredImage": expected_image, "configPath": "config/platform-infra/sub2api.yaml", "aligned": image_aligned, "runningImages": sub2api_deployment.get("images", []) if sub2api_deployment else [], }, "security": { "urlAllowlist": { "configPath": "config/platform-infra/sub2api.yaml", "expected": expected_url_allowlist, "runtime": url_allowlist_runtime, "podEnv": url_allowlist_pod_env, "aligned": url_allowlist_aligned, "configMapAligned": url_allowlist_configmap_aligned, "podEnvAligned": url_allowlist_pod_env_aligned, "podEnvRequired": not (external_pending and expected_app_replicas == 0), "configMapExists": rc("configmap") == 0, "podEnvProbe": { "podName": text("pod-name.txt"), "exitCode": rc("pod-env"), "stderr": text("pod-env.err")[-1000:], }, } }, "egressProxy": { "enabled": expected_proxy_env.get("enabled"), "deploymentName": expected_proxy_env.get("deploymentName"), "serviceName": expected_proxy_env.get("serviceName"), "expectedHttpProxy": expected_proxy_env.get("httpProxy"), "expectedNoProxy": expected_proxy_env.get("noProxy"), "podEnv": proxy_env_runtime, "aligned": proxy_env_aligned, "valuesPrinted": False, }, "boundary": boundary, "serviceDns": "${serviceName}.${target.namespace}.svc.cluster.local:8080", "next": { "apply": "bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm", "validate": "bun scripts/cli.ts platform-infra sub2api validate --target ${target.id}", }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } function validateScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { const cleanup = sub2api.defaults.cleanup; const dependencyImages = targetDependencyImages(sub2api, target); const postgresStatefulSetName = cleanup.externalDbState.postgresStatefulSetName; const postgresServiceName = cleanup.externalDbState.postgresServiceName; const redisService = sub2api.runtime.redis.serviceName; return ` set -u tmp="$(mktemp -d)" probe_suffix="$(date +%s)-$$" pg_probe="unidesk-sub2api-netcheck-pg-$probe_suffix" redis_probe="unidesk-sub2api-netcheck-redis-$probe_suffix" cleanup() { kubectl -n ${target.namespace} delete pod "$pg_probe" "$redis_probe" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true rm -rf "$tmp" } trap cleanup EXIT kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/health >"$tmp/health.body" 2>"$tmp/health.err" health_rc=$? kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/ >"$tmp/root.body" 2>"$tmp/root.err" root_rc=$? kubectl -n ${target.namespace} get networkpolicy allow-all -o json >"$tmp/network-policy.json" 2>"$tmp/network-policy.err" network_policy_rc=$? pg_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${postgresStatefulSetName} -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pg-pod.err")" redis_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${redisService} -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/redis-pod.err")" if [ -n "$pg_pod" ]; then kubectl -n ${target.namespace} exec "$pg_pod" -- pg_isready -U sub2api -d sub2api -h 127.0.0.1 >"$tmp/pg.out" 2>"$tmp/pg.err" pg_rc=$? else pg_rc=1 printf '%s\\n' 'sub2api postgres pod not found' >"$tmp/pg.err" fi if [ -n "$redis_pod" ]; then kubectl -n ${target.namespace} exec "$redis_pod" -- redis-cli ping >"$tmp/redis.out" 2>"$tmp/redis.err" redis_rc=$? else redis_rc=1 printf '%s\\n' 'sub2api redis pod not found' >"$tmp/redis.err" fi if ! command -v timeout >/dev/null 2>&1; then printf '%s\\n' 'timeout command is required for bounded cross-pod probes' >"$tmp/pg-cross.err" printf '%s\\n' 'timeout command is required for bounded cross-pod probes' >"$tmp/redis-cross.err" : >"$tmp/pg-cross.out" : >"$tmp/redis-cross.out" pg_cross_rc=127 redis_cross_rc=127 else timeout 35s kubectl -n ${target.namespace} run "$pg_probe" --restart=Never --rm -i --image=${dependencyImages.postgres} --image-pull-policy=IfNotPresent --command -- pg_isready -h ${postgresServiceName} -U sub2api -d sub2api -t 5 >"$tmp/pg-cross.out" 2>"$tmp/pg-cross.err" pg_cross_rc=$? timeout 35s kubectl -n ${target.namespace} run "$redis_probe" --restart=Never --rm -i --image=${dependencyImages.redis} --image-pull-policy=IfNotPresent --command -- redis-cli -h ${redisService} -p 6379 ping >"$tmp/redis-cross.out" 2>"$tmp/redis-cross.err" redis_cross_rc=$? fi python3 - "$tmp" "$health_rc" "$root_rc" "$pg_rc" "$redis_rc" "$network_policy_rc" "$pg_cross_rc" "$redis_cross_rc" <<'PY' import json import os import sys tmp = sys.argv[1] health_rc, root_rc, pg_rc, redis_rc, network_policy_rc, pg_cross_rc, redis_cross_rc = [int(value) for value in sys.argv[2:]] def text(name, limit=4000): path = os.path.join(tmp, name) try: data = open(path, encoding="utf-8", errors="replace").read() except FileNotFoundError: return "" return data[-limit:] def json_file(name): path = os.path.join(tmp, name) try: return json.load(open(path, encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return None 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") == [{}] ) health_body = text("health.body", 2000) root_body = text("root.body", 2000) network_policy_obj = json_file("network-policy.json") network_policy_ok = network_policy_rc == 0 and is_allow_all_network_policy(network_policy_obj) payload = { "ok": health_rc == 0 and root_rc == 0 and pg_rc == 0 and redis_rc == 0 and network_policy_ok and pg_cross_rc == 0 and redis_cross_rc == 0, "target": "${target.id}", "namespace": "${target.namespace}", "serviceDns": "${serviceName}.${target.namespace}.svc.cluster.local:8080", "checks": { "allowAllNetworkPolicy": { "exitCode": network_policy_rc, "ok": network_policy_ok, "method": "kubectl -n ${target.namespace} get networkpolicy allow-all -o json", "policyTypes": ((network_policy_obj or {}).get("spec") or {}).get("policyTypes") if isinstance(network_policy_obj, dict) else None, "podSelector": ((network_policy_obj or {}).get("spec") or {}).get("podSelector") if isinstance(network_policy_obj, dict) else None, "ingress": ((network_policy_obj or {}).get("spec") or {}).get("ingress") if isinstance(network_policy_obj, dict) else None, "egress": ((network_policy_obj or {}).get("spec") or {}).get("egress") if isinstance(network_policy_obj, dict) else None, "stderr": text("network-policy.err", 2000), }, "sub2apiHealthViaKubernetesServiceProxy": { "exitCode": health_rc, "method": "kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/health", "bodyPreview": health_body, "stderr": text("health.err", 2000), }, "sub2apiRootViaKubernetesServiceProxy": { "exitCode": root_rc, "method": "kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/", "bodyBytes": len(root_body.encode("utf-8")), "bodyPreview": root_body[:400], "stderr": text("root.err", 2000), }, "postgresPgIsReady": { "exitCode": pg_rc, "stdout": text("pg.out", 2000), "stderr": text("pg.err", 2000), }, "postgresCrossPodPgIsReady": { "exitCode": pg_cross_rc, "method": "temporary ${dependencyImages.postgres} pod pg_isready -h ${postgresServiceName} -U sub2api -d sub2api -t 5, bounded by outer timeout", "stdout": text("pg-cross.out", 2000), "stderr": text("pg-cross.err", 2000), }, "redisPing": { "exitCode": redis_rc, "stdout": text("redis.out", 2000), "stderr": text("redis.err", 2000), }, "redisCrossPodPing": { "exitCode": redis_cross_rc, "method": "temporary ${dependencyImages.redis} pod redis-cli -h ${redisService} -p 6379 ping, bounded by outer timeout", "stdout": text("redis-cross.out", 2000), "stderr": text("redis-cross.err", 2000), }, }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } function validateExternalActiveScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string { const database = sub2api.runtime.database; const redisService = sub2api.runtime.redis.serviceName; const cleanupPlan = managedResourceCleanupPlan(sub2api, target); const exposure = target.publicExposure?.enabled ? target.publicExposure : null; const proxy = target.egressProxy?.enabled ? target.egressProxy : null; const proxyUrl = proxy === null ? "" : `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`; const publicProbeBlock = exposure === null ? ` public_dns_rc=0 public_probe_rc=0 : >"$tmp/public-dns.out" : >"$tmp/public-dns.err" : >"$tmp/public-probe.out" : >"$tmp/public-probe.err" ` : ` public_dns_rc=1 { for resolver in ${exposure.dns.resolvers.map((resolver) => shQuote(resolver)).join(" ")}; do printf 'resolver=%s\\n' "$resolver" dig +short "@$resolver" ${shQuote(exposure.dns.hostname)} A || true done } >"$tmp/public-dns.out" 2>"$tmp/public-dns.err" if grep -Fx ${shQuote(exposure.dns.expectedA)} "$tmp/public-dns.out" >/dev/null 2>&1; then public_dns_rc=0; fi curl -fsS --max-time 20 ${shQuote(`${exposure.publicBaseUrl}/health`)} >"$tmp/public-probe.out" 2>"$tmp/public-probe.err" public_probe_rc=$? `; const egressProbeBlock = proxy === null ? ` egress_deploy_rc=0 egress_svc_rc=0 egress_probe_rc=0 : >"$tmp/egress-deploy.json" : >"$tmp/egress-deploy.err" : >"$tmp/egress-svc.json" : >"$tmp/egress-svc.err" : >"$tmp/egress-probe.out" : >"$tmp/egress-probe.err" ` : ` capture_json egress-deploy kubectl -n ${target.namespace} get deployment ${proxy.deploymentName} egress_deploy_rc=$(cat "$tmp/egress-deploy.rc") capture_json egress-svc kubectl -n ${target.namespace} get service ${proxy.serviceName} egress_svc_rc=$(cat "$tmp/egress-svc.rc") if [ -n "$app_pod" ]; then kubectl -n ${target.namespace} exec "$app_pod" -- sh -c 'HTTP_PROXY="$1" HTTPS_PROXY="$1" ALL_PROXY="$1" NO_PROXY="$2" curl -fsS --max-time 20 -o /dev/null -w "status=%{http_code}\\n" "$3"' sh ${shQuote(proxyUrl)} ${shQuote(proxy.noProxy.join(","))} ${shQuote(proxy.healthProbeUrl)} >"$tmp/egress-probe.out" 2>"$tmp/egress-probe.err" egress_probe_rc=$? else egress_probe_rc=1 printf '%s\\n' 'sub2api app pod not found' >"$tmp/egress-probe.err" fi `; const egressProbeJson = proxy === null ? "None" : `{ "enabled": True, "mode": "master-vpn-subscription-http-proxy", "deploymentName": "${proxy.deploymentName}", "serviceName": "${proxy.serviceName}", "proxyUrl": "${proxyUrl}", "healthProbeUrl": "${proxy.healthProbeUrl}", "applyToSub2Api": ${proxy.applyToSub2Api ? "True" : "False"}, "applyToSentinel": ${proxy.applyToSentinel ? "True" : "False"}, "deployment": { "exitCode": egress_deploy_rc, "ready": deployment_ready(load("egress-deploy")), "stderr": text("egress-deploy.err", 2000), }, "service": { "exitCode": egress_svc_rc, "type": ((load("egress-svc") or {}).get("spec") or {}).get("type"), "clusterIP": ((load("egress-svc") or {}).get("spec") or {}).get("clusterIP"), "stderr": text("egress-svc.err", 2000), }, "probe": { "exitCode": egress_probe_rc, "stdout": text("egress-probe.out", 2000), "stderr": text("egress-probe.err", 2000), }, "valuesPrinted": False, }`; const egressProbeOkExpr = proxy === null ? "True" : "(egress_deploy_rc == 0 and egress_svc_rc == 0 and deployment_ready(load('egress-deploy')) and egress_probe_rc == 0)"; const publicProbeJson = exposure === null ? "None" : `{ "enabled": True, "publicBaseUrl": "${exposure.publicBaseUrl}", "hostname": "${exposure.dns.hostname}", "expectedA": "${exposure.dns.expectedA}", "mode": "pk01-caddy-frp-direct", "dataPath": "client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API", "dns": { "exitCode": public_dns_rc, "resolvers": ${JSON.stringify(exposure.dns.resolvers)}, "stdout": text("public-dns.out", 2000), "stderr": text("public-dns.err", 2000), }, "health": { "exitCode": public_probe_rc, "url": "${exposure.publicBaseUrl}/health", "stdout": text("public-probe.out", 2000), "stderr": text("public-probe.err", 2000), }, }`; const publicProbeOkExpr = exposure === null ? "True" : "(public_dns_rc == 0 and public_probe_rc == 0)"; 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 networkpolicy kubectl -n ${target.namespace} get networkpolicy allow-all capture_json secret kubectl -n ${target.namespace} get secret ${database.secretName} capture_json statefulsets kubectl -n ${target.namespace} get statefulsets capture_json pvc kubectl -n ${target.namespace} get pvc app_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${serviceName},app.kubernetes.io/component=app -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/app-pod.err" || true)" redis_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${redisService},app.kubernetes.io/component=redis -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/redis-pod.err" || true)" printf '%s' "$app_pod" >"$tmp/app-pod.txt" printf '%s' "$redis_pod" >"$tmp/redis-pod.txt" kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/health >"$tmp/health.body" 2>"$tmp/health.err" health_rc=$? kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/ >"$tmp/root.body" 2>"$tmp/root.err" root_rc=$? if [ -n "$app_pod" ]; then kubectl -n ${target.namespace} exec "$app_pod" -- sh -c 'printf "DATABASE_HOST=%s\\n" "$DATABASE_HOST"; printf "DATABASE_PORT=%s\\n" "$DATABASE_PORT"; printf "DATABASE_USER=%s\\n" "$DATABASE_USER"; printf "DATABASE_DBNAME=%s\\n" "$DATABASE_DBNAME"; printf "DATABASE_SSLMODE=%s\\n" "$DATABASE_SSLMODE"; printf "REDIS_HOST=%s\\n" "$REDIS_HOST"; if [ -n "$DATABASE_PASSWORD" ]; then printf "DATABASE_PASSWORD_PRESENT=true\\n"; else printf "DATABASE_PASSWORD_PRESENT=false\\n"; fi' >"$tmp/app-env.out" 2>"$tmp/app-env.err" printf '%s' "$?" >"$tmp/app-env.rc" else : >"$tmp/app-env.out" printf '%s\\n' 'sub2api app pod not found' >"$tmp/app-env.err" printf '%s' "1" >"$tmp/app-env.rc" fi if [ -n "$redis_pod" ]; then kubectl -n ${target.namespace} exec "$redis_pod" -- redis-cli ping >"$tmp/redis.out" 2>"$tmp/redis.err" redis_rc=$? else redis_rc=1 printf '%s\\n' 'sub2api redis pod not found' >"$tmp/redis.err" fi ${publicProbeBlock} ${egressProbeBlock} python3 - "$tmp" "$health_rc" "$root_rc" "$redis_rc" "$public_dns_rc" "$public_probe_rc" "$egress_deploy_rc" "$egress_svc_rc" "$egress_probe_rc" <<'PY' import json import os import sys tmp = sys.argv[1] health_rc, root_rc, redis_rc, public_dns_rc, public_probe_rc, egress_deploy_rc, egress_svc_rc, egress_probe_rc = [int(value) for value in sys.argv[2:]] 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=4000): 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_ready(item): if not isinstance(item, dict): return False spec = item.get("spec") or {} status = item.get("status") or {} desired = spec.get("replicas", 1) return status.get("availableReplicas", 0) >= desired env = {} for line in text("app-env.out").splitlines(): if "=" not in line: continue key, value = line.split("=", 1) env[key] = value network_policy_obj = load("networkpolicy") network_policy_ok = rc("networkpolicy") == 0 and is_allow_all_network_policy(network_policy_obj) secret = load("secret") secret_keys = sorted(((secret or {}).get("data") or {}).keys()) missing_secret_keys = [key for key in ${JSON.stringify(requiredSecretKeys)} if key not in secret_keys] cleanup_plan = json.loads(${JSON.stringify(JSON.stringify(cleanupPlan))}) external_db_state = cleanup_plan["externalDbState"] redis_persistent_state = cleanup_plan["redisPersistentState"] local_postgres_present = ( any(item.get("metadata", {}).get("name") == external_db_state["postgresStatefulSetName"] for item in items("statefulsets")) or any(item.get("metadata", {}).get("name") in [external_db_state["postgresPvcName"], external_db_state["appDataPvcName"]] for item in items("pvc")) ) redis_pvc_present = any(item.get("metadata", {}).get("name") == redis_persistent_state["pvcName"] for item in items("pvc")) env_aligned = ( rc("app-env") == 0 and env.get("DATABASE_HOST") == "${database.host}" and env.get("DATABASE_PORT") == "${database.port}" and env.get("DATABASE_USER") == "${database.user}" and env.get("DATABASE_DBNAME") == "${database.dbName}" and env.get("DATABASE_SSLMODE") == "${database.sslMode}" and env.get("REDIS_HOST") == "${redisService}" and env.get("DATABASE_PASSWORD_PRESENT") == "true" ) payload = { "ok": health_rc == 0 and root_rc == 0 and redis_rc == 0 and network_policy_ok and len(missing_secret_keys) == 0 and env_aligned and not local_postgres_present and not redis_pvc_present and ${publicProbeOkExpr} and ${egressProbeOkExpr}, "target": "${target.id}", "namespace": "${target.namespace}", "status": "external-db-active", "databaseMode": "${target.databaseMode}", "externalDatabase": { "host": "${database.host}", "port": ${database.port}, "user": "${database.user}", "dbName": "${database.dbName}", "sslMode": "${database.sslMode}", "secretName": "${database.secretName}", "passwordKey": "${database.passwordKey}", "passwordPrinted": False, }, "publicExposure": ${publicProbeJson}, "egressProxy": ${egressProbeJson}, "checks": { "allowAllNetworkPolicy": {"exitCode": rc("networkpolicy"), "ok": network_policy_ok, "stderr": text("networkpolicy.err", 2000)}, "secretReady": {"ok": len(missing_secret_keys) == 0, "missingKeys": missing_secret_keys, "valuesPrinted": False}, "sub2apiHealthViaKubernetesServiceProxy": { "exitCode": health_rc, "method": "kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/health", "bodyPreview": text("health.body", 2000), "stderr": text("health.err", 2000), }, "sub2apiRootViaKubernetesServiceProxy": { "exitCode": root_rc, "method": "kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/", "bodyBytes": len(text("root.body").encode("utf-8")), "bodyPreview": text("root.body", 400), "stderr": text("root.err", 2000), }, "appEnvAligned": { "ok": env_aligned, "podName": text("app-pod.txt"), "env": {key: env.get(key) for key in ["DATABASE_HOST", "DATABASE_PORT", "DATABASE_USER", "DATABASE_DBNAME", "DATABASE_SSLMODE", "REDIS_HOST", "DATABASE_PASSWORD_PRESENT"]}, "stderr": text("app-env.err", 2000), }, "redisPing": {"exitCode": redis_rc, "podName": text("redis-pod.txt"), "stdout": text("redis.out", 2000), "stderr": text("redis.err", 2000)}, "externalStateOnly": {"ok": not local_postgres_present and not redis_pvc_present, "localPostgresPresent": local_postgres_present, "redisPvcPresent": redis_pvc_present}, }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } 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 `; } 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; } function asRecordOrNull(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; }