Files
pikasTech-unidesk/scripts/src/platform-infra/validate-script.ts
T
2026-06-25 16:16:25 +00:00

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
`;
}