fix: move sub2api runtime to PK01 host docker
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user