diff --git a/scripts/src/platform-infra.ts b/scripts/src/platform-infra.ts index 8192c03d..b6b51873 100644 --- a/scripts/src/platform-infra.ts +++ b/scripts/src/platform-infra.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import type { UniDeskConfig } from "./config"; import { rootPath } from "./config"; @@ -193,9 +194,17 @@ function manifest(): string { const sub2api = readSub2ApiConfig(); const template = readFileSync(manifestPath, "utf8"); const urlAllowlist = sub2api.security.urlAllowlist; + const configHash = createHash("sha256") + .update(JSON.stringify({ + image: sub2api.image, + security: sub2api.security, + })) + .digest("hex") + .slice(0, 16); return template .replaceAll("__SUB2API_IMAGE__", imageRef(sub2api)) .replaceAll("__SUB2API_IMAGE_PULL_POLICY__", sub2api.image.pullPolicy) + .replaceAll("__SUB2API_CONFIG_HASH__", configHash) .replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ENABLED__", String(urlAllowlist.enabled)) .replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP__", String(urlAllowlist.allowInsecureHttp)) .replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS__", String(urlAllowlist.allowPrivateHosts)) @@ -550,6 +559,16 @@ capture_json configmap kubectl -n ${namespace} get configmap sub2api-config capture_json ingresses kubectl -n ${namespace} get ingress capture_json quotas kubectl -n ${namespace} get resourcequota capture_json limitranges kubectl -n ${namespace} get limitrange +pod_name="$(kubectl -n ${namespace} get pod -l app.kubernetes.io/name=${serviceName},app.kubernetes.io/component=app -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pod-name.err" || true)" +printf '%s' "$pod_name" >"$tmp/pod-name.txt" +if [ -n "$pod_name" ]; then + kubectl -n ${namespace} exec "$pod_name" -- sh -c 'printf "SECURITY_URL_ALLOWLIST_ENABLED=%s\\n" "$SECURITY_URL_ALLOWLIST_ENABLED"; printf "SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=%s\\n" "$SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP"; printf "SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=%s\\n" "$SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS"; printf "SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=%s\\n" "$SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS"' >"$tmp/pod-env.out" 2>"$tmp/pod-env.err" + printf '%s' "$?" >"$tmp/pod-env.rc" +else + : >"$tmp/pod-env.out" + printf '%s\\n' 'sub2api app pod not found' >"$tmp/pod-env.err" + printf '%s' "1" >"$tmp/pod-env.rc" +fi python3 - "$tmp" <<'PY' import json import os @@ -578,6 +597,13 @@ def items(name): return [] return data.get("items") or [] +def text(name): + path = os.path.join(tmp, name) + try: + return open(path, encoding="utf-8", errors="replace").read() + except FileNotFoundError: + return "" + def deployment_summary(item): spec = item.get("spec") or {} status = item.get("status") or {} @@ -729,12 +755,27 @@ url_allowlist_runtime = { "allowPrivateHosts": configmap_data.get("SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS"), "upstreamHosts": configmap_data.get("SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS"), } -url_allowlist_aligned = ( - url_allowlist_runtime["enabled"] == str(expected_url_allowlist.get("enabled")).lower() - and url_allowlist_runtime["allowInsecureHttp"] == str(expected_url_allowlist.get("allowInsecureHttp")).lower() - and url_allowlist_runtime["allowPrivateHosts"] == str(expected_url_allowlist.get("allowPrivateHosts")).lower() - and url_allowlist_runtime["upstreamHosts"] == ",".join(expected_url_allowlist.get("upstreamHosts") or []) -) +pod_env = {} +for line in text("pod-env.out").splitlines(): + if "=" not in line: + continue + key, value = line.split("=", 1) + pod_env[key] = value +url_allowlist_pod_env = { + "enabled": pod_env.get("SECURITY_URL_ALLOWLIST_ENABLED"), + "allowInsecureHttp": pod_env.get("SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP"), + "allowPrivateHosts": pod_env.get("SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS"), + "upstreamHosts": pod_env.get("SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS"), +} +expected_url_allowlist_strings = { + "enabled": str(expected_url_allowlist.get("enabled")).lower(), + "allowInsecureHttp": str(expected_url_allowlist.get("allowInsecureHttp")).lower(), + "allowPrivateHosts": str(expected_url_allowlist.get("allowPrivateHosts")).lower(), + "upstreamHosts": ",".join(expected_url_allowlist.get("upstreamHosts") or []), +} +url_allowlist_configmap_aligned = url_allowlist_runtime == expected_url_allowlist_strings +url_allowlist_pod_env_aligned = rc("pod-env") == 0 and url_allowlist_pod_env == expected_url_allowlist_strings +url_allowlist_aligned = url_allowlist_configmap_aligned and url_allowlist_pod_env_aligned boundary = { "internalOnly": len(service_violations) == 0 and len(items("ingresses")) == 0, "serviceViolations": service_violations, @@ -771,8 +812,16 @@ payload = { "configPath": "config/platform-infra/sub2api.yaml", "expected": expected_url_allowlist, "runtime": url_allowlist_runtime, + "podEnv": url_allowlist_pod_env, "aligned": url_allowlist_aligned, + "configMapAligned": url_allowlist_configmap_aligned, + "podEnvAligned": url_allowlist_pod_env_aligned, "configMapExists": rc("configmap") == 0, + "podEnvProbe": { + "podName": text("pod-name.txt"), + "exitCode": rc("pod-env"), + "stderr": text("pod-env.err")[-1000:], + }, } }, "boundary": boundary, diff --git a/src/components/platform-infra/sub2api/sub2api.k8s.yaml b/src/components/platform-infra/sub2api/sub2api.k8s.yaml index 7073a341..45c92a3d 100644 --- a/src/components/platform-infra/sub2api/sub2api.k8s.yaml +++ b/src/components/platform-infra/sub2api/sub2api.k8s.yaml @@ -327,6 +327,8 @@ spec: app.kubernetes.io/component: app template: metadata: + annotations: + unidesk.ai/sub2api-config-hash: "__SUB2API_CONFIG_HASH__" labels: app.kubernetes.io/name: sub2api app.kubernetes.io/component: app