468 lines
22 KiB
TypeScript
468 lines
22 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. validate-script module for scripts/src/platform-infra.ts.
|
|
|
|
// Moved mechanically from scripts/src/platform-infra.ts:3539-4147 for #903.
|
|
|
|
import { createHash, randomBytes } from "node:crypto";
|
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, isAbsolute, join } from "node:path";
|
|
import type { UniDeskConfig } from "../config";
|
|
import { rootPath } from "../config";
|
|
import { startJob } from "../jobs";
|
|
import type { RenderedCliResult } from "../output";
|
|
import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy";
|
|
import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service";
|
|
import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library";
|
|
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
|
|
|
|
import type { Sub2ApiConfig, Sub2ApiTargetConfig } from "./entry";
|
|
import { requiredSecretKeys, serviceName } from "./entry";
|
|
import { managedResourceCleanupPlan, targetDependencyImages } from "./manifest";
|
|
import { safeEgressProxySubscriptionDiagnostics } from "./secrets-and-egress";
|
|
|
|
export function validateScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
|
|
const cleanup = sub2api.defaults.cleanup;
|
|
const dependencyImages = targetDependencyImages(sub2api, target);
|
|
const postgresStatefulSetName = cleanup.externalDbState.postgresStatefulSetName;
|
|
const postgresServiceName = cleanup.externalDbState.postgresServiceName;
|
|
const redisService = sub2api.runtime.redis.serviceName;
|
|
return `
|
|
set -u
|
|
tmp="$(mktemp -d)"
|
|
probe_suffix="$(date +%s)-$$"
|
|
pg_probe="unidesk-sub2api-netcheck-pg-$probe_suffix"
|
|
redis_probe="unidesk-sub2api-netcheck-redis-$probe_suffix"
|
|
cleanup() {
|
|
kubectl -n ${target.namespace} delete pod "$pg_probe" "$redis_probe" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true
|
|
rm -rf "$tmp"
|
|
}
|
|
trap cleanup EXIT
|
|
kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/health >"$tmp/health.body" 2>"$tmp/health.err"
|
|
health_rc=$?
|
|
kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/ >"$tmp/root.body" 2>"$tmp/root.err"
|
|
root_rc=$?
|
|
kubectl -n ${target.namespace} get networkpolicy allow-all -o json >"$tmp/network-policy.json" 2>"$tmp/network-policy.err"
|
|
network_policy_rc=$?
|
|
pg_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${postgresStatefulSetName} -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pg-pod.err")"
|
|
redis_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${redisService} -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/redis-pod.err")"
|
|
if [ -n "$pg_pod" ]; then
|
|
kubectl -n ${target.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 ${target.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
|
|
if ! command -v timeout >/dev/null 2>&1; then
|
|
printf '%s\\n' 'timeout command is required for bounded cross-pod probes' >"$tmp/pg-cross.err"
|
|
printf '%s\\n' 'timeout command is required for bounded cross-pod probes' >"$tmp/redis-cross.err"
|
|
: >"$tmp/pg-cross.out"
|
|
: >"$tmp/redis-cross.out"
|
|
pg_cross_rc=127
|
|
redis_cross_rc=127
|
|
else
|
|
timeout 35s kubectl -n ${target.namespace} run "$pg_probe" --restart=Never --rm -i --image=${dependencyImages.postgres} --image-pull-policy=IfNotPresent --command -- pg_isready -h ${postgresServiceName} -U sub2api -d sub2api -t 5 >"$tmp/pg-cross.out" 2>"$tmp/pg-cross.err"
|
|
pg_cross_rc=$?
|
|
timeout 35s kubectl -n ${target.namespace} run "$redis_probe" --restart=Never --rm -i --image=${dependencyImages.redis} --image-pull-policy=IfNotPresent --command -- redis-cli -h ${redisService} -p 6379 ping >"$tmp/redis-cross.out" 2>"$tmp/redis-cross.err"
|
|
redis_cross_rc=$?
|
|
fi
|
|
python3 - "$tmp" "$health_rc" "$root_rc" "$pg_rc" "$redis_rc" "$network_policy_rc" "$pg_cross_rc" "$redis_cross_rc" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
tmp = sys.argv[1]
|
|
health_rc, root_rc, pg_rc, redis_rc, network_policy_rc, pg_cross_rc, redis_cross_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:]
|
|
|
|
def json_file(name):
|
|
path = os.path.join(tmp, name)
|
|
try:
|
|
return json.load(open(path, encoding="utf-8"))
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return None
|
|
|
|
def is_allow_all_network_policy(item):
|
|
if not isinstance(item, dict):
|
|
return False
|
|
spec = item.get("spec") or {}
|
|
return (
|
|
item.get("metadata", {}).get("name") == "allow-all"
|
|
and spec.get("podSelector") == {}
|
|
and set(spec.get("policyTypes") or []) == {"Ingress", "Egress"}
|
|
and spec.get("ingress") == [{}]
|
|
and spec.get("egress") == [{}]
|
|
)
|
|
|
|
health_body = text("health.body", 2000)
|
|
root_body = text("root.body", 2000)
|
|
network_policy_obj = json_file("network-policy.json")
|
|
network_policy_ok = network_policy_rc == 0 and is_allow_all_network_policy(network_policy_obj)
|
|
payload = {
|
|
"ok": health_rc == 0 and root_rc == 0 and pg_rc == 0 and redis_rc == 0 and network_policy_ok and pg_cross_rc == 0 and redis_cross_rc == 0,
|
|
"target": "${target.id}",
|
|
"namespace": "${target.namespace}",
|
|
"serviceDns": "${serviceName}.${target.namespace}.svc.cluster.local:8080",
|
|
"checks": {
|
|
"allowAllNetworkPolicy": {
|
|
"exitCode": network_policy_rc,
|
|
"ok": network_policy_ok,
|
|
"method": "kubectl -n ${target.namespace} get networkpolicy allow-all -o json",
|
|
"policyTypes": ((network_policy_obj or {}).get("spec") or {}).get("policyTypes") if isinstance(network_policy_obj, dict) else None,
|
|
"podSelector": ((network_policy_obj or {}).get("spec") or {}).get("podSelector") if isinstance(network_policy_obj, dict) else None,
|
|
"ingress": ((network_policy_obj or {}).get("spec") or {}).get("ingress") if isinstance(network_policy_obj, dict) else None,
|
|
"egress": ((network_policy_obj or {}).get("spec") or {}).get("egress") if isinstance(network_policy_obj, dict) else None,
|
|
"stderr": text("network-policy.err", 2000),
|
|
},
|
|
"sub2apiHealthViaKubernetesServiceProxy": {
|
|
"exitCode": health_rc,
|
|
"method": "kubectl get --raw /api/v1/namespaces/${target.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/${target.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),
|
|
},
|
|
"postgresCrossPodPgIsReady": {
|
|
"exitCode": pg_cross_rc,
|
|
"method": "temporary ${dependencyImages.postgres} pod pg_isready -h ${postgresServiceName} -U sub2api -d sub2api -t 5, bounded by outer timeout",
|
|
"stdout": text("pg-cross.out", 2000),
|
|
"stderr": text("pg-cross.err", 2000),
|
|
},
|
|
"redisPing": {
|
|
"exitCode": redis_rc,
|
|
"stdout": text("redis.out", 2000),
|
|
"stderr": text("redis.err", 2000),
|
|
},
|
|
"redisCrossPodPing": {
|
|
"exitCode": redis_cross_rc,
|
|
"method": "temporary ${dependencyImages.redis} pod redis-cli -h ${redisService} -p 6379 ping, bounded by outer timeout",
|
|
"stdout": text("redis-cross.out", 2000),
|
|
"stderr": text("redis-cross.err", 2000),
|
|
},
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|
|
|
|
export function validateExternalActiveScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
|
|
const database = sub2api.runtime.database;
|
|
const redisService = sub2api.runtime.redis.serviceName;
|
|
const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
|
|
const exposure = target.publicExposure?.enabled ? target.publicExposure : null;
|
|
const proxy = target.egressProxy?.enabled ? target.egressProxy : null;
|
|
const proxyUrl = proxy === null ? "" : `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
|
|
const egressSubscriptionDiagnostics = proxy === null ? null : safeEgressProxySubscriptionDiagnostics(sub2api, proxy);
|
|
const egressSubscriptionJson = egressSubscriptionDiagnostics === null ? "None" : `json.loads(${JSON.stringify(JSON.stringify(egressSubscriptionDiagnostics))})`;
|
|
const egressSubscriptionOkExpr = egressSubscriptionDiagnostics === null ? "True" : (egressSubscriptionDiagnostics.ok ? "True" : "False");
|
|
const publicProbeBlock = exposure === null ? `
|
|
public_dns_rc=0
|
|
public_probe_rc=0
|
|
: >"$tmp/public-dns.out"
|
|
: >"$tmp/public-dns.err"
|
|
: >"$tmp/public-probe.out"
|
|
: >"$tmp/public-probe.err"
|
|
` : `
|
|
public_dns_rc=1
|
|
{
|
|
for resolver in ${exposure.dns.resolvers.map((resolver) => shQuote(resolver)).join(" ")}; do
|
|
printf 'resolver=%s\\n' "$resolver"
|
|
dig +short "@$resolver" ${shQuote(exposure.dns.hostname)} A || true
|
|
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"
|
|
public_probe_rc=$?
|
|
`;
|
|
const egressProbeBlock = proxy === null ? `
|
|
egress_deploy_rc=0
|
|
egress_svc_rc=0
|
|
egress_probe_rc=0
|
|
: >"$tmp/egress-deploy.json"
|
|
: >"$tmp/egress-deploy.err"
|
|
: >"$tmp/egress-svc.json"
|
|
: >"$tmp/egress-svc.err"
|
|
: >"$tmp/egress-probe.out"
|
|
: >"$tmp/egress-probe.err"
|
|
` : `
|
|
capture_json egress-deploy kubectl -n ${target.namespace} get deployment ${proxy.deploymentName}
|
|
egress_deploy_rc=$(cat "$tmp/egress-deploy.rc")
|
|
capture_json egress-svc kubectl -n ${target.namespace} get service ${proxy.serviceName}
|
|
egress_svc_rc=$(cat "$tmp/egress-svc.rc")
|
|
if [ -n "$app_pod" ]; then
|
|
kubectl -n ${target.namespace} exec "$app_pod" -- sh -c 'HTTP_PROXY="$1" HTTPS_PROXY="$1" ALL_PROXY="$1" NO_PROXY="$2" curl -fsS --max-time 20 -o /dev/null -w "status=%{http_code}\\n" "$3"' sh ${shQuote(proxyUrl)} ${shQuote(proxy.noProxy.join(","))} ${shQuote(proxy.healthProbeUrl)} >"$tmp/egress-probe.out" 2>"$tmp/egress-probe.err"
|
|
egress_probe_rc=$?
|
|
else
|
|
egress_probe_rc=1
|
|
printf '%s\\n' 'sub2api app pod not found' >"$tmp/egress-probe.err"
|
|
fi
|
|
`;
|
|
const egressProbeJson = proxy === null ? "None" : `{
|
|
"enabled": True,
|
|
"mode": "${proxy.sourceType === "master-shadowsocks" ? "master-shadowsocks-http-proxy" : "master-vpn-subscription-http-proxy"}",
|
|
"deploymentName": "${proxy.deploymentName}",
|
|
"serviceName": "${proxy.serviceName}",
|
|
"proxyUrl": "${proxyUrl}",
|
|
"healthProbeUrl": "${proxy.healthProbeUrl}",
|
|
"applyToSub2Api": ${proxy.applyToSub2Api ? "True" : "False"},
|
|
"applyToSentinel": ${proxy.applyToSentinel ? "True" : "False"},
|
|
"source": ${egressSubscriptionJson},
|
|
"deployment": {
|
|
"exitCode": egress_deploy_rc,
|
|
"ready": deployment_ready(load("egress-deploy")),
|
|
"stderr": text("egress-deploy.err", 2000),
|
|
},
|
|
"service": {
|
|
"exitCode": egress_svc_rc,
|
|
"type": ((load("egress-svc") or {}).get("spec") or {}).get("type"),
|
|
"clusterIP": ((load("egress-svc") or {}).get("spec") or {}).get("clusterIP"),
|
|
"stderr": text("egress-svc.err", 2000),
|
|
},
|
|
"probe": {
|
|
"exitCode": egress_probe_rc,
|
|
"failureFamily": egress_failure_family(egress_probe_rc, text("egress-probe.err", 2000), text("egress-probe.out", 2000)),
|
|
"stdout": text("egress-probe.out", 2000),
|
|
"stderr": text("egress-probe.err", 2000),
|
|
},
|
|
"valuesPrinted": False,
|
|
}`;
|
|
const egressProbeOkExpr = proxy === null ? "True" : `(egress_deploy_rc == 0 and egress_svc_rc == 0 and deployment_ready(load('egress-deploy')) and egress_probe_rc == 0 and ${egressSubscriptionOkExpr})`;
|
|
const publicProbeJson = exposure === null ? "None" : `{
|
|
"enabled": True,
|
|
"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",
|
|
"dns": {
|
|
"exitCode": public_dns_rc,
|
|
"resolvers": ${JSON.stringify(exposure.dns.resolvers)},
|
|
"stdout": text("public-dns.out", 2000),
|
|
"stderr": text("public-dns.err", 2000),
|
|
},
|
|
"health": {
|
|
"exitCode": public_probe_rc,
|
|
"url": "${exposure.publicBaseUrl}/health",
|
|
"stdout": text("public-probe.out", 2000),
|
|
"stderr": text("public-probe.err", 2000),
|
|
},
|
|
}`;
|
|
const publicProbeOkExpr = exposure === null ? "True" : "(public_dns_rc == 0 and public_probe_rc == 0)";
|
|
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 networkpolicy kubectl -n ${target.namespace} get networkpolicy allow-all
|
|
capture_json secret kubectl -n ${target.namespace} get secret ${database.secretName}
|
|
capture_json statefulsets kubectl -n ${target.namespace} get statefulsets
|
|
capture_json pvc kubectl -n ${target.namespace} get pvc
|
|
app_pod="$(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/app-pod.err" || true)"
|
|
redis_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${redisService},app.kubernetes.io/component=redis -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/redis-pod.err" || true)"
|
|
printf '%s' "$app_pod" >"$tmp/app-pod.txt"
|
|
printf '%s' "$redis_pod" >"$tmp/redis-pod.txt"
|
|
kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/health >"$tmp/health.body" 2>"$tmp/health.err"
|
|
health_rc=$?
|
|
kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/ >"$tmp/root.body" 2>"$tmp/root.err"
|
|
root_rc=$?
|
|
if [ -n "$app_pod" ]; then
|
|
kubectl -n ${target.namespace} exec "$app_pod" -- sh -c 'printf "DATABASE_HOST=%s\\n" "$DATABASE_HOST"; printf "DATABASE_PORT=%s\\n" "$DATABASE_PORT"; printf "DATABASE_USER=%s\\n" "$DATABASE_USER"; printf "DATABASE_DBNAME=%s\\n" "$DATABASE_DBNAME"; printf "DATABASE_SSLMODE=%s\\n" "$DATABASE_SSLMODE"; printf "REDIS_HOST=%s\\n" "$REDIS_HOST"; if [ -n "$DATABASE_PASSWORD" ]; then printf "DATABASE_PASSWORD_PRESENT=true\\n"; else printf "DATABASE_PASSWORD_PRESENT=false\\n"; fi' >"$tmp/app-env.out" 2>"$tmp/app-env.err"
|
|
printf '%s' "$?" >"$tmp/app-env.rc"
|
|
else
|
|
: >"$tmp/app-env.out"
|
|
printf '%s\\n' 'sub2api app pod not found' >"$tmp/app-env.err"
|
|
printf '%s' "1" >"$tmp/app-env.rc"
|
|
fi
|
|
if [ -n "$redis_pod" ]; then
|
|
kubectl -n ${target.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
|
|
${publicProbeBlock}
|
|
${egressProbeBlock}
|
|
python3 - "$tmp" "$health_rc" "$root_rc" "$redis_rc" "$public_dns_rc" "$public_probe_rc" "$egress_deploy_rc" "$egress_svc_rc" "$egress_probe_rc" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
tmp = sys.argv[1]
|
|
health_rc, root_rc, redis_rc, public_dns_rc, public_probe_rc, egress_deploy_rc, egress_svc_rc, egress_probe_rc = [int(value) for value in sys.argv[2:]]
|
|
|
|
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 text(name, limit=4000):
|
|
path = os.path.join(tmp, name)
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
|
|
def egress_failure_family(exit_code, stderr, stdout):
|
|
if exit_code == 0:
|
|
return "ok"
|
|
combined = f"{stderr}\\n{stdout}".lower()
|
|
if "connection refused" in combined or "could not connect" in combined:
|
|
return "proxy-unreachable-or-refused"
|
|
if "connection reset" in combined or "recv failure" in combined:
|
|
return "proxy-upstream-reset"
|
|
if "timed out" in combined or "timeout" in combined or "deadline exceeded" in combined:
|
|
return "proxy-upstream-timeout"
|
|
if "empty reply" in combined or "eof" in combined:
|
|
return "proxy-upstream-eof"
|
|
if "could not resolve" in combined or "no such host" in combined:
|
|
return "dns-failure"
|
|
if "status=502" in combined or "http 502" in combined:
|
|
return "upstream-http-502"
|
|
return "unknown"
|
|
|
|
def is_allow_all_network_policy(item):
|
|
if not isinstance(item, dict):
|
|
return False
|
|
spec = item.get("spec") or {}
|
|
return (
|
|
item.get("metadata", {}).get("name") == "allow-all"
|
|
and spec.get("podSelector") == {}
|
|
and set(spec.get("policyTypes") or []) == {"Ingress", "Egress"}
|
|
and spec.get("ingress") == [{}]
|
|
and spec.get("egress") == [{}]
|
|
)
|
|
|
|
def deployment_ready(item):
|
|
if not isinstance(item, dict):
|
|
return False
|
|
spec = item.get("spec") or {}
|
|
status = item.get("status") or {}
|
|
desired = spec.get("replicas", 1)
|
|
return status.get("availableReplicas", 0) >= desired
|
|
|
|
env = {}
|
|
for line in text("app-env.out").splitlines():
|
|
if "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
env[key] = value
|
|
|
|
network_policy_obj = load("networkpolicy")
|
|
network_policy_ok = rc("networkpolicy") == 0 and is_allow_all_network_policy(network_policy_obj)
|
|
secret = load("secret")
|
|
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]
|
|
cleanup_plan = json.loads(${JSON.stringify(JSON.stringify(cleanupPlan))})
|
|
external_db_state = cleanup_plan["externalDbState"]
|
|
redis_persistent_state = cleanup_plan["redisPersistentState"]
|
|
local_postgres_present = (
|
|
any(item.get("metadata", {}).get("name") == external_db_state["postgresStatefulSetName"] for item in items("statefulsets"))
|
|
or any(item.get("metadata", {}).get("name") in [external_db_state["postgresPvcName"], external_db_state["appDataPvcName"]] for item in items("pvc"))
|
|
)
|
|
redis_pvc_present = any(item.get("metadata", {}).get("name") == redis_persistent_state["pvcName"] for item in items("pvc"))
|
|
env_aligned = (
|
|
rc("app-env") == 0
|
|
and env.get("DATABASE_HOST") == "${database.host}"
|
|
and env.get("DATABASE_PORT") == "${database.port}"
|
|
and env.get("DATABASE_USER") == "${database.user}"
|
|
and env.get("DATABASE_DBNAME") == "${database.dbName}"
|
|
and env.get("DATABASE_SSLMODE") == "${database.sslMode}"
|
|
and env.get("REDIS_HOST") == "${redisService}"
|
|
and env.get("DATABASE_PASSWORD_PRESENT") == "true"
|
|
)
|
|
payload = {
|
|
"ok": health_rc == 0 and root_rc == 0 and redis_rc == 0 and network_policy_ok and len(missing_secret_keys) == 0 and env_aligned and not local_postgres_present and not redis_pvc_present and ${publicProbeOkExpr} and ${egressProbeOkExpr},
|
|
"target": "${target.id}",
|
|
"namespace": "${target.namespace}",
|
|
"status": "external-db-active",
|
|
"databaseMode": "${target.databaseMode}",
|
|
"externalDatabase": {
|
|
"host": "${database.host}",
|
|
"port": ${database.port},
|
|
"user": "${database.user}",
|
|
"dbName": "${database.dbName}",
|
|
"sslMode": "${database.sslMode}",
|
|
"secretName": "${database.secretName}",
|
|
"passwordKey": "${database.passwordKey}",
|
|
"passwordPrinted": False,
|
|
},
|
|
"publicExposure": ${publicProbeJson},
|
|
"egressProxy": ${egressProbeJson},
|
|
"checks": {
|
|
"allowAllNetworkPolicy": {"exitCode": rc("networkpolicy"), "ok": network_policy_ok, "stderr": text("networkpolicy.err", 2000)},
|
|
"secretReady": {"ok": len(missing_secret_keys) == 0, "missingKeys": missing_secret_keys, "valuesPrinted": False},
|
|
"sub2apiHealthViaKubernetesServiceProxy": {
|
|
"exitCode": health_rc,
|
|
"method": "kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/health",
|
|
"bodyPreview": text("health.body", 2000),
|
|
"stderr": text("health.err", 2000),
|
|
},
|
|
"sub2apiRootViaKubernetesServiceProxy": {
|
|
"exitCode": root_rc,
|
|
"method": "kubectl get --raw /api/v1/namespaces/${target.namespace}/services/${serviceName}:8080/proxy/",
|
|
"bodyBytes": len(text("root.body").encode("utf-8")),
|
|
"bodyPreview": text("root.body", 400),
|
|
"stderr": text("root.err", 2000),
|
|
},
|
|
"appEnvAligned": {
|
|
"ok": env_aligned,
|
|
"podName": text("app-pod.txt"),
|
|
"env": {key: env.get(key) for key in ["DATABASE_HOST", "DATABASE_PORT", "DATABASE_USER", "DATABASE_DBNAME", "DATABASE_SSLMODE", "REDIS_HOST", "DATABASE_PASSWORD_PRESENT"]},
|
|
"stderr": text("app-env.err", 2000),
|
|
},
|
|
"redisPing": {"exitCode": redis_rc, "podName": text("redis-pod.txt"), "stdout": text("redis.out", 2000), "stderr": text("redis.err", 2000)},
|
|
"externalStateOnly": {"ok": not local_postgres_present and not redis_pvc_present, "localPostgresPresent": local_postgres_present, "redisPvcPresent": redis_pvc_present},
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|