Files
pikasTech-unidesk/scripts/src/platform-infra.ts
T
2026-06-19 13:49:39 +00:00

4155 lines
180 KiB
TypeScript

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<Sub2ApiConfig["image"]>;
dependencyImages: Partial<Sub2ApiConfig["dependencyImages"]>;
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<typeof requiredSecretKeys[number], string>;
}
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|observability ...",
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 <requestId>",
"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",
"bun scripts/cli.ts platform-infra observability plan --target D601",
"bun scripts/cli.ts platform-infra observability apply --target D601 --dry-run",
"bun scripts/cli.ts platform-infra observability apply --target D601 --confirm",
"bun scripts/cli.ts platform-infra observability status --target D601",
"bun scripts/cli.ts platform-infra observability validate --target D601",
"bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id <traceId>",
],
description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows and OpenTelemetry tracing. 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 <requestId>",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
],
module: "scripts/src/platform-infra-sub2api-codex.ts",
},
};
}
function sub2ApiHelpTargetSummary(): Record<string, unknown> {
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<Record<string, unknown> | 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 === "observability") {
const { runPlatformObservabilityCommand } = await import("./platform-infra-observability");
return await runPlatformObservabilityCommand(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<string, unknown> {
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<string>): 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<string, unknown>;
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<string, unknown>;
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<string, unknown>, "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<string, unknown>): 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<string, unknown>): 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<string, unknown>;
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<string, unknown>, 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<string, unknown>;
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<string>();
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<string, unknown>, path: string): Partial<Sub2ApiConfig["image"]> {
if (record.image === undefined) return {};
const image = objectField(record, "image", path);
const override: Partial<Sub2ApiConfig["image"]> = {};
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<string, unknown>, path: string): Partial<Sub2ApiConfig["dependencyImages"]> {
if (record.dependencyImages === undefined) return {};
const dependencyImages = objectField(record, "dependencyImages", path);
const override: Partial<Sub2ApiConfig["dependencyImages"]> = {};
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<string, unknown>;
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<string, unknown>;
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<string, unknown>): 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<string, unknown>;
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<string, unknown>, 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<string, unknown>, key: string, path: string): Record<string, unknown> {
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<string, unknown>;
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
return yamlBooleanField(obj, key, configPath, path);
}
function stringArrayField(obj: Record<string, unknown>, 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<const T extends readonly string[]>(obj: Record<string, unknown>, 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<string, unknown>, key: string, path: string): number {
return yamlIntegerField(obj, key, configPath, path);
}
function integerArrayField(obj: Record<string, unknown>, 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"] {
if (!existsSync(codexPoolConfigPath)) throw new Error(`${codexPoolConfigPath} is required for Sub2API codex-pool sentinel cleanup resource names`);
const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${codexPoolConfigPath} must be an object`);
const sentinel = (parsed as Record<string, unknown>).sentinel;
if (typeof sentinel !== "object" || sentinel === null || Array.isArray(sentinel)) throw new Error(`${codexPoolConfigPath}.sentinel must be an object`);
const record = sentinel as Record<string, unknown>;
const resourceName = (key: string): string => {
const value = stringField(record, key, "sentinel");
validateKubernetesResourceName(value, `sentinel.${key}`);
return value;
};
return {
enabled: false,
cronJobName: resourceName("cronJobName"),
configMapName: resourceName("configMapName"),
credentialsSecretName: resourceName("credentialsSecretName"),
stateConfigMapName: resourceName("stateConfigMapName"),
serviceAccountName: resourceName("serviceAccountName"),
roleName: resourceName("roleName"),
roleBindingName: resourceName("roleBindingName"),
};
}
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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
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<Record<string, unknown>> {
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, ["sh"], 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, ["sh"], 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<Record<string, unknown>> {
const sub2api = readSub2ApiConfig();
const target = resolveTarget(sub2api, options.targetId);
const result = await capture(config, target.route, ["sh"], 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<Record<string, unknown>> {
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, ["sh"], 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<typeof requiredSecretKeys[number], string> = {
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<string, unknown> = {};
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
if (child !== undefined) output[key] = stripUndefined(child);
}
return output;
}
return value;
}
function redactSubscriptionUri(uri: string): string {
return uri.replace(/\/\/[^@]+@/u, "//<redacted>@").replace(/password=[^&#]+/giu, "password=<redacted>").replace(/pbk=[^&#]+/giu, "pbk=<redacted>");
}
async function applyPk01PublicExposure(config: UniDeskConfig, target: Sub2ApiTargetConfig): Promise<Record<string, unknown>> {
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<Record<string, unknown>> {
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, ["sh"], 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<Record<string, unknown>> {
const observations: Array<Record<string, unknown>> = [];
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<string, unknown> }> {
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, ["sh"], 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<string, string>): 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<string, unknown> | 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<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}