462 lines
20 KiB
TypeScript
462 lines
20 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. apply-script module for scripts/src/platform-infra.ts.
|
|
|
|
// Moved mechanically from scripts/src/platform-infra.ts:2703-3090 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 { EgressProxySecretMaterial, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
|
|
import { fieldManager, requiredSecretKeys, sub2apiCaddyManagedMarker } from "./entry";
|
|
import { managedResourceCleanupPlan } from "./manifest";
|
|
|
|
export function renderPk01Caddyfile(target: Sub2ApiTargetConfig, exposure: Sub2ApiPublicExposureConfig): string {
|
|
const apexHost = baseDomain(exposure.dns.hostname);
|
|
const apiBlock = renderCaddyManagedBlock(
|
|
sub2apiCaddyManagedMarker(target),
|
|
renderSimpleReverseProxyCaddySiteBlock({
|
|
hostname: exposure.dns.hostname,
|
|
upstream: `127.0.0.1:${exposure.frpc.remotePort}`,
|
|
responseHeaderTimeoutSeconds: exposure.pk01.responseHeaderTimeoutSeconds,
|
|
}),
|
|
);
|
|
return `{
|
|
email ${exposure.pk01.caddyEmail}
|
|
storage file_system {
|
|
root ${exposure.pk01.caddyStorageDir}
|
|
}
|
|
}
|
|
|
|
${apexHost}, www.${apexHost} {
|
|
reverse_proxy 127.0.0.1:${exposure.pk01.pikanodeHttpHostPort}
|
|
}
|
|
|
|
${apiBlock.trim()}
|
|
`;
|
|
}
|
|
|
|
export function renderPk01CaddyService(exposure: Sub2ApiPublicExposureConfig): string {
|
|
return `[Unit]
|
|
Description=UniDesk PK01 Caddy edge for PikaPython and Sub2API
|
|
After=network-online.target docker.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=${exposure.pk01.caddyBinaryPath} run --environ --config ${exposure.pk01.caddyConfigPath}
|
|
ExecReload=${exposure.pk01.caddyBinaryPath} reload --config ${exposure.pk01.caddyConfigPath} --force
|
|
Restart=always
|
|
RestartSec=3
|
|
LimitNOFILE=1048576
|
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
NoNewPrivileges=true
|
|
Environment=XDG_DATA_HOME=${exposure.pk01.caddyStorageDir}
|
|
Environment=XDG_CONFIG_HOME=/etc/caddy
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`;
|
|
}
|
|
|
|
export function baseDomain(hostname: string): string {
|
|
const parts = hostname.split(".");
|
|
return parts.length <= 2 ? hostname : parts.slice(-2).join(".");
|
|
}
|
|
|
|
export function secretRoot(sub2api: Sub2ApiConfig): string {
|
|
const root = sub2api.runtime.secrets.root;
|
|
return isAbsolute(root) ? root : rootPath(root);
|
|
}
|
|
|
|
export function writeEnvFile(path: string, values: Record<string, string>): void {
|
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
const lines = Object.keys(values)
|
|
.sort()
|
|
.map((key) => `${key}=${quoteEnv(values[key])}`);
|
|
writeFileSync(path, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 });
|
|
chmodSync(path, 0o600);
|
|
}
|
|
|
|
export function quoteEnv(value: string): string {
|
|
if (/^[A-Za-z0-9_./:@%?&=+-]+$/u.test(value)) return value;
|
|
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
}
|
|
|
|
export function dryRunScript(yaml: string, target: Sub2ApiTargetConfig): string {
|
|
const encoded = Buffer.from(yaml, "utf8").toString("base64");
|
|
return `
|
|
set -u
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
manifest="$tmp/sub2api.k8s.yaml"
|
|
printf '%s' '${encoded}' | base64 -d > "$manifest"
|
|
client_out="$tmp/client.out"
|
|
client_err="$tmp/client.err"
|
|
server_out="$tmp/server.out"
|
|
server_err="$tmp/server.err"
|
|
kubectl apply --dry-run=client -f "$manifest" >"$client_out" 2>"$client_err"
|
|
client_rc=$?
|
|
if kubectl get namespace ${target.namespace} >/dev/null 2>&1; then
|
|
namespace_exists=true
|
|
kubectl apply --server-side --dry-run=server --field-manager=${fieldManager} -f "$manifest" >"$server_out" 2>"$server_err"
|
|
server_rc=$?
|
|
else
|
|
namespace_exists=false
|
|
server_rc=0
|
|
printf '%s\\n' 'server dry-run skipped because namespace does not exist yet; first apply creates it before namespaced resources' >"$server_out"
|
|
: >"$server_err"
|
|
fi
|
|
python3 - "$client_rc" "$server_rc" "$namespace_exists" "$client_out" "$client_err" "$server_out" "$server_err" <<'PY'
|
|
import json
|
|
import sys
|
|
client_rc = int(sys.argv[1])
|
|
server_rc = int(sys.argv[2])
|
|
namespace_exists = sys.argv[3] == "true"
|
|
paths = sys.argv[4:]
|
|
def text(path):
|
|
try:
|
|
return open(path, encoding="utf-8").read()
|
|
except FileNotFoundError:
|
|
return ""
|
|
payload = {
|
|
"ok": client_rc == 0 and server_rc == 0,
|
|
"target": "${target.id}",
|
|
"namespace": "${target.namespace}",
|
|
"namespaceExistsBeforeDryRun": namespace_exists,
|
|
"clientDryRun": {
|
|
"exitCode": client_rc,
|
|
"stdout": text(paths[0])[-4000:],
|
|
"stderr": text(paths[1])[-4000:],
|
|
},
|
|
"serverDryRun": {
|
|
"exitCode": server_rc,
|
|
"disposition": "executed" if namespace_exists else "skipped-namespace-missing",
|
|
"stdout": text(paths[2])[-4000:],
|
|
"stderr": text(paths[3])[-4000:],
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|
|
|
|
export function applyScript(
|
|
sub2api: Sub2ApiConfig,
|
|
yaml: string,
|
|
target: Sub2ApiTargetConfig,
|
|
secretMaterial: ExternalActiveSecretMaterial | null,
|
|
publicExposureSecretMaterial: PublicExposureSecretMaterial | null,
|
|
egressProxySecretMaterial: EgressProxySecretMaterial | null,
|
|
): string {
|
|
const encoded = Buffer.from(yaml, "utf8").toString("base64");
|
|
const cleanupPlan = managedResourceCleanupPlan(sub2api, target);
|
|
const appSecretName = sub2api.runtime.database.secretName;
|
|
const publicExposureSecretName = publicExposureSecretMaterial?.secretName ?? sub2api.defaults.cleanup.publicExposure.secretName;
|
|
const publicExposureSecretKey = publicExposureSecretMaterial?.secretKey ?? "frpc.toml";
|
|
const egressProxySecretName = egressProxySecretMaterial?.secretName ?? sub2api.defaults.cleanup.egressProxy.secretName;
|
|
const egressProxySecretKey = egressProxySecretMaterial?.secretKey ?? "config.json";
|
|
if (target.databaseMode === "external-active" && secretMaterial === null) throw new Error("external-active apply requires secret material");
|
|
const externalSecretFiles = secretMaterial === null
|
|
? ""
|
|
: requiredSecretKeys.map((key) => {
|
|
const value = Buffer.from(secretMaterial.values[key], "utf8").toString("base64");
|
|
return ` printf '%s' '${value}' | base64 -d >"$tmp/secret.${key}"`;
|
|
}).join("\n");
|
|
const secretSourceSummary = secretMaterial === null
|
|
? "None"
|
|
: `json.loads(${JSON.stringify(JSON.stringify({
|
|
sourceRef: secretMaterial.sourceRef,
|
|
sourcePath: secretMaterial.sourcePath,
|
|
appSourceRef: secretMaterial.appSourceRef,
|
|
appSourcePath: secretMaterial.appSourcePath,
|
|
sourceAction: secretMaterial.action,
|
|
fingerprint: secretMaterial.fingerprint,
|
|
valuesPrinted: false,
|
|
}))})`;
|
|
const exposureSecretFile = publicExposureSecretMaterial === null
|
|
? ""
|
|
: ` printf '%s' '${Buffer.from(publicExposureSecretMaterial.frpcToml, "utf8").toString("base64")}' | base64 -d >"$tmp/frpc.toml"`;
|
|
const exposureSecretSummary = publicExposureSecretMaterial === null
|
|
? "None"
|
|
: `json.loads(${JSON.stringify(JSON.stringify({
|
|
sourceRef: publicExposureSecretMaterial.sourceRef,
|
|
sourcePath: publicExposureSecretMaterial.sourcePath,
|
|
secretName: publicExposureSecretMaterial.secretName,
|
|
secretKey: publicExposureSecretMaterial.secretKey,
|
|
fingerprint: publicExposureSecretMaterial.fingerprint,
|
|
valuesPrinted: false,
|
|
}))})`;
|
|
const egressProxySecretFile = egressProxySecretMaterial === null
|
|
? ""
|
|
: ` printf '%s' '${Buffer.from(egressProxySecretMaterial.configJson, "utf8").toString("base64")}' | base64 -d >"$tmp/egress-proxy-config.json"`;
|
|
const egressProxySecretSummary = egressProxySecretMaterial === null
|
|
? "None"
|
|
: `json.loads(${JSON.stringify(JSON.stringify({
|
|
sourceRef: egressProxySecretMaterial.sourceRef,
|
|
sourcePath: egressProxySecretMaterial.sourcePath,
|
|
secretName: egressProxySecretMaterial.secretName,
|
|
secretKey: egressProxySecretMaterial.secretKey,
|
|
fingerprint: egressProxySecretMaterial.fingerprint,
|
|
sourceBytes: egressProxySecretMaterial.sourceBytes,
|
|
selectedOutbound: egressProxySecretMaterial.selectedOutbound,
|
|
sourceDiagnostics: egressProxySecretMaterial.sourceDiagnostics,
|
|
proxyUrl: egressProxySecretMaterial.proxyUrl,
|
|
noProxy: egressProxySecretMaterial.noProxy,
|
|
valuesPrinted: false,
|
|
}))})`;
|
|
return `
|
|
set -u
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
manifest="$tmp/sub2api.k8s.yaml"
|
|
printf '%s' '${encoded}' | base64 -d > "$manifest"
|
|
ns_out="$tmp/ns.out"
|
|
ns_err="$tmp/ns.err"
|
|
secret_out="$tmp/secret.out"
|
|
secret_err="$tmp/secret.err"
|
|
exposure_secret_out="$tmp/exposure-secret.out"
|
|
exposure_secret_err="$tmp/exposure-secret.err"
|
|
egress_secret_out="$tmp/egress-secret.out"
|
|
egress_secret_err="$tmp/egress-secret.err"
|
|
apply_out="$tmp/apply.out"
|
|
apply_err="$tmp/apply.err"
|
|
cleanup_out="$tmp/cleanup.out"
|
|
cleanup_err="$tmp/cleanup.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"
|
|
secret_rc=0
|
|
exposure_secret_action="not-enabled"
|
|
exposure_secret_rc=0
|
|
egress_secret_action="not-enabled"
|
|
egress_secret_rc=0
|
|
if [ "$ns_rc" -eq 0 ]; then
|
|
if [ "${target.databaseMode}" = "external-pending" ]; then
|
|
secret_action="external-pending-not-managed"
|
|
: >"$secret_out"
|
|
printf '%s\\n' 'external DB target expects its DB credential Secret from the platform DB handoff; predeploy does not create placeholder secrets' >"$secret_err"
|
|
elif [ "${target.databaseMode}" = "external-active" ]; then
|
|
${externalSecretFiles}
|
|
kubectl -n ${target.namespace} create secret generic ${appSecretName} \\
|
|
--from-file=POSTGRES_PASSWORD="$tmp/secret.POSTGRES_PASSWORD" \\
|
|
--from-file=ADMIN_PASSWORD="$tmp/secret.ADMIN_PASSWORD" \\
|
|
--from-file=JWT_SECRET="$tmp/secret.JWT_SECRET" \\
|
|
--from-file=TOTP_ENCRYPTION_KEY="$tmp/secret.TOTP_ENCRYPTION_KEY" \\
|
|
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err"
|
|
secret_rc=$?
|
|
secret_action="external-active-synced"
|
|
elif kubectl -n ${target.namespace} get secret ${appSecretName} >/dev/null 2>&1; then
|
|
secret_action="kept-existing"
|
|
: >"$secret_out"
|
|
: >"$secret_err"
|
|
else
|
|
rand_hex() {
|
|
bytes="$1"
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
openssl rand -hex "$bytes"
|
|
else
|
|
dd if=/dev/urandom bs="$bytes" count=1 2>/dev/null | od -An -tx1 | tr -d ' \\n'
|
|
fi
|
|
}
|
|
kubectl -n ${target.namespace} create secret generic ${appSecretName} \\
|
|
--from-literal=POSTGRES_PASSWORD="$(rand_hex 32)" \\
|
|
--from-literal=ADMIN_PASSWORD="$(rand_hex 16)" \\
|
|
--from-literal=JWT_SECRET="$(rand_hex 32)" \\
|
|
--from-literal=TOTP_ENCRYPTION_KEY="$(rand_hex 32)" \\
|
|
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err"
|
|
secret_rc=$?
|
|
secret_action="created"
|
|
fi
|
|
if [ "${publicExposureSecretMaterial === null ? "false" : "true"}" = "true" ]; then
|
|
${exposureSecretFile}
|
|
kubectl -n ${target.namespace} create secret generic ${publicExposureSecretName} \\
|
|
--from-file=${publicExposureSecretKey}="$tmp/frpc.toml" \\
|
|
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$exposure_secret_out" 2>"$exposure_secret_err"
|
|
exposure_secret_rc=$?
|
|
exposure_secret_action="synced"
|
|
else
|
|
: >"$exposure_secret_out"
|
|
: >"$exposure_secret_err"
|
|
fi
|
|
if [ "${egressProxySecretMaterial === null ? "false" : "true"}" = "true" ]; then
|
|
${egressProxySecretFile}
|
|
kubectl -n ${target.namespace} create secret generic ${egressProxySecretName} \\
|
|
--from-file=${egressProxySecretKey}="$tmp/egress-proxy-config.json" \\
|
|
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$egress_secret_out" 2>"$egress_secret_err"
|
|
egress_secret_rc=$?
|
|
egress_secret_action="synced"
|
|
else
|
|
: >"$egress_secret_out"
|
|
: >"$egress_secret_err"
|
|
fi
|
|
fi
|
|
apply_rc=1
|
|
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ] && [ "$exposure_secret_rc" -eq 0 ] && [ "$egress_secret_rc" -eq 0 ]; then
|
|
kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err"
|
|
apply_rc=$?
|
|
else
|
|
: >"$apply_out"
|
|
printf '%s\\n' 'skipped because namespace, app secret, public exposure secret, or egress proxy secret step failed' >"$apply_err"
|
|
fi
|
|
cleanup_rc=0
|
|
if [ "$apply_rc" -eq 0 ]; then
|
|
python3 - "$cleanup_out" "$cleanup_err" <<'PY'
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
out_path, err_path = sys.argv[1:3]
|
|
namespace = ${JSON.stringify(target.namespace)}
|
|
plan = json.loads(${JSON.stringify(JSON.stringify(cleanupPlan))})
|
|
|
|
def run(args):
|
|
proc = subprocess.run(["kubectl", "-n", namespace, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
return {
|
|
"args": ["kubectl", "-n", namespace, *args],
|
|
"exitCode": proc.returncode,
|
|
"stdout": proc.stdout.decode("utf-8", errors="replace")[-2000:],
|
|
"stderr": proc.stderr.decode("utf-8", errors="replace")[-2000:],
|
|
}
|
|
|
|
def delete(kind, name):
|
|
return run(["delete", kind, name, "--ignore-not-found=true", "--wait=false"])
|
|
|
|
items = []
|
|
external_db_state = plan["externalDbState"]
|
|
redis_persistent_state = plan["redisPersistentState"]
|
|
if external_db_state["enabled"]:
|
|
items.append({"name": "legacy-postgres-statefulset", **delete("statefulset", external_db_state["postgresStatefulSetName"])})
|
|
items.append({"name": "legacy-postgres-service", **delete("service", external_db_state["postgresServiceName"])})
|
|
items.append({"name": "legacy-postgres-pvc", **delete("pvc", external_db_state["postgresPvcName"])})
|
|
items.append({"name": "legacy-app-data-pvc", **delete("pvc", external_db_state["appDataPvcName"])})
|
|
if redis_persistent_state["enabled"]:
|
|
items.append({"name": "legacy-redis-pvc", **delete("pvc", redis_persistent_state["pvcName"])})
|
|
if not plan["publicExposure"]["enabled"]:
|
|
items.append({"name": "disabled-public-frpc-deployment", **delete("deployment", plan["publicExposure"]["deploymentName"])})
|
|
items.append({"name": "disabled-public-frpc-configmap", **delete("configmap", plan["publicExposure"]["configMapName"])})
|
|
items.append({"name": "disabled-public-frpc-secret", **delete("secret", plan["publicExposure"]["secretName"])})
|
|
if not plan["egressProxy"]["enabled"]:
|
|
items.append({"name": "disabled-egress-proxy-deployment", **delete("deployment", plan["egressProxy"]["deploymentName"])})
|
|
items.append({"name": "disabled-egress-proxy-service", **delete("service", plan["egressProxy"]["serviceName"])})
|
|
items.append({"name": "disabled-egress-proxy-secret", **delete("secret", plan["egressProxy"]["secretName"])})
|
|
if not plan["sentinel"]["enabled"]:
|
|
items.append({"name": "disabled-sentinel-cronjob", **delete("cronjob", plan["sentinel"]["cronJobName"])})
|
|
items.append({"name": "disabled-sentinel-jobs", **run(["delete", "job", "-l", f"app.kubernetes.io/name={plan['sentinel']['cronJobName']}", "--ignore-not-found=true", "--wait=false"])})
|
|
items.append({"name": "disabled-sentinel-configmap", **delete("configmap", plan["sentinel"]["configMapName"])})
|
|
items.append({"name": "disabled-sentinel-credentials-secret", **delete("secret", plan["sentinel"]["credentialsSecretName"])})
|
|
items.append({"name": "disabled-sentinel-serviceaccount", **delete("serviceaccount", plan["sentinel"]["serviceAccountName"])})
|
|
items.append({"name": "disabled-sentinel-role", **delete("role", plan["sentinel"]["roleName"])})
|
|
items.append({"name": "disabled-sentinel-rolebinding", **delete("rolebinding", plan["sentinel"]["roleBindingName"])})
|
|
|
|
watch = []
|
|
if external_db_state["enabled"]:
|
|
watch.extend([
|
|
("statefulset", external_db_state["postgresStatefulSetName"]),
|
|
("service", external_db_state["postgresServiceName"]),
|
|
("pvc", external_db_state["postgresPvcName"]),
|
|
("pvc", external_db_state["appDataPvcName"]),
|
|
])
|
|
if redis_persistent_state["enabled"]:
|
|
watch.append(("pvc", redis_persistent_state["pvcName"]))
|
|
if not plan["publicExposure"]["enabled"]:
|
|
watch.append(("deployment", plan["publicExposure"]["deploymentName"]))
|
|
if not plan["egressProxy"]["enabled"]:
|
|
watch.append(("deployment", plan["egressProxy"]["deploymentName"]))
|
|
watch.append(("service", plan["egressProxy"]["serviceName"]))
|
|
if not plan["sentinel"]["enabled"]:
|
|
watch.append(("cronjob", plan["sentinel"]["cronJobName"]))
|
|
|
|
deadline = time.time() + 90
|
|
remaining = []
|
|
while True:
|
|
remaining = []
|
|
for kind, name in watch:
|
|
result = run(["get", kind, name])
|
|
if result["exitCode"] == 0:
|
|
remaining.append({"kind": kind, "name": name})
|
|
if not remaining or time.time() >= deadline:
|
|
break
|
|
time.sleep(3)
|
|
|
|
payload = {
|
|
"ok": all(item["exitCode"] == 0 for item in items) and not remaining,
|
|
"plan": plan,
|
|
"items": items,
|
|
"remainingAfterWait": remaining,
|
|
"valuesPrinted": False,
|
|
}
|
|
open(out_path, "w", encoding="utf-8").write(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
open(err_path, "w", encoding="utf-8").write("")
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
cleanup_rc=$?
|
|
else
|
|
: >"$cleanup_out"
|
|
printf '%s\\n' 'skipped because apply failed' >"$cleanup_err"
|
|
cleanup_rc=1
|
|
fi
|
|
python3 - "$ns_rc" "$secret_rc" "$exposure_secret_rc" "$egress_secret_rc" "$apply_rc" "$cleanup_rc" "$secret_action" "$exposure_secret_action" "$egress_secret_action" "$ns_out" "$ns_err" "$secret_out" "$secret_err" "$exposure_secret_out" "$exposure_secret_err" "$egress_secret_out" "$egress_secret_err" "$apply_out" "$apply_err" "$cleanup_out" "$cleanup_err" <<'PY'
|
|
import json
|
|
import sys
|
|
ns_rc = int(sys.argv[1])
|
|
secret_rc = int(sys.argv[2])
|
|
exposure_secret_rc = int(sys.argv[3])
|
|
egress_secret_rc = int(sys.argv[4])
|
|
apply_rc = int(sys.argv[5])
|
|
cleanup_rc = int(sys.argv[6])
|
|
secret_action = sys.argv[7]
|
|
exposure_secret_action = sys.argv[8]
|
|
egress_secret_action = sys.argv[9]
|
|
paths = sys.argv[10:]
|
|
def text(path):
|
|
try:
|
|
return open(path, encoding="utf-8").read()
|
|
except FileNotFoundError:
|
|
return ""
|
|
def parsed(path):
|
|
try:
|
|
return json.load(open(path, encoding="utf-8"))
|
|
except Exception:
|
|
return None
|
|
payload = {
|
|
"ok": ns_rc == 0 and secret_rc == 0 and exposure_secret_rc == 0 and egress_secret_rc == 0 and apply_rc == 0 and cleanup_rc == 0,
|
|
"target": "${target.id}",
|
|
"namespace": "${target.namespace}",
|
|
"databaseMode": "${target.databaseMode}",
|
|
"appReplicas": ${target.appReplicas},
|
|
"redisReplicas": ${target.redisReplicas},
|
|
"secret": {
|
|
"name": "${appSecretName}",
|
|
"action": secret_action,
|
|
"requiredKeys": ${JSON.stringify(requiredSecretKeys)},
|
|
"managedByThisApply": ${target.databaseMode === "external-pending" ? "False" : "True"},
|
|
"externalActiveSource": ${secretSourceSummary},
|
|
"valuesPrinted": False,
|
|
},
|
|
"publicExposure": ${exposureSecretSummary},
|
|
"egressProxy": ${egressProxySecretSummary},
|
|
"steps": {
|
|
"namespace": {"exitCode": ns_rc, "stdout": text(paths[0])[-4000:], "stderr": text(paths[1])[-4000:]},
|
|
"secret": {"exitCode": secret_rc, "stdout": text(paths[2])[-4000:], "stderr": text(paths[3])[-4000:]},
|
|
"publicExposureSecret": {"exitCode": exposure_secret_rc, "action": exposure_secret_action, "stdout": text(paths[4])[-4000:], "stderr": text(paths[5])[-4000:]},
|
|
"egressProxySecret": {"exitCode": egress_secret_rc, "action": egress_secret_action, "stdout": text(paths[6])[-4000:], "stderr": text(paths[7])[-4000:]},
|
|
"apply": {"exitCode": apply_rc, "stdout": text(paths[8])[-8000:], "stderr": text(paths[9])[-4000:]},
|
|
"cleanup": {"exitCode": cleanup_rc, "summary": parsed(paths[10]), "stdout": text(paths[10])[-8000:], "stderr": text(paths[11])[-4000:]},
|
|
},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|