542 lines
22 KiB
TypeScript
542 lines
22 KiB
TypeScript
import { Buffer } from "node:buffer";
|
|
import { createHash } from "node:crypto";
|
|
import { Resolver } from "node:dns/promises";
|
|
import type { UniDeskConfig } from "./config";
|
|
import { applyPk01CaddyManagedBlock, caddyManagedBlockMarkers } from "./pk01-caddy";
|
|
import { capture, compactCapture, fingerprintValues, parseJsonOutput, redactText, shQuote } from "./platform-infra-ops-library";
|
|
export { capture, compactCapture, fingerprintValues, parseJsonOutput, redactText, shQuote };
|
|
|
|
export interface PublicServiceExposure {
|
|
enabled: boolean;
|
|
publicBaseUrl: string;
|
|
dns: { hostname: string; expectedA: string; resolvers: string[] };
|
|
frpc: {
|
|
deploymentName: string;
|
|
secretName: string;
|
|
secretKey: string;
|
|
image: string;
|
|
serverAddr: string;
|
|
serverPort: number;
|
|
proxyName: string;
|
|
remotePort: number;
|
|
localIP: string;
|
|
localPort: number;
|
|
tokenSourceRef: string;
|
|
tokenSourceKey: string;
|
|
};
|
|
pk01: {
|
|
route: string;
|
|
caddyConfigPath: string;
|
|
caddyServiceName: string;
|
|
responseHeaderTimeoutSeconds: number;
|
|
};
|
|
}
|
|
|
|
export interface PublicServiceTarget {
|
|
id: string;
|
|
route: string;
|
|
namespace: string;
|
|
replicas: number;
|
|
publicExposure: PublicServiceExposure;
|
|
}
|
|
|
|
export interface FrpcSecretMaterial {
|
|
sourceRef: string;
|
|
sourcePath: string;
|
|
secretName: string;
|
|
secretKey: string;
|
|
frpcToml: string;
|
|
fingerprint: string;
|
|
valuesPrinted: false;
|
|
}
|
|
|
|
export interface FrpcTokenSecretReference {
|
|
configMapName: string;
|
|
configKey: string;
|
|
tokenSecretName: string;
|
|
tokenSecretKey: string;
|
|
tokenEnvName: string;
|
|
}
|
|
|
|
export interface PublicServiceRuntimeResources {
|
|
appDeploymentName: string;
|
|
serviceName: string;
|
|
persistentVolumeClaimName: string;
|
|
configMapName: string;
|
|
secretName: string;
|
|
requiredSecretKeys: string[];
|
|
sharedNetworkPolicyName?: string;
|
|
}
|
|
|
|
export async function applyPk01CaddyBlock(
|
|
config: UniDeskConfig,
|
|
serviceId: string,
|
|
exposure: PublicServiceExposure,
|
|
markers: { start: string; end: string } = caddyManagedBlockMarkers(serviceId),
|
|
): Promise<Record<string, unknown>> {
|
|
return await applyPk01CaddyManagedBlock(config, serviceId, exposure, markers);
|
|
}
|
|
|
|
export function prepareFrpcSecret(params: {
|
|
secretRoot: string;
|
|
exposure: PublicServiceExposure;
|
|
sourcePathRedactor: (path: string) => string;
|
|
parseEnvFile: (text: string) => Record<string, string>;
|
|
requiredEnvValue: (values: Record<string, string>, key: string, sourceRef: string) => string;
|
|
readTextFile: (path: string) => string;
|
|
}): FrpcSecretMaterial {
|
|
const { exposure } = params;
|
|
const sourcePath = `${params.secretRoot.replace(/\/+$/u, "")}/${exposure.frpc.tokenSourceRef}`;
|
|
const values = params.parseEnvFile(params.readTextFile(sourcePath));
|
|
const token = params.requiredEnvValue(values, exposure.frpc.tokenSourceKey, exposure.frpc.tokenSourceRef);
|
|
const frpcToml = renderFrpcToml(exposure, escapeTomlString(token));
|
|
return {
|
|
sourceRef: exposure.frpc.tokenSourceRef,
|
|
sourcePath: params.sourcePathRedactor(sourcePath),
|
|
secretName: exposure.frpc.secretName,
|
|
secretKey: exposure.frpc.secretKey,
|
|
frpcToml,
|
|
fingerprint: fingerprintValues({ token, frpcToml }, ["token", "frpcToml"]),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function renderEnvSecretFrpcManifest(target: PublicServiceTarget, secret: FrpcTokenSecretReference): string {
|
|
const exposure = target.publicExposure;
|
|
if (!exposure.enabled) return "";
|
|
const frpcToml = renderFrpcToml(exposure, `{{ .Envs.${secret.tokenEnvName} }}`);
|
|
const configFingerprint = createHash("sha256").update(frpcToml).digest("hex");
|
|
return `---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: ${secret.configMapName}
|
|
namespace: ${target.namespace}
|
|
labels:
|
|
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
|
|
app.kubernetes.io/component: tunnel
|
|
app.kubernetes.io/part-of: platform-infra
|
|
app.kubernetes.io/managed-by: unidesk
|
|
data:
|
|
${secret.configKey}: |
|
|
${indentYamlBlock(frpcToml, 4)}
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ${exposure.frpc.deploymentName}
|
|
namespace: ${target.namespace}
|
|
labels:
|
|
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
|
|
app.kubernetes.io/component: tunnel
|
|
app.kubernetes.io/part-of: platform-infra
|
|
app.kubernetes.io/managed-by: unidesk
|
|
unidesk.ai/runtime-node: ${target.id}
|
|
unidesk.ai/public-hostname: ${exposure.dns.hostname}
|
|
spec:
|
|
replicas: 1
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
|
|
app.kubernetes.io/component: tunnel
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
|
|
app.kubernetes.io/component: tunnel
|
|
app.kubernetes.io/part-of: platform-infra
|
|
annotations:
|
|
unidesk.ai/public-base-url: "${exposure.publicBaseUrl}"
|
|
unidesk.ai/frp-server: "${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}"
|
|
unidesk.ai/frp-remote-port: "${exposure.frpc.remotePort}"
|
|
unidesk.ai/frpc-config-sha256: "${configFingerprint}"
|
|
spec:
|
|
containers:
|
|
- name: frpc
|
|
image: ${exposure.frpc.image}
|
|
imagePullPolicy: IfNotPresent
|
|
args:
|
|
- -c
|
|
- /etc/frp/frpc.toml
|
|
env:
|
|
- name: ${secret.tokenEnvName}
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: ${secret.tokenSecretName}
|
|
key: ${secret.tokenSecretKey}
|
|
volumeMounts:
|
|
- name: frpc-config
|
|
mountPath: /etc/frp/frpc.toml
|
|
subPath: ${secret.configKey}
|
|
readOnly: true
|
|
volumes:
|
|
- name: frpc-config
|
|
configMap:
|
|
name: ${secret.configMapName}
|
|
`;
|
|
}
|
|
|
|
export function renderFrpcManifest(target: PublicServiceTarget): string {
|
|
const exposure = target.publicExposure;
|
|
if (!exposure.enabled) return "";
|
|
return `---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ${exposure.frpc.deploymentName}
|
|
namespace: ${target.namespace}
|
|
labels:
|
|
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
|
|
app.kubernetes.io/component: tunnel
|
|
app.kubernetes.io/part-of: platform-infra
|
|
app.kubernetes.io/managed-by: unidesk
|
|
unidesk.ai/runtime-node: ${target.id}
|
|
unidesk.ai/public-hostname: ${exposure.dns.hostname}
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
|
|
app.kubernetes.io/component: tunnel
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: ${exposure.frpc.deploymentName}
|
|
app.kubernetes.io/component: tunnel
|
|
app.kubernetes.io/part-of: platform-infra
|
|
annotations:
|
|
unidesk.ai/public-base-url: "${exposure.publicBaseUrl}"
|
|
unidesk.ai/frp-server: "${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}"
|
|
unidesk.ai/frp-remote-port: "${exposure.frpc.remotePort}"
|
|
spec:
|
|
containers:
|
|
- name: frpc
|
|
image: ${exposure.frpc.image}
|
|
imagePullPolicy: IfNotPresent
|
|
args:
|
|
- -c
|
|
- /etc/frp/frpc.toml
|
|
volumeMounts:
|
|
- name: frpc-config
|
|
mountPath: /etc/frp/frpc.toml
|
|
subPath: ${exposure.frpc.secretKey}
|
|
readOnly: true
|
|
volumes:
|
|
- name: frpc-config
|
|
secret:
|
|
secretName: ${exposure.frpc.secretName}
|
|
`;
|
|
}
|
|
|
|
export function publicServicePolicyChecks(
|
|
yaml: string,
|
|
target: PublicServiceTarget,
|
|
serviceName: string,
|
|
options: { requireAllowAllManifest?: boolean } = {},
|
|
): Array<Record<string, unknown>> {
|
|
const checks: Array<Record<string, unknown>> = [
|
|
{ name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: `${serviceName} public exposure must use PK01 Caddy+FRP, not Kubernetes Ingress.` },
|
|
{ name: "no-nodeport-or-loadbalancer", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Services must stay ClusterIP." },
|
|
{ name: "no-host-network", ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml), detail: "Pods must not use host network." },
|
|
{ name: "no-host-port", ok: !/^\s*hostPort:\s*[0-9]+\s*$/mu.test(yaml), detail: "Pods must not expose host ports." },
|
|
{ name: "no-cpu-memory-resources", ok: !/^\s*(cpu|memory):\s*/mu.test(yaml), detail: "No CPU/memory request or limit objects are rendered." },
|
|
];
|
|
if (options.requireAllowAllManifest ?? true) checks.push({ name: "allow-all-network-policy", ok: hasAllowAllNetworkPolicy(yaml, target.namespace), detail: `NetworkPolicy/allow-all exists in ${target.namespace}.` });
|
|
return checks;
|
|
}
|
|
|
|
export function dryRunManifestScript(params: { yaml: string; target: PublicServiceTarget; fieldManager: string; manifestName: string }): string {
|
|
const encoded = Buffer.from(params.yaml, "utf8").toString("base64");
|
|
return `
|
|
set -u
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
manifest="$tmp/${params.manifestName}.k8s.yaml"
|
|
printf '%s' '${encoded}' | base64 -d > "$manifest"
|
|
kubectl apply --dry-run=client -f "$manifest" >"$tmp/client.out" 2>"$tmp/client.err"
|
|
client_rc=$?
|
|
if kubectl get namespace ${params.target.namespace} >/dev/null 2>&1; then
|
|
kubectl apply --server-side --dry-run=server --field-manager=${params.fieldManager} -f "$manifest" >"$tmp/server.out" 2>"$tmp/server.err"
|
|
server_rc=$?
|
|
server_disposition=executed
|
|
else
|
|
: >"$tmp/server.err"
|
|
printf '%s\\n' 'server dry-run skipped because namespace does not exist yet' >"$tmp/server.out"
|
|
server_rc=0
|
|
server_disposition=skipped-namespace-missing
|
|
fi
|
|
python3 - "$client_rc" "$server_rc" "$server_disposition" "$tmp/client.out" "$tmp/client.err" "$tmp/server.out" "$tmp/server.err" <<'PY'
|
|
import json, sys
|
|
client_rc, server_rc = int(sys.argv[1]), int(sys.argv[2])
|
|
def text(path):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()
|
|
except FileNotFoundError:
|
|
return ""
|
|
payload = {
|
|
"ok": client_rc == 0 and server_rc == 0,
|
|
"target": "${params.target.id}",
|
|
"namespace": "${params.target.namespace}",
|
|
"clientDryRun": {"exitCode": client_rc, "stdout": text(sys.argv[4])[-4000:], "stderr": text(sys.argv[5])[-4000:]},
|
|
"serverDryRun": {"exitCode": server_rc, "disposition": sys.argv[3], "stdout": text(sys.argv[6])[-4000:], "stderr": text(sys.argv[7])[-4000:]},
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|
|
|
|
export function publicHttpProbe(baseUrl: string, path: string, options: { headers?: string[] } = {}): Record<string, unknown> {
|
|
const url = `${baseUrl.replace(/\/+$/u, "")}${path}`;
|
|
const args = ["-fsS", "--connect-timeout", "10", "--max-time", "30", "-o", "-", "-w", "\n%{http_code}"];
|
|
for (const header of options.headers ?? []) {
|
|
args.unshift(header);
|
|
args.unshift("-H");
|
|
}
|
|
args.push(url);
|
|
const result = Bun.spawnSync(["curl", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
const stdout = new TextDecoder().decode(result.stdout);
|
|
const stderr = new TextDecoder().decode(result.stderr);
|
|
const lines = stdout.split(/\r?\n/u);
|
|
const statusText = lines.pop() ?? "";
|
|
const status = Number(statusText);
|
|
const body = lines.join("\n");
|
|
return {
|
|
ok: result.exitCode === 0 && Number.isInteger(status) && status >= 200 && status < 500,
|
|
url,
|
|
status: Number.isInteger(status) ? status : null,
|
|
bodyBytes: Buffer.byteLength(body, "utf8"),
|
|
bodyPreview: redactText(body).slice(0, 2000),
|
|
stderrTail: redactText(stderr).slice(-2000),
|
|
headersUsed: options.headers?.length ?? 0,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export async function publicDnsProbe(dns: PublicServiceExposure["dns"]): Promise<Record<string, unknown>> {
|
|
const observations = await Promise.all(dns.resolvers.map(async (resolverAddress) => {
|
|
const resolver = new Resolver();
|
|
resolver.setServers([resolverAddress]);
|
|
try {
|
|
const addresses = [...new Set(await resolver.resolve4(dns.hostname))].sort();
|
|
return { resolver: resolverAddress, ok: addresses.includes(dns.expectedA), addresses, error: null };
|
|
} catch (error) {
|
|
return { resolver: resolverAddress, ok: false, addresses: [], error: redactText(error instanceof Error ? error.message : String(error)).slice(0, 500) };
|
|
}
|
|
}));
|
|
return {
|
|
ok: observations.length > 0 && observations.every((item) => item.ok),
|
|
hostname: dns.hostname,
|
|
expectedA: dns.expectedA,
|
|
observations,
|
|
};
|
|
}
|
|
|
|
export function applyManifestWithExistingSecretScript(params: {
|
|
yaml: string;
|
|
target: PublicServiceTarget;
|
|
fieldManager: string;
|
|
manifestName: string;
|
|
secretName: string;
|
|
requiredSecretKeys: string[];
|
|
deploymentNames: string[];
|
|
waitTimeoutSeconds: number;
|
|
}): string {
|
|
const encoded = Buffer.from(params.yaml, "utf8").toString("base64");
|
|
const requiredSecretKeys = JSON.stringify(params.requiredSecretKeys);
|
|
const deploymentNames = JSON.stringify(params.deploymentNames);
|
|
return `
|
|
set -u
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
manifest="$tmp/${params.manifestName}.k8s.yaml"
|
|
printf '%s' '${encoded}' | base64 -d > "$manifest"
|
|
kubectl -n ${params.target.namespace} get secret ${params.secretName} -o json >"$tmp/secret.json" 2>"$tmp/secret.err"
|
|
secret_get_rc=$?
|
|
python3 - "$tmp/secret.json" "$tmp/secret-check.json" '${requiredSecretKeys}' "$secret_get_rc" <<'PY'
|
|
import json, sys
|
|
source_path, result_path, required_json, get_rc_text = sys.argv[1:]
|
|
required = json.loads(required_json)
|
|
get_rc = int(get_rc_text)
|
|
data = {}
|
|
if get_rc == 0:
|
|
try:
|
|
data = json.load(open(source_path, encoding="utf-8")).get("data", {})
|
|
except Exception:
|
|
data = {}
|
|
present = sorted(key for key in required if isinstance(data.get(key), str) and len(data[key]) > 0)
|
|
missing = sorted(set(required) - set(present))
|
|
payload = {"ok": get_rc == 0 and not missing, "name": "${params.secretName}", "requiredKeys": sorted(required), "presentKeys": present, "missingKeys": missing, "valuesPrinted": False}
|
|
open(result_path, "w", encoding="utf-8").write(json.dumps(payload, ensure_ascii=False))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
secret_check_rc=$?
|
|
if [ "$secret_check_rc" -eq 0 ]; then
|
|
kubectl apply --server-side --force-conflicts --field-manager=${params.fieldManager} -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
|
|
apply_rc=$?
|
|
else
|
|
: >"$tmp/apply.out"
|
|
printf '%s\n' 'manifest apply skipped because the declared runtime Secret is missing or incomplete' >"$tmp/apply.err"
|
|
apply_rc=1
|
|
fi
|
|
printf '%s' '${deploymentNames}' >"$tmp/deployments.json"
|
|
if [ "$apply_rc" -eq 0 ]; then
|
|
python3 - "$tmp/deployments.json" <<'PY' >"$tmp/deployments.txt"
|
|
import json, sys
|
|
for value in json.load(open(sys.argv[1], encoding="utf-8")):
|
|
print(value)
|
|
PY
|
|
rollout_rc=0
|
|
: >"$tmp/rollout.out"
|
|
: >"$tmp/rollout.err"
|
|
while IFS= read -r deployment; do
|
|
[ -n "$deployment" ] || continue
|
|
timeout ${params.waitTimeoutSeconds} kubectl -n ${params.target.namespace} rollout status "deployment/$deployment" --timeout=${params.waitTimeoutSeconds}s >>"$tmp/rollout.out" 2>>"$tmp/rollout.err" || rollout_rc=$?
|
|
done <"$tmp/deployments.txt"
|
|
else
|
|
rollout_rc=1
|
|
: >"$tmp/rollout.out"
|
|
printf '%s\n' 'rollout skipped because manifest apply failed' >"$tmp/rollout.err"
|
|
fi
|
|
python3 - "$secret_get_rc" "$secret_check_rc" "$apply_rc" "$rollout_rc" "$tmp/secret-check.json" "$tmp/secret.err" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout.out" "$tmp/rollout.err" <<'PY'
|
|
import json, sys
|
|
secret_get_rc, secret_check_rc, apply_rc, rollout_rc = [int(value) for value in sys.argv[1:5]]
|
|
def text(path, limit):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
try:
|
|
secret = json.load(open(sys.argv[5], encoding="utf-8"))
|
|
except Exception:
|
|
secret = {"ok": False, "name": "${params.secretName}", "requiredKeys": ${requiredSecretKeys}, "presentKeys": [], "missingKeys": ${requiredSecretKeys}, "valuesPrinted": False}
|
|
payload = {
|
|
"ok": secret_check_rc == 0 and apply_rc == 0 and rollout_rc == 0,
|
|
"target": "${params.target.id}",
|
|
"namespace": "${params.target.namespace}",
|
|
"secret": secret,
|
|
"steps": {
|
|
"secret": {"exitCode": secret_get_rc, "stderrTail": text(sys.argv[6], 2000)},
|
|
"apply": {"exitCode": apply_rc, "stdoutTail": text(sys.argv[7], 6000), "stderrTail": text(sys.argv[8], 4000)},
|
|
"rollout": {"exitCode": rollout_rc, "stdoutTail": text(sys.argv[9], 6000), "stderrTail": text(sys.argv[10], 4000)},
|
|
},
|
|
"valuesPrinted": False,
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|
|
|
|
export function publicServiceStatusScript(params: {
|
|
target: PublicServiceTarget;
|
|
resources: PublicServiceRuntimeResources;
|
|
healthPath: string;
|
|
servicePort: number;
|
|
}): string {
|
|
const requiredSecretKeys = JSON.stringify(params.resources.requiredSecretKeys);
|
|
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"
|
|
printf '%s' "$?" >"$tmp/$name.rc"
|
|
}
|
|
capture_json app kubectl -n ${params.target.namespace} get deployment ${params.resources.appDeploymentName}
|
|
capture_json frpc kubectl -n ${params.target.namespace} get deployment ${params.target.publicExposure.frpc.deploymentName}
|
|
capture_json service kubectl -n ${params.target.namespace} get service ${params.resources.serviceName}
|
|
capture_json pvc kubectl -n ${params.target.namespace} get pvc ${params.resources.persistentVolumeClaimName}
|
|
capture_json configmap kubectl -n ${params.target.namespace} get configmap ${params.resources.configMapName}
|
|
capture_json secret kubectl -n ${params.target.namespace} get secret ${params.resources.secretName}
|
|
capture_json endpoints kubectl -n ${params.target.namespace} get endpoints ${params.resources.serviceName}
|
|
${params.resources.sharedNetworkPolicyName === undefined ? "" : `capture_json networkpolicy kubectl -n ${params.target.namespace} get networkpolicy ${params.resources.sharedNetworkPolicyName}`}
|
|
python3 - "$tmp" '${requiredSecretKeys}' <<'PY'
|
|
import json, os, sys
|
|
tmp, required_json = sys.argv[1:]
|
|
required = json.loads(required_json)
|
|
def rc(name):
|
|
try:
|
|
return int(open(os.path.join(tmp, f"{name}.rc"), encoding="utf-8").read() or "1")
|
|
except Exception:
|
|
return 1
|
|
def load(name):
|
|
try:
|
|
return json.load(open(os.path.join(tmp, f"{name}.json"), encoding="utf-8"))
|
|
except Exception:
|
|
return None
|
|
def deployment(name):
|
|
item = load(name) or {}
|
|
spec, status = item.get("spec", {}), item.get("status", {})
|
|
desired = int(spec.get("replicas", 0) or 0)
|
|
ready = int(status.get("readyReplicas", 0) or 0)
|
|
return {"present": rc(name) == 0, "desired": desired, "ready": ready, "available": int(status.get("availableReplicas", 0) or 0), "ok": rc(name) == 0 and desired > 0 and ready == desired}
|
|
secret_item = load("secret") or {}
|
|
secret_data = secret_item.get("data", {}) if isinstance(secret_item.get("data", {}), dict) else {}
|
|
present_keys = sorted(key for key in required if isinstance(secret_data.get(key), str) and len(secret_data[key]) > 0)
|
|
missing_keys = sorted(set(required) - set(present_keys))
|
|
pvc = load("pvc") or {}
|
|
endpoints = load("endpoints") or {}
|
|
addresses = sum(len(subset.get("addresses", []) or []) for subset in endpoints.get("subsets", []) or [])
|
|
app = deployment("app")
|
|
frpc = deployment("frpc")
|
|
payload = {
|
|
"ok": app["ok"] and frpc["ok"] and rc("service") == 0 and rc("pvc") == 0 and pvc.get("status", {}).get("phase") == "Bound" and rc("configmap") == 0 and rc("secret") == 0 and not missing_keys and addresses > 0${params.resources.sharedNetworkPolicyName === undefined ? "" : ' and rc("networkpolicy") == 0'},
|
|
"target": "${params.target.id}",
|
|
"namespace": "${params.target.namespace}",
|
|
"deployments": {"app": app, "frpc": frpc},
|
|
"service": {"name": "${params.resources.serviceName}", "present": rc("service") == 0, "endpointAddresses": addresses},
|
|
"persistentVolumeClaim": {"name": "${params.resources.persistentVolumeClaimName}", "present": rc("pvc") == 0, "phase": pvc.get("status", {}).get("phase")},
|
|
"configMap": {"name": "${params.resources.configMapName}", "present": rc("configmap") == 0},
|
|
"secret": {"name": "${params.resources.secretName}", "present": rc("secret") == 0, "requiredKeys": sorted(required), "presentKeys": present_keys, "missingKeys": missing_keys, "valuesPrinted": False},
|
|
"sharedNetworkPolicy": {"name": ${params.resources.sharedNetworkPolicyName === undefined ? "None" : JSON.stringify(params.resources.sharedNetworkPolicyName)}, "present": ${params.resources.sharedNetworkPolicyName === undefined ? "None" : 'rc("networkpolicy") == 0'}, "managedByService": False},
|
|
"health": {"path": "${params.healthPath}", "source": "deployment-readiness", "probed": False},
|
|
"valuesPrinted": False,
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
`;
|
|
}
|
|
|
|
function renderFrpcToml(exposure: PublicServiceExposure, token: string): string {
|
|
return [
|
|
`serverAddr = "${exposure.frpc.serverAddr}"`,
|
|
`serverPort = ${exposure.frpc.serverPort}`,
|
|
"loginFailExit = true",
|
|
`auth.token = "${token}"`,
|
|
"",
|
|
"[[proxies]]",
|
|
`name = "${exposure.frpc.proxyName}"`,
|
|
'type = "tcp"',
|
|
`localIP = "${exposure.frpc.localIP}"`,
|
|
`localPort = ${exposure.frpc.localPort}`,
|
|
`remotePort = ${exposure.frpc.remotePort}`,
|
|
"",
|
|
].join("\n");
|
|
}
|
|
|
|
function indentYamlBlock(value: string, spaces: number): string {
|
|
const prefix = " ".repeat(spaces);
|
|
return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
|
|
}
|
|
|
|
export function escapeTomlString(value: string): string {
|
|
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
}
|
|
|
|
function hasAllowAllNetworkPolicy(yaml: string, namespaceName: string): boolean {
|
|
return yaml.split(/^---\s*$/mu).some((document) => /^\s*kind:\s*NetworkPolicy\s*$/mu.test(document)
|
|
&& /^\s*name:\s*allow-all\s*$/mu.test(document)
|
|
&& new RegExp(`^\\s*namespace:\\s*${escapeRegExp(namespaceName)}\\s*$`, "mu").test(document)
|
|
&& /^\s*podSelector:\s*\{\}\s*$/mu.test(document));
|
|
}
|
|
|
|
function escapeRegExp(value: string): string {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
}
|