Merge pull request #403 from pikasTech/feat/issue-402-sub2api-target-adapter

#402 R1:Sub2API target adapter 化
This commit is contained in:
Lyon
2026-06-15 00:48:46 +08:00
committed by GitHub
2 changed files with 323 additions and 270 deletions
+44
View File
@@ -47,6 +47,28 @@ targets:
redisMode: local-ephemeral
appReplicas: 0
redisReplicas: 0
codexPool:
sentinelImageBuild:
baseImageCachePolicy: pull
noProxy:
- localhost
- 127.0.0.1
- ::1
- host.docker.internal
- 74.48.78.17
- 192.168.0.0/16
- 10.0.0.0/8
- 172.16.0.0/12
- 10.42.0.0/16
- 10.43.0.0/16
- .svc
- .svc.cluster.local
- .cluster.local
- kubernetes
- kubernetes.default
- kubernetes.default.svc
- 127.0.0.1:5000
- localhost:5000
- id: D601
route: D601:k3s
namespace: platform-infra
@@ -63,6 +85,28 @@ targets:
dependencyImages:
postgres: docker.m.daocloud.io/library/postgres:18-alpine
redis: docker.m.daocloud.io/library/redis:8-alpine
codexPool:
sentinelImageBuild:
baseImageCachePolicy: local-if-present
noProxy:
- localhost
- 127.0.0.1
- ::1
- host.docker.internal
- 74.48.78.17
- 192.168.0.0/16
- 10.0.0.0/8
- 172.16.0.0/12
- 10.42.0.0/16
- 10.43.0.0/16
- .svc
- .svc.cluster.local
- .cluster.local
- kubernetes
- kubernetes.default
- kubernetes.default.svc
- 127.0.0.1:5000
- localhost:5000
publicExposure:
enabled: true
publicBaseUrl: https://api.pikapython.com
+279 -270
View File
@@ -5,7 +5,7 @@ import { join } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import type { RenderedCliResult } from "./output";
import { applyLocalCaddyManagedSite } from "./pk01-caddy";
import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "./platform-infra-public-service";
import {
codexPoolSentinelSummary,
codexPoolSentinelRuntimeImage,
@@ -14,9 +14,9 @@ import {
type CodexPoolSentinelConfig,
type CodexPoolSentinelProfileSecret,
} from "./platform-infra-sub2api-codex-sentinel";
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "./secrets";
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
const g14K3sRoute = "G14:k3s";
const serviceName = "sub2api";
const fieldManager = "unidesk-platform-infra";
const sub2apiConfigPath = rootPath("config", "platform-infra", "sub2api.yaml");
@@ -70,7 +70,13 @@ interface CodexPoolRuntimeTarget {
serviceName: string;
serviceDns: string;
publicBaseUrl: string | null;
publicExposure: PublicServiceExposure | null;
appSecretName: string;
secretsRoot: string;
sentinelImageBuild: {
baseImageCachePolicy: "pull" | "local-if-present";
noProxy: string;
};
egressProxy: {
enabled: boolean;
applyToSentinel: boolean;
@@ -285,8 +291,8 @@ export function codexPoolHelp(): unknown {
poolGroupName: pool.groupName,
poolApiKeySecretName: pool.apiKeySecretName,
poolApiKeySecretKey: pool.apiKeySecretKey,
publicBaseUrl: pool.publicExposure.enabled ? pool.publicExposure.publicBaseUrl : runtimeTarget.publicBaseUrl,
masterBaseUrl: pool.publicExposure.enabled ? pool.publicExposure.masterBaseUrl : null,
publicBaseUrl: runtimeTarget.publicBaseUrl,
publicExposureSource: `${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure`,
sentinelMonitorEnabled: pool.sentinel.monitor.enabled,
sentinelActionsEnabled: pool.sentinel.actions.enabled,
secretValuesPrinted: false,
@@ -591,6 +597,7 @@ function stripBooleanOptions(args: string[], stripped: Set<string>): string[] {
interface Sub2ApiRuntimeConfig {
defaultTargetId: string;
appSecretName: string;
secretsRoot: string;
targets: Record<string, unknown>[];
}
@@ -605,10 +612,14 @@ function readSub2ApiRuntimeConfig(): Sub2ApiRuntimeConfig {
const appSecretName = database === null ? null : stringValue(database.secretName);
if (appSecretName === null) throw new Error(`${sub2apiConfigPath}.runtime.database.secretName is required`);
validateKubernetesName(appSecretName, `${sub2apiConfigPath}.runtime.database.secretName`, true);
const secrets = runtime !== null && isRecord(runtime.secrets) ? runtime.secrets : null;
const secretsRoot = secrets === null ? null : stringValue(secrets.root);
if (secretsRoot === null || !secretsRoot.startsWith("/")) throw new Error(`${sub2apiConfigPath}.runtime.secrets.root must be an absolute path`);
if (!Array.isArray(parsed.targets) || !parsed.targets.every(isRecord)) throw new Error(`${sub2apiConfigPath}.targets must be a list`);
return {
defaultTargetId,
appSecretName,
secretsRoot,
targets: parsed.targets,
};
}
@@ -628,6 +639,7 @@ function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarget {
if (route.length === 0) throw new Error(`${sub2apiConfigPath}.targets[${id}].route is required`);
if (targetNamespace === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].namespace is required`);
validateKubernetesName(targetNamespace, `${sub2apiConfigPath}.targets[${id}].namespace`, true);
const sentinelImageBuild = readTargetSentinelImageBuild(raw, id);
let egressProxy: CodexPoolRuntimeTarget["egressProxy"] = null;
if (isRecord(raw.egressProxy) && raw.egressProxy.enabled === true) {
@@ -649,10 +661,8 @@ function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarget {
}
let publicBaseUrl: string | null = null;
if (isRecord(raw.publicExposure) && raw.publicExposure.enabled === true) {
publicBaseUrl = normalizeBaseUrl(stringValue(raw.publicExposure.publicBaseUrl));
if (publicBaseUrl === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].publicExposure.publicBaseUrl must be a valid http(s) URL when target public exposure is enabled`);
}
const publicExposure = readTargetPublicExposure(raw, id);
if (publicExposure !== null && publicExposure.enabled) publicBaseUrl = publicExposure.publicBaseUrl;
return {
id,
@@ -661,11 +671,131 @@ function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarget {
serviceName,
serviceDns: `${serviceName}.${targetNamespace}.svc.cluster.local:8080`,
publicBaseUrl,
publicExposure,
appSecretName: runtimeConfig.appSecretName,
secretsRoot: runtimeConfig.secretsRoot,
sentinelImageBuild,
egressProxy,
};
}
function readTargetSentinelImageBuild(raw: Record<string, unknown>, targetId: string): CodexPoolRuntimeTarget["sentinelImageBuild"] {
const codexPool = isRecord(raw.codexPool) ? raw.codexPool : null;
const imageBuild = codexPool !== null && isRecord(codexPool.sentinelImageBuild) ? codexPool.sentinelImageBuild : null;
if (imageBuild === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild must be a YAML object`);
const policy = stringValue(imageBuild.baseImageCachePolicy);
if (policy !== "pull" && policy !== "local-if-present") {
throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.baseImageCachePolicy must be pull or local-if-present`);
}
return {
baseImageCachePolicy: policy,
noProxy: readTargetNoProxy(imageBuild.noProxy, `${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.noProxy`),
};
}
function readTargetPublicExposure(raw: Record<string, unknown>, targetId: string): PublicServiceExposure | null {
if (raw.publicExposure === undefined || raw.publicExposure === null) return null;
if (!isRecord(raw.publicExposure)) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure must be a YAML object`);
const exposure = raw.publicExposure;
const enabled = readRequiredTargetBoolean(exposure.enabled, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.enabled`);
const dns = readRequiredTargetRecord(exposure.dns, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns`);
const frpc = readRequiredTargetRecord(exposure.frpc, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc`);
const pk01 = readRequiredTargetRecord(exposure.pk01, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01`);
const publicBaseUrl = readTargetBaseUrl(exposure.publicBaseUrl, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.publicBaseUrl`, "https:");
const hostname = readRequiredTargetString(dns.hostname, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.hostname`);
if (new URL(publicBaseUrl).hostname !== hostname) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure publicBaseUrl hostname must match dns.hostname`);
const config: PublicServiceExposure = {
enabled,
publicBaseUrl,
dns: {
hostname,
expectedA: readRequiredTargetString(dns.expectedA, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.expectedA`),
resolvers: readTargetStringArray(dns.resolvers, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.resolvers`),
},
frpc: {
deploymentName: readRequiredTargetString(frpc.deploymentName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.deploymentName`),
secretName: readRequiredTargetString(frpc.secretName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretName`),
secretKey: readRequiredTargetString(frpc.secretKey, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretKey`),
image: readRequiredTargetString(frpc.image, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.image`),
serverAddr: readRequiredTargetString(frpc.serverAddr, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverAddr`),
serverPort: readTargetPort(frpc.serverPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverPort`),
proxyName: readRequiredTargetString(frpc.proxyName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.proxyName`),
remotePort: readTargetPort(frpc.remotePort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.remotePort`),
localIP: readRequiredTargetString(frpc.localIP, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localIP`),
localPort: readTargetPort(frpc.localPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localPort`),
tokenSourceRef: readRequiredTargetString(frpc.tokenSourceRef, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.tokenSourceRef`),
tokenSourceKey: readRequiredTargetString(frpc.tokenSourceKey, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.tokenSourceKey`),
},
pk01: {
route: readRequiredTargetString(pk01.route, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.route`),
caddyConfigPath: readAbsoluteTargetPath(pk01.caddyConfigPath, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.caddyConfigPath`),
caddyServiceName: readRequiredTargetString(pk01.caddyServiceName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.caddyServiceName`),
responseHeaderTimeoutSeconds: readPositiveTargetInteger(pk01.responseHeaderTimeoutSeconds, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.responseHeaderTimeoutSeconds`),
},
};
validateKubernetesName(config.frpc.deploymentName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.deploymentName`, true);
validateKubernetesName(config.frpc.secretName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretName`, true);
validateProxyName(config.frpc.proxyName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.proxyName`);
validatePort(config.frpc.serverPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverPort`);
validatePort(config.frpc.remotePort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.remotePort`);
validatePort(config.frpc.localPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localPort`);
return config;
}
function readRequiredTargetRecord(value: unknown, path: string): Record<string, unknown> {
if (!isRecord(value)) throw new Error(`${path} must be a YAML object`);
return value;
}
function readRequiredTargetString(value: unknown, path: string): string {
const text = stringValue(value);
if (text === null) throw new Error(`${path} must be a non-empty string`);
if (/[\r\n]/u.test(text)) throw new Error(`${path} must not contain newlines`);
return text;
}
function readRequiredTargetBoolean(value: unknown, path: string): boolean {
const parsed = booleanValue(value);
if (parsed === null) throw new Error(`${path} must be a boolean`);
return parsed;
}
function readTargetBaseUrl(value: unknown, path: string, protocol: "http:" | "https:" | null = null): string {
const baseUrl = normalizeBaseUrl(stringValue(value));
if (baseUrl === null) throw new Error(`${path} must be a valid http(s) URL`);
if (protocol !== null && new URL(baseUrl).protocol !== protocol) throw new Error(`${path} must use ${protocol}//`);
return baseUrl;
}
function readTargetPort(value: unknown, path: string): number {
const port = numberValue(value);
if (port === null || !Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${path} must be an integer TCP port`);
return port;
}
function readPositiveTargetInteger(value: unknown, path: string): number {
const number = numberValue(value);
if (number === null || !Number.isInteger(number) || number < 1) throw new Error(`${path} must be a positive integer`);
return number;
}
function readAbsoluteTargetPath(value: unknown, path: string): string {
const text = readRequiredTargetString(value, path);
if (!text.startsWith("/")) throw new Error(`${path} must be an absolute path`);
return text;
}
function readTargetStringArray(value: unknown, path: string): string[] {
if (!Array.isArray(value)) throw new Error(`${path} must be a YAML array`);
const result = value.map((item, index) => readRequiredTargetString(item, `${path}[${index}]`));
if (result.length === 0) throw new Error(`${path} must not be empty`);
return result;
}
function readTargetNoProxy(value: unknown, path: string): string {
return readTargetStringArray(value, path).join(",");
}
function targetFlag(target: CodexPoolRuntimeTarget): string {
return target.id === defaultCodexPoolRuntimeTargetId() ? "" : ` --target ${target.id}`;
}
@@ -676,7 +806,7 @@ function codexPoolPlan(options?: DisclosureOptions): Record<string, unknown> {
const runtimeTarget = codexPoolRuntimeTarget(resolvedOptions.targetId);
const profiles = collectCodexProfiles();
const ok = profiles.length > 0 && profiles.every((profile) => profile.ok);
const consumerBaseUrl = codexConsumerBaseUrl(pool, runtimeTarget);
const consumerBaseUrl = runtimeTarget.publicBaseUrl === null ? null : codexConsumerBaseUrl(pool, runtimeTarget);
return {
ok,
action: "platform-infra-sub2api-codex-pool-plan",
@@ -689,7 +819,7 @@ function codexPoolPlan(options?: DisclosureOptions): Record<string, unknown> {
target: poolTarget(pool, runtimeTarget),
config: {
path: codexPoolConfigPath,
pool: options.full ? pool : codexPoolConfigSummary(pool),
pool: options.full ? pool : codexPoolConfigSummary(pool, runtimeTarget),
},
profiles: options.full ? profiles.map(redactProfile) : profiles.map(compactProfile),
decision: {
@@ -699,11 +829,9 @@ function codexPoolPlan(options?: DisclosureOptions): Record<string, unknown> {
sentinel: pool.sentinel.monitor.enabled
? `Account sentinel is enabled as k8s CronJob ${runtimeTarget.namespace}/${pool.sentinel.cronJobName}; actions.enabled=${pool.sentinel.actions.enabled}.`
: "Account sentinel monitoring is disabled by YAML.",
publicExposure: pool.publicExposure.enabled
? `Default Codex consumers use ${consumerBaseUrl}; bounded master-local probes may use ${pool.publicExposure.masterBaseUrl}. Legacy Codex-pool FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.`
: runtimeTarget.publicBaseUrl === null
? "Public FRP exposure is disabled by YAML."
: `Legacy Codex-pool FRP exposure is disabled by YAML; Codex consumers for target ${runtimeTarget.id} use target-level public exposure ${consumerBaseUrl}.`,
publicExposure: runtimeTarget.publicBaseUrl === null
? `Target-level public exposure is disabled or absent in ${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure.`
: `Codex consumers for target ${runtimeTarget.id} use target-level public exposure ${consumerBaseUrl}.`,
idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files for YAML-managed profiles only; managed accounts missing from YAML are preserved unless --prune-removed is explicitly provided.",
configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.",
manualAccountProtection: pool.manualAccounts.protected.length === 0
@@ -1049,12 +1177,13 @@ async function codexPoolCleanupProbes(config: UniDeskConfig, options: ConfirmOpt
async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
if (!pool.publicExposure.enabled) {
if (runtimeTarget.publicExposure === null || !runtimeTarget.publicExposure.enabled) {
return {
ok: true,
action: "platform-infra-sub2api-codex-pool-expose",
mode: "disabled-by-yaml",
target: poolTarget(pool, runtimeTarget),
source: `${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure`,
};
}
if (!options.confirm) {
@@ -1063,24 +1192,24 @@ async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions):
action: "platform-infra-sub2api-codex-pool-expose",
mode: "dry-run",
target: poolTarget(pool, runtimeTarget),
publicExposure: publicExposureSummary(pool, runtimeTarget),
publicExposure: targetPublicExposureSummary(runtimeTarget),
next: {
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool expose${targetFlag(runtimeTarget)} --confirm`,
},
};
}
const masterResult = await applyMasterFrpsAllowPort(pool);
const caddyResult = await applyMasterCaddySite(pool);
const remoteResult = await capture(config, runtimeTarget.route, ["script"], exposeScript(pool, runtimeTarget));
const secretMaterial = prepareTargetPublicExposureSecret(runtimeTarget);
const caddyResult = await applyPk01CaddyBlock(config, serviceName, runtimeTarget.publicExposure);
const remoteResult = await capture(config, runtimeTarget.route, ["script"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial));
const parsed = parseJsonOutput(remoteResult.stdout);
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, "public");
const ok = masterResult.ok && caddyResult.ok && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, runtimeTarget);
const ok = caddyResult.ok === true && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
if (options.raw) {
return {
ok,
action: "platform-infra-sub2api-codex-pool-expose",
masterFrps: masterResult,
masterCaddy: caddyResult,
frpcSecret: secretMaterialSummary(secretMaterial),
pk01Caddy: caddyResult,
remote: compactCapture(remoteResult, { full: true }),
parsed,
publicProbe,
@@ -1090,14 +1219,14 @@ async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions):
ok,
action: "platform-infra-sub2api-codex-pool-expose",
mode: "confirmed",
publicExposure: publicExposureSummary(pool, runtimeTarget),
masterFrps: masterResult,
masterCaddy: caddyResult,
publicExposure: targetPublicExposureSummary(runtimeTarget),
frpcSecret: secretMaterialSummary(secretMaterial),
pk01Caddy: caddyResult,
remote: parsed ?? compactCapture(remoteResult, { full: options.full || remoteResult.exitCode !== 0 }),
publicProbe,
next: {
configureLocal: "bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
validate: "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
configureLocal: `bun scripts/cli.ts platform-infra sub2api codex-pool configure-local${targetFlag(runtimeTarget)} --confirm`,
validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`,
},
};
}
@@ -1105,7 +1234,7 @@ async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions):
async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const consumerExposureAvailable = pool.publicExposure.enabled || runtimeTarget.publicBaseUrl !== null;
const consumerExposureAvailable = runtimeTarget.publicBaseUrl !== null;
const codexDir = join(homedir(), ".codex");
const configPath = join(codexDir, "config.toml");
const authPath = join(codexDir, "auth.json");
@@ -1137,7 +1266,7 @@ async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOp
},
};
}
if (!consumerExposureAvailable) throw new Error(`${codexPoolConfigPath}.publicExposure.enabled is false and ${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure.enabled is not true; configure-local needs one consumer URL`);
if (!consumerExposureAvailable) throw new Error(`${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure.enabled must be true; configure-local needs one consumer URL`);
const keyResult = await fetchPoolApiKey(config, pool, runtimeTarget);
if (keyResult.apiKey === null) {
return {
@@ -1927,7 +2056,7 @@ function tempUnschedulableSummary(policy: CodexTempUnschedulablePolicy): Record<
};
}
function codexPoolConfigSummary(pool: CodexPoolConfig): Record<string, unknown> {
function codexPoolConfigSummary(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown> {
const accountCapacityTotal = desiredAccountCapacityTotal(pool);
return {
version: pool.version,
@@ -1954,7 +2083,7 @@ function codexPoolConfigSummary(pool: CodexPoolConfig): Record<string, unknown>
protected: pool.manualAccounts.protected,
controlPolicy: "manual accounts are not created, credential-updated, pruned, probed, or frozen by UniDesk codex-pool sync/sentinel; optional proxy_id and pool group membership bindings are narrow YAML-controlled exceptions",
},
publicExposure: publicExposureSummary(pool),
publicExposure: targetPublicExposureSummary(target),
localCodex: pool.localCodex,
sentinel: codexPoolSentinelSummary(pool.sentinel),
disclosure: {
@@ -2899,11 +3028,17 @@ function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRuntimeTarge
service: serviceName,
serviceDns: target.serviceDns,
publicBaseUrl: target.publicBaseUrl,
consumerBaseUrl: pool.publicExposure.enabled || target.publicBaseUrl !== null ? codexConsumerBaseUrl(pool, target) : null,
consumerBaseUrl: target.publicBaseUrl === null ? null : codexConsumerBaseUrl(pool, target),
configPath: codexPoolConfigPath,
groupName: pool.groupName,
apiKeyName: pool.apiKeyName,
apiKeySecret: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
publicExposure: targetPublicExposureSummary(target),
sentinelImageBuild: {
source: `${sub2apiConfigPath}.targets[${target.id}].codexPool.sentinelImageBuild`,
baseImageCachePolicy: target.sentinelImageBuild.baseImageCachePolicy,
noProxy: target.sentinelImageBuild.noProxy,
},
minOwnerConcurrency: pool.minOwnerConcurrency,
minOwnerConcurrencySource: pool.minOwnerConcurrencySource,
accountCapacityTotal: desiredAccountCapacityTotal(pool),
@@ -2943,206 +3078,139 @@ function sentinelProfileSecrets(profiles: CodexProfile[]): CodexPoolSentinelProf
}));
}
function publicExposureSummary(pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): Record<string, unknown> {
function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Record<string, unknown> | null {
const exposure = target.publicExposure;
if (exposure === null) return null;
return {
enabled: pool.publicExposure.enabled,
proxyName: pool.publicExposure.proxyName,
namespace: target.namespace,
configMapName: pool.publicExposure.configMapName,
deploymentName: pool.publicExposure.deploymentName,
frpcImage: pool.publicExposure.frpcImage,
frps: {
serverAddr: pool.publicExposure.serverAddr,
serverPort: pool.publicExposure.serverPort,
remotePort: pool.publicExposure.remotePort,
masterConfigPath: pool.publicExposure.masterFrps.configPath,
masterContainerName: pool.publicExposure.masterFrps.containerName,
},
caddy: {
enabled: pool.publicExposure.masterCaddy.enabled,
domain: pool.publicExposure.masterCaddy.domain,
configPath: pool.publicExposure.masterCaddy.configPath,
serviceName: pool.publicExposure.masterCaddy.serviceName,
upstreamBaseUrl: pool.publicExposure.masterCaddy.upstreamBaseUrl,
responseHeaderTimeoutSeconds: pool.publicExposure.masterCaddy.responseHeaderTimeoutSeconds,
edgeRetry: pool.publicExposure.masterCaddy.edgeRetry,
},
upstream: {
localIP: pool.publicExposure.localIP,
localPort: pool.publicExposure.localPort,
source: `${sub2apiConfigPath}.targets[${target.id}].publicExposure`,
enabled: exposure.enabled,
target: {
id: target.id,
route: target.route,
namespace: target.namespace,
serviceDns: target.serviceDns,
},
urls: {
publicBaseUrl: pool.publicExposure.publicBaseUrl,
masterBaseUrl: pool.publicExposure.masterBaseUrl,
publicBaseUrl: exposure.publicBaseUrl,
consumerBaseUrl: target.publicBaseUrl === null ? null : `${target.publicBaseUrl.replace(/\/+$/u, "")}/`,
},
};
}
async function applyMasterFrpsAllowPort(pool: CodexPoolConfig): Promise<Record<string, unknown>> {
const path = pool.publicExposure.masterFrps.configPath;
const port = pool.publicExposure.remotePort;
const container = pool.publicExposure.masterFrps.containerName;
if (!existsSync(path)) {
return { ok: false, error: "master-frps-config-missing", path, valuesPrinted: false };
}
const before = readFileSync(path, "utf8");
const alreadyAllowed = frpsAllowPortExists(before, port);
let backupPath: string | null = null;
let action = "kept-existing";
if (!alreadyAllowed) {
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
backupPath = `${path}.bak-sub2api-${stamp}`;
copyFileSync(path, backupPath);
const next = before.replace(/\s*$/u, "") + `\n\n[[allowPorts]]\nstart = ${port}\nend = ${port}\n`;
writeFileSync(path, next, "utf8");
chmodSync(path, statSync(backupPath).mode & 0o777);
action = "added-allow-port";
}
const restart = alreadyAllowed ? null : Bun.spawnSync(["docker", "restart", container]);
const inspect = Bun.spawnSync(["docker", "inspect", "-f", "{{.State.Running}}", container]);
const ok = alreadyAllowed || (restart?.exitCode === 0 && inspect.exitCode === 0 && String(inspect.stdout).trim() === "true");
return {
ok,
action,
path,
backupPath,
remotePort: port,
containerName: container,
restart: restart === null ? null : {
exitCode: restart.exitCode,
stdoutTail: Buffer.from(restart.stdout).toString("utf8").slice(-1000),
stderrTail: Buffer.from(restart.stderr).toString("utf8").slice(-1000),
},
inspect: {
exitCode: inspect.exitCode,
running: String(inspect.stdout).trim() === "true",
stderrTail: Buffer.from(inspect.stderr).toString("utf8").slice(-1000),
dns: exposure.dns,
frpc: {
deploymentName: exposure.frpc.deploymentName,
secretName: exposure.frpc.secretName,
secretKey: exposure.frpc.secretKey,
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,
tokenSourceKey: exposure.frpc.tokenSourceKey,
},
pk01: exposure.pk01,
valuesPrinted: false,
};
}
async function applyMasterCaddySite(pool: CodexPoolConfig): Promise<Record<string, unknown>> {
const caddy = pool.publicExposure.masterCaddy;
if (!caddy.enabled) return { ok: true, action: "disabled-by-yaml", valuesPrinted: false };
const path = caddy.configPath;
if (!existsSync(path)) return { ok: false, error: "master-caddy-config-missing", path, valuesPrinted: false };
const desiredBlock = renderCaddySiteBlock(caddy.domain, caddy.upstreamBaseUrl, caddy.responseHeaderTimeoutSeconds, caddy.edgeRetry);
const applied = applyLocalCaddyManagedSite({
serviceId: "sub2api-codex-pool",
markerName: "sub2api-codex-pool",
configPath: path,
serviceName: caddy.serviceName,
siteBlock: desiredBlock,
legacySiteDomain: caddy.domain,
function prepareTargetPublicExposureSecret(target: CodexPoolRuntimeTarget): ReturnType<typeof prepareFrpcSecret> {
if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`);
return prepareFrpcSecret({
secretRoot: target.secretsRoot,
exposure: target.publicExposure,
sourcePathRedactor: redactRepoPath,
parseEnvFile,
requiredEnvValue,
readTextFile: (sourcePath) => {
if (!existsSync(sourcePath)) throw new Error(`publicExposure requires ${redactRepoPath(sourcePath)} with ${target.publicExposure?.frpc.tokenSourceKey}; materialize the PK01 frps token source first`);
return readTextFile(sourcePath);
},
});
}
function secretMaterialSummary(secret: ReturnType<typeof prepareFrpcSecret>): Record<string, unknown> {
return {
...applied,
domain: caddy.domain,
upstreamBaseUrl: caddy.upstreamBaseUrl,
responseHeaderTimeoutSeconds: caddy.responseHeaderTimeoutSeconds,
edgeRetry: caddy.edgeRetry,
sourceRef: secret.sourceRef,
sourcePath: secret.sourcePath,
secretName: secret.secretName,
secretKey: secret.secretKey,
fingerprint: secret.fingerprint,
valuesPrinted: false,
};
}
export function renderCaddySiteBlock(
domain: string,
upstreamBaseUrl: string,
responseHeaderTimeoutSeconds = 180,
edgeRetry: CodexPoolCaddyEdgeRetryConfig | null = null,
): string {
const upstream = new URL(upstreamBaseUrl);
const upstreamHost = `${upstream.hostname}${upstream.port ? `:${upstream.port}` : ""}`;
const retryBlock = renderCaddyEdgeRetryBlock(edgeRetry);
return `${domain} {
encode zstd gzip
reverse_proxy ${upstreamHost} {
${retryBlock} header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
dial_timeout 5s
response_header_timeout ${responseHeaderTimeoutSeconds}s
}
}
}`;
}
function renderCaddyEdgeRetryBlock(edgeRetry: CodexPoolCaddyEdgeRetryConfig | null): string {
if (edgeRetry === null || !edgeRetry.enabled) return "";
const matchLines = [
edgeRetry.retryMatch.methods.length > 0 ? ` method ${edgeRetry.retryMatch.methods.join(" ")}` : null,
edgeRetry.retryMatch.paths.length > 0 ? ` path ${edgeRetry.retryMatch.paths.join(" ")}` : null,
].filter((line): line is string => line !== null);
return ` lb_try_duration ${edgeRetry.tryDurationSeconds}s
lb_try_interval ${edgeRetry.tryIntervalMilliseconds}ms
lb_retry_match {
${matchLines.join("\n")}
}
`;
}
function frpsAllowPortExists(toml: string, port: number): boolean {
const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
return sections.some((section) => {
if (!section.includes("[[allowPorts]]")) return false;
const start = section.match(/^\s*start\s*=\s*(\d+)\s*$/mu);
const end = section.match(/^\s*end\s*=\s*(\d+)\s*$/mu);
return Number(start?.[1]) === port && Number(end?.[1]) === port;
});
}
function exposeScript(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
const manifest = frpcManifest(pool, target);
const encoded = Buffer.from(manifest, "utf8").toString("base64");
function targetPublicExposureApplyScript(target: CodexPoolRuntimeTarget, secret: ReturnType<typeof prepareFrpcSecret>): string {
if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`);
const manifest = renderFrpcManifest({
id: target.id,
route: target.route,
namespace: target.namespace,
replicas: 1,
publicExposure: target.publicExposure,
} satisfies PublicServiceTarget);
const manifestB64 = Buffer.from(manifest, "utf8").toString("base64");
const frpcTomlB64 = Buffer.from(secret.frpcToml, "utf8").toString("base64");
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/sub2api-frpc.yaml"
printf '%s' '${encoded}' | base64 -d > "$manifest"
manifest="$tmp/sub2api-public-exposure.yaml"
frpc_toml="$tmp/frpc.toml"
printf '%s' '${manifestB64}' | base64 -d > "$manifest"
printf '%s' '${frpcTomlB64}' | base64 -d > "$frpc_toml"
ns_out="$tmp/ns.out"
ns_err="$tmp/ns.err"
secret_out="$tmp/secret.out"
secret_err="$tmp/secret.err"
apply_out="$tmp/apply.out"
apply_err="$tmp/apply.err"
rollout_out="$tmp/rollout.out"
rollout_err="$tmp/rollout.err"
kubectl get namespace ${target.namespace} >"$ns_out" 2>"$ns_err"
pods_json="$tmp/pods.json"
pods_err="$tmp/pods.err"
logs_out="$tmp/logs.out"
logs_err="$tmp/logs.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_rc=1
apply_rc=1
rollout_rc=1
if [ "$ns_rc" -eq 0 ]; then
kubectl -n ${target.namespace} create secret generic ${secret.secretName} \\
--from-file=${secret.secretKey}="$frpc_toml" \\
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err"
secret_rc=$?
else
: >"$secret_out"
printf '%s\\n' 'skipped because namespace apply failed' >"$secret_err"
fi
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ]; then
kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err"
apply_rc=$?
if [ "$apply_rc" -eq 0 ]; then
kubectl -n ${target.namespace} rollout status deployment/${pool.publicExposure.deploymentName} --timeout=30s >"$rollout_out" 2>"$rollout_err"
kubectl -n ${target.namespace} rollout status deployment/${target.publicExposure.frpc.deploymentName} --timeout=60s >"$rollout_out" 2>"$rollout_err"
rollout_rc=$?
else
printf '%s\\n' 'skipped because apply failed' >"$rollout_err"
fi
else
: >"$apply_out"
printf '%s\\n' 'skipped because namespace is missing' >"$apply_err"
printf '%s\\n' 'skipped because namespace or secret step failed' >"$apply_err"
: >"$rollout_out"
printf '%s\\n' 'skipped because namespace is missing' >"$rollout_err"
printf '%s\\n' 'skipped because namespace or secret step failed' >"$rollout_err"
fi
pods_json="$tmp/pods.json"
pods_err="$tmp/pods.err"
kubectl -n ${target.namespace} get pods -l app.kubernetes.io/name=${pool.publicExposure.deploymentName} -o json >"$pods_json" 2>"$pods_err"
kubectl -n ${target.namespace} get pods -l app.kubernetes.io/name=${target.publicExposure.frpc.deploymentName} -o json >"$pods_json" 2>"$pods_err"
pods_rc=$?
logs_out="$tmp/logs.out"
logs_err="$tmp/logs.err"
kubectl -n ${target.namespace} logs deployment/${pool.publicExposure.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err"
kubectl -n ${target.namespace} logs deployment/${target.publicExposure.frpc.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err"
logs_rc=$?
python3 - "$tmp" "$ns_rc" "$apply_rc" "$rollout_rc" "$pods_rc" "$logs_rc" <<'PY'
python3 - "$tmp" "$ns_rc" "$secret_rc" "$apply_rc" "$rollout_rc" "$pods_rc" "$logs_rc" <<'PY'
import json
import os
import sys
tmp = sys.argv[1]
ns_rc, apply_rc, rollout_rc, pods_rc, logs_rc = [int(value) for value in sys.argv[2:]]
ns_rc, secret_rc, apply_rc, rollout_rc, pods_rc, logs_rc = [int(value) for value in sys.argv[2:]]
def text(name, limit=4000):
path = os.path.join(tmp, name)
@@ -3173,23 +3241,26 @@ if isinstance(pods_obj, dict):
logs = text("logs.out", 4000)
payload = {
"ok": ns_rc == 0 and apply_rc == 0 and rollout_rc == 0 and pods_rc == 0 and len(pods) > 0 and all(p["ready"] for p in pods),
"ok": ns_rc == 0 and secret_rc == 0 and apply_rc == 0 and rollout_rc == 0 and pods_rc == 0 and len(pods) > 0 and all(p["ready"] for p in pods),
"target": "${target.id}",
"namespace": "${target.namespace}",
"source": "${sub2apiConfigPath}.targets[${target.id}].publicExposure",
"proxy": {
"name": "${pool.publicExposure.proxyName}",
"remotePort": ${JSON.stringify(pool.publicExposure.remotePort)},
"publicBaseUrl": "${pool.publicExposure.publicBaseUrl}",
"masterBaseUrl": "${pool.publicExposure.masterBaseUrl}",
"localIP": "${pool.publicExposure.localIP}",
"localPort": ${JSON.stringify(pool.publicExposure.localPort)},
"name": "${target.publicExposure.frpc.proxyName}",
"remotePort": ${JSON.stringify(target.publicExposure.frpc.remotePort)},
"publicBaseUrl": "${target.publicExposure.publicBaseUrl}",
"localIP": "${target.publicExposure.frpc.localIP}",
"localPort": ${JSON.stringify(target.publicExposure.frpc.localPort)},
},
"resources": {
"configMap": "${pool.publicExposure.configMapName}",
"deployment": "${pool.publicExposure.deploymentName}",
"image": "${pool.publicExposure.frpcImage}",
"secret": "${secret.secretName}",
"secretKey": "${secret.secretKey}",
"deployment": "${target.publicExposure.frpc.deploymentName}",
"image": "${target.publicExposure.frpc.image}",
},
"steps": {
"namespace": {"exitCode": ns_rc, "stdout": text("ns.out"), "stderr": text("ns.err")},
"secret": {"exitCode": secret_rc, "stdout": text("secret.out"), "stderr": text("secret.err"), "valuesPrinted": False},
"apply": {"exitCode": apply_rc, "stdout": text("apply.out"), "stderr": text("apply.err")},
"rollout": {"exitCode": rollout_rc, "stdout": text("rollout.out"), "stderr": text("rollout.err")},
"pods": {"exitCode": pods_rc, "items": pods, "stderr": text("pods.err")},
@@ -3200,6 +3271,7 @@ payload = {
"stderrTail": text("logs.err"),
},
},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
@@ -3207,68 +3279,6 @@ PY
`;
}
function frpcManifest(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
return `apiVersion: v1
kind: ConfigMap
metadata:
name: ${pool.publicExposure.configMapName}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
data:
frpc.toml: |
serverAddr = "${pool.publicExposure.serverAddr}"
serverPort = ${pool.publicExposure.serverPort}
loginFailExit = true
[[proxies]]
name = "${pool.publicExposure.proxyName}"
type = "tcp"
localIP = "${pool.publicExposure.localIP}"
localPort = ${pool.publicExposure.localPort}
remotePort = ${pool.publicExposure.remotePort}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${pool.publicExposure.deploymentName}
namespace: ${target.namespace}
labels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
template:
metadata:
labels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
containers:
- name: frpc
image: ${pool.publicExposure.frpcImage}
imagePullPolicy: IfNotPresent
args:
- -c
- /etc/frp/frpc.toml
volumeMounts:
- name: config
mountPath: /etc/frp
readOnly: true
volumes:
- name: config
configMap:
name: ${pool.publicExposure.configMapName}
`;
}
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): Promise<{ apiKey: string | null; error: string | null }> {
const result = await capture(config, target.route, ["script"], `
set -u
@@ -3360,7 +3370,9 @@ function updateCodexConfigToml(current: string, pool: CodexPoolConfig, target =
}
function codexConsumerBaseUrl(pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): string {
const baseUrl = target.publicBaseUrl ?? pool.publicExposure.publicBaseUrl;
void pool;
const baseUrl = target.publicBaseUrl;
if (baseUrl === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure.enabled must be true before resolving the Codex consumer base URL`);
return `${baseUrl.replace(/\/+$/u, "")}/`;
}
@@ -3458,8 +3470,7 @@ function upsertTomlSectionBoolean(text: string, sectionName: string, key: string
}
async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: string, target = codexPoolRuntimeTarget()): Promise<Record<string, unknown>> {
const probeBase = target.publicBaseUrl === null ? "public" : "target-public";
const probe = await probePublicModels(pool, "with-api-key", apiKey, probeBase, target);
const probe = await probePublicModels(pool, "with-api-key", apiKey, target);
return {
...probe,
ok: probe.ok === true,
@@ -3469,12 +3480,10 @@ async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: strin
};
}
async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string, base: "master" | "public" | "target-public" = "master", target = codexPoolRuntimeTarget()): Promise<Record<string, unknown>> {
const baseUrl = base === "target-public"
? (target.publicBaseUrl ?? pool.publicExposure.publicBaseUrl)
: base === "public"
? pool.publicExposure.publicBaseUrl
: pool.publicExposure.masterBaseUrl;
async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string, target = codexPoolRuntimeTarget()): Promise<Record<string, unknown>> {
void pool;
if (target.publicBaseUrl === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure.enabled must be true before probing the public gateway`);
const baseUrl = target.publicBaseUrl;
const url = `${baseUrl.replace(/\/+$/u, "")}/v1/models`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
@@ -3967,7 +3976,7 @@ repo=${shQuote("platform-infra/sub2api-account-sentinel")}
tag=${shQuote(target.tag)}
base_image=${shQuote(target.baseImage)}
openai_version=${shQuote(sentinel.sdk.openaiPythonVersion)}
runtime_target=${shQuote(targetRuntime.id)}
base_image_cache_policy=${shQuote(targetRuntime.sentinelImageBuild.baseImageCachePolicy)}
work=/tmp/unidesk-sub2api-sentinel-image
mkdir -p "$work"
dockerfile_path="$work/sentinel.Dockerfile"
@@ -4023,11 +4032,11 @@ fi
base64 -d > "$dockerfile_path" <<'UNIDESK_SENTINEL_DOCKERFILE_B64'
${dockerfileB64}
UNIDESK_SENTINEL_DOCKERFILE_B64
export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000
export NO_PROXY=${shQuote(targetRuntime.sentinelImageBuild.noProxy)}
export no_proxy=$NO_PROXY
set -- --pull
base_image_source="registry"
if [ "$runtime_target" != "G14" ] && docker image inspect "$base_image" >/dev/null 2>&1; then
if [ "$base_image_cache_policy" = "local-if-present" ] && docker image inspect "$base_image" >/dev/null 2>&1; then
set --
base_image_source="local-cache"
fi