Files
pikasTech-unidesk/scripts/src/platform-infra/host-docker.ts
T

594 lines
22 KiB
TypeScript

import { shQuote } from "../platform-infra-public-service";
import type { ExternalActiveSecretMaterial, Sub2ApiConfig, Sub2ApiHostDockerConfig, Sub2ApiTargetConfig } from "./entry";
import { requiredSecretKeys } from "./entry";
import { imageRef, targetDatabase, targetDependencyImages, targetImage } from "./manifest";
export function requireHostDockerConfig(target: Sub2ApiTargetConfig): Sub2ApiHostDockerConfig {
if (target.hostDocker === null) throw new Error(`target ${target.id} requires hostDocker config when runtimeMode=host-docker`);
return target.hostDocker;
}
export function hostDockerPlanSummary(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): Record<string, unknown> {
const host = requireHostDockerConfig(target);
const database = targetDatabase(sub2api, target);
return {
runtimeMode: "host-docker",
route: target.route,
projectName: host.projectName,
workDir: host.workDir,
composePath: host.composePath,
envPath: host.envPath,
appDataDir: host.appDataDir,
appPort: host.appPort,
redisPort: host.redisPort,
databaseHost: database.host,
databasePort: database.port,
databaseSslMode: database.sslMode,
noProxy: host.noProxy,
valuesPrinted: false,
};
}
export function hostDockerComposeYaml(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
const host = requireHostDockerConfig(target);
const dependencyImages = targetDependencyImages(sub2api, target);
return `services:
redis:
image: ${yamlString(dependencyImages.redis)}
container_name: ${yamlString(`${host.projectName}-redis`)}
network_mode: host
restart: unless-stopped
command:
- redis-server
- --save
- ""
- --appendonly
- "no"
- --bind
- "127.0.0.1"
- --port
- ${yamlString(String(host.redisPort))}
app:
image: ${yamlString(imageRef(sub2api, target))}
container_name: ${yamlString(`${host.projectName}-app`)}
network_mode: host
restart: unless-stopped
env_file:
- ${yamlString(host.envPath)}
volumes:
- ${yamlString(`${host.appDataDir}:/app/data`)}
depends_on:
- redis
`;
}
export function hostDockerEnvFile(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig, secretMaterial: ExternalActiveSecretMaterial): string {
const host = requireHostDockerConfig(target);
const database = targetDatabase(sub2api, target);
const urlAllowlist = sub2api.security.urlAllowlist;
const values: Record<string, string> = {
AUTO_SETUP: "true",
SERVER_HOST: "127.0.0.1",
SERVER_PORT: String(host.appPort),
SERVER_MODE: "release",
RUN_MODE: "standard",
DATABASE_HOST: database.host,
DATABASE_PORT: String(database.port),
DATABASE_USER: database.user,
DATABASE_DBNAME: database.dbName,
DATABASE_SSLMODE: database.sslMode,
DATABASE_PASSWORD: secretMaterial.values.POSTGRES_PASSWORD,
DATABASE_MAX_OPEN_CONNS: "10",
DATABASE_MAX_IDLE_CONNS: "2",
DATABASE_CONN_MAX_LIFETIME_MINUTES: "30",
DATABASE_CONN_MAX_IDLE_TIME_MINUTES: "5",
REDIS_HOST: "127.0.0.1",
REDIS_PORT: String(host.redisPort),
REDIS_PASSWORD: "",
REDIS_DB: "0",
REDIS_POOL_SIZE: "32",
REDIS_MIN_IDLE_CONNS: "2",
REDIS_ENABLE_TLS: "false",
ADMIN_EMAIL: "admin@sub2api.platform-infra.local",
ADMIN_PASSWORD: secretMaterial.values.ADMIN_PASSWORD,
JWT_SECRET: secretMaterial.values.JWT_SECRET,
TOTP_ENCRYPTION_KEY: secretMaterial.values.TOTP_ENCRYPTION_KEY,
JWT_EXPIRE_HOUR: "24",
TZ: "Asia/Shanghai",
SECURITY_URL_ALLOWLIST_ENABLED: String(urlAllowlist.enabled),
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP: String(urlAllowlist.allowInsecureHttp),
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS: String(urlAllowlist.allowPrivateHosts),
SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS: urlAllowlist.upstreamHosts.join(","),
UPDATE_PROXY_URL: "",
HTTP_PROXY: "",
HTTPS_PROXY: "",
ALL_PROXY: "",
http_proxy: "",
https_proxy: "",
all_proxy: "",
NO_PROXY: host.noProxy.join(","),
no_proxy: host.noProxy.join(","),
GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT: "0",
GATEWAY_OPENAI_HTTP2_ENABLED: "true",
GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1: "true",
GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD: "2",
GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS: "60",
GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS: "600",
GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT: "900",
GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL: "10",
GATEWAY_IMAGE_CONCURRENCY_ENABLED: "false",
GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS: "0",
GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE: "reject",
GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS: "30",
GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS: "100",
};
return `${Object.entries(values).map(([key, value]) => `${key}=${envFileValue(value)}`).join("\n")}\n`;
}
export function hostDockerDryRunScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
const host = requireHostDockerConfig(target);
const compose = hostDockerComposeYaml(sub2api, target);
const composeB64 = Buffer.from(compose, "utf8").toString("base64");
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
compose="$tmp/docker-compose.yml"
printf '%s' '${composeB64}' | base64 -d >"$compose"
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
compose_out="$tmp/compose.out"
compose_err="$tmp/compose.err"
ports_out="$tmp/ports.out"
ports_err="$tmp/ports.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
$docker_bin ps --format '{{json .}}' >"$docker_out" 2>"$docker_err"
docker_rc=$?
if $docker_bin compose version >"$compose_out" 2>"$compose_err"; then
compose_rc=0
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >"$compose_out" 2>"$compose_err"; then
compose_rc=0
else
compose_rc=0
printf '%s\\n' 'docker compose is absent; apply will use raw docker run fallback' >"$compose_out"
fi
else
docker_rc=1
compose_rc=1
: >"$docker_out"
printf '%s\\n' 'docker daemon is not accessible' >"$docker_err"
: >"$compose_out"
printf '%s\\n' 'docker compose is not accessible' >"$compose_err"
fi
{
for port in ${host.appPort} ${host.redisPort}; do
state="free"
if command -v ss >/dev/null 2>&1 && ss -ltn | awk '{print $4}' | grep -E "(:|\\])$port$" >/dev/null 2>&1; then
state="listening"
fi
printf 'port=%s state=%s\\n' "$port" "$state"
done
} >"$ports_out" 2>"$ports_err"
ports_rc=$?
python3 - "$docker_rc" "$compose_rc" "$ports_rc" "$docker_out" "$docker_err" "$compose_out" "$compose_err" "$ports_out" "$ports_err" <<'PY'
import json
import sys
docker_rc, compose_rc, ports_rc = [int(value) for value in sys.argv[1:4]]
paths = sys.argv[4:]
def text(path, limit=4000):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
payload = {
"ok": docker_rc == 0 and compose_rc == 0 and ports_rc == 0,
"target": "${target.id}",
"runtimeMode": "host-docker",
"route": "${target.route}",
"projectName": "${host.projectName}",
"composePath": "${host.composePath}",
"envPath": "${host.envPath}",
"appDataDir": "${host.appDataDir}",
"image": "${imageRef(sub2api, target)}",
"dependencyImages": ${JSON.stringify(targetDependencyImages(sub2api, target))},
"ports": {
"app": ${host.appPort},
"redis": ${host.redisPort},
"observed": text(paths[4], 2000),
},
"steps": {
"docker": {"exitCode": docker_rc, "stdout": text(paths[0]), "stderr": text(paths[1])},
"dockerCompose": {"exitCode": compose_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
"portObservation": {"exitCode": ports_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
export function hostDockerApplyScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig, secretMaterial: ExternalActiveSecretMaterial | null): string {
if (secretMaterial === null) throw new Error("host-docker apply requires external-active secret material");
const host = requireHostDockerConfig(target);
const compose = hostDockerComposeYaml(sub2api, target);
const env = hostDockerEnvFile(sub2api, target, secretMaterial);
const composeB64 = Buffer.from(compose, "utf8").toString("base64");
const envB64 = Buffer.from(env, "utf8").toString("base64");
const image = targetImage(sub2api, target);
const dependencyImages = targetDependencyImages(sub2api, target);
const pullCommand = image.pullPolicy === "Always"
? `$docker_bin pull ${shQuote(imageRef(sub2api, target))} >"$pull_out" 2>"$pull_err"\napp_pull_rc=$?\n$docker_bin pull ${shQuote(dependencyImages.redis)} >>"$pull_out" 2>>"$pull_err"\nredis_pull_rc=$?\nif [ "$app_pull_rc" -eq 0 ] && [ "$redis_pull_rc" -eq 0 ]; then pull_rc=0; else pull_rc=1; fi`
: `: >"$pull_out"\n: >"$pull_err"\npull_rc=0`;
const secretSourceSummary = {
sourceRef: secretMaterial.sourceRef,
sourcePath: secretMaterial.sourcePath,
appSourceRef: secretMaterial.appSourceRef,
appSourcePath: secretMaterial.appSourcePath,
sourceAction: secretMaterial.action,
fingerprint: secretMaterial.fingerprint,
valuesPrinted: false,
};
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
compose_tmp="$tmp/docker-compose.yml"
env_tmp="$tmp/sub2api.env"
printf '%s' '${composeB64}' | base64 -d >"$compose_tmp"
printf '%s' '${envB64}' | base64 -d >"$env_tmp"
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
install_out="$tmp/install.out"
install_err="$tmp/install.err"
pull_out="$tmp/pull.out"
pull_err="$tmp/pull.err"
up_out="$tmp/up.out"
up_err="$tmp/up.err"
health_out="$tmp/health.out"
health_err="$tmp/health.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
if $docker_bin compose version >"$docker_out" 2>"$docker_err"; then
compose_cmd="$docker_bin compose"
compose_mode="plugin"
docker_rc=0
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >"$docker_out" 2>"$docker_err"; then
compose_cmd="docker-compose"
compose_mode="standalone"
docker_rc=0
else
compose_cmd=""
compose_mode="raw-docker"
$docker_bin version >"$docker_out" 2>"$docker_err"
docker_rc=$?
fi
else
compose_cmd=""
compose_mode="none"
docker_rc=1
: >"$docker_out"
printf '%s\\n' 'docker daemon or docker compose is not accessible' >"$docker_err"
fi
install_rc=1
if [ "$docker_rc" -eq 0 ]; then
sudo mkdir -p ${shQuote(host.workDir)} ${shQuote(host.appDataDir)} "$(dirname ${shQuote(host.composePath)})" "$(dirname ${shQuote(host.envPath)})" >"$install_out" 2>"$install_err"
install_rc=$?
if [ "$install_rc" -eq 0 ]; then
sudo install -m 0644 "$compose_tmp" ${shQuote(host.composePath)} >>"$install_out" 2>>"$install_err"
install_rc=$?
fi
if [ "$install_rc" -eq 0 ]; then
current_uid="$(id -u)"
current_gid="$(id -g)"
sudo install -m 0600 -o "$current_uid" -g "$current_gid" "$env_tmp" ${shQuote(host.envPath)} >>"$install_out" 2>>"$install_err"
install_rc=$?
fi
else
: >"$install_out"
: >"$install_err"
fi
if [ "$install_rc" -eq 0 ]; then
${pullCommand}
else
: >"$pull_out"
printf '%s\\n' 'skipped because install failed' >"$pull_err"
pull_rc=1
fi
if [ "$pull_rc" -eq 0 ]; then
if [ -n "$compose_cmd" ]; then
$compose_cmd -p ${shQuote(host.projectName)} -f ${shQuote(host.composePath)} up -d --remove-orphans >"$up_out" 2>"$up_err"
up_rc=$?
else
$docker_bin rm -f ${shQuote(`${host.projectName}-app`)} ${shQuote(`${host.projectName}-redis`)} >"$up_out" 2>"$up_err" || true
$docker_bin run -d --name ${shQuote(`${host.projectName}-redis`)} \\
--restart unless-stopped \\
--network host \\
${shQuote(dependencyImages.redis)} \\
redis-server --save "" --appendonly no --bind 127.0.0.1 --port ${host.redisPort} >>"$up_out" 2>>"$up_err"
redis_run_rc=$?
if [ "$redis_run_rc" -eq 0 ]; then
$docker_bin run -d --name ${shQuote(`${host.projectName}-app`)} \\
--restart unless-stopped \\
--network host \\
--env-file ${shQuote(host.envPath)} \\
-v ${shQuote(`${host.appDataDir}:/app/data`)} \\
${shQuote(imageRef(sub2api, target))} >>"$up_out" 2>>"$up_err"
app_run_rc=$?
else
app_run_rc=1
fi
if [ "$redis_run_rc" -eq 0 ] && [ "$app_run_rc" -eq 0 ]; then up_rc=0; else up_rc=1; fi
fi
else
: >"$up_out"
printf '%s\\n' 'skipped because pull failed' >"$up_err"
up_rc=1
fi
health_rc=1
if [ "$up_rc" -eq 0 ]; then
deadline=$(( $(date +%s) + 180 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
if curl -fsS --max-time 5 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err"; then
health_rc=0
break
fi
sleep 3
done
else
: >"$health_out"
printf '%s\\n' 'skipped because compose up failed' >"$health_err"
fi
python3 - "$docker_rc" "$install_rc" "$pull_rc" "$up_rc" "$health_rc" "$compose_mode" "$docker_out" "$docker_err" "$install_out" "$install_err" "$pull_out" "$pull_err" "$up_out" "$up_err" "$health_out" "$health_err" <<'PY'
import json
import sys
docker_rc, install_rc, pull_rc, up_rc, health_rc = [int(value) for value in sys.argv[1:6]]
docker_mode = sys.argv[6]
paths = sys.argv[7:]
def text(path, limit=4000):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
payload = {
"ok": docker_rc == 0 and install_rc == 0 and pull_rc == 0 and up_rc == 0 and health_rc == 0,
"target": "${target.id}",
"runtimeMode": "host-docker",
"route": "${target.route}",
"projectName": "${host.projectName}",
"dockerMode": docker_mode,
"composePath": "${host.composePath}",
"envPath": "${host.envPath}",
"appDataDir": "${host.appDataDir}",
"image": "${imageRef(sub2api, target)}",
"secret": {
"requiredKeys": ${JSON.stringify(requiredSecretKeys)},
"externalActiveSource": json.loads(${JSON.stringify(JSON.stringify(secretSourceSummary))}),
"valuesPrinted": False,
},
"steps": {
"docker": {"exitCode": docker_rc, "stdout": text(paths[0]), "stderr": text(paths[1])},
"installFiles": {"exitCode": install_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
"pull": {"exitCode": pull_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
"composeUp": {"exitCode": up_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
"localHealth": {"exitCode": health_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
export function hostDockerStatusScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
const host = requireHostDockerConfig(target);
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
ps_out="$tmp/ps.out"
ps_err="$tmp/ps.err"
health_out="$tmp/health.out"
health_err="$tmp/health.err"
caddy_out="$tmp/caddy.out"
caddy_err="$tmp/caddy.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
$docker_bin version --format '{{json .}}' >"$docker_out" 2>"$docker_err"
docker_rc=$?
$docker_bin ps -a --filter label=com.docker.compose.project=${shQuote(host.projectName)} --format '{{json .}}' >"$ps_out" 2>"$ps_err"
$docker_bin ps -a --filter name=${shQuote(`${host.projectName}-`)} --format '{{json .}}' >>"$ps_out" 2>>"$ps_err"
ps_rc=$?
else
docker_rc=1
ps_rc=1
: >"$docker_out"
printf '%s\\n' 'docker daemon is not accessible' >"$docker_err"
: >"$ps_out"
: >"$ps_err"
fi
curl -fsS --max-time 5 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err"
health_rc=$?
systemctl is-active caddy >"$caddy_out" 2>"$caddy_err"
caddy_rc=$?
python3 - "$docker_rc" "$ps_rc" "$health_rc" "$caddy_rc" "$docker_out" "$docker_err" "$ps_out" "$ps_err" "$health_out" "$health_err" "$caddy_out" "$caddy_err" <<'PY'
import json
import sys
docker_rc, ps_rc, health_rc, caddy_rc = [int(value) for value in sys.argv[1:5]]
paths = sys.argv[5:]
def text(path, limit=4000):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
containers = []
for line in text(paths[2], 20000).splitlines():
try:
containers.append(json.loads(line))
except json.JSONDecodeError:
pass
payload = {
"ok": docker_rc == 0 and ps_rc == 0 and health_rc == 0,
"target": "${target.id}",
"runtimeMode": "host-docker",
"route": "${target.route}",
"projectName": "${host.projectName}",
"serviceDns": "host-docker:127.0.0.1:${host.appPort}",
"containers": containers,
"secret": {"managedByThisApply": True, "valuesPrinted": False},
"networkPolicy": {"ok": True, "mode": "not-applicable-host-docker"},
"egressProxy": {"enabled": False},
"sentinel": {"enabled": False, "mode": "not-deployed-host-docker"},
"checks": {
"docker": {"exitCode": docker_rc, "stdout": text(paths[0]), "stderr": text(paths[1])},
"composeContainers": {"exitCode": ps_rc, "stdout": text(paths[2], 8000), "stderr": text(paths[3])},
"localHealth": {"exitCode": health_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
"caddyService": {"exitCode": caddy_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
export function hostDockerValidateScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
const host = requireHostDockerConfig(target);
const database = targetDatabase(sub2api, target);
const exposure = target.publicExposure?.enabled ? target.publicExposure : null;
const publicProbe = exposure === null ? `
public_rc=0
: >"$tmp/public.out"
: >"$tmp/public.err"
` : `
curl -kfsS --max-time 20 --resolve ${shQuote(`${exposure.dns.hostname}:443:127.0.0.1`)} ${shQuote(`${exposure.publicBaseUrl}/health`)} >"$tmp/public.out" 2>"$tmp/public.err"
public_rc=$?
`;
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
app_out="$tmp/app.out"
app_err="$tmp/app.err"
redis_out="$tmp/redis.out"
redis_err="$tmp/redis.err"
pg_out="$tmp/pg.out"
pg_err="$tmp/pg.err"
health_out="$tmp/health.out"
health_err="$tmp/health.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
$docker_bin inspect -f '{{.State.Running}}' ${shQuote(`${host.projectName}-app`)} >"$app_out" 2>"$app_err"
app_rc=$?
$docker_bin exec ${shQuote(`${host.projectName}-redis`)} redis-cli -h 127.0.0.1 -p ${host.redisPort} ping >"$redis_out" 2>"$redis_err"
redis_rc=$?
$docker_bin ps --filter label=com.docker.compose.project=${shQuote(host.projectName)} --format '{{json .}}' >"$docker_out" 2>"$docker_err"
$docker_bin ps --filter name=${shQuote(`${host.projectName}-`)} --format '{{json .}}' >>"$docker_out" 2>>"$docker_err"
docker_rc=$?
else
app_rc=1
redis_rc=1
docker_rc=1
: >"$app_out"
: >"$redis_out"
: >"$docker_out"
printf '%s\\n' 'docker daemon is not accessible' >"$app_err"
printf '%s\\n' 'docker daemon is not accessible' >"$redis_err"
printf '%s\\n' 'docker daemon is not accessible' >"$docker_err"
fi
if command -v pg_isready >/dev/null 2>&1; then
pg_isready -h ${shQuote(database.host)} -p ${database.port} -U ${shQuote(database.user)} -d ${shQuote(database.dbName)} >"$pg_out" 2>"$pg_err"
pg_rc=$?
else
pg_rc=127
: >"$pg_out"
printf '%s\\n' 'pg_isready is not installed on PK01 host' >"$pg_err"
fi
curl -fsS --max-time 10 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err"
health_rc=$?
${publicProbe}
python3 - "$docker_rc" "$app_rc" "$redis_rc" "$pg_rc" "$health_rc" "$public_rc" "$docker_out" "$docker_err" "$app_out" "$app_err" "$redis_out" "$redis_err" "$pg_out" "$pg_err" "$health_out" "$health_err" "$tmp/public.out" "$tmp/public.err" <<'PY'
import json
import sys
docker_rc, app_rc, redis_rc, pg_rc, health_rc, public_rc = [int(value) for value in sys.argv[1:7]]
paths = sys.argv[7:]
def text(path, limit=4000):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
payload = {
"ok": docker_rc == 0 and app_rc == 0 and redis_rc == 0 and pg_rc == 0 and health_rc == 0 and public_rc == 0,
"target": "${target.id}",
"runtimeMode": "host-docker",
"route": "${target.route}",
"serviceDns": "host-docker:127.0.0.1:${host.appPort}",
"checks": {
"dockerComposeProject": {"exitCode": docker_rc, "stdout": text(paths[0], 8000), "stderr": text(paths[1])},
"appContainerRunning": {"exitCode": app_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
"redisPing": {"exitCode": redis_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
"postgresLocalPgIsReady": {"exitCode": pg_rc, "host": "${database.host}", "port": ${database.port}, "user": "${database.user}", "dbName": "${database.dbName}", "stdout": text(paths[6]), "stderr": text(paths[7])},
"sub2apiLocalHealth": {"exitCode": health_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
"sub2apiPublicHealthViaPk01Caddy": {"exitCode": public_rc, "stdout": text(paths[10]), "stderr": text(paths[11])},
"sentinel": {"ok": True, "mode": "not-deployed-host-docker"},
"egressProxy": {"ok": True, "mode": "not-deployed-host-docker"},
},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
function yamlString(value: string): string {
return JSON.stringify(value);
}
function envFileValue(value: string): string {
if (/^[A-Za-z0-9_./:@,+-]*$/u.test(value)) return value;
return JSON.stringify(value);
}