feat: deploy NC01 Sub2API target
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success

This commit is contained in:
Codex
2026-07-09 13:16:58 +02:00
parent 21935e7319
commit 8347d5ed1b
17 changed files with 483 additions and 37 deletions
+16 -1
View File
@@ -14,7 +14,11 @@ export interface UniDeskConfig {
database: { port: number; containerPort: number };
providerIngress: { port: number; containerPort: number };
providerData: { port: number; containerPort: number };
restrictedHostAccess?: { bindHost: string; allowedSourceCidrs: string[] };
restrictedHostAccess?: {
bindHost: string;
allowedSourceCidrs: string[];
allowedForwardRules: Array<{ id: string; sourceCidr: string; destinationCidr: string; port: number; purpose: string }>;
};
};
auth: { username: string; password: string; sessionSecret: string; sessionTtlSeconds: number };
database: { user: string; password: string; name: string; volume: string; volumeSize: string };
@@ -181,9 +185,20 @@ function optionalRestrictedHostAccess(network: Record<string, unknown>): UniDesk
const record = asRecord(value, "network.restrictedHostAccess");
const allowedSourceCidrs = stringArrayField(record, "allowedSourceCidrs", "network.restrictedHostAccess");
if (allowedSourceCidrs.length === 0) throw new Error("network.restrictedHostAccess.allowedSourceCidrs must not be empty");
const allowedForwardRules = optionalArray(record.allowedForwardRules, "network.restrictedHostAccess.allowedForwardRules").map((item, index) => {
const path = `network.restrictedHostAccess.allowedForwardRules[${index}]`;
return {
id: stringField(item, "id", path),
sourceCidr: stringField(item, "sourceCidr", path),
destinationCidr: stringField(item, "destinationCidr", path),
port: numberField(item, "port", path),
purpose: stringField(item, "purpose", path),
};
});
return {
bindHost: stringField(record, "bindHost", "network.restrictedHostAccess"),
allowedSourceCidrs,
allowedForwardRules,
};
}
+5
View File
@@ -548,6 +548,11 @@ function restrictedHostAccessScript(config: UniDeskConfig): string {
];
return [
"iptables -N DOCKER-USER 2>/dev/null || true",
...access.allowedForwardRules.map((rule) => [
`iptables -C DOCKER-USER -s ${shellQuote(rule.sourceCidr)} -d ${shellQuote(rule.destinationCidr)} -p tcp --dport ${rule.port} -j ACCEPT 2>/dev/null`,
` || iptables -I DOCKER-USER 1 -s ${shellQuote(rule.sourceCidr)} -d ${shellQuote(rule.destinationCidr)} -p tcp --dport ${rule.port} -j ACCEPT`,
`echo ${shellJoin(["restricted_host_forward_access", rule.id, rule.sourceCidr, rule.destinationCidr, String(rule.port), rule.purpose])}`,
].join(" \\\n")),
...ports.flatMap((port) => [
...access.allowedSourceCidrs.map((source) => [
`iptables -C DOCKER-USER -s ${shellQuote(source)} -p tcp --dport ${port.containerPort} -j ACCEPT 2>/dev/null`,
+44 -14
View File
@@ -49,15 +49,32 @@ export function publicExposureSummary(exposure: Sub2ApiPublicExposureConfig): Re
tokenSourceRef: exposure.frpc.tokenSourceRef,
valuesPrinted: false,
},
pk01: {
route: exposure.pk01.route,
mode: "caddy-edge",
caddyBinaryPath: exposure.pk01.caddyBinaryPath,
caddyDownloadProxyUrl: exposure.pk01.caddyDownloadProxyUrl,
caddyConfigPath: exposure.pk01.caddyConfigPath,
caddyServiceName: exposure.pk01.caddyServiceName,
pikanodeHttpHostPort: exposure.pk01.pikanodeHttpHostPort,
},
localHttps: exposure.localHttps === null
? null
: {
route: exposure.localHttps.route,
mode: "node-local-docker-caddy",
upstreamMode: exposure.localHttps.upstreamMode,
composePath: exposure.localHttps.composePath,
projectName: exposure.localHttps.projectName,
serviceName: exposure.localHttps.serviceName,
containerName: exposure.localHttps.containerName,
image: exposure.localHttps.image,
bind: `${exposure.localHttps.hostBind}:${exposure.localHttps.httpsPort}`,
tlsMode: exposure.localHttps.tlsMode,
valuesPrinted: false,
},
pk01: exposure.pk01 === null
? null
: {
route: exposure.pk01.route,
mode: "caddy-edge",
caddyBinaryPath: exposure.pk01.caddyBinaryPath,
caddyDownloadProxyUrl: exposure.pk01.caddyDownloadProxyUrl,
caddyConfigPath: exposure.pk01.caddyConfigPath,
caddyServiceName: exposure.pk01.caddyServiceName,
pikanodeHttpHostPort: exposure.pk01.pikanodeHttpHostPort,
},
};
}
@@ -142,6 +159,7 @@ export function plan(options: TargetOptions): Record<string, unknown> {
security: sub2api.security,
target: {
runtimeMode: target.runtimeMode,
runtime: target.runtime,
databaseMode: target.databaseMode,
redisMode: target.redisMode,
appReplicas: target.appReplicas,
@@ -178,7 +196,9 @@ export function plan(options: TargetOptions): Record<string, unknown> {
exposure: target.publicExposure?.enabled
? isHostDockerTarget(target)
? `Public HTTPS ${target.publicExposure.publicBaseUrl} through PK01 Caddy to PK01 local Docker; no D601 egress, no k3s Ingress/NodePort/LoadBalancer.`
: `Public HTTPS ${target.publicExposure.publicBaseUrl} through PK01 Caddy and ${target.id} frpc; no master server forwarding and no Kubernetes Ingress/NodePort/LoadBalancer.`
: target.publicExposure.mode === "node-local-https"
? `Node-local HTTPS ${target.publicExposure.publicBaseUrl} through ${target.id} Docker Caddy to the ClusterIP Sub2API Service; no PK01 Caddy/FRP and no Kubernetes Ingress/NodePort/LoadBalancer.`
: `Public HTTPS ${target.publicExposure.publicBaseUrl} through PK01 Caddy and ${target.id} frpc; no master server forwarding and no Kubernetes Ingress/NodePort/LoadBalancer.`
: "ClusterIP only; no public ingress or node-level exposure.",
resourcePolicy: isHostDockerTarget(target) ? "PK01 host-Docker compose is controlled by YAML; no Kubernetes resources are rendered." : "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.",
@@ -186,11 +206,19 @@ export function plan(options: TargetOptions): Record<string, unknown> {
networkPolicy: isHostDockerTarget(target) ? "Not applicable for PK01 host-Docker deployment." : "NetworkPolicy/allow-all is rendered with the deployment so kube-router cannot silently default-deny Sub2API cross-pod traffic.",
publicExposure: target.publicExposure?.enabled
? {
mode: target.publicExposure.mode === "pk01-local" ? "pk01-caddy-local-docker" : "pk01-caddy-frp-direct",
mode: target.publicExposure.mode === "pk01-local"
? "pk01-caddy-local-docker"
: target.publicExposure.mode === "node-local-https"
? "node-local-docker-caddy"
: "pk01-caddy-frp-direct",
dataPath: target.publicExposure.mode === "pk01-local"
? `client -> PK01 Caddy -> ${target.publicExposure.local?.upstreamHost}:${target.publicExposure.local?.upstreamPort} -> PK01 Docker Sub2API`
: `client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API`,
pikanodeRole: `pikapython.com upstream only; ${target.publicExposure.dns.hostname} does not pass through pikanode Express`,
: target.publicExposure.mode === "node-local-https"
? `client -> ${target.id} local HTTPS port ${target.publicExposure.localHttps?.httpsPort} -> ${target.id} Docker Caddy -> ${serviceName}.${target.namespace}.svc.cluster.local:8080`
: `client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API`,
pikanodeRole: target.publicExposure.mode === "node-local-https"
? "not involved; this target does not touch PK01 pikanode"
: `pikapython.com upstream only; ${target.publicExposure.dns.hostname} does not pass through pikanode Express`,
publicBaseUrl: target.publicExposure.publicBaseUrl,
hostname: target.publicExposure.dns.hostname,
expectedA: target.publicExposure.dns.expectedA,
@@ -327,7 +355,9 @@ export async function apply(config: UniDeskConfig, options: ApplyOptions): Promi
: applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial, accountLocalProxySecretMaterial),
);
const parsed = parseJsonOutput(result.stdout);
const pk01Exposure = target.publicExposure === null ? null : await applyPk01PublicExposure(config, target);
const pk01Exposure = target.publicExposure === null || !target.publicExposure.enabled || target.publicExposure.mode === "node-local-https"
? null
: await applyPk01PublicExposure(config, target);
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false) && (pk01Exposure === null || pk01Exposure.ok === true),
action: "platform-infra-sub2api-apply",
+106 -8
View File
@@ -15,10 +15,11 @@ import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import type { AccountLocalProxySecretMaterial, EgressProxySecretMaterial, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
import { fieldManager, requiredSecretKeys, sub2apiCaddyManagedMarker } from "./entry";
import { fieldManager, requiredSecretKeys, serviceName, sub2apiCaddyManagedMarker } from "./entry";
import { managedResourceCleanupPlan } from "./manifest";
export function renderPk01Caddyfile(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when rendering PK01 Caddy`);
const apexHost = baseDomain(exposure.dns.hostname);
const apiBlock = renderCaddyManagedBlock(
sub2apiCaddyManagedMarker(target),
@@ -48,11 +49,16 @@ export function publicExposureUpstream(target: Sub2ApiTargetConfig, exposure: Su
if (exposure.local === null) throw new Error(`publicExposure.local is required for target ${target.id} when mode=pk01-local`);
return `${exposure.local.upstreamHost}:${exposure.local.upstreamPort}`;
}
if (exposure.mode === "node-local-https") {
if (exposure.localHttps === null) throw new Error(`publicExposure.localHttps is required for target ${target.id} when mode=node-local-https`);
return `${exposure.dns.hostname}:${exposure.localHttps.httpsPort}`;
}
if (exposure.frpc === null) throw new Error(`publicExposure.frpc is required for target ${target.id} when mode=frp`);
return `127.0.0.1:${exposure.frpc.remotePort}`;
}
export function renderPk01CaddyService(exposure: Sub2ApiPublicExposureConfig): string {
if (exposure.pk01 === null) throw new Error("publicExposure.pk01 is required when rendering PK01 Caddy service");
return `[Unit]
Description=UniDesk PK01 Caddy edge for PikaPython and Sub2API
After=network-online.target docker.service
@@ -227,6 +233,8 @@ export function applyScript(
): string {
const encoded = Buffer.from(yaml, "utf8").toString("base64");
const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
const localHttpsExposure = target.publicExposure?.enabled === true && target.publicExposure.mode === "node-local-https" ? target.publicExposure : null;
const localHttps = localHttpsExposure?.localHttps ?? null;
const appSecretName = sub2api.runtime.database.secretName;
const publicExposureSecretName = publicExposureSecretMaterial?.secretName ?? sub2api.defaults.cleanup.publicExposure.secretName;
const publicExposureSecretKey = publicExposureSecretMaterial?.secretKey ?? "frpc.toml";
@@ -299,6 +307,24 @@ export function applyScript(
proxyUrl: accountLocalProxySecretMaterial.proxyUrl,
valuesPrinted: false,
}))})`;
const localHttpsSummary = localHttps === null
? "None"
: `json.loads(${JSON.stringify(JSON.stringify({
publicBaseUrl: localHttpsExposure?.publicBaseUrl,
hostname: localHttpsExposure?.dns.hostname,
expectedA: localHttpsExposure?.dns.expectedA,
mode: "node-local-docker-caddy",
route: localHttps.route,
upstreamMode: localHttps.upstreamMode,
composePath: localHttps.composePath,
projectName: localHttps.projectName,
serviceName: localHttps.serviceName,
containerName: localHttps.containerName,
image: localHttps.image,
bind: `${localHttps.hostBind}:${localHttps.httpsPort}`,
tlsMode: localHttps.tlsMode,
valuesPrinted: false,
}))})`;
return `
set -u
tmp="$(mktemp -d)"
@@ -319,6 +345,8 @@ apply_out="$tmp/apply.out"
apply_err="$tmp/apply.err"
cleanup_out="$tmp/cleanup.out"
cleanup_err="$tmp/cleanup.err"
local_https_out="$tmp/local-https.out"
local_https_err="$tmp/local-https.err"
kubectl create namespace ${target.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"
@@ -329,6 +357,8 @@ egress_secret_action="not-enabled"
egress_secret_rc=0
account_local_proxy_secret_action="not-enabled"
account_local_proxy_secret_rc=0
local_https_action="not-enabled"
local_https_rc=0
if [ "$ns_rc" -eq 0 ]; then
if [ "${target.databaseMode}" = "external-pending" ]; then
secret_action="external-pending-not-managed"
@@ -506,7 +536,71 @@ else
printf '%s\\n' 'skipped because apply failed' >"$cleanup_err"
cleanup_rc=1
fi
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$account_local_proxy_secret_rc" "$apply_rc" "$cleanup_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$account_local_proxy_secret_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$account_local_proxy_secret_out" "$account_local_proxy_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_err" <<'PY'
if [ "${localHttps === null ? "false" : "true"}" = "true" ]; then
if [ "$apply_rc" -eq 0 ] && [ "$cleanup_rc" -eq 0 ]; then
edge_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.workDir)}
compose_path=${localHttps === null ? "\"\"" : shQuote(localHttps.composePath)}
caddyfile_path=${localHttps === null ? "\"\"" : shQuote(localHttps.caddyfilePath)}
data_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.dataDir)}
config_dir=${localHttps === null ? "\"\"" : shQuote(localHttps.configDir)}
mkdir -p "$edge_dir" "$(dirname "$compose_path")" "$(dirname "$caddyfile_path")" "$data_dir" "$config_dir" >"$local_https_out" 2>"$local_https_err"
local_https_rc=$?
if [ "$local_https_rc" -eq 0 ]; then
service_cluster_ip="$(kubectl -n ${target.namespace} get service ${serviceName} -o jsonpath='{.spec.clusterIP}' 2>>"$local_https_err" || true)"
if [ -z "$service_cluster_ip" ] || [ "$service_cluster_ip" = "None" ]; then
printf '%s\\n' 'sub2api service clusterIP is empty; cannot render node-local HTTPS edge' >>"$local_https_err"
local_https_rc=1
fi
fi
if [ "$local_https_rc" -eq 0 ]; then
cat >"$caddyfile_path" <<'CADDY'
https://${localHttpsExposure?.dns.hostname}:${localHttps?.httpsPort} {
tls internal
reverse_proxy __SUB2API_SERVICE_CLUSTER_IP__:8080 {
transport http {
response_header_timeout ${localHttps?.responseHeaderTimeoutSeconds}s
}
}
}
CADDY
sed -i "s/__SUB2API_SERVICE_CLUSTER_IP__/$service_cluster_ip/g" "$caddyfile_path"
cat >"$compose_path" <<'COMPOSE'
name: ${localHttps?.projectName}
services:
${localHttps?.serviceName}:
image: ${localHttps?.image}
container_name: ${localHttps?.containerName}
restart: unless-stopped
ports:
- "${localHttps?.hostBind}:${localHttps?.httpsPort}:${localHttps?.httpsPort}"
volumes:
- ${localHttps?.caddyfilePath}:/etc/caddy/Caddyfile:ro
- ${localHttps?.dataDir}:/data
- ${localHttps?.configDir}:/config
COMPOSE
chmod 0644 "$caddyfile_path" "$compose_path" >>"$local_https_out" 2>>"$local_https_err" || local_https_rc=$?
fi
if [ "$local_https_rc" -eq 0 ]; then
docker compose -f "$compose_path" -p ${localHttps === null ? "\"\"" : shQuote(localHttps.projectName)} up -d --force-recreate --remove-orphans >>"$local_https_out" 2>>"$local_https_err"
local_https_rc=$?
fi
if [ "$local_https_rc" -eq 0 ]; then
sleep 2
curl --noproxy '*' -kfsS --connect-timeout 5 --max-time 20 --resolve ${localHttpsExposure === null || localHttps === null ? "\"\"" : shQuote(`${localHttpsExposure.dns.hostname}:${localHttps.httpsPort}:127.0.0.1`)} ${localHttpsExposure === null ? "\"\"" : shQuote(`${localHttpsExposure.publicBaseUrl}/health`)} >>"$local_https_out" 2>>"$local_https_err"
local_https_rc=$?
fi
local_https_action="synced"
else
printf '%s\\n' 'skipped because Kubernetes apply or cleanup failed' >"$local_https_err"
: >"$local_https_out"
local_https_action="skipped-after-k8s-failure"
fi
else
: >"$local_https_out"
: >"$local_https_err"
fi
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$account_local_proxy_secret_rc" "$apply_rc" "$cleanup_rc" "$local_https_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$account_local_proxy_secret_action" "$local_https_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$account_local_proxy_secret_out" "$account_local_proxy_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_err" "$local_https_out" "$local_https_err" <<'PY'
import json
import sys
ns_rc = int(sys.argv[1])
@@ -516,11 +610,13 @@ egress_secret_rc = int(sys.argv[4])
account_local_proxy_secret_rc = int(sys.argv[5])
apply_rc = int(sys.argv[6])
cleanup_rc = int(sys.argv[7])
secret_action = sys.argv[8]
exposure_secret_action = sys.argv[9]
egress_secret_action = sys.argv[10]
account_local_proxy_secret_action = sys.argv[11]
paths = sys.argv[12:]
local_https_rc = int(sys.argv[8])
secret_action = sys.argv[9]
exposure_secret_action = sys.argv[10]
egress_secret_action = sys.argv[11]
account_local_proxy_secret_action = sys.argv[12]
local_https_action = sys.argv[13]
paths = sys.argv[14:]
def text(path):
try:
return open(path, encoding="utf-8").read()
@@ -532,7 +628,7 @@ def parsed(path):
except Exception:
return None
payload = {
"ok": ns_rc == 0 and secret_rc == 0 and exposure_secret_rc == 0 and egress_secret_rc == 0 and account_local_proxy_secret_rc == 0 and apply_rc == 0 and cleanup_rc == 0,
"ok": ns_rc == 0 and secret_rc == 0 and exposure_secret_rc == 0 and egress_secret_rc == 0 and account_local_proxy_secret_rc == 0 and apply_rc == 0 and cleanup_rc == 0 and local_https_rc == 0,
"target": "${target.id}",
"namespace": "${target.namespace}",
"databaseMode": "${target.databaseMode}",
@@ -547,6 +643,7 @@ payload = {
"valuesPrinted": False,
},
"publicExposure": ${exposureSecretSummary},
"localHttpsExposure": ${localHttpsSummary},
"egressProxy": ${egressProxySecretSummary},
"accountLocalProxy": ${accountLocalProxySecretSummary},
"steps": {
@@ -557,6 +654,7 @@ payload = {
"accountLocalProxySecret": {"exitCode": account_local_proxy_secret_rc, "action": account_local_proxy_secret_action, "stdout": text(paths[8])[-4000:], "stderr": text(paths[9])[-4000:]},
"apply": {"exitCode": apply_rc, "stdout": text(paths[10])[-8000:], "stderr": text(paths[11])[-4000:]},
"cleanup": {"exitCode": cleanup_rc, "summary": parsed(paths[12]), "stdout": text(paths[12])[-8000:], "stderr": text(paths[13])[-4000:]},
"localHttpsExposure": {"exitCode": local_https_rc, "action": local_https_action, "stdout": text(paths[14])[-4000:], "stderr": text(paths[15])[-4000:]},
},
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
+51 -4
View File
@@ -169,6 +169,7 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
const redisMode = enumField(record, "redisMode", path, ["bundled-persistent", "local-ephemeral"] as const);
const appReplicas = integerField(record, "appReplicas", path);
const redisReplicas = integerField(record, "redisReplicas", path);
const runtime = parseTargetRuntime(record.runtime, path);
const image = targetImageOverride(record, path);
const dependencyImages = targetDependencyImageOverride(record, path);
const hostDocker = parseHostDockerConfig(record.hostDocker, path, runtimeMode);
@@ -184,7 +185,8 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
if (runtimeMode === "host-docker" && egressProxy?.enabled === true) throw new Error(`${configPath}.${path}.egressProxy must be disabled or omitted for runtimeMode=host-docker`);
if (runtimeMode === "host-docker" && accountLocalProxy?.enabled === true) throw new Error(`${configPath}.${path}.accountLocalProxy must be disabled or omitted for runtimeMode=host-docker`);
if (runtimeMode === "host-docker" && publicExposure?.enabled === true && publicExposure.mode !== "pk01-local") throw new Error(`${configPath}.${path}.publicExposure.mode must be pk01-local for runtimeMode=host-docker`);
return { id, route, namespace: targetNamespace, runtimeMode, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, image, dependencyImages, hostDocker, publicExposure, egressProxy, accountLocalProxy };
if (runtimeMode === "k3s" && publicExposure?.enabled === true && publicExposure.mode === "pk01-local") throw new Error(`${configPath}.${path}.publicExposure.mode=pk01-local is only supported for runtimeMode=host-docker`);
return { id, route, namespace: targetNamespace, runtimeMode, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, runtime, image, dependencyImages, hostDocker, publicExposure, egressProxy, accountLocalProxy };
});
const ids = new Set<string>();
for (const target of targets) {
@@ -195,6 +197,15 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
return targets;
}
export function parseTargetRuntime(value: unknown, path: string): Sub2ApiTargetConfig["runtime"] {
if (value === undefined || value === null) return { autoSetup: true };
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.runtime must be an object`);
const record = value as Record<string, unknown>;
return {
autoSetup: record.autoSetup === undefined ? true : booleanField(record, "autoSetup", `${path}.runtime`),
};
}
export function parseHostDockerConfig(value: unknown, path: string, runtimeMode: Sub2ApiTargetConfig["runtimeMode"]): Sub2ApiHostDockerConfig | null {
if (value === undefined || value === null) {
if (runtimeMode === "host-docker") throw new Error(`${configPath}.${path}.hostDocker is required when runtimeMode=host-docker`);
@@ -268,17 +279,20 @@ export function parsePublicExposureConfig(value: unknown, path: string): Sub2Api
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.publicExposure must be an object`);
const record = value as Record<string, unknown>;
const enabled = booleanField(record, "enabled", `${path}.publicExposure`);
const mode = record.mode === undefined ? "frp" : enumField(record, "mode", `${path}.publicExposure`, ["frp", "pk01-local"] as const);
const mode = record.mode === undefined ? "frp" : enumField(record, "mode", `${path}.publicExposure`, ["frp", "pk01-local", "node-local-https"] as const);
const publicBaseUrl = stringField(record, "publicBaseUrl", `${path}.publicExposure`);
const dnsRaw = objectField(record, "dns", `${path}.publicExposure`);
const localRaw = record.local === undefined || record.local === null ? null : objectField(record, "local", `${path}.publicExposure`);
const localHttpsRaw = record.localHttps === undefined || record.localHttps === null ? null : objectField(record, "localHttps", `${path}.publicExposure`);
const frpcRaw = record.frpc === undefined || record.frpc === null ? null : objectField(record, "frpc", `${path}.publicExposure`);
const pk01Raw = objectField(record, "pk01", `${path}.publicExposure`);
const pk01Raw = record.pk01 === undefined || record.pk01 === null ? null : objectField(record, "pk01", `${path}.publicExposure`);
const hostname = stringField(dnsRaw, "hostname", `${path}.publicExposure.dns`);
const expectedA = stringField(dnsRaw, "expectedA", `${path}.publicExposure.dns`);
const resolvers = stringArrayField(dnsRaw, "resolvers", `${path}.publicExposure.dns`);
if (mode === "frp" && frpcRaw === null) throw new Error(`${configPath}.${path}.publicExposure.frpc is required when mode=frp`);
if (mode === "pk01-local" && localRaw === null) throw new Error(`${configPath}.${path}.publicExposure.local is required when mode=pk01-local`);
if (mode === "node-local-https" && localHttpsRaw === null) throw new Error(`${configPath}.${path}.publicExposure.localHttps is required when mode=node-local-https`);
if ((mode === "frp" || mode === "pk01-local") && pk01Raw === null) throw new Error(`${configPath}.${path}.publicExposure.pk01 is required when mode=${mode}`);
const exposure: Sub2ApiPublicExposureConfig = {
enabled,
mode,
@@ -292,6 +306,23 @@ export function parsePublicExposureConfig(value: unknown, path: string): Sub2Api
upstreamHost: stringField(localRaw, "upstreamHost", `${path}.publicExposure.local`),
upstreamPort: integerField(localRaw, "upstreamPort", `${path}.publicExposure.local`),
},
localHttps: localHttpsRaw === null ? null : {
route: stringField(localHttpsRaw, "route", `${path}.publicExposure.localHttps`),
upstreamMode: enumField(localHttpsRaw, "upstreamMode", `${path}.publicExposure.localHttps`, ["kubernetes-service-cluster-ip"] as const),
workDir: stringField(localHttpsRaw, "workDir", `${path}.publicExposure.localHttps`),
composePath: stringField(localHttpsRaw, "composePath", `${path}.publicExposure.localHttps`),
projectName: stringField(localHttpsRaw, "projectName", `${path}.publicExposure.localHttps`),
serviceName: stringField(localHttpsRaw, "serviceName", `${path}.publicExposure.localHttps`),
containerName: stringField(localHttpsRaw, "containerName", `${path}.publicExposure.localHttps`),
image: stringField(localHttpsRaw, "image", `${path}.publicExposure.localHttps`),
hostBind: stringField(localHttpsRaw, "hostBind", `${path}.publicExposure.localHttps`),
httpsPort: integerField(localHttpsRaw, "httpsPort", `${path}.publicExposure.localHttps`),
caddyfilePath: stringField(localHttpsRaw, "caddyfilePath", `${path}.publicExposure.localHttps`),
dataDir: stringField(localHttpsRaw, "dataDir", `${path}.publicExposure.localHttps`),
configDir: stringField(localHttpsRaw, "configDir", `${path}.publicExposure.localHttps`),
tlsMode: enumField(localHttpsRaw, "tlsMode", `${path}.publicExposure.localHttps`, ["internal"] as const),
responseHeaderTimeoutSeconds: integerField(localHttpsRaw, "responseHeaderTimeoutSeconds", `${path}.publicExposure.localHttps`),
},
frpc: frpcRaw === null ? null : {
deploymentName: stringField(frpcRaw, "deploymentName", `${path}.publicExposure.frpc`),
secretName: stringField(frpcRaw, "secretName", `${path}.publicExposure.frpc`),
@@ -306,7 +337,7 @@ export function parsePublicExposureConfig(value: unknown, path: string): Sub2Api
tokenSourceRef: stringField(frpcRaw, "tokenSourceRef", `${path}.publicExposure.frpc`),
tokenSourceKey: stringField(frpcRaw, "tokenSourceKey", `${path}.publicExposure.frpc`),
},
pk01: {
pk01: pk01Raw === null ? null : {
route: stringField(pk01Raw, "route", `${path}.publicExposure.pk01`),
caddyBinaryPath: stringField(pk01Raw, "caddyBinaryPath", `${path}.publicExposure.pk01`),
caddyDownloadUrl: stringField(pk01Raw, "caddyDownloadUrl", `${path}.publicExposure.pk01`),
@@ -572,6 +603,22 @@ export function validatePublicExposureConfig(config: Sub2ApiPublicExposureConfig
validateHostOrIp(config.local.upstreamHost, `${path}.publicExposure.local.upstreamHost`);
validatePort(config.local.upstreamPort, `${path}.publicExposure.local.upstreamPort`);
}
if (config.mode === "node-local-https") {
const localHttps = config.localHttps;
if (localHttps === null) throw new Error(`${configPath}.${path}.publicExposure.localHttps is required when mode=node-local-https`);
if (!/^[A-Za-z0-9:_./-]+$/u.test(localHttps.route)) throw new Error(`${configPath}.${path}.publicExposure.localHttps.route has an unsupported format`);
for (const key of ["workDir", "composePath", "caddyfilePath", "dataDir", "configDir"] as const) {
if (!localHttps[key].startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.localHttps.${key} must be absolute`);
}
validateProxyName(localHttps.projectName, `${path}.publicExposure.localHttps.projectName`);
validateProxyName(localHttps.serviceName, `${path}.publicExposure.localHttps.serviceName`);
validateProxyName(localHttps.containerName, `${path}.publicExposure.localHttps.containerName`);
if (!isImageReference(localHttps.image)) throw new Error(`${configPath}.${path}.publicExposure.localHttps.image has an unsupported format`);
validateHostOrIp(localHttps.hostBind, `${path}.publicExposure.localHttps.hostBind`);
validatePort(localHttps.httpsPort, `${path}.publicExposure.localHttps.httpsPort`);
if (localHttps.responseHeaderTimeoutSeconds < 1) throw new Error(`${configPath}.${path}.publicExposure.localHttps.responseHeaderTimeoutSeconds must be >= 1`);
}
if (config.pk01 === null) return;
if (!/^[A-Za-z0-9:_./-]+$/u.test(config.pk01.route)) throw new Error(`${configPath}.${path}.publicExposure.pk01.route has an unsupported format`);
if (!config.pk01.caddyBinaryPath.startsWith("/")) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyBinaryPath must be absolute`);
if (!/^https:\/\//u.test(config.pk01.caddyDownloadUrl)) throw new Error(`${configPath}.${path}.publicExposure.pk01.caddyDownloadUrl must use https://`);
+22 -2
View File
@@ -119,6 +119,9 @@ export interface Sub2ApiTargetConfig {
redisMode: RedisMode;
appReplicas: number;
redisReplicas: number;
runtime: {
autoSetup: boolean;
};
image: Partial<Sub2ApiConfig["image"]>;
dependencyImages: Partial<Sub2ApiConfig["dependencyImages"]>;
hostDocker: Sub2ApiHostDockerConfig | null;
@@ -142,7 +145,7 @@ export interface Sub2ApiHostDockerConfig {
export interface Sub2ApiPublicExposureConfig {
enabled: boolean;
mode: "frp" | "pk01-local";
mode: "frp" | "pk01-local" | "node-local-https";
publicBaseUrl: string;
dns: {
hostname: string;
@@ -153,6 +156,23 @@ export interface Sub2ApiPublicExposureConfig {
upstreamHost: string;
upstreamPort: number;
} | null;
localHttps: {
route: string;
upstreamMode: "kubernetes-service-cluster-ip";
workDir: string;
composePath: string;
projectName: string;
serviceName: string;
containerName: string;
image: string;
hostBind: string;
httpsPort: number;
caddyfilePath: string;
dataDir: string;
configDir: string;
tlsMode: "internal";
responseHeaderTimeoutSeconds: number;
} | null;
frpc: {
deploymentName: string;
secretName: string;
@@ -181,7 +201,7 @@ export interface Sub2ApiPublicExposureConfig {
pikanodeImage: string;
pikanodeHttpHostPort: number;
responseHeaderTimeoutSeconds: number;
};
} | null;
}
export interface Sub2ApiEgressProxyConfig {
+1 -1
View File
@@ -68,7 +68,7 @@ export function hostDockerEnvFile(sub2api: Sub2ApiConfig, target: Sub2ApiTargetC
const database = targetDatabase(sub2api, target);
const urlAllowlist = sub2api.security.urlAllowlist;
const values: Record<string, string> = {
AUTO_SETUP: "true",
AUTO_SETUP: String(target.runtime.autoSetup),
SERVER_HOST: "127.0.0.1",
SERVER_PORT: String(host.appPort),
SERVER_MODE: "release",
+76 -1
View File
@@ -217,6 +217,78 @@ export function codexPoolSentinelResourceNames(): ManagedResourceCleanupPlan["se
};
}
function renderRuntimeConfigInitContainer(
target: Sub2ApiTargetConfig,
database: ExternalDatabaseConfig,
dependencyImages: Sub2ApiConfig["dependencyImages"],
): string {
if (target.runtime.autoSetup) return "";
return ` - name: write-runtime-config
image: ${dependencyImages.postgres}
imagePullPolicy: IfNotPresent
command:
- sh
- -ec
- |
set -eu
yaml_quote() {
value=$(printf '%s' "$1" | sed "s/'/''/g")
printf "'%s'" "$value"
}
mkdir -p /app/data
{
printf '%s\\n' 'server:'
printf ' host: %s\\n' "$(yaml_quote "$SERVER_HOST")"
printf ' port: %s\\n' "$SERVER_PORT"
printf ' mode: %s\\n' "$(yaml_quote "$SERVER_MODE")"
printf '%s\\n' 'database:'
printf ' host: %s\\n' "$(yaml_quote "$DATABASE_HOST")"
printf ' port: %s\\n' "$DATABASE_PORT"
printf ' user: %s\\n' "$(yaml_quote "$DATABASE_USER")"
printf ' password: %s\\n' "$(yaml_quote "$DATABASE_PASSWORD")"
printf ' dbname: %s\\n' "$(yaml_quote "$DATABASE_DBNAME")"
printf ' sslmode: %s\\n' "$(yaml_quote "$DATABASE_SSLMODE")"
printf '%s\\n' 'redis:'
printf ' host: %s\\n' "$(yaml_quote "$REDIS_HOST")"
printf ' port: %s\\n' "$REDIS_PORT"
printf ' password: %s\\n' "$(yaml_quote "$REDIS_PASSWORD")"
printf ' db: %s\\n' "$REDIS_DB"
printf ' enable_tls: %s\\n' "$REDIS_ENABLE_TLS"
printf '%s\\n' 'jwt:'
printf ' secret: %s\\n' "$(yaml_quote "$JWT_SECRET")"
printf ' expire_hour: %s\\n' "$JWT_EXPIRE_HOUR"
printf '%s\\n' 'default:'
printf '%s\\n' ' user_concurrency: 5'
printf '%s\\n' ' user_balance: 0'
printf '%s\\n' " api_key_prefix: 'sk-'"
printf '%s\\n' ' rate_multiplier: 1'
printf '%s\\n' 'rate_limit:'
printf '%s\\n' ' requests_per_minute: 60'
printf '%s\\n' ' burst_size: 10'
printf 'timezone: %s\\n' "$(yaml_quote "$TZ")"
} > /app/data/config.yaml
chmod 600 /app/data/config.yaml
chown -R 1000:1000 /app/data 2>/dev/null || true
envFrom:
- configMapRef:
name: sub2api-config
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: ${database.secretName}
key: ${database.passwordKey}
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: ${database.secretName}
key: JWT_SECRET
volumeMounts:
- name: sub2api-data
mountPath: /app/data
`;
}
export function externalPendingManifest(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
const urlAllowlist = sub2api.security.urlAllowlist;
const database = targetDatabase(sub2api, target);
@@ -229,6 +301,7 @@ export function externalPendingManifest(sub2api: Sub2ApiConfig, target: Sub2ApiT
const publicExposure = renderPublicExposureManifest(target);
const egressProxy = renderEgressProxyManifest(target);
const proxyEnv = sub2ApiProxyEnv(target);
const runtimeConfigInitContainer = renderRuntimeConfigInitContainer(target, database, dependencyImages);
const accountLocalProxyContainer = renderAccountLocalProxyContainer(target.accountLocalProxy);
const accountLocalProxyVolume = renderAccountLocalProxyVolume(target.accountLocalProxy);
return `apiVersion: v1
@@ -270,7 +343,7 @@ metadata:
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
data:
AUTO_SETUP: "true"
AUTO_SETUP: "${target.runtime.autoSetup}"
SERVER_HOST: "0.0.0.0"
SERVER_PORT: "8080"
SERVER_MODE: "release"
@@ -465,6 +538,7 @@ spec:
securityContext:
fsGroup: 1000
initContainers:
${runtimeConfigInitContainer}
- name: wait-postgres
image: ${dependencyImages.postgres}
imagePullPolicy: IfNotPresent
@@ -587,6 +661,7 @@ export function renderPublicExposureManifest(target: Sub2ApiTargetConfig): strin
const exposure = target.publicExposure;
if (exposure === null || !exposure.enabled) return "";
if (exposure.mode === "pk01-local") return "";
if (exposure.mode === "node-local-https") return "";
if (exposure.frpc === null) throw new Error(`publicExposure.frpc is required for target ${target.id} when mode=frp`);
return `---
apiVersion: apps/v1
+3
View File
@@ -138,6 +138,7 @@ function renderSub2ApiPlan(result: Record<string, unknown>): RenderedCliResult {
["TARGET", stringValue(target.id), "route", stringValue(target.route)],
["NAMESPACE", stringValue(target.namespace), "role", stringValue(target.role)],
["DATABASE", stringValue(config.databaseMode), "redis", stringValue(config.redisMode)],
["AUTO_SETUP", boolText(record(config.runtime).autoSetup), "mode", stringValue(config.runtimeMode)],
["REPLICAS", `${stringValue(config.appReplicas)}/${stringValue(config.redisReplicas)}`, "service", stringValue(target.serviceDns)],
["PUBLIC", boolText(exposure.enabled), "url", stringValue(exposure.publicBaseUrl, "-")],
["EGRESS", boolText(egress.enabled), "source", `${stringValue(egress.sourceType, "-")} ${stringValue(egress.sourceFingerprint, "-")}`],
@@ -163,6 +164,7 @@ function renderSub2ApiStatus(result: Record<string, unknown>): RenderedCliResult
const summary = record(result.summary);
const secret = record(summary.secret);
const egress = record(summary.egressProxy);
const localHttps = record(summary.localHttpsExposure);
const networkPolicy = record(summary.networkPolicy);
const checks = record(summary.checks);
const isHostDocker = stringValue(summary.runtimeMode, "") === "host-docker";
@@ -214,6 +216,7 @@ function renderSub2ApiStatus(result: Record<string, unknown>): RenderedCliResult
["namespace", boolText(summary.namespaceExists), stringValue(summary.namespace)],
["secret", boolText(secret.ready), `name=${stringValue(secret.name, "-")} missing=${arrayValues(secret.missingKeys).length}`],
["egressProxy", boolText(egress.aligned), `enabled=${boolText(egress.enabled)} valuesPrinted=false`],
["localHttps", boolText(localHttps.ok), stringValue(localHttps.publicBaseUrl, "not-enabled")],
["networkPolicy", boolText(networkPolicy.ok), `required=${stringValue(networkPolicy.requiredName, "-")}`],
]),
"",
@@ -219,6 +219,8 @@ export function redactSubscriptionUri(uri: string): string {
export async function applyPk01PublicExposure(config: UniDeskConfig, target: Sub2ApiTargetConfig): Promise<Record<string, unknown>> {
const exposure = target.publicExposure;
if (exposure === null) return { ok: true, action: "not-configured" };
if (exposure.mode === "node-local-https") return { ok: true, action: "not-applicable-node-local-https" };
if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when applying PK01 public exposure`);
if (!exposure.enabled) return await removePk01PublicExposure(config, target, exposure);
const start = await startPk01PublicExposureJob(config, target, exposure);
if (!start.ok || typeof start.remoteJobId !== "string") {
@@ -247,6 +249,7 @@ export async function applyPk01PublicExposure(config: UniDeskConfig, target: Sub
}
export async function removePk01PublicExposure(config: UniDeskConfig, target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): Promise<Record<string, unknown>> {
if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when removing PK01 public exposure`);
const marker = sub2apiCaddyManagedMarker(target);
const script = `
set -u
@@ -380,6 +383,7 @@ PY
}
export async function startPk01PublicExposureJob(config: UniDeskConfig, target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): Promise<Record<string, unknown>> {
if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when starting PK01 public exposure`);
const jobId = `sub2api-pk01-exposure-${new Date().toISOString().replace(/[^0-9A-Za-z]/gu, "")}-${randomBytes(3).toString("hex")}`;
const payload = pk01PublicExposureScript(target, exposure);
const payloadB64 = Buffer.from(payload, "utf8").toString("base64");
@@ -483,6 +487,7 @@ PY
}
export async function waitPk01PublicExposureJob(config: UniDeskConfig, exposure: Sub2ApiPublicExposureConfig, remoteJobId: string): Promise<Record<string, unknown>> {
if (exposure.pk01 === null) throw new Error("publicExposure.pk01 is required when waiting for PK01 public exposure");
const observations: Array<Record<string, unknown>> = [];
const maxPolls = 120;
let lastProgressKey: string | null = null;
@@ -524,6 +529,7 @@ export async function waitPk01PublicExposureJob(config: UniDeskConfig, exposure:
}
export async function readPk01PublicExposureJob(config: UniDeskConfig, exposure: Sub2ApiPublicExposureConfig, remoteJobId: string): Promise<{ status: string | null; summary: Record<string, unknown> }> {
if (exposure.pk01 === null) throw new Error("publicExposure.pk01 is required when reading PK01 public exposure job");
const safeId = remoteJobId.replace(/[^A-Za-z0-9_.-]/gu, "");
const script = `
set +e
@@ -569,6 +575,7 @@ if [ -f "$download_cache" ]; then stat -c 'cache_bytes=%s cache_mtime=%y' "$down
}
export function pk01PublicExposureScript(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
if (exposure.pk01 === null) throw new Error(`publicExposure.pk01 is required for target ${target.id} when rendering PK01 public exposure script`);
const caddyfile = renderPk01Caddyfile(target, exposure);
const serviceUnit = renderPk01CaddyService(exposure);
const exposureMode = exposure.mode === "pk01-local" ? "pk01-caddy-local-docker" : "pk01-caddy-frp-direct";
@@ -96,6 +96,7 @@ export function preparePublicExposureSecret(sub2api: Sub2ApiConfig, target: Sub2
const exposure = target.publicExposure;
if (exposure === null || !exposure.enabled) return null;
if (exposure.mode === "pk01-local") return null;
if (exposure.mode === "node-local-https") return null;
if (exposure.frpc === null) throw new Error(`publicExposure.frpc is required for target ${target.id} when mode=frp`);
return prepareFrpcSecret({
secretRoot: secretRoot(sub2api),
+45 -3
View File
@@ -24,6 +24,8 @@ export function statusScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig
const externalPending = target.databaseMode === "external-pending";
const externalActive = target.databaseMode === "external-active";
const proxy = target.egressProxy?.enabled ? target.egressProxy : null;
const localHttps = target.publicExposure?.enabled === true && target.publicExposure.mode === "node-local-https" ? target.publicExposure.localHttps : null;
const localHttpsExposure = localHttps === null ? null : target.publicExposure;
const expectedProxyEnv = sub2ApiProxyEnv(target);
const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
const appSecretName = sub2api.runtime.database.secretName;
@@ -52,6 +54,32 @@ capture_json cronjobs kubectl -n ${target.namespace} get cronjob -l app.kubernet
capture_json ingresses kubectl -n ${target.namespace} get ingress
capture_json quotas kubectl -n ${target.namespace} get resourcequota
capture_json limitranges kubectl -n ${target.namespace} get limitrange
local_https_rc=0
local_https_ps_rc=0
local_https_probe_rc=0
local_https_action="not-enabled"
local_https_ps=""
local_https_probe_url=""
if [ "${localHttps === null ? "false" : "true"}" = "true" ]; then
local_https_action="inspect"
local_https_ps="$(docker ps --filter ${localHttps === null ? "\"\"" : shQuote(`name=^/${localHttps.containerName}$`)} --format '{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>"$tmp/local-https-ps.err" || true)"
if [ -n "$local_https_ps" ]; then local_https_ps_rc=0; else local_https_ps_rc=1; fi
printf '%s' "$local_https_ps" >"$tmp/local-https-ps.out"
local_https_probe_url=${localHttpsExposure === null ? "\"\"" : shQuote(`${localHttpsExposure.publicBaseUrl}/health`)}
curl --noproxy '*' -kfsS --connect-timeout 5 --max-time 20 --resolve ${localHttpsExposure === null || localHttps === null ? "\"\"" : shQuote(`${localHttpsExposure.dns.hostname}:${localHttps.httpsPort}:127.0.0.1`)} "$local_https_probe_url" >"$tmp/local-https-probe.out" 2>"$tmp/local-https-probe.err"
local_https_probe_rc=$?
if [ "$local_https_ps_rc" -ne 0 ] || [ "$local_https_probe_rc" -ne 0 ]; then local_https_rc=1; fi
else
: >"$tmp/local-https-ps.out"
: >"$tmp/local-https-ps.err"
: >"$tmp/local-https-probe.out"
: >"$tmp/local-https-probe.err"
fi
printf '%s' "$local_https_rc" >"$tmp/local-https.rc"
printf '%s' "$local_https_ps_rc" >"$tmp/local-https-ps.rc"
printf '%s' "$local_https_probe_rc" >"$tmp/local-https-probe.rc"
printf '%s' "$local_https_action" >"$tmp/local-https-action.txt"
printf '%s' "$local_https_probe_url" >"$tmp/local-https-probe-url.txt"
pod_name="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${serviceName},app.kubernetes.io/component=app -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pod-name.err" || true)"
printf '%s' "$pod_name" >"$tmp/pod-name.txt"
if [ -n "$pod_name" ]; then
@@ -90,10 +118,11 @@ def items(name):
return []
return data.get("items") or []
def text(name):
def text(name, limit=None):
path = os.path.join(tmp, name)
try:
return open(path, encoding="utf-8", errors="replace").read()
data = open(path, encoding="utf-8", errors="replace").read()
return data if limit is None else data[-limit:]
except FileNotFoundError:
return ""
@@ -383,7 +412,7 @@ else:
state_model_ok = local_postgres_present and sub2api_desired_aligned and redis_desired_aligned
status_label = "pending-external-db" if external_pending else "external-db-active" if external_active else "active"
payload = {
"ok": rc("ns") == 0 and workload_ready and image_aligned and url_allowlist_aligned and proxy_env_aligned and network_policy["ok"] and boundary["internalOnly"] and len(resource_violations) == 0 and boundary["resourceQuotaCount"] == 0 and boundary["limitRangeCount"] == 0 and secret_ok and state_model_ok,
"ok": rc("ns") == 0 and workload_ready and image_aligned and url_allowlist_aligned and proxy_env_aligned and network_policy["ok"] and boundary["internalOnly"] and len(resource_violations) == 0 and boundary["resourceQuotaCount"] == 0 and boundary["limitRangeCount"] == 0 and secret_ok and state_model_ok and rc("local-https") == 0,
"target": "${target.id}",
"route": "${target.route}",
"namespace": "${target.namespace}",
@@ -399,6 +428,19 @@ payload = {
"services": [service_summary(item) for item in services],
"pvcs": [pvc_summary(item) for item in pvcs],
"networkPolicy": network_policy,
"localHttpsExposure": {
"enabled": ${localHttps === null ? "False" : "True"},
"action": text("local-https-action.txt"),
"ok": rc("local-https") == 0,
"mode": ${localHttps === null ? "None" : JSON.stringify("node-local-docker-caddy")},
"publicBaseUrl": ${localHttpsExposure === null ? "None" : JSON.stringify(localHttpsExposure.publicBaseUrl)},
"hostname": ${localHttpsExposure === null ? "None" : JSON.stringify(localHttpsExposure.dns.hostname)},
"bind": ${localHttps === null ? "None" : JSON.stringify(`${localHttps.hostBind}:${localHttps.httpsPort}`)},
"containerName": ${localHttps === null ? "None" : JSON.stringify(localHttps.containerName)},
"ps": {"exitCode": rc("local-https-ps"), "stdout": text("local-https-ps.out"), "stderr": text("local-https-ps.err")},
"probe": {"exitCode": rc("local-https-probe"), "url": text("local-https-probe-url.txt"), "stdout": text("local-https-probe.out", 2000), "stderr": text("local-https-probe.err", 2000)},
"valuesPrinted": False,
},
"secret": {
"name": "${appSecretName}",
"exists": rc("secrets") == 0,
@@ -196,7 +196,7 @@ public_dns_rc=1
done
} >"$tmp/public-dns.out" 2>"$tmp/public-dns.err"
if grep -Fx ${shQuote(exposure.dns.expectedA)} "$tmp/public-dns.out" >/dev/null 2>&1; then public_dns_rc=0; fi
curl -fsS --max-time 20 ${shQuote(`${exposure.publicBaseUrl}/health`)} >"$tmp/public-probe.out" 2>"$tmp/public-probe.err"
curl ${exposure.mode === "node-local-https" ? "--noproxy '*' -k" : ""} -fsS --max-time 20 ${shQuote(`${exposure.publicBaseUrl}/health`)} >"$tmp/public-probe.out" 2>"$tmp/public-probe.err"
public_probe_rc=$?
`;
const egressProbeBlock = proxy === null ? `
@@ -257,8 +257,8 @@ fi
"publicBaseUrl": "${exposure.publicBaseUrl}",
"hostname": "${exposure.dns.hostname}",
"expectedA": "${exposure.dns.expectedA}",
"mode": "pk01-caddy-frp-direct",
"dataPath": "client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API",
"mode": "${exposure.mode === "node-local-https" ? "node-local-docker-caddy" : "pk01-caddy-frp-direct"}",
"dataPath": "${exposure.mode === "node-local-https" ? `client -> ${target.id} local HTTPS port ${exposure.localHttps?.httpsPort} -> ${target.id} Docker Caddy -> Sub2API ClusterIP Service` : `client -> PK01 Caddy -> PK01 frps remotePort -> ${target.id} frpc -> Sub2API`}",
"dns": {
"exitCode": public_dns_rc,
"resolvers": ${JSON.stringify(exposure.dns.resolvers)},