fix: move sub2api runtime to PK01 host docker

This commit is contained in:
Codex
2026-06-28 14:24:08 +00:00
parent 1d07df3df1
commit d225fd1a61
18 changed files with 1088 additions and 186 deletions
@@ -27,7 +27,7 @@ import { desiredAccountNames } from "./accounts";
import { collectCodexProfiles, readCodexPoolConfig } from "./config";
import { codexPoolSentinelProbeConfigFingerprint, fingerprint } from "./config-utils";
import { apiKeyPreview, codexConsumerBaseUrl, fetchPoolApiKey, probePublicModels, validatePublicGatewayWithKey, writeLocalCodexConfig } from "./local-codex";
import { manualBindingSourcePlan, poolTarget, prepareTargetPublicExposureSecret, resolvedManualAccountProtections, secretMaterialSummary, sentinelProfileSecrets, targetPublicExposureApplyScript, targetPublicExposureSummary } from "./public-exposure";
import { manualBindingSourcePlan, poolTarget, prepareTargetPublicExposureSecret, resolvedManualAccountProtections, secretMaterialSummary, sentinelProfileSecrets, targetFrpPublicExposure, targetPublicExposureApplyScript, targetPublicExposureSummary } from "./public-exposure";
import { codexPoolConfigSummary, compactProfile, compactSentinelProbeResult, redactProfile, renderSub2ApiTempUnschedulableCredentials } from "./redaction";
import { boolField, capture, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
import { cleanupProbesScript, sentinelImageBuildScript, sentinelImageStatusScript, sentinelProbeScript, sentinelReportScript, syncScript, traceScript, validateScript } from "./remote-scripts";
@@ -74,7 +74,13 @@ export function codexPoolPlan(options?: DisclosureOptions): Record<string, unkno
: `${pool.manualAccounts.protected.length} manual Sub2API account(s) are protected from UniDesk-managed credentials, prune, sentinel probe, and sentinel freeze paths; only explicitly declared proxy/group bindings are reconciled.`,
},
next: ok
? { sync: `bun scripts/cli.ts platform-infra sub2api codex-pool sync${targetFlag(runtimeTarget)} --confirm` }
? runtimeTarget.runtimeMode === "host-docker"
? {
expose: `bun scripts/cli.ts platform-infra sub2api codex-pool expose${targetFlag(runtimeTarget)} --confirm`,
validate: `bun scripts/cli.ts platform-infra sub2api validate${targetFlag(runtimeTarget)}`,
note: "PK01 host-Docker target does not run the k3s codex-pool sync path.",
}
: { sync: `bun scripts/cli.ts platform-infra sub2api codex-pool sync${targetFlag(runtimeTarget)} --confirm` }
: { fix: "Ensure every discovered config.toml profile has a base_url and either auth.json OPENAI_API_KEY or the configured env_key present in this shell." },
};
}
@@ -84,6 +90,24 @@ export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions)
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const profiles = collectCodexProfiles();
const planOk = profiles.length > 0 && profiles.every((profile) => profile.ok);
if (runtimeTarget.runtimeMode === "host-docker" && options.confirm) {
return {
ok: false,
action: "platform-infra-sub2api-codex-pool-sync",
mode: "blocked-host-docker-sync-unsupported",
target: poolTarget(pool, runtimeTarget),
reason: "PK01 host-Docker target does not run the k3s codex-pool sync path; Sub2API runtime is controlled by platform-infra sub2api apply/validate.",
local: {
profileCount: profiles.length,
invalidProfiles: profiles.filter((profile) => !profile.ok).map(compactProfile),
valuesPrinted: false,
},
next: {
expose: `bun scripts/cli.ts platform-infra sub2api codex-pool expose${targetFlag(runtimeTarget)} --confirm`,
validate: `bun scripts/cli.ts platform-infra sub2api validate${targetFlag(runtimeTarget)}`,
},
};
}
if (!options.confirm || !planOk) {
return {
...codexPoolPlan(options),
@@ -454,8 +478,24 @@ export async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOpt
},
};
}
if (runtimeTarget.publicExposure.mode === "pk01-local") {
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, runtimeTarget);
const ok = publicProbe.httpStatus === 401;
return {
ok,
action: "platform-infra-sub2api-codex-pool-expose",
mode: "pk01-local-managed-by-sub2api-apply",
publicExposure: targetPublicExposureSummary(runtimeTarget),
publicProbe,
next: {
apply: `bun scripts/cli.ts platform-infra sub2api apply${targetFlag(runtimeTarget)} --confirm --wait`,
validate: `bun scripts/cli.ts platform-infra sub2api validate${targetFlag(runtimeTarget)}`,
configureLocal: `bun scripts/cli.ts platform-infra sub2api codex-pool configure-local${targetFlag(runtimeTarget)} --confirm`,
},
};
}
const secretMaterial = prepareTargetPublicExposureSecret(runtimeTarget);
const caddyResult = await applyPk01CaddyBlock(config, serviceName, runtimeTarget.publicExposure);
const caddyResult = await applyPk01CaddyBlock(config, serviceName, targetFrpPublicExposure(runtimeTarget));
const remoteResult = await capture(config, runtimeTarget.route, ["sh"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial));
const parsed = parseJsonOutput(remoteResult.stdout);
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, runtimeTarget);
@@ -284,9 +284,20 @@ export function readManualBindingSources(value: unknown, key: string): CodexPool
kind: readManualBindingKind(rawSource.kind, `${path}.kind`),
provider: readManualBindingProvider(rawSource.provider, `${path}.provider`),
description: readManualAccountReason(rawSource.description, `${path}.description`),
fixedProxy: null,
};
if (source.kind === "proxy" && source.provider !== "target-egress-proxy") {
throw new Error(`${codexPoolConfigPath}.${path}.provider must be target-egress-proxy for kind=proxy`);
if (source.kind === "proxy" && source.provider === "fixed-http-proxy") {
const fixedProxy = requiredRecordConfigField(rawSource, "fixedProxy", path);
const protocol = requiredStringConfigField(fixedProxy, "protocol", `${path}.fixedProxy`);
if (protocol !== "http") throw new Error(`${codexPoolConfigPath}.${path}.fixedProxy.protocol must be http`);
const host = requiredStringConfigField(fixedProxy, "host", `${path}.fixedProxy`);
if (!/^[A-Za-z0-9._:-]+$/u.test(host)) throw new Error(`${codexPoolConfigPath}.${path}.fixedProxy.host has an unsupported format`);
const port = integerConfigField(fixedProxy, "port", `${path}.fixedProxy`);
validatePort(port, `${path}.fixedProxy.port`);
source.fixedProxy = { protocol, host, port };
}
if (source.kind === "proxy" && source.provider !== "target-egress-proxy" && source.provider !== "fixed-http-proxy") {
throw new Error(`${codexPoolConfigPath}.${path}.provider must be target-egress-proxy or fixed-http-proxy for kind=proxy`);
}
if (source.kind === "group" && source.provider !== "pool-group") {
throw new Error(`${codexPoolConfigPath}.${path}.provider must be pool-group for kind=group`);
@@ -343,7 +354,7 @@ export function readManualBindingKind(value: unknown, key: string): CodexPoolMan
export function readManualBindingProvider(value: unknown, key: string): CodexPoolManualBindingProvider {
const text = stringValue(value);
if (text !== "target-egress-proxy" && text !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key} must be target-egress-proxy or pool-group`);
if (text !== "target-egress-proxy" && text !== "fixed-http-proxy" && text !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key} must be target-egress-proxy, fixed-http-proxy, or pool-group`);
return text;
}
@@ -33,6 +33,7 @@ export function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRunti
id: target.id,
route: target.route,
namespace: target.namespace,
runtimeMode: target.runtimeMode,
service: serviceName,
serviceDns: target.serviceDns,
publicBaseUrl: target.publicBaseUrl,
@@ -102,6 +103,9 @@ export function resolveManualProxyBinding(binding: CodexPoolManualAccountProxyBi
if (binding.enabled && source.provider === "target-egress-proxy" && (target.egressProxy === null || target.egressProxy.enabled !== true)) {
throw new Error(`${codexPoolConfigPath}.manualAccounts.bindingSources.${source.id} requires ${sub2apiConfigPath}.targets[${target.id}].egressProxy.enabled=true`);
}
if (binding.enabled && source.provider === "fixed-http-proxy" && source.fixedProxy === null) {
throw new Error(`${codexPoolConfigPath}.manualAccounts.bindingSources.${source.id} requires fixedProxy`);
}
return {
enabled: binding.enabled,
source: source.id,
@@ -118,6 +122,7 @@ export function resolveManualProxyBinding(binding: CodexPoolManualAccountProxyBi
noProxy: target.egressProxy.noProxy,
}
: null,
fixedProxy: source.provider === "fixed-http-proxy" ? source.fixedProxy : null,
},
};
}
@@ -149,6 +154,7 @@ export function manualBindingSourcePlan(source: CodexPoolManualBindingSource): R
kind: source.kind,
provider: source.provider,
description: source.description,
fixedProxy: source.fixedProxy,
};
}
@@ -162,14 +168,17 @@ export function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Rec
id: target.id,
route: target.route,
namespace: target.namespace,
runtimeMode: target.runtimeMode,
serviceDns: target.serviceDns,
},
urls: {
publicBaseUrl: exposure.publicBaseUrl,
consumerBaseUrl: target.publicBaseUrl === null ? null : `${target.publicBaseUrl.replace(/\/+$/u, "")}/`,
},
mode: exposure.mode,
dns: exposure.dns,
frpc: {
local: exposure.local,
frpc: exposure.frpc === null ? null : {
deploymentName: exposure.frpc.deploymentName,
secretName: exposure.frpc.secretName,
secretKey: exposure.frpc.secretKey,
@@ -188,16 +197,31 @@ export function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Rec
};
}
export function targetFrpPublicExposure(target: CodexPoolRuntimeTarget): PublicServiceExposure {
const exposure = target.publicExposure;
if (exposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`);
if (exposure.mode !== "frp" || exposure.frpc === null) {
throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure.frpc is required when using codex-pool expose frp operations`);
}
return {
enabled: exposure.enabled,
publicBaseUrl: exposure.publicBaseUrl,
dns: exposure.dns,
frpc: exposure.frpc,
pk01: exposure.pk01,
};
}
export function prepareTargetPublicExposureSecret(target: CodexPoolRuntimeTarget): ReturnType<typeof prepareFrpcSecret> {
if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`);
const exposure = targetFrpPublicExposure(target);
return prepareFrpcSecret({
secretRoot: target.secretsRoot,
exposure: target.publicExposure,
exposure,
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`);
if (!existsSync(sourcePath)) throw new Error(`publicExposure requires ${redactRepoPath(sourcePath)} with ${exposure.frpc.tokenSourceKey}; materialize the PK01 frps token source first`);
return readTextFile(sourcePath);
},
});
@@ -215,13 +239,13 @@ export function secretMaterialSummary(secret: ReturnType<typeof prepareFrpcSecre
}
export function targetPublicExposureApplyScript(target: CodexPoolRuntimeTarget, secret: ReturnType<typeof prepareFrpcSecret>): string {
if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`);
const exposure = targetFrpPublicExposure(target);
const manifest = renderFrpcManifest({
id: target.id,
route: target.route,
namespace: target.namespace,
replicas: 1,
publicExposure: target.publicExposure,
publicExposure: exposure,
} satisfies PublicServiceTarget);
const manifestB64 = Buffer.from(manifest, "utf8").toString("base64");
const frpcTomlB64 = Buffer.from(secret.frpcToml, "utf8").toString("base64");
@@ -263,7 +287,7 @@ 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/${target.publicExposure.frpc.deploymentName} --timeout=60s >"$rollout_out" 2>"$rollout_err"
kubectl -n ${target.namespace} rollout status deployment/${exposure.frpc.deploymentName} --timeout=60s >"$rollout_out" 2>"$rollout_err"
rollout_rc=$?
else
printf '%s\\n' 'skipped because apply failed' >"$rollout_err"
@@ -274,9 +298,9 @@ else
: >"$rollout_out"
printf '%s\\n' 'skipped because namespace or secret step failed' >"$rollout_err"
fi
kubectl -n ${target.namespace} get pods -l app.kubernetes.io/name=${target.publicExposure.frpc.deploymentName} -o json >"$pods_json" 2>"$pods_err"
kubectl -n ${target.namespace} get pods -l app.kubernetes.io/name=${exposure.frpc.deploymentName} -o json >"$pods_json" 2>"$pods_err"
pods_rc=$?
kubectl -n ${target.namespace} logs deployment/${target.publicExposure.frpc.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err"
kubectl -n ${target.namespace} logs deployment/${exposure.frpc.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err"
logs_rc=$?
python3 - "$tmp" "$ns_rc" "$secret_rc" "$apply_rc" "$rollout_rc" "$pods_rc" "$logs_rc" <<'PY'
import json
@@ -320,17 +344,17 @@ payload = {
"namespace": "${target.namespace}",
"source": "${sub2apiConfigPath}.targets[${target.id}].publicExposure",
"proxy": {
"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)},
"name": "${exposure.frpc.proxyName}",
"remotePort": ${JSON.stringify(exposure.frpc.remotePort)},
"publicBaseUrl": "${exposure.publicBaseUrl}",
"localIP": "${exposure.frpc.localIP}",
"localPort": ${JSON.stringify(exposure.frpc.localPort)},
},
"resources": {
"secret": "${secret.secretName}",
"secretKey": "${secret.secretKey}",
"deployment": "${target.publicExposure.frpc.deploymentName}",
"image": "${target.publicExposure.frpc.image}",
"deployment": "${exposure.frpc.deploymentName}",
"image": "${exposure.frpc.image}",
},
"steps": {
"namespace": {"exitCode": ns_rc, "stdout": text("ns.out"), "stderr": text("ns.err")},
@@ -436,8 +436,32 @@ def desired_manual_proxy_payload(protection):
if not isinstance(binding, dict) or binding.get("enabled") is not True:
return None
plan = manual_binding_plan(binding, "proxy")
if plan.get("provider") != "target-egress-proxy":
if plan.get("provider") not in ("target-egress-proxy", "fixed-http-proxy"):
raise RuntimeError(f"manual account proxyBinding source {plan.get('id')} uses unsupported provider {plan.get('provider')}")
if plan.get("provider") == "fixed-http-proxy":
fixed_proxy = plan.get("fixedProxy")
if not isinstance(fixed_proxy, dict):
raise RuntimeError(f"manual account proxyBinding source {plan.get('id')} requires fixedProxy")
proxy_name = binding.get("proxyName")
protocol = fixed_proxy.get("protocol")
host = fixed_proxy.get("host")
port = fixed_proxy.get("port")
if not isinstance(proxy_name, str) or not proxy_name:
raise RuntimeError("manual account proxyBinding proxyName is missing")
if protocol != "http":
raise RuntimeError("fixed proxy protocol must be http")
if not isinstance(host, str) or not host:
raise RuntimeError("fixed proxy host is missing")
if not isinstance(port, int) or port <= 0:
raise RuntimeError("fixed proxy port is missing")
return {
"name": proxy_name,
"protocol": protocol,
"host": host,
"port": port,
"fallback_mode": "none",
"expiry_warn_days": 0,
}
target_proxy = plan.get("targetEgressProxy")
if not isinstance(target_proxy, dict):
raise RuntimeError(f"manual account proxyBinding source {plan.get('id')} requires an enabled target egressProxy")
@@ -22,7 +22,7 @@ import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "..
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { Sub2ApiRuntimeConfig } from "./options";
import type { CodexPoolRuntimeTarget } from "./types";
import type { CodexPoolRuntimePublicExposure, CodexPoolRuntimeTarget } from "./types";
import { validatePort, validateProxyName } from "./config";
import { booleanValue, isRecord, normalizeBaseUrl, numberValue, stringValue, validateKubernetesName } from "./config-utils";
import { serviceName, sub2apiConfigPath } from "./types";
@@ -66,11 +66,14 @@ export function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarge
if (!isRecord(raw)) throw new Error(`${sub2apiConfigPath}.targets does not contain target ${targetId}`);
const id = stringValue(raw.id) ?? resolvedTargetId;
const route = stringValue(raw.route) ?? "";
const runtimeMode = stringValue(raw.runtimeMode) ?? "k3s";
if (runtimeMode !== "k3s" && runtimeMode !== "host-docker") throw new Error(`${sub2apiConfigPath}.targets[${id}].runtimeMode must be k3s or host-docker`);
const targetNamespace = stringValue(raw.namespace);
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);
const sentinelEnabled = runtimeConfig.sentinelEnabledOnTargets.some((entry) => entry.toLowerCase() === id.toLowerCase());
const sentinelImageBuild = readTargetSentinelImageBuild(raw, id, sentinelEnabled);
let egressProxy: CodexPoolRuntimeTarget["egressProxy"] = null;
if (isRecord(raw.egressProxy) && raw.egressProxy.enabled === true) {
@@ -94,26 +97,33 @@ export function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarge
let publicBaseUrl: string | null = null;
const publicExposure = readTargetPublicExposure(raw, id);
if (publicExposure !== null && publicExposure.enabled) publicBaseUrl = publicExposure.publicBaseUrl;
const hostDocker = runtimeMode === "host-docker" && isRecord(raw.hostDocker) ? raw.hostDocker : null;
const hostDockerAppPort = hostDocker === null ? null : numberValue(hostDocker.appPort);
if (runtimeMode === "host-docker" && (hostDockerAppPort === null || !Number.isInteger(hostDockerAppPort) || hostDockerAppPort < 1 || hostDockerAppPort > 65535)) {
throw new Error(`${sub2apiConfigPath}.targets[${id}].hostDocker.appPort must be an integer TCP port when runtimeMode=host-docker`);
}
return {
id,
route,
namespace: targetNamespace,
runtimeMode,
serviceName,
serviceDns: `${serviceName}.${targetNamespace}.svc.cluster.local:8080`,
serviceDns: runtimeMode === "host-docker" ? `host-docker:127.0.0.1:${hostDockerAppPort}` : `${serviceName}.${targetNamespace}.svc.cluster.local:8080`,
publicBaseUrl,
publicExposure,
appSecretName: runtimeConfig.appSecretName,
secretsRoot: runtimeConfig.secretsRoot,
sentinelEnabled: runtimeConfig.sentinelEnabledOnTargets.some((entry) => entry.toLowerCase() === id.toLowerCase()),
sentinelEnabled,
sentinelImageBuild,
egressProxy,
};
}
export function readTargetSentinelImageBuild(raw: Record<string, unknown>, targetId: string): CodexPoolRuntimeTarget["sentinelImageBuild"] {
export function readTargetSentinelImageBuild(raw: Record<string, unknown>, targetId: string, required = true): CodexPoolRuntimeTarget["sentinelImageBuild"] {
const codexPool = isRecord(raw.codexPool) ? raw.codexPool : null;
const imageBuild = codexPool !== null && isRecord(codexPool.sentinelImageBuild) ? codexPool.sentinelImageBuild : null;
if (imageBuild === null && !required) return { baseImageCachePolicy: "pull", noProxy: [] };
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") {
@@ -125,26 +135,36 @@ export function readTargetSentinelImageBuild(raw: Record<string, unknown>, targe
};
}
export function readTargetPublicExposure(raw: Record<string, unknown>, targetId: string): PublicServiceExposure | null {
export function readTargetPublicExposure(raw: Record<string, unknown>, targetId: string): CodexPoolRuntimePublicExposure | 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 mode = stringValue(exposure.mode) ?? "frp";
if (mode !== "frp" && mode !== "pk01-local") throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure.mode must be frp or pk01-local`);
const dns = readRequiredTargetRecord(exposure.dns, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns`);
const frpc = readRequiredTargetRecord(exposure.frpc, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc`);
const local = exposure.local === undefined || exposure.local === null ? null : readRequiredTargetRecord(exposure.local, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local`);
const frpc = exposure.frpc === undefined || exposure.frpc === null ? null : readRequiredTargetRecord(exposure.frpc, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc`);
if (mode === "frp" && frpc === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc is required when mode=frp`);
if (mode === "pk01-local" && local === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure.local is required when mode=pk01-local`);
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 = {
const config: CodexPoolRuntimePublicExposure = {
enabled,
mode,
publicBaseUrl,
dns: {
hostname,
expectedA: readRequiredTargetString(dns.expectedA, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.expectedA`),
resolvers: readTargetStringArray(dns.resolvers, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.resolvers`),
},
frpc: {
local: local === null ? null : {
upstreamHost: readRequiredTargetString(local.upstreamHost, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local.upstreamHost`),
upstreamPort: readTargetPort(local.upstreamPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local.upstreamPort`),
},
frpc: frpc === null ? null : {
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`),
@@ -165,12 +185,15 @@ export function readTargetPublicExposure(raw: Record<string, unknown>, targetId:
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`);
if (config.frpc !== null) {
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`);
}
if (config.local !== null) validatePort(config.local.upstreamPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local.upstreamPort`);
return config;
}
@@ -81,10 +81,11 @@ export interface CodexPoolRuntimeTarget {
id: string;
route: string;
namespace: string;
runtimeMode: "k3s" | "host-docker";
serviceName: string;
serviceDns: string;
publicBaseUrl: string | null;
publicExposure: PublicServiceExposure | null;
publicExposure: CodexPoolRuntimePublicExposure | null;
appSecretName: string;
secretsRoot: string;
sentinelEnabled: boolean;
@@ -102,6 +103,19 @@ export interface CodexPoolRuntimeTarget {
} | null;
}
export interface CodexPoolRuntimePublicExposure {
enabled: boolean;
mode: "frp" | "pk01-local";
publicBaseUrl: string;
dns: { hostname: string; expectedA: string; resolvers: string[] };
local: {
upstreamHost: string;
upstreamPort: number;
} | null;
frpc: PublicServiceExposure["frpc"] | null;
pk01: PublicServiceExposure["pk01"];
}
export interface CodexProfile {
profile: string;
accountName: string;
@@ -177,7 +191,13 @@ export interface CodexPoolManualAccountsConfig {
export type CodexPoolManualBindingKind = "proxy" | "group";
export type CodexPoolManualBindingProvider = "target-egress-proxy" | "pool-group";
export type CodexPoolManualBindingProvider = "target-egress-proxy" | "fixed-http-proxy" | "pool-group";
export interface CodexPoolManualFixedProxyConfig {
protocol: "http";
host: string;
port: number;
}
export interface CodexPoolManualBindingSource {
id: string;
@@ -185,6 +205,7 @@ export interface CodexPoolManualBindingSource {
kind: CodexPoolManualBindingKind;
provider: CodexPoolManualBindingProvider;
description: string | null;
fixedProxy: CodexPoolManualFixedProxyConfig | null;
}
export interface CodexPoolManualBindingSourcesConfig {