feat: activate D601 sub2api external edge
This commit is contained in:
@@ -80,6 +80,10 @@ export interface CodexPoolSentinelManifestOptions {
|
||||
serviceName: string;
|
||||
serviceDns: string;
|
||||
appSecretName: string;
|
||||
proxy?: {
|
||||
httpProxy: string;
|
||||
noProxy: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function defaultCodexPoolSentinelConfig(): CodexPoolSentinelConfig {
|
||||
@@ -317,6 +321,25 @@ export function renderCodexPoolSentinelManifest(
|
||||
const activeDeadlineSeconds = Math.max(300, Math.min(3600, config.probe.timeoutSeconds + 240));
|
||||
const command = sentinelContainerShellCommand(config);
|
||||
const runtimeImage = codexPoolSentinelRuntimeImage(config).runtimeImage;
|
||||
const proxyEnv = options.proxy?.httpProxy
|
||||
? ` - name: HTTP_PROXY
|
||||
value: ${JSON.stringify(options.proxy.httpProxy)}
|
||||
- name: HTTPS_PROXY
|
||||
value: ${JSON.stringify(options.proxy.httpProxy)}
|
||||
- name: ALL_PROXY
|
||||
value: ${JSON.stringify(options.proxy.httpProxy)}
|
||||
- name: http_proxy
|
||||
value: ${JSON.stringify(options.proxy.httpProxy)}
|
||||
- name: https_proxy
|
||||
value: ${JSON.stringify(options.proxy.httpProxy)}
|
||||
- name: all_proxy
|
||||
value: ${JSON.stringify(options.proxy.httpProxy)}
|
||||
- name: NO_PROXY
|
||||
value: ${JSON.stringify(options.proxy.noProxy)}
|
||||
- name: no_proxy
|
||||
value: ${JSON.stringify(options.proxy.noProxy)}
|
||||
`
|
||||
: "";
|
||||
return `apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -446,6 +469,7 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
${proxyEnv}
|
||||
volumeMounts:
|
||||
- name: sentinel-config
|
||||
mountPath: /opt/sentinel
|
||||
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
|
||||
const g14K3sRoute = "G14:k3s";
|
||||
const defaultTargetId = "G14";
|
||||
const namespace = "platform-infra";
|
||||
const serviceName = "sub2api";
|
||||
const serviceDns = `${serviceName}.${namespace}.svc.cluster.local:8080`;
|
||||
const fieldManager = "unidesk-platform-infra";
|
||||
const appSecretName = "sub2api-secrets";
|
||||
const sub2apiConfigPath = rootPath("config", "platform-infra", "sub2api.yaml");
|
||||
const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");
|
||||
const sentinelImageDockerfilePath = rootPath("src", "components", "platform-infra", "sub2api", "sentinel.Dockerfile");
|
||||
const defaultPoolGroupName = "unidesk-codex-pool";
|
||||
@@ -37,6 +39,7 @@ const remoteJobPollMs = 5_000;
|
||||
interface DisclosureOptions {
|
||||
full: boolean;
|
||||
raw: boolean;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
interface SyncOptions extends DisclosureOptions {
|
||||
@@ -70,6 +73,21 @@ interface SentinelImageOptions extends DisclosureOptions {
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
interface CodexPoolRuntimeTarget {
|
||||
id: string;
|
||||
route: string;
|
||||
namespace: string;
|
||||
serviceName: string;
|
||||
serviceDns: string;
|
||||
appSecretName: string;
|
||||
egressProxy: {
|
||||
enabled: boolean;
|
||||
applyToSentinel: boolean;
|
||||
httpProxy: string;
|
||||
noProxy: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface CodexProfile {
|
||||
profile: string;
|
||||
accountName: string;
|
||||
@@ -204,14 +222,15 @@ export function codexPoolHelp(): unknown {
|
||||
output: "json, except trace and sentinel-report default to low-noise text tables",
|
||||
usage: [
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm [--prune-removed]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report [--events 20|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sync [--target D601] --confirm [--prune-removed]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--target D601] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool trace [--target D601] --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status [--target D601]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build [--target D601] --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe [--target D601] --account unidesk-codex-hy --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report [--target D601] [--events 20|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes [--target D601] --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
|
||||
],
|
||||
@@ -254,14 +273,14 @@ export async function runCodexPoolCommand(config: UniDeskConfig, args: string[])
|
||||
}
|
||||
|
||||
function parseSyncOptions(args: string[]): SyncOptions {
|
||||
validateOptions(args, new Set(["--confirm", "--prune-removed", "--full", "--raw"]));
|
||||
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm" && arg !== "--prune-removed"));
|
||||
validateOptions(args, new Set(["--confirm", "--prune-removed", "--full", "--raw", "--target"]));
|
||||
const disclosure = parseDisclosureOptions(stripBooleanOptions(args, new Set(["--confirm", "--prune-removed"])));
|
||||
return { ...disclosure, confirm: args.includes("--confirm"), pruneRemoved: args.includes("--prune-removed") };
|
||||
}
|
||||
|
||||
function parseConfirmOptions(args: string[]): ConfirmOptions {
|
||||
validateOptions(args, new Set(["--confirm", "--full", "--raw"]));
|
||||
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm"));
|
||||
validateOptions(args, new Set(["--confirm", "--full", "--raw", "--target"]));
|
||||
const disclosure = parseDisclosureOptions(stripBooleanOptions(args, new Set(["--confirm"])));
|
||||
return { ...disclosure, confirm: args.includes("--confirm") };
|
||||
}
|
||||
|
||||
@@ -271,7 +290,8 @@ function parseSentinelImageOptions(args: string[]): SentinelImageOptions {
|
||||
let confirm = false;
|
||||
let explicitDryRun = false;
|
||||
const disclosureArgs: string[] = [];
|
||||
for (const arg of rest) {
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const arg = rest[index]!;
|
||||
if (arg === "--confirm") {
|
||||
confirm = true;
|
||||
continue;
|
||||
@@ -280,8 +300,14 @@ function parseSentinelImageOptions(args: string[]): SentinelImageOptions {
|
||||
explicitDryRun = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--full" || arg === "--raw") {
|
||||
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
|
||||
disclosureArgs.push(arg);
|
||||
if (arg === "--target") {
|
||||
const value = rest[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
|
||||
disclosureArgs.push(value);
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported option: ${arg}`);
|
||||
@@ -306,8 +332,14 @@ function parseSentinelProbeOptions(args: string[]): SentinelProbeOptions {
|
||||
confirm = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--full" || arg === "--raw") {
|
||||
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
|
||||
disclosureArgs.push(arg);
|
||||
if (arg === "--target") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
|
||||
disclosureArgs.push(value);
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === "--account") {
|
||||
@@ -336,8 +368,14 @@ function parseSentinelReportOptions(args: string[]): SentinelReportOptions {
|
||||
const disclosureArgs: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index]!;
|
||||
if (arg === "--full" || arg === "--raw") {
|
||||
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
|
||||
disclosureArgs.push(arg);
|
||||
if (arg === "--target") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
|
||||
disclosureArgs.push(value);
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === "--events") {
|
||||
@@ -369,8 +407,14 @@ function parseTraceOptions(args: string[]): TraceOptions {
|
||||
const disclosureArgs: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index]!;
|
||||
if (arg === "--full" || arg === "--raw") {
|
||||
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
|
||||
disclosureArgs.push(arg);
|
||||
if (arg === "--target") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
|
||||
disclosureArgs.push(value);
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === "--show-lines") {
|
||||
@@ -452,9 +496,26 @@ function readReportEventLimit(raw: string, option: string): number {
|
||||
}
|
||||
|
||||
function parseDisclosureOptions(args: string[]): DisclosureOptions {
|
||||
validateOptions(args, new Set(["--full", "--raw"]));
|
||||
validateOptions(args, new Set(["--full", "--raw", "--target"]));
|
||||
const raw = args.includes("--raw");
|
||||
return { full: raw || args.includes("--full"), raw };
|
||||
return { full: raw || args.includes("--full"), raw, targetId: parseTargetId(args) };
|
||||
}
|
||||
|
||||
function parseTargetId(args: string[]): string {
|
||||
let targetId = defaultTargetId;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index]!;
|
||||
if (arg === "--target") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
|
||||
targetId = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length);
|
||||
}
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error("--target must be a simple target id");
|
||||
return targetId;
|
||||
}
|
||||
|
||||
function splitAccountNames(value: string): string[] {
|
||||
@@ -462,14 +523,68 @@ function splitAccountNames(value: string): string[] {
|
||||
}
|
||||
|
||||
function validateOptions(args: string[], booleanOptions: Set<string>): void {
|
||||
for (const arg of args) {
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index]!;
|
||||
if (arg === "--target") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--target=") && booleanOptions.has("--target")) continue;
|
||||
if (booleanOptions.has(arg)) continue;
|
||||
throw new Error(`unsupported option: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false }): Record<string, unknown> {
|
||||
function stripBooleanOptions(args: string[], stripped: Set<string>): string[] {
|
||||
return args.filter((arg) => !stripped.has(arg));
|
||||
}
|
||||
|
||||
function codexPoolRuntimeTarget(targetId: string = defaultTargetId): CodexPoolRuntimeTarget {
|
||||
const parsed = Bun.YAML.parse(readFileSync(sub2apiConfigPath, "utf8")) as unknown;
|
||||
if (!isRecord(parsed) || !Array.isArray(parsed.targets)) throw new Error(`${sub2apiConfigPath}.targets must be a list`);
|
||||
const raw = parsed.targets.find((item) => isRecord(item) && String(item.id ?? "").toLowerCase() === targetId.toLowerCase());
|
||||
if (!isRecord(raw)) throw new Error(`${sub2apiConfigPath}.targets does not contain target ${targetId}`);
|
||||
const id = stringValue(raw.id) ?? targetId;
|
||||
const route = stringValue(raw.route) ?? (id === defaultTargetId ? g14K3sRoute : "");
|
||||
const targetNamespace = stringValue(raw.namespace) ?? namespace;
|
||||
if (route.length === 0) throw new Error(`${sub2apiConfigPath}.targets[${id}].route is required`);
|
||||
validateKubernetesName(targetNamespace, `${sub2apiConfigPath}.targets[${id}].namespace`, true);
|
||||
|
||||
let egressProxy: CodexPoolRuntimeTarget["egressProxy"] = null;
|
||||
if (isRecord(raw.egressProxy) && raw.egressProxy.enabled === true) {
|
||||
const proxyServiceName = stringValue(raw.egressProxy.serviceName);
|
||||
const listenPort = numberValue(raw.egressProxy.listenPort);
|
||||
if (proxyServiceName === null || listenPort === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName/listenPort are required`);
|
||||
validateKubernetesName(proxyServiceName, `${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName`, true);
|
||||
if (!Number.isInteger(listenPort) || listenPort < 1 || listenPort > 65535) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.listenPort must be a TCP port`);
|
||||
const noProxyRaw = Array.isArray(raw.egressProxy.noProxy) ? raw.egressProxy.noProxy : [];
|
||||
const noProxy = noProxyRaw.map((entry) => stringValue(entry)).filter((entry): entry is string => entry !== null && entry.length > 0).join(",");
|
||||
egressProxy = {
|
||||
enabled: true,
|
||||
applyToSentinel: raw.egressProxy.applyToSentinel === undefined ? true : raw.egressProxy.applyToSentinel === true,
|
||||
httpProxy: `http://${proxyServiceName}.${targetNamespace}.svc.cluster.local:${listenPort}`,
|
||||
noProxy,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
route,
|
||||
namespace: targetNamespace,
|
||||
serviceName,
|
||||
serviceDns: `${serviceName}.${targetNamespace}.svc.cluster.local:8080`,
|
||||
appSecretName,
|
||||
egressProxy,
|
||||
};
|
||||
}
|
||||
|
||||
function targetFlag(target: CodexPoolRuntimeTarget): string {
|
||||
return target.id === defaultTargetId ? "" : ` --target ${target.id}`;
|
||||
}
|
||||
|
||||
function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false, targetId: defaultTargetId }): Record<string, unknown> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const profiles = collectCodexProfiles();
|
||||
const ok = profiles.length > 0 && profiles.every((profile) => profile.ok);
|
||||
return {
|
||||
@@ -481,7 +596,7 @@ function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false })
|
||||
authPattern: "YAML-selected auth files under ~/.codex",
|
||||
valuesPrinted: false,
|
||||
},
|
||||
target: poolTarget(),
|
||||
target: poolTarget(pool, runtimeTarget),
|
||||
config: {
|
||||
path: codexPoolConfigPath,
|
||||
pool: options.full ? pool : codexPoolConfigSummary(pool),
|
||||
@@ -490,9 +605,9 @@ function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false })
|
||||
decision: {
|
||||
accountType: "openai/apikey",
|
||||
grouping: `All discovered Codex profiles are bound to one Sub2API group named ${pool.groupName}.`,
|
||||
unifiedApiKey: `The client-facing API_KEY is controlled by k3s Secret ${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`,
|
||||
unifiedApiKey: `The client-facing API_KEY is controlled by k3s Secret ${runtimeTarget.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`,
|
||||
sentinel: pool.sentinel.monitor.enabled
|
||||
? `Account sentinel is enabled as k8s CronJob ${namespace}/${pool.sentinel.cronJobName}; actions.enabled=${pool.sentinel.actions.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 ${codexConsumerBaseUrl(pool)}; bounded master-local probes may use ${pool.publicExposure.masterBaseUrl}. FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.`
|
||||
@@ -501,13 +616,14 @@ function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false })
|
||||
configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.",
|
||||
},
|
||||
next: ok
|
||||
? { sync: "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm" }
|
||||
? { 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." },
|
||||
};
|
||||
}
|
||||
|
||||
async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promise<Record<string, unknown>> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const profiles = collectCodexProfiles();
|
||||
const planOk = profiles.length > 0 && profiles.every((profile) => profile.ok);
|
||||
if (!options.confirm || !planOk) {
|
||||
@@ -522,7 +638,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
|
||||
}
|
||||
|
||||
const sentinelImage = pool.sentinel.monitor.enabled
|
||||
? await runCodexPoolSentinelImage(config, pool, { action: "build", confirm: true, dryRun: false, full: options.full, raw: false })
|
||||
? await runCodexPoolSentinelImage(config, pool, { action: "build", confirm: true, dryRun: false, full: options.full, raw: false, targetId: options.targetId })
|
||||
: { ok: true, mode: "skipped-monitor-disabled" };
|
||||
if (sentinelImage.ok !== true) {
|
||||
return {
|
||||
@@ -531,7 +647,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
|
||||
mode: "blocked-sentinel-image",
|
||||
sentinelImage,
|
||||
next: {
|
||||
image: "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm",
|
||||
image: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build${targetFlag(runtimeTarget)} --confirm`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -540,10 +656,14 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
|
||||
pruneRemoved: options.pruneRemoved,
|
||||
sentinel: {
|
||||
manifest: renderCodexPoolSentinelManifest(pool.sentinel, sentinelProfileSecrets(profiles), {
|
||||
namespace,
|
||||
serviceName,
|
||||
serviceDns,
|
||||
appSecretName,
|
||||
namespace: runtimeTarget.namespace,
|
||||
serviceName: runtimeTarget.serviceName,
|
||||
serviceDns: runtimeTarget.serviceDns,
|
||||
appSecretName: runtimeTarget.appSecretName,
|
||||
proxy: runtimeTarget.egressProxy?.applyToSentinel ? {
|
||||
httpProxy: runtimeTarget.egressProxy.httpProxy,
|
||||
noProxy: runtimeTarget.egressProxy.noProxy,
|
||||
} : null,
|
||||
}),
|
||||
summary: codexPoolSentinelSummary(pool.sentinel),
|
||||
},
|
||||
@@ -589,7 +709,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
|
||||
tempUnschedulableCredentials: renderSub2ApiTempUnschedulableCredentials(profile.tempUnschedulable),
|
||||
})),
|
||||
};
|
||||
const result = await runRemoteCodexPoolScript(config, "sync", syncScript(payload, pool));
|
||||
const result = await runRemoteCodexPoolScript(config, "sync", syncScript(payload, pool, runtimeTarget), runtimeTarget);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -614,7 +734,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
|
||||
? compactCapture(result, { full: options.full || result.exitCode !== 0 })
|
||||
: options.full ? parsed : codexPoolSyncSummary(parsed),
|
||||
next: {
|
||||
validate: "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
|
||||
validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -625,6 +745,7 @@ async function codexPoolSentinelImage(config: UniDeskConfig, options: SentinelIm
|
||||
}
|
||||
|
||||
async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolConfig, options: SentinelImageOptions): Promise<Record<string, unknown>> {
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const target = codexPoolSentinelRuntimeImage(pool.sentinel);
|
||||
if (options.action === "build" && options.dryRun) {
|
||||
return {
|
||||
@@ -635,13 +756,13 @@ async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolC
|
||||
dockerfile: sentinelImageDockerfilePath,
|
||||
mutation: false,
|
||||
next: {
|
||||
confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm",
|
||||
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build${targetFlag(runtimeTarget)} --confirm`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const mode: RemoteCodexPoolMode = options.action === "status" ? "sentinel-image-status" : "sentinel-image-build";
|
||||
const script = options.action === "status" ? sentinelImageStatusScript(pool) : sentinelImageBuildScript(pool);
|
||||
const result = await runRemoteCodexPoolScript(config, mode, script);
|
||||
const script = options.action === "status" ? sentinelImageStatusScript(pool, runtimeTarget) : sentinelImageBuildScript(pool, runtimeTarget);
|
||||
const result = await runRemoteCodexPoolScript(config, mode, script, runtimeTarget);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -665,7 +786,8 @@ async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolC
|
||||
|
||||
async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const result = await runRemoteCodexPoolScript(config, "validate", validateScript(pool));
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await runRemoteCodexPoolScript(config, "validate", validateScript(pool, runtimeTarget), runtimeTarget);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -685,7 +807,8 @@ async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptio
|
||||
|
||||
async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const result = await capture(config, g14K3sRoute, ["script"], traceScript(pool, options));
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], traceScript(pool, options, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
||||
if (options.raw) {
|
||||
@@ -707,7 +830,8 @@ async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Pro
|
||||
|
||||
async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelReportOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const result = await capture(config, g14K3sRoute, ["script"], sentinelReportScript(pool, options.events));
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], sentinelReportScript(pool, options.events, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
||||
if (options.raw) {
|
||||
@@ -729,6 +853,7 @@ async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelR
|
||||
|
||||
async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelProbeOptions): Promise<Record<string, unknown>> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const configuredAccounts = desiredAccountNames(pool);
|
||||
const missing = options.accounts.filter((account) => !configuredAccounts.includes(account));
|
||||
if (missing.length > 0) {
|
||||
@@ -746,11 +871,11 @@ async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelPr
|
||||
ok: true,
|
||||
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
|
||||
mode: "dry-run",
|
||||
target: poolTarget(pool),
|
||||
target: poolTarget(pool, runtimeTarget),
|
||||
accounts: options.accounts,
|
||||
effect: "Would create one Kubernetes Job from the managed sentinel CronJob and force an immediate marker probe for the requested account(s).",
|
||||
next: {
|
||||
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account ${options.accounts.join(",")} --confirm`,
|
||||
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe${targetFlag(runtimeTarget)} --account ${options.accounts.join(",")} --confirm`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -758,7 +883,7 @@ async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelPr
|
||||
const payload = {
|
||||
accounts: options.accounts,
|
||||
};
|
||||
const result = await runRemoteCodexPoolScript(config, "sentinel-probe", sentinelProbeScript(payload, pool));
|
||||
const result = await runRemoteCodexPoolScript(config, "sentinel-probe", sentinelProbeScript(payload, pool, runtimeTarget), runtimeTarget);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -777,19 +902,20 @@ async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelPr
|
||||
}
|
||||
|
||||
async function codexPoolCleanupProbes(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
if (!options.confirm) {
|
||||
return {
|
||||
ok: true,
|
||||
action: "platform-infra-sub2api-codex-pool-cleanup-probes",
|
||||
mode: "dry-run",
|
||||
target: poolTarget(),
|
||||
target: poolTarget(readCodexPoolConfig(), runtimeTarget),
|
||||
scope: "Only deletes temporary resources whose names start with unidesk-probe-.",
|
||||
next: { confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --confirm" },
|
||||
next: { confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes${targetFlag(runtimeTarget)} --confirm` },
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const pool = readCodexPoolConfig();
|
||||
const result = await capture(config, g14K3sRoute, ["script"], cleanupProbesScript(pool));
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], cleanupProbesScript(pool, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -2485,16 +2611,17 @@ function codexPoolSyncSummary(parsed: Record<string, unknown> | null): Record<st
|
||||
};
|
||||
}
|
||||
|
||||
function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
|
||||
function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRuntimeTarget(defaultTargetId)): Record<string, unknown> {
|
||||
return {
|
||||
route: g14K3sRoute,
|
||||
namespace,
|
||||
id: target.id,
|
||||
route: target.route,
|
||||
namespace: target.namespace,
|
||||
service: serviceName,
|
||||
serviceDns,
|
||||
serviceDns: target.serviceDns,
|
||||
configPath: codexPoolConfigPath,
|
||||
groupName: pool.groupName,
|
||||
apiKeyName: pool.apiKeyName,
|
||||
apiKeySecret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
|
||||
apiKeySecret: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
|
||||
minOwnerConcurrency: pool.minOwnerConcurrency,
|
||||
minOwnerConcurrencySource: pool.minOwnerConcurrencySource,
|
||||
accountCapacityTotal: desiredAccountCapacityTotal(pool),
|
||||
@@ -2507,6 +2634,12 @@ function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
|
||||
cronJobName: pool.sentinel.cronJobName,
|
||||
stateConfigMapName: pool.sentinel.stateConfigMapName,
|
||||
},
|
||||
egressProxy: target.egressProxy === null ? null : {
|
||||
enabled: target.egressProxy.enabled,
|
||||
applyToSentinel: target.egressProxy.applyToSentinel,
|
||||
httpProxy: target.egressProxy.httpProxy,
|
||||
noProxy: target.egressProxy.noProxy,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
@@ -2902,10 +3035,10 @@ spec:
|
||||
`;
|
||||
}
|
||||
|
||||
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig): Promise<{ apiKey: string | null; error: string | null }> {
|
||||
const result = await capture(config, g14K3sRoute, ["script"], `
|
||||
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig, target = codexPoolRuntimeTarget(defaultTargetId)): Promise<{ apiKey: string | null; error: string | null }> {
|
||||
const result = await capture(config, target.route, ["script"], `
|
||||
set -u
|
||||
kubectl -n ${namespace} get secret ${pool.apiKeySecretName} -o json
|
||||
kubectl -n ${target.namespace} get secret ${pool.apiKeySecretName} -o json
|
||||
`);
|
||||
if (result.exitCode !== 0) {
|
||||
return { apiKey: null, error: `read pool API key secret failed: ${result.stderr.slice(-1000)}` };
|
||||
@@ -2913,7 +3046,7 @@ kubectl -n ${namespace} get secret ${pool.apiKeySecretName} -o json
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const data = isRecord(parsed?.data) ? parsed.data : null;
|
||||
const encoded = typeof data?.[pool.apiKeySecretKey] === "string" ? data[pool.apiKeySecretKey] : null;
|
||||
if (encoded === null) return { apiKey: null, error: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey} missing` };
|
||||
if (encoded === null) return { apiKey: null, error: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey} missing` };
|
||||
try {
|
||||
const apiKey = Buffer.from(encoded, "base64").toString("utf8");
|
||||
return apiKey.length > 0 ? { apiKey, error: null } : { apiKey: null, error: "decoded API key is empty" };
|
||||
@@ -3208,16 +3341,16 @@ export function codexPoolSentinelProbeConfigFingerprint(input: {
|
||||
}));
|
||||
}
|
||||
|
||||
function syncScript(payload: unknown, pool: CodexPoolConfig): string {
|
||||
function syncScript(payload: unknown, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
||||
return remotePythonScript("sync", encoded, pool);
|
||||
return remotePythonScript("sync", encoded, pool, target);
|
||||
}
|
||||
|
||||
function validateScript(pool: CodexPoolConfig): string {
|
||||
return remotePythonScript("validate", "", pool);
|
||||
function validateScript(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
return remotePythonScript("validate", "", pool, target);
|
||||
}
|
||||
|
||||
function traceScript(pool: CodexPoolConfig, options: TraceOptions): string {
|
||||
function traceScript(pool: CodexPoolConfig, options: TraceOptions, target: CodexPoolRuntimeTarget): string {
|
||||
const encoded = Buffer.from(JSON.stringify({
|
||||
requestId: options.requestId,
|
||||
since: options.since,
|
||||
@@ -3225,15 +3358,15 @@ function traceScript(pool: CodexPoolConfig, options: TraceOptions): string {
|
||||
contextSeconds: options.contextSeconds,
|
||||
showLines: options.showLines,
|
||||
}), "utf8").toString("base64");
|
||||
return remotePythonScript("trace", encoded, pool);
|
||||
return remotePythonScript("trace", encoded, pool, target);
|
||||
}
|
||||
|
||||
function sentinelProbeScript(payload: unknown, pool: CodexPoolConfig): string {
|
||||
function sentinelProbeScript(payload: unknown, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
||||
return remotePythonScript("sentinel-probe", encoded, pool);
|
||||
return remotePythonScript("sentinel-probe", encoded, pool, target);
|
||||
}
|
||||
|
||||
function sentinelReportScript(pool: CodexPoolConfig, events: number): string {
|
||||
function sentinelReportScript(pool: CodexPoolConfig, events: number, target: CodexPoolRuntimeTarget): string {
|
||||
const stateName = pool.sentinel.stateConfigMapName;
|
||||
const cronJobName = pool.sentinel.cronJobName;
|
||||
return `
|
||||
@@ -3243,7 +3376,7 @@ import json
|
||||
import subprocess
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
NAMESPACE = ${JSON.stringify(namespace)}
|
||||
NAMESPACE = ${JSON.stringify(target.namespace)}
|
||||
STATE_NAME = ${JSON.stringify(stateName)}
|
||||
CRONJOB_NAME = ${JSON.stringify(cronJobName)}
|
||||
EVENT_LIMIT = ${JSON.stringify(events)}
|
||||
@@ -3445,22 +3578,22 @@ PY
|
||||
`;
|
||||
}
|
||||
|
||||
function cleanupProbesScript(pool: CodexPoolConfig): string {
|
||||
return remotePythonScript("cleanup-probes", "", pool);
|
||||
function cleanupProbesScript(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
return remotePythonScript("cleanup-probes", "", pool, target);
|
||||
}
|
||||
|
||||
function sentinelImageStatusScript(pool: CodexPoolConfig): string {
|
||||
function sentinelImageStatusScript(pool: CodexPoolConfig, targetRuntime: CodexPoolRuntimeTarget): string {
|
||||
const target = codexPoolSentinelRuntimeImage(pool.sentinel);
|
||||
return remoteSentinelImageScript("status", target, pool.sentinel, null);
|
||||
return remoteSentinelImageScript("status", target, pool.sentinel, null, targetRuntime);
|
||||
}
|
||||
|
||||
function sentinelImageBuildScript(pool: CodexPoolConfig): string {
|
||||
function sentinelImageBuildScript(pool: CodexPoolConfig, targetRuntime: CodexPoolRuntimeTarget): string {
|
||||
const target = codexPoolSentinelRuntimeImage(pool.sentinel);
|
||||
const dockerfile = readFileSync(sentinelImageDockerfilePath, "utf8");
|
||||
return remoteSentinelImageScript("build", target, pool.sentinel, dockerfile);
|
||||
return remoteSentinelImageScript("build", target, pool.sentinel, dockerfile, targetRuntime);
|
||||
}
|
||||
|
||||
function remoteSentinelImageScript(mode: "status" | "build", target: ReturnType<typeof codexPoolSentinelRuntimeImage>, sentinel: CodexPoolSentinelConfig, dockerfile: string | null): string {
|
||||
function remoteSentinelImageScript(mode: "status" | "build", target: ReturnType<typeof codexPoolSentinelRuntimeImage>, sentinel: CodexPoolSentinelConfig, dockerfile: string | null, targetRuntime: CodexPoolRuntimeTarget): string {
|
||||
const dockerfileB64 = dockerfile === null ? "" : Buffer.from(dockerfile, "utf8").toString("base64");
|
||||
return `
|
||||
set -eu
|
||||
@@ -3470,6 +3603,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)}
|
||||
work=/tmp/unidesk-sub2api-sentinel-image
|
||||
mkdir -p "$work"
|
||||
dockerfile_path="$work/sentinel.Dockerfile"
|
||||
@@ -3527,7 +3661,13 @@ ${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=$NO_PROXY
|
||||
docker build --pull \\
|
||||
set -- --pull
|
||||
base_image_source="registry"
|
||||
if [ "$runtime_target" != "G14" ] && docker image inspect "$base_image" >/dev/null 2>&1; then
|
||||
set --
|
||||
base_image_source="local-cache"
|
||||
fi
|
||||
docker build "$@" \\
|
||||
--build-arg BASE_IMAGE="$base_image" \\
|
||||
--build-arg OPENAI_PYTHON_VERSION="$openai_version" \\
|
||||
--build-arg HTTP_PROXY= --build-arg HTTPS_PROXY= --build-arg http_proxy= --build-arg https_proxy= \\
|
||||
@@ -3546,6 +3686,7 @@ print(json.dumps({
|
||||
"image": "${target.runtimeImage}",
|
||||
"baseImage": "${target.baseImage}",
|
||||
"tag": "${target.tag}",
|
||||
"baseImageSource": "${"${base_image_source}"}",
|
||||
"digest": "${"${digest}"}" or None,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
@@ -3620,7 +3761,7 @@ function desiredAccountTempUnschedulableMap(pool: CodexPoolConfig): Record<strin
|
||||
return policies;
|
||||
}
|
||||
|
||||
function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig): string {
|
||||
function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
return `
|
||||
set -u
|
||||
python3 - <<'PY'
|
||||
@@ -3635,11 +3776,12 @@ import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from urllib.parse import quote
|
||||
|
||||
NAMESPACE = "${namespace}"
|
||||
SERVICE_NAME = "${serviceName}"
|
||||
SERVICE_DNS = "${serviceDns}"
|
||||
TARGET_ID = ${JSON.stringify(target.id)}
|
||||
NAMESPACE = ${JSON.stringify(target.namespace)}
|
||||
SERVICE_NAME = ${JSON.stringify(target.serviceName)}
|
||||
SERVICE_DNS = ${JSON.stringify(target.serviceDns)}
|
||||
FIELD_MANAGER = "${fieldManager}"
|
||||
APP_SECRET_NAME = "${appSecretName}"
|
||||
APP_SECRET_NAME = ${JSON.stringify(target.appSecretName)}
|
||||
POOL_GROUP_NAME = "${pool.groupName}"
|
||||
POOL_API_KEY_NAME = "${pool.apiKeyName}"
|
||||
POOL_API_KEY_SECRET_NAME = "${pool.apiKeySecretName}"
|
||||
@@ -6099,17 +6241,17 @@ async function capture(config: UniDeskConfig, target: string, args: string[], in
|
||||
|
||||
type RemoteCodexPoolMode = "sync" | "validate" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build";
|
||||
|
||||
async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string): Promise<SshCaptureResult> {
|
||||
async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget(defaultTargetId)): Promise<SshCaptureResult> {
|
||||
const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const startedAtMs = Date.now();
|
||||
const start = await capture(config, g14K3sRoute, ["script"], remoteJobStartScript(jobName, script));
|
||||
const start = await capture(config, target.route, ["script"], remoteJobStartScript(jobName, script));
|
||||
const started = parseJsonOutput(start.stdout);
|
||||
if (start.exitCode !== 0 || boolField(started, "ok", false) !== true) return start;
|
||||
|
||||
let latest: RemoteCodexPoolJobStatus | null = null;
|
||||
while (Date.now() - startedAtMs <= remoteJobTimeoutMs) {
|
||||
await sleep(remoteJobPollMs);
|
||||
const probe = await capture(config, g14K3sRoute, ["script"], remoteJobStatusScript(jobName));
|
||||
const probe = await capture(config, target.route, ["script"], remoteJobStatusScript(jobName));
|
||||
const parsed = parseJsonOutput(probe.stdout);
|
||||
latest = normalizeRemoteJobStatus(parsed);
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
|
||||
+1985
-60
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user