feat: add platform infra sub2api deployment

This commit is contained in:
Codex
2026-06-09 02:24:17 +00:00
parent 8c413aa557
commit 80ef698498
6 changed files with 1220 additions and 0 deletions
+5
View File
@@ -2,6 +2,11 @@
UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文件是项目顶级索引,也承担 `scripts/cli.ts` 的 CLI 使用说明入口。
## P0 最高优先级:自有配置 YAML 优先规则
- P0: UniDesk 自有配置一律优先使用 YAML(`.yaml`/`.yml`),包括 `config/` 下的运行面、平台基础设施、节点/lane、部署参数和可调版本配置;除非外部工具硬性要求 JSON/TOML/ENV 等格式,禁止新增 JSON 作为 UniDesk 自有配置真相。
- P0: 需要代码读取的 YAML 配置必须显式校验 schema、字段类型和必填项;禁止静默 fallback、宽松猜测或把配置藏进脚本常量,后续版本、镜像、namespace、endpoint 等可调项必须从 YAML 配置进入受控 CLI。
## P0 最高优先级:CaseRun 无服务与单步调试规则
- P0: CaseRun、case registry 产物整理、trace 语义化、harness 诊断、短连接 CLI 和本地/目标 host 上可直接运行的 runner 调试,默认是无服务工作流;只要不需要变更 cloud-api、web、gateway、GitOps、k3s runtime 或其他常驻服务,就必须直接无服务运行和验证,禁止为了运行 CaseRun 触发 CI/CD、rollout 或服务发布。
+4
View File
@@ -0,0 +1,4 @@
image:
repository: weishaw/sub2api
tag: 0.1.135
pullPolicy: IfNotPresent
+9
View File
@@ -29,6 +29,7 @@ import { runHwlabG14Command } from "./src/hwlab-g14";
import { runHwlabNodeCommand } from "./src/hwlab-node";
import { runGcCommand } from "./src/gc";
import { runAgentRunCommand } from "./src/agentrun";
import { runPlatformInfraCommand } from "./src/platform-infra";
const remoteOptions = extractRemoteCliOptions(process.argv.slice(2));
const args = remoteOptions.args;
@@ -320,6 +321,14 @@ async function main(): Promise<void> {
return;
}
if (top === "platform-infra") {
const result = await runPlatformInfraCommand(readConfig(), args.slice(1));
const ok = (result as { ok?: unknown }).ok !== false;
emitJson(commandName, result, ok);
if (!ok) process.exitCode = 1;
return;
}
const config = readConfig();
const autoRemoteCiPublishPlan = autoRemoteCiPublishUserServiceDryRunPlan(config, args);
if (autoRemoteCiPublishPlan.enabled && autoRemoteCiPublishPlan.host !== null) {
+3
View File
@@ -4,6 +4,7 @@ import { hwlabHelp } from "./hwlab-cd";
import { hwlabG14Help } from "./hwlab-g14";
import { hwlabNodeHelp } from "./hwlab-node";
import { agentRunHelp } from "./agentrun";
import { platformInfraHelp } from "./platform-infra";
export function rootHelp(): unknown {
return {
@@ -62,6 +63,7 @@ export function rootHelp(): unknown {
{ command: "hwlab nodes control-plane|git-mirror|secret --node G14 --lane v03", description: "Manage HWLAB node/lane runtime prerequisites for v0.3+ with the node identity passed as data instead of a command family." },
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync|flush | hwlab g14 tools-image status|build", description: "Start the legacy G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror/relay maintenance, or fixed HWLAB CI tools image actions; long confirmed trigger/sync/flush actions return async jobs by default." },
{ command: "agentrun v01 queue|sessions|control-plane|git-mirror", description: "Use AgentRun v0.1 Queue and Sessions as the active commander entry through the official G14 CLI bridge, plus bounded Tekton/Argo and git-mirror operations." },
{ command: "platform-infra sub2api plan|apply|status|validate", description: "Deploy and validate the G14 k3s internal-only Sub2API service in the platform-infra namespace through the controlled UniDesk CLI." },
{ command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Legacy D601 HWLAB DEV CD wrapper kept for explicit old-path diagnostics; current HWLAB rollout uses G14 GitOps." },
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
@@ -615,6 +617,7 @@ export function staticNamespaceHelp(args: string[]): unknown | null {
if (top === "auth-broker") return authBrokerHelp();
if (top === "gh") return ghHelp();
if (top === "agentrun") return agentRunHelp();
if (top === "platform-infra") return platformInfraHelp();
if (top === "hwlab" && (sub === "node" || sub === "nodes")) return hwlabNodeHelp();
if (top === "hwlab" && sub === "g14") return hwlabG14Help();
if (top === "hwlab") return hwlabHelp();
+787
View File
@@ -0,0 +1,787 @@
import { readFileSync } from "node:fs";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import { startJob } from "./jobs";
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
const g14K3sRoute = "G14:k3s";
const namespace = "platform-infra";
const serviceName = "sub2api";
const fieldManager = "unidesk-platform-infra";
const manifestPath = rootPath("src", "components", "platform-infra", "sub2api", "sub2api.k8s.yaml");
const configPath = rootPath("config", "platform-infra", "sub2api.yaml");
const secretName = "sub2api-secrets";
const requiredSecretKeys = ["POSTGRES_PASSWORD", "ADMIN_PASSWORD", "JWT_SECRET", "TOTP_ENCRYPTION_KEY"] as const;
interface Sub2ApiConfig {
image: {
repository: string;
tag: string;
pullPolicy: "Always" | "IfNotPresent" | "Never";
};
}
export function platformInfraHelp(): unknown {
return {
command: "platform-infra sub2api plan|apply|status|validate",
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2api plan",
"bun scripts/cli.ts platform-infra sub2api apply --dry-run",
"bun scripts/cli.ts platform-infra sub2api apply --confirm",
"bun scripts/cli.ts platform-infra sub2api status [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api validate [--full|--raw]",
],
description: "Operate the G14 k3s internal-only Sub2API deployment in the shared platform-infra namespace. This entry creates no Ingress, NodePort, LoadBalancer, hostPort, hostNetwork, ResourceQuota, LimitRange, or CPU/memory resource requests/limits.",
target: {
route: g14K3sRoute,
namespace,
service: serviceName,
serviceDns: `${serviceName}.${namespace}.svc.cluster.local:8080`,
exposure: "k3s-cluster-internal-only",
resourceLimits: "unset-by-policy",
versionConfigPath: configPath,
},
};
}
export async function runPlatformInfraCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
const [target, action] = args;
if (target !== "sub2api") return unsupported(args);
if (action === "plan" || action === undefined) return plan();
if (action === "apply") return await apply(config, parseApplyOptions(args.slice(2)));
if (action === "status") return await status(config, parseDisclosureOptions(args.slice(2)));
if (action === "validate") return await validate(config, parseDisclosureOptions(args.slice(2)));
return unsupported(args);
}
interface ApplyOptions {
dryRun: boolean;
confirm: boolean;
wait: boolean;
}
interface DisclosureOptions {
full: boolean;
raw: boolean;
}
interface PolicyCheck {
name: string;
ok: boolean;
detail: string;
}
function unsupported(args: string[]): Record<string, unknown> {
return {
ok: false,
error: "unsupported-platform-infra-command",
args,
help: platformInfraHelp(),
};
}
function parseApplyOptions(args: string[]): ApplyOptions {
validateOptions(args, new Set(["--dry-run", "--confirm", "--wait"]));
if (args.includes("--dry-run") && args.includes("--confirm")) throw new Error("apply accepts only one of --dry-run or --confirm");
return {
dryRun: args.includes("--dry-run") || !args.includes("--confirm"),
confirm: args.includes("--confirm"),
wait: args.includes("--wait"),
};
}
function parseDisclosureOptions(args: string[]): DisclosureOptions {
validateOptions(args, new Set(["--full", "--raw"]));
const raw = args.includes("--raw");
return { full: raw || args.includes("--full"), raw };
}
function validateOptions(args: string[], booleanOptions: Set<string>): void {
for (const arg of args) {
if (booleanOptions.has(arg)) continue;
throw new Error(`unsupported option: ${arg}`);
}
}
function readSub2ApiConfig(): Sub2ApiConfig {
const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${configPath} must contain a YAML object`);
const image = (parsed as { image?: unknown }).image;
if (typeof image !== "object" || image === null || Array.isArray(image)) throw new Error(`${configPath}.image must be an object`);
const record = image as Record<string, unknown>;
const repository = stringField(record, "repository", "image");
const tag = stringField(record, "tag", "image");
const pullPolicy = stringField(record, "pullPolicy", "image");
if (pullPolicy !== "Always" && pullPolicy !== "IfNotPresent" && pullPolicy !== "Never") throw new Error(`${configPath}.image.pullPolicy must be Always, IfNotPresent, or Never`);
if (!/^[a-z0-9._/-]+(?::[0-9]+)?$/u.test(repository)) throw new Error(`${configPath}.image.repository has an unsupported format`);
if (!/^[A-Za-z0-9._-]+$/u.test(tag)) throw new Error(`${configPath}.image.tag has an unsupported format`);
return { image: { repository, tag, pullPolicy } };
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
if (typeof value !== "string" || value.length === 0) throw new Error(`${configPath}.${path}.${key} must be a non-empty string`);
return value;
}
function imageRef(sub2api: Sub2ApiConfig): string {
return `${sub2api.image.repository}:${sub2api.image.tag}`;
}
function manifest(): string {
const sub2api = readSub2ApiConfig();
const template = readFileSync(manifestPath, "utf8");
return template
.replaceAll("__SUB2API_IMAGE__", imageRef(sub2api))
.replaceAll("__SUB2API_IMAGE_PULL_POLICY__", sub2api.image.pullPolicy);
}
function plan(): Record<string, unknown> {
const sub2api = readSub2ApiConfig();
const yaml = manifest();
const policy = policyChecks(yaml);
return {
ok: policy.every((check) => check.ok),
action: "platform-infra-sub2api-plan",
target: {
route: g14K3sRoute,
namespace,
manifestPath,
configPath,
fieldManager,
serviceDns: `${serviceName}.${namespace}.svc.cluster.local:8080`,
},
config: {
image: imageRef(sub2api),
pullPolicy: sub2api.image.pullPolicy,
},
decision: {
owner: "UniDesk",
namespace,
reason: "Sub2API is an internal shared platform utility for G14 k3s workloads, so it belongs with platform infrastructure rather than a user workload namespace.",
exposure: "ClusterIP only; no public ingress or node-level exposure.",
resourcePolicy: "No Kubernetes CPU/memory requests or limits, matching issue #220.",
imageVersionControl: "Sub2API image repository/tag/pullPolicy are controlled by config/platform-infra/sub2api.yaml in the UniDesk repository.",
dataStores: ["PostgreSQL 18", "Redis 8"],
appPoolCaps: {
databaseMaxOpenConns: 10,
databaseMaxIdleConns: 2,
redisPoolSize: 32,
redisMinIdleConns: 2,
},
},
policy,
next: {
dryRun: "bun scripts/cli.ts platform-infra sub2api apply --dry-run",
apply: "bun scripts/cli.ts platform-infra sub2api apply --confirm",
status: "bun scripts/cli.ts platform-infra sub2api status",
validate: "bun scripts/cli.ts platform-infra sub2api validate",
},
};
}
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
const yaml = manifest();
const policy = policyChecks(yaml);
if (!policy.every((check) => check.ok)) {
return {
ok: false,
action: "platform-infra-sub2api-apply",
mode: "policy-blocked",
policy,
};
}
if (options.confirm && !options.wait) {
const job = startJob(
"platform_infra_sub2api_apply",
["bun", "scripts/cli.ts", "platform-infra", "sub2api", "apply", "--confirm", "--wait"],
"Apply G14 k3s platform-infra Sub2API manifests through the controlled UniDesk CLI",
);
return {
ok: true,
action: "platform-infra-sub2api-apply",
mode: "async-job",
job,
statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
next: {
status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
rollout: "bun scripts/cli.ts platform-infra sub2api status",
validate: "bun scripts/cli.ts platform-infra sub2api validate",
},
};
}
if (options.dryRun) {
const result = await capture(config, g14K3sRoute, ["script"], dryRunScript(yaml));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-apply",
mode: "dry-run",
policy,
remote: parsed ?? compactCapture(result, { full: true }),
};
}
const result = await capture(config, g14K3sRoute, ["script"], applyScript(yaml));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-apply",
mode: "confirmed",
policy,
remote: parsed ?? compactCapture(result, { full: true }),
next: {
status: "bun scripts/cli.ts platform-infra sub2api status",
validate: "bun scripts/cli.ts platform-infra sub2api validate",
},
};
}
async function status(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
const result = await capture(config, g14K3sRoute, ["script"], statusScript(imageRef(readSub2ApiConfig())));
const parsed = parseJsonOutput(result.stdout);
if (options.raw) {
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-status",
remote: compactCapture(result, { full: true }),
parsed,
};
}
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-status",
summary: parsed,
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
};
}
async function validate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
const result = await capture(config, g14K3sRoute, ["script"], validateScript());
const parsed = parseJsonOutput(result.stdout);
if (options.raw) {
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-validate",
remote: compactCapture(result, { full: true }),
parsed,
};
}
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-validate",
summary: parsed,
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
};
}
function policyChecks(yaml: string): PolicyCheck[] {
return [
{
name: "no-ingress",
ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml),
detail: "Sub2API must not be exposed through Kubernetes Ingress.",
},
{
name: "no-nodeport-or-loadbalancer",
ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml),
detail: "Services must remain ClusterIP/internal-only.",
},
{
name: "no-host-network",
ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml),
detail: "Pods must not join the host network.",
},
{
name: "no-host-port",
ok: !/^\s*hostPort:\s*[0-9]+\s*$/mu.test(yaml),
detail: "Pods must not expose host ports.",
},
{
name: "no-cpu-memory-resources",
ok: !/^\s*(cpu|memory):\s*/mu.test(yaml),
detail: "Issue #220 requires no Kubernetes CPU/memory requests or limits.",
},
{
name: "no-resource-quota-or-limit-range",
ok: !/^\s*kind:\s*(ResourceQuota|LimitRange)\s*$/mu.test(yaml),
detail: "The platform-infra namespace must not receive quota/default limit objects for this deployment.",
},
{
name: "expected-namespace",
ok: new RegExp(`^\\s*name:\\s*${namespace}\\s*$`, "mu").test(yaml),
detail: `Manifest declares namespace ${namespace}.`,
},
];
}
function dryRunScript(yaml: string): string {
const encoded = Buffer.from(yaml, "utf8").toString("base64");
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/sub2api.k8s.yaml"
printf '%s' '${encoded}' | base64 -d > "$manifest"
client_out="$tmp/client.out"
client_err="$tmp/client.err"
server_out="$tmp/server.out"
server_err="$tmp/server.err"
kubectl apply --dry-run=client -f "$manifest" >"$client_out" 2>"$client_err"
client_rc=$?
if kubectl get namespace ${namespace} >/dev/null 2>&1; then
namespace_exists=true
kubectl apply --server-side --dry-run=server --field-manager=${fieldManager} -f "$manifest" >"$server_out" 2>"$server_err"
server_rc=$?
else
namespace_exists=false
server_rc=0
printf '%s\\n' 'server dry-run skipped because namespace does not exist yet; first apply creates it before namespaced resources' >"$server_out"
: >"$server_err"
fi
python3 - "$client_rc" "$server_rc" "$namespace_exists" "$client_out" "$client_err" "$server_out" "$server_err" <<'PY'
import json
import sys
client_rc = int(sys.argv[1])
server_rc = int(sys.argv[2])
namespace_exists = sys.argv[3] == "true"
paths = sys.argv[4:]
def text(path):
try:
return open(path, encoding="utf-8").read()
except FileNotFoundError:
return ""
payload = {
"ok": client_rc == 0 and server_rc == 0,
"namespace": "${namespace}",
"namespaceExistsBeforeDryRun": namespace_exists,
"clientDryRun": {
"exitCode": client_rc,
"stdout": text(paths[0])[-4000:],
"stderr": text(paths[1])[-4000:],
},
"serverDryRun": {
"exitCode": server_rc,
"disposition": "executed" if namespace_exists else "skipped-namespace-missing",
"stdout": text(paths[2])[-4000:],
"stderr": text(paths[3])[-4000:],
},
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
function applyScript(yaml: string): string {
const encoded = Buffer.from(yaml, "utf8").toString("base64");
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/sub2api.k8s.yaml"
printf '%s' '${encoded}' | base64 -d > "$manifest"
ns_out="$tmp/ns.out"
ns_err="$tmp/ns.err"
secret_out="$tmp/secret.out"
secret_err="$tmp/secret.err"
apply_out="$tmp/apply.out"
apply_err="$tmp/apply.err"
kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$ns_out" 2>"$ns_err"
ns_rc=$?
secret_action="unknown"
secret_rc=0
if [ "$ns_rc" -eq 0 ]; then
if kubectl -n ${namespace} get secret ${secretName} >/dev/null 2>&1; then
secret_action="kept-existing"
: >"$secret_out"
: >"$secret_err"
else
rand_hex() {
bytes="$1"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$bytes"
else
dd if=/dev/urandom bs="$bytes" count=1 2>/dev/null | od -An -tx1 | tr -d ' \\n'
fi
}
kubectl -n ${namespace} create secret generic ${secretName} \\
--from-literal=POSTGRES_PASSWORD="$(rand_hex 32)" \\
--from-literal=ADMIN_PASSWORD="$(rand_hex 16)" \\
--from-literal=JWT_SECRET="$(rand_hex 32)" \\
--from-literal=TOTP_ENCRYPTION_KEY="$(rand_hex 32)" \\
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err"
secret_rc=$?
secret_action="created"
fi
fi
apply_rc=1
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=$?
else
: >"$apply_out"
printf '%s\\n' 'skipped because namespace or secret step failed' >"$apply_err"
fi
python3 - "$ns_rc" "$secret_rc" "$apply_rc" "$secret_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$apply_out" "$apply_err" <<'PY'
import json
import sys
ns_rc = int(sys.argv[1])
secret_rc = int(sys.argv[2])
apply_rc = int(sys.argv[3])
secret_action = sys.argv[4]
paths = sys.argv[5:]
def text(path):
try:
return open(path, encoding="utf-8").read()
except FileNotFoundError:
return ""
payload = {
"ok": ns_rc == 0 and secret_rc == 0 and apply_rc == 0,
"namespace": "${namespace}",
"secret": {
"name": "${secretName}",
"action": secret_action,
"requiredKeys": ${JSON.stringify(requiredSecretKeys)},
"valuesPrinted": False,
},
"steps": {
"namespace": {"exitCode": ns_rc, "stdout": text(paths[0])[-4000:], "stderr": text(paths[1])[-4000:]},
"secret": {"exitCode": secret_rc, "stdout": text(paths[2])[-4000:], "stderr": text(paths[3])[-4000:]},
"apply": {"exitCode": apply_rc, "stdout": text(paths[4])[-8000:], "stderr": text(paths[5])[-4000:]},
},
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
function statusScript(expectedImage: string): string {
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
capture_json() {
name="$1"
shift
"$@" -o json >"$tmp/$name.json" 2>"$tmp/$name.err"
rc=$?
printf '%s' "$rc" >"$tmp/$name.rc"
}
capture_json ns kubectl get namespace ${namespace}
capture_json deployments kubectl -n ${namespace} get deployments -l app.kubernetes.io/part-of=platform-infra
capture_json statefulsets kubectl -n ${namespace} get statefulsets -l app.kubernetes.io/part-of=platform-infra
capture_json pods kubectl -n ${namespace} get pods -l app.kubernetes.io/part-of=platform-infra
capture_json services kubectl -n ${namespace} get services -l app.kubernetes.io/part-of=platform-infra
capture_json pvc kubectl -n ${namespace} get pvc -l app.kubernetes.io/part-of=platform-infra
capture_json secrets kubectl -n ${namespace} get secret ${secretName}
capture_json ingresses kubectl -n ${namespace} get ingress
capture_json quotas kubectl -n ${namespace} get resourcequota
capture_json limitranges kubectl -n ${namespace} get limitrange
python3 - "$tmp" <<'PY'
import json
import os
import sys
tmp = sys.argv[1]
def rc(name):
try:
return int(open(os.path.join(tmp, f"{name}.rc"), encoding="utf-8").read() or "1")
except FileNotFoundError:
return 1
def load(name):
path = os.path.join(tmp, f"{name}.json")
if not os.path.exists(path):
return None
try:
return json.load(open(path, encoding="utf-8"))
except json.JSONDecodeError:
return None
def items(name):
data = load(name)
if not isinstance(data, dict):
return []
return data.get("items") or []
def deployment_summary(item):
spec = item.get("spec") or {}
status = item.get("status") or {}
desired = spec.get("replicas", 1)
available = status.get("availableReplicas", 0)
init_containers = ((spec.get("template") or {}).get("spec") or {}).get("initContainers", [])
containers = ((spec.get("template") or {}).get("spec") or {}).get("containers", [])
return {
"name": item["metadata"]["name"],
"desired": desired,
"readyReplicas": status.get("readyReplicas", 0),
"availableReplicas": available,
"updatedReplicas": status.get("updatedReplicas", 0),
"ready": available >= desired,
"images": [c.get("image") for c in containers],
"initImages": [c.get("image") for c in init_containers],
}
def statefulset_summary(item):
spec = item.get("spec") or {}
status = item.get("status") or {}
desired = spec.get("replicas", 1)
ready = status.get("readyReplicas", 0)
return {
"name": item["metadata"]["name"],
"desired": desired,
"readyReplicas": ready,
"currentReplicas": status.get("currentReplicas", 0),
"updatedReplicas": status.get("updatedReplicas", 0),
"ready": ready >= desired,
"images": [c.get("image") for c in ((spec.get("template") or {}).get("spec") or {}).get("containers", [])],
}
def pod_summary(item):
status = item.get("status") or {}
container_statuses = status.get("containerStatuses") or []
return {
"name": item["metadata"]["name"],
"phase": status.get("phase"),
"ready": all((cs.get("ready") is True) for cs in container_statuses) if container_statuses else False,
"restarts": sum(int(cs.get("restartCount") or 0) for cs in container_statuses),
"nodeName": (item.get("spec") or {}).get("nodeName"),
"containers": [
{
"name": cs.get("name"),
"ready": cs.get("ready"),
"restartCount": cs.get("restartCount"),
"image": cs.get("image"),
"state": list((cs.get("state") or {}).keys()),
}
for cs in container_statuses
],
}
def service_summary(item):
spec = item.get("spec") or {}
return {
"name": item["metadata"]["name"],
"type": spec.get("type", "ClusterIP"),
"clusterIP": spec.get("clusterIP"),
"ports": [
{
"name": p.get("name"),
"port": p.get("port"),
"targetPort": p.get("targetPort"),
"nodePort": p.get("nodePort"),
}
for p in spec.get("ports", [])
],
}
def pvc_summary(item):
spec = item.get("spec") or {}
status = item.get("status") or {}
req = (spec.get("resources") or {}).get("requests") or {}
return {
"name": item["metadata"]["name"],
"phase": status.get("phase"),
"storageClassName": spec.get("storageClassName"),
"requestedStorage": req.get("storage"),
}
def resource_findings(kind, collection):
findings = []
for item in collection:
spec = item.get("spec") or {}
template_spec = ((spec.get("template") or {}).get("spec") or {})
if template_spec.get("hostNetwork") is True:
findings.append({"kind": kind, "name": item["metadata"]["name"], "field": "hostNetwork"})
all_containers = [(container, "containers") for container in template_spec.get("containers", [])] + [(container, "initContainers") for container in template_spec.get("initContainers", [])]
for container, container_group in all_containers:
resources = container.get("resources") or {}
if resources.get("requests"):
findings.append({"kind": kind, "name": item["metadata"]["name"], "container": container.get("name"), "containerGroup": container_group, "field": "resources.requests"})
if resources.get("limits"):
findings.append({"kind": kind, "name": item["metadata"]["name"], "container": container.get("name"), "containerGroup": container_group, "field": "resources.limits"})
for port in container.get("ports", []):
if "hostPort" in port:
findings.append({"kind": kind, "name": item["metadata"]["name"], "container": container.get("name"), "containerGroup": container_group, "field": "hostPort", "value": port.get("hostPort")})
return findings
deployments = items("deployments")
statefulsets = items("statefulsets")
services = items("services")
pods = items("pods")
pvcs = items("pvc")
secret = load("secrets")
secret_keys = sorted(((secret or {}).get("data") or {}).keys())
missing_secret_keys = [key for key in ${JSON.stringify(requiredSecretKeys)} if key not in secret_keys]
service_violations = []
for svc in services:
spec = svc.get("spec") or {}
if spec.get("type", "ClusterIP") != "ClusterIP":
service_violations.append({"name": svc["metadata"]["name"], "type": spec.get("type")})
for port in spec.get("ports", []):
if "nodePort" in port:
service_violations.append({"name": svc["metadata"]["name"], "nodePort": port.get("nodePort")})
resource_violations = resource_findings("Deployment", deployments) + resource_findings("StatefulSet", statefulsets)
expected_image = "${expectedImage}"
sub2api_deployment = next((deployment_summary(item) for item in deployments if item["metadata"]["name"] == "${serviceName}"), None)
image_aligned = sub2api_deployment is not None and expected_image in sub2api_deployment.get("images", [])
boundary = {
"internalOnly": len(service_violations) == 0 and len(items("ingresses")) == 0,
"serviceViolations": service_violations,
"ingressCount": len(items("ingresses")),
"resourceQuotaCount": len(items("quotas")),
"limitRangeCount": len(items("limitranges")),
"resourceViolations": resource_violations,
}
workload_ready = all(d["ready"] for d in map(deployment_summary, deployments)) and all(s["ready"] for s in map(statefulset_summary, statefulsets))
payload = {
"ok": rc("ns") == 0 and workload_ready and image_aligned and boundary["internalOnly"] and len(resource_violations) == 0 and boundary["resourceQuotaCount"] == 0 and boundary["limitRangeCount"] == 0 and len(missing_secret_keys) == 0,
"namespace": "${namespace}",
"namespaceExists": rc("ns") == 0,
"deployments": [deployment_summary(item) for item in deployments],
"statefulsets": [statefulset_summary(item) for item in statefulsets],
"pods": [pod_summary(item) for item in pods],
"services": [service_summary(item) for item in services],
"pvcs": [pvc_summary(item) for item in pvcs],
"secret": {
"name": "${secretName}",
"exists": rc("secrets") == 0,
"requiredKeys": ${JSON.stringify(requiredSecretKeys)},
"missingKeys": missing_secret_keys,
"valuesPrinted": False,
},
"imageControl": {
"desiredImage": expected_image,
"configPath": "config/platform-infra/sub2api.yaml",
"aligned": image_aligned,
"runningImages": sub2api_deployment.get("images", []) if sub2api_deployment else [],
},
"boundary": boundary,
"serviceDns": "${serviceName}.${namespace}.svc.cluster.local:8080",
"next": {
"apply": "bun scripts/cli.ts platform-infra sub2api apply --confirm",
"validate": "bun scripts/cli.ts platform-infra sub2api validate",
},
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
function validateScript(): string {
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
kubectl get --raw /api/v1/namespaces/${namespace}/services/${serviceName}:8080/proxy/health >"$tmp/health.body" 2>"$tmp/health.err"
health_rc=$?
kubectl get --raw /api/v1/namespaces/${namespace}/services/${serviceName}:8080/proxy/ >"$tmp/root.body" 2>"$tmp/root.err"
root_rc=$?
pg_pod="$(kubectl -n ${namespace} get pod -l app.kubernetes.io/name=sub2api-postgres -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pg-pod.err")"
redis_pod="$(kubectl -n ${namespace} get pod -l app.kubernetes.io/name=sub2api-redis -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/redis-pod.err")"
if [ -n "$pg_pod" ]; then
kubectl -n ${namespace} exec "$pg_pod" -- pg_isready -U sub2api -d sub2api -h 127.0.0.1 >"$tmp/pg.out" 2>"$tmp/pg.err"
pg_rc=$?
else
pg_rc=1
printf '%s\\n' 'sub2api postgres pod not found' >"$tmp/pg.err"
fi
if [ -n "$redis_pod" ]; then
kubectl -n ${namespace} exec "$redis_pod" -- redis-cli ping >"$tmp/redis.out" 2>"$tmp/redis.err"
redis_rc=$?
else
redis_rc=1
printf '%s\\n' 'sub2api redis pod not found' >"$tmp/redis.err"
fi
python3 - "$tmp" "$health_rc" "$root_rc" "$pg_rc" "$redis_rc" <<'PY'
import json
import os
import sys
tmp = sys.argv[1]
health_rc, root_rc, pg_rc, redis_rc = [int(value) for value in sys.argv[2:]]
def text(name, limit=4000):
path = os.path.join(tmp, name)
try:
data = open(path, encoding="utf-8", errors="replace").read()
except FileNotFoundError:
return ""
return data[-limit:]
health_body = text("health.body", 2000)
root_body = text("root.body", 2000)
payload = {
"ok": health_rc == 0 and root_rc == 0 and pg_rc == 0 and redis_rc == 0,
"namespace": "${namespace}",
"serviceDns": "${serviceName}.${namespace}.svc.cluster.local:8080",
"checks": {
"sub2apiHealthViaKubernetesServiceProxy": {
"exitCode": health_rc,
"method": "kubectl get --raw /api/v1/namespaces/${namespace}/services/${serviceName}:8080/proxy/health",
"bodyPreview": health_body,
"stderr": text("health.err", 2000),
},
"sub2apiRootViaKubernetesServiceProxy": {
"exitCode": root_rc,
"method": "kubectl get --raw /api/v1/namespaces/${namespace}/services/${serviceName}:8080/proxy/",
"bodyBytes": len(root_body.encode("utf-8")),
"bodyPreview": root_body[:400],
"stderr": text("root.err", 2000),
},
"postgresPgIsReady": {
"exitCode": pg_rc,
"stdout": text("pg.out", 2000),
"stderr": text("pg.err", 2000),
},
"redisPing": {
"exitCode": redis_rc,
"stdout": text("redis.out", 2000),
"stderr": text("redis.err", 2000),
},
},
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
async function capture(config: UniDeskConfig, target: string, args: string[], input?: string): Promise<SshCaptureResult> {
return await runSshCommandCapture(config, target, args, input);
}
function parseJsonOutput(stdout: string): Record<string, unknown> | null {
const trimmed = stdout.trim();
if (trimmed.length === 0) return null;
const start = trimmed.indexOf("{");
const end = trimmed.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) return null;
try {
const parsed = JSON.parse(trimmed.slice(start, end + 1)) as unknown;
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
} catch {
return null;
}
}
function boolField(value: Record<string, unknown> | null, key: string, defaultValue: boolean): boolean {
if (value === null) return defaultValue;
const field = value[key];
return typeof field === "boolean" ? field : defaultValue;
}
function compactCapture(result: SshCaptureResult, options: { full?: boolean } = {}): Record<string, unknown> {
const full = options.full ?? false;
return {
exitCode: result.exitCode,
stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
stdoutTail: full || result.exitCode !== 0 ? result.stdout.slice(-8000) : "",
stderrTail: full || result.exitCode !== 0 ? result.stderr.slice(-4000) : "",
};
}
@@ -0,0 +1,412 @@
apiVersion: v1
kind: Namespace
metadata:
name: platform-infra
labels:
app.kubernetes.io/name: platform-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: G14
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sub2api-config
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
data:
AUTO_SETUP: "true"
SERVER_HOST: "0.0.0.0"
SERVER_PORT: "8080"
SERVER_MODE: "release"
RUN_MODE: "standard"
DATABASE_HOST: "sub2api-postgres"
DATABASE_PORT: "5432"
DATABASE_USER: "sub2api"
DATABASE_DBNAME: "sub2api"
DATABASE_SSLMODE: "disable"
DATABASE_MAX_OPEN_CONNS: "10"
DATABASE_MAX_IDLE_CONNS: "2"
DATABASE_CONN_MAX_LIFETIME_MINUTES: "30"
DATABASE_CONN_MAX_IDLE_TIME_MINUTES: "5"
REDIS_HOST: "sub2api-redis"
REDIS_PORT: "6379"
REDIS_PASSWORD: ""
REDIS_DB: "0"
REDIS_POOL_SIZE: "32"
REDIS_MIN_IDLE_CONNS: "2"
REDIS_ENABLE_TLS: "false"
ADMIN_EMAIL: "admin@sub2api.platform-infra.local"
JWT_EXPIRE_HOUR: "24"
TZ: "Asia/Shanghai"
SECURITY_URL_ALLOWLIST_ENABLED: "false"
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP: "false"
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS: "false"
SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS: ""
UPDATE_PROXY_URL: ""
GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT: "0"
GATEWAY_OPENAI_HTTP2_ENABLED: "true"
GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1: "true"
GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD: "2"
GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS: "60"
GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS: "600"
GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT: "900"
GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL: "10"
GATEWAY_IMAGE_CONCURRENCY_ENABLED: "false"
GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS: "0"
GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE: "reject"
GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS: "30"
GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS: "100"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sub2api-data
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sub2api-redis-data
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: Service
metadata:
name: sub2api-postgres
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api-postgres
app.kubernetes.io/component: postgres
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
selector:
app.kubernetes.io/name: sub2api-postgres
app.kubernetes.io/component: postgres
ports:
- name: postgres
port: 5432
targetPort: postgres
---
apiVersion: v1
kind: Service
metadata:
name: sub2api-redis
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
selector:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
ports:
- name: redis
port: 6379
targetPort: redis
---
apiVersion: v1
kind: Service
metadata:
name: sub2api
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
selector:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
ports:
- name: http
port: 8080
targetPort: http
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sub2api-postgres
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api-postgres
app.kubernetes.io/component: postgres
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
serviceName: sub2api-postgres
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: sub2api-postgres
app.kubernetes.io/component: postgres
template:
metadata:
labels:
app.kubernetes.io/name: sub2api-postgres
app.kubernetes.io/component: postgres
app.kubernetes.io/part-of: platform-infra
spec:
securityContext:
fsGroup: 70
containers:
- name: postgres
image: postgres:18-alpine
imagePullPolicy: IfNotPresent
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_USER
value: sub2api
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: sub2api-secrets
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
value: sub2api
- name: PGDATA
value: /var/lib/postgresql/data
- name: TZ
value: Asia/Shanghai
readinessProbe:
exec:
command:
- sh
- -c
- pg_isready -U sub2api -d sub2api -h 127.0.0.1
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command:
- sh
- -c
- pg_isready -U sub2api -d sub2api -h 127.0.0.1
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
labels:
app.kubernetes.io/name: sub2api-postgres
app.kubernetes.io/component: postgres
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sub2api-redis
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
template:
metadata:
labels:
app.kubernetes.io/name: sub2api-redis
app.kubernetes.io/component: redis
app.kubernetes.io/part-of: platform-infra
spec:
securityContext:
fsGroup: 999
containers:
- name: redis
image: redis:8-alpine
imagePullPolicy: IfNotPresent
command:
- sh
- -c
args:
- redis-server --save 60 1 --appendonly yes --appendfsync everysec
ports:
- name: redis
containerPort: 6379
env:
- name: TZ
value: Asia/Shanghai
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: sub2api-redis-data
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sub2api
namespace: platform-infra
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
template:
metadata:
labels:
app.kubernetes.io/name: sub2api
app.kubernetes.io/component: app
app.kubernetes.io/part-of: platform-infra
spec:
securityContext:
fsGroup: 1000
initContainers:
- name: wait-postgres
image: postgres:18-alpine
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- until pg_isready -h sub2api-postgres -U sub2api -d sub2api; do sleep 2; done
- name: wait-redis
image: redis:8-alpine
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- until redis-cli -h sub2api-redis ping | grep -q PONG; do sleep 2; done
containers:
- name: sub2api
image: __SUB2API_IMAGE__
imagePullPolicy: __SUB2API_IMAGE_PULL_POLICY__
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: sub2api-config
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: sub2api-secrets
key: POSTGRES_PASSWORD
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: sub2api-secrets
key: ADMIN_PASSWORD
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: sub2api-secrets
key: JWT_SECRET
- name: TOTP_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: sub2api-secrets
key: TOTP_ENCRYPTION_KEY
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
startupProbe:
httpGet:
path: /health
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
volumeMounts:
- name: sub2api-data
mountPath: /app/data
volumes:
- name: sub2api-data
persistentVolumeClaim:
claimName: sub2api-data