import { Buffer } from "node:buffer"; import type { UniDeskConfig } from "./config"; import { applyPk01CaddyManagedBlock, caddyManagedBlockMarkers } from "./pk01-caddy"; import { capture, compactCapture, fingerprintValues, parseJsonOutput, redactText, shQuote } from "./platform-infra-ops-library"; export { capture, compactCapture, fingerprintValues, parseJsonOutput, redactText, shQuote }; export interface PublicServiceExposure { enabled: boolean; publicBaseUrl: string; dns: { hostname: string; expectedA: string; resolvers: string[] }; frpc: { deploymentName: string; secretName: string; secretKey: string; image: string; serverAddr: string; serverPort: number; proxyName: string; remotePort: number; localIP: string; localPort: number; tokenSourceRef: string; tokenSourceKey: string; }; pk01: { route: string; caddyConfigPath: string; caddyServiceName: string; responseHeaderTimeoutSeconds: number; }; } export interface PublicServiceTarget { id: string; route: string; namespace: string; replicas: number; publicExposure: PublicServiceExposure; } export interface FrpcSecretMaterial { sourceRef: string; sourcePath: string; secretName: string; secretKey: string; frpcToml: string; fingerprint: string; valuesPrinted: false; } export async function applyPk01CaddyBlock( config: UniDeskConfig, serviceId: string, exposure: PublicServiceExposure, markers: { start: string; end: string } = caddyManagedBlockMarkers(serviceId), ): Promise> { return await applyPk01CaddyManagedBlock(config, serviceId, exposure, markers); } export function prepareFrpcSecret(params: { secretRoot: string; exposure: PublicServiceExposure; sourcePathRedactor: (path: string) => string; parseEnvFile: (text: string) => Record; requiredEnvValue: (values: Record, key: string, sourceRef: string) => string; readTextFile: (path: string) => string; }): FrpcSecretMaterial { const { exposure } = params; const sourcePath = `${params.secretRoot.replace(/\/+$/u, "")}/${exposure.frpc.tokenSourceRef}`; const values = params.parseEnvFile(params.readTextFile(sourcePath)); const token = params.requiredEnvValue(values, exposure.frpc.tokenSourceKey, exposure.frpc.tokenSourceRef); const frpcToml = [ `serverAddr = "${exposure.frpc.serverAddr}"`, `serverPort = ${exposure.frpc.serverPort}`, "loginFailExit = true", `auth.token = "${escapeTomlString(token)}"`, "", "[[proxies]]", `name = "${exposure.frpc.proxyName}"`, 'type = "tcp"', `localIP = "${exposure.frpc.localIP}"`, `localPort = ${exposure.frpc.localPort}`, `remotePort = ${exposure.frpc.remotePort}`, "", ].join("\n"); return { sourceRef: exposure.frpc.tokenSourceRef, sourcePath: params.sourcePathRedactor(sourcePath), secretName: exposure.frpc.secretName, secretKey: exposure.frpc.secretKey, frpcToml, fingerprint: fingerprintValues({ token, frpcToml }, ["token", "frpcToml"]), valuesPrinted: false, }; } export function renderFrpcManifest(target: PublicServiceTarget): string { const exposure = target.publicExposure; if (!exposure.enabled) return ""; return `--- apiVersion: apps/v1 kind: Deployment metadata: name: ${exposure.frpc.deploymentName} namespace: ${target.namespace} labels: app.kubernetes.io/name: ${exposure.frpc.deploymentName} app.kubernetes.io/component: tunnel app.kubernetes.io/part-of: platform-infra app.kubernetes.io/managed-by: unidesk unidesk.ai/runtime-node: ${target.id} unidesk.ai/public-hostname: ${exposure.dns.hostname} spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: ${exposure.frpc.deploymentName} app.kubernetes.io/component: tunnel template: metadata: labels: app.kubernetes.io/name: ${exposure.frpc.deploymentName} app.kubernetes.io/component: tunnel app.kubernetes.io/part-of: platform-infra annotations: unidesk.ai/public-base-url: "${exposure.publicBaseUrl}" unidesk.ai/frp-server: "${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}" unidesk.ai/frp-remote-port: "${exposure.frpc.remotePort}" spec: containers: - name: frpc image: ${exposure.frpc.image} imagePullPolicy: IfNotPresent args: - -c - /etc/frp/frpc.toml volumeMounts: - name: frpc-config mountPath: /etc/frp/frpc.toml subPath: ${exposure.frpc.secretKey} readOnly: true volumes: - name: frpc-config secret: secretName: ${exposure.frpc.secretName} `; } export function publicServicePolicyChecks(yaml: string, target: PublicServiceTarget, serviceName: string): Array> { return [ { name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: `${serviceName} public exposure must use PK01 Caddy+FRP, not Kubernetes Ingress.` }, { name: "no-nodeport-or-loadbalancer", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Services must stay ClusterIP." }, { name: "no-host-network", ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml), detail: "Pods must not use 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: "No CPU/memory request or limit objects are rendered." }, { name: "allow-all-network-policy", ok: hasAllowAllNetworkPolicy(yaml, target.namespace), detail: `NetworkPolicy/allow-all exists in ${target.namespace}.` }, ]; } export function dryRunManifestScript(params: { yaml: string; target: PublicServiceTarget; fieldManager: string; manifestName: string }): string { const encoded = Buffer.from(params.yaml, "utf8").toString("base64"); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT manifest="$tmp/${params.manifestName}.k8s.yaml" printf '%s' '${encoded}' | base64 -d > "$manifest" kubectl apply --dry-run=client -f "$manifest" >"$tmp/client.out" 2>"$tmp/client.err" client_rc=$? if kubectl get namespace ${params.target.namespace} >/dev/null 2>&1; then kubectl apply --server-side --dry-run=server --field-manager=${params.fieldManager} -f "$manifest" >"$tmp/server.out" 2>"$tmp/server.err" server_rc=$? server_disposition=executed else : >"$tmp/server.err" printf '%s\\n' 'server dry-run skipped because namespace does not exist yet' >"$tmp/server.out" server_rc=0 server_disposition=skipped-namespace-missing fi python3 - "$client_rc" "$server_rc" "$server_disposition" "$tmp/client.out" "$tmp/client.err" "$tmp/server.out" "$tmp/server.err" <<'PY' import json, sys client_rc, server_rc = int(sys.argv[1]), int(sys.argv[2]) def text(path): try: return open(path, encoding="utf-8", errors="replace").read() except FileNotFoundError: return "" payload = { "ok": client_rc == 0 and server_rc == 0, "target": "${params.target.id}", "namespace": "${params.target.namespace}", "clientDryRun": {"exitCode": client_rc, "stdout": text(sys.argv[4])[-4000:], "stderr": text(sys.argv[5])[-4000:]}, "serverDryRun": {"exitCode": server_rc, "disposition": sys.argv[3], "stdout": text(sys.argv[6])[-4000:], "stderr": text(sys.argv[7])[-4000:]}, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } export function publicHttpProbe(baseUrl: string, path: string, options: { headers?: string[] } = {}): Record { const url = `${baseUrl.replace(/\/+$/u, "")}${path}`; const args = ["-fsS", "--connect-timeout", "10", "--max-time", "30", "-o", "-", "-w", "\n%{http_code}"]; for (const header of options.headers ?? []) { args.unshift(header); args.unshift("-H"); } args.push(url); const result = Bun.spawnSync(["curl", ...args], { stdout: "pipe", stderr: "pipe" }); const stdout = new TextDecoder().decode(result.stdout); const stderr = new TextDecoder().decode(result.stderr); const lines = stdout.split(/\r?\n/u); const statusText = lines.pop() ?? ""; const status = Number(statusText); const body = lines.join("\n"); return { ok: result.exitCode === 0 && Number.isInteger(status) && status >= 200 && status < 500, url, status: Number.isInteger(status) ? status : null, bodyBytes: Buffer.byteLength(body, "utf8"), bodyPreview: redactText(body).slice(0, 2000), stderrTail: redactText(stderr).slice(-2000), headersUsed: options.headers?.length ?? 0, valuesPrinted: false, }; } export function escapeTomlString(value: string): string { return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\""); } function hasAllowAllNetworkPolicy(yaml: string, namespaceName: string): boolean { return yaml.split(/^---\s*$/mu).some((document) => /^\s*kind:\s*NetworkPolicy\s*$/mu.test(document) && /^\s*name:\s*allow-all\s*$/mu.test(document) && new RegExp(`^\\s*namespace:\\s*${escapeRegExp(namespaceName)}\\s*$`, "mu").test(document) && /^\s*podSelector:\s*\{\}\s*$/mu.test(document)); } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); }