import type { UniDeskConfig } from "./config"; import { rootPath } from "./config"; import { startJob } from "./jobs"; import { arrayField, booleanField, capture, compactCapture, integerField, recordField, parseJsonOutput, parseOpsApplyOptions, parseOpsCommonOptions, readYamlRecord, shQuote, stringField, } from "./platform-infra-ops-library"; import { fingerprintSecretValues, readEnvSourceFile, requiredEnvValue } from "./secrets"; const configPath = rootPath("config", "platform-infra", "temporal.yaml"); const configLabel = "config/platform-infra/temporal.yaml"; const secretRoot = "/root/.unidesk/.state/secrets"; const fieldManager = "unidesk-platform-temporal"; export interface TemporalTarget { id: string; route: string; namespace: string; createNamespace: boolean; enabled: boolean; } export interface TemporalWebProbeSmokeProfile { path: string; readySelector: string; minimumBodyTextBytes: number; screenshotName: string; viewport: { width: number; height: number }; navigationTimeoutMs: number; settleMs: number; commandTimeoutSeconds: number; outputLimits: { failures: number; network: number; console: number }; } export interface TemporalWebProbeSpec { enabled: boolean; productId: "temporal"; runner: { node: string; lane: string }; origin: { browserProxyMode: "auto" | "direct" }; authentication: { mode: "basic-auth"; path: string; authenticatedSelector: string; credentials: { configRef: string; targetId: string; secretName: string; passwordTargetKey: string }; }; defaultSmokeProfile: string; smokeProfiles: Record; } export interface TemporalConfig { defaults: { targetId: string }; images: { server: string; ui: string; authProxy: string; pullPolicy: string }; targets: TemporalTarget[]; database: { configRef: string; sourceRef: string; sourceKeys: { user: string; password: string; database: string; visibilityDatabase: string }; host: string; port: number; sslMode: string; disableHostVerification: boolean; skipCreate: boolean; secretName: string; }; runtime: { server: { deploymentName: string; serviceName: string; replicas: number; frontendPort: number; dynamicConfigFilePath: string; enableServiceLinks: boolean; }; ui: { deploymentName: string; serviceName: string; replicas: number; port: number; enableServiceLinks: boolean; auth: { username: string; proxyPort: number; secretName: string; passwordKey: string }; }; logicalNamespaces: Array<{ name: string; retention: string }>; rolloutTimeoutSeconds: number; }; publicExposure: { enabled: boolean; publicBaseUrl: string; hostname: string; upstream: string; healthPath: string; expectedA: string; }; webProbe: TemporalWebProbeSpec; } interface DatabaseMaterial { user: string; password: string; database: string; visibilityDatabase: string; fingerprint: string; } export function temporalHelp(): Record { return { command: "platform-infra temporal plan|apply|status|validate", usage: [ "bun scripts/cli.ts platform-infra temporal plan --target NC01", "bun scripts/cli.ts platform-infra temporal apply --target NC01 --dry-run", "bun scripts/cli.ts platform-infra temporal apply --target NC01 --confirm", "bun scripts/cli.ts platform-infra temporal status --target NC01", "bun scripts/cli.ts platform-infra temporal validate --target NC01", ], configTruth: configLabel, databaseTruth: "config/platform-db/postgres-nc01.yaml", publicUrl: "https://temporal.hwpod.com", secretPolicy: "YAML sourceRef values are rendered into a Kubernetes Secret and never printed.", }; } export async function runTemporalCommand(config: UniDeskConfig, args: string[]): Promise> { const [action = "plan"] = args; if (action === "help" || action === "--help" || action === "-h") return temporalHelp(); if (action === "plan") return plan(parseOpsCommonOptions(args.slice(1))); if (action === "apply") return await apply(config, parseOpsApplyOptions(args.slice(1))); if (action === "status") return await observe(config, "status", parseOpsCommonOptions(args.slice(1))); if (action === "validate") return await observe(config, "validate", parseOpsCommonOptions(args.slice(1))); return { ok: false, error: "unsupported-platform-infra-temporal-command", args, help: temporalHelp() }; } function plan(options: ReturnType): Record { const temporal = readTemporalConfig(); const target = resolveTemporalTarget(temporal, options.targetId); const manifest = renderManifest(temporal, target, null); const checks = policyChecks(temporal, target, manifest); return { ok: checks.every((check) => check.ok), action: "platform-infra-temporal-plan", mutation: false, config: summary(temporal, target), policy: checks, rendered: { objectCount: manifest.split("\n---\n").length, secretValuesPrepared: false }, next: { databasePlan: `bun scripts/cli.ts platform-db postgres plan --config ${temporal.database.configRef}`, databaseApply: `bun scripts/cli.ts platform-db postgres apply --config ${temporal.database.configRef} --confirm`, dryRun: `bun scripts/cli.ts platform-infra temporal apply --target ${target.id} --dry-run`, apply: `bun scripts/cli.ts platform-infra temporal apply --target ${target.id} --confirm`, publicEdge: `bun scripts/cli.ts platform-infra public-edge plan --target ${target.id}`, webAdminSecret: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope temporal --target temporal-nc01 --confirm", }, }; } async function apply(config: UniDeskConfig, options: ReturnType): Promise> { const temporal = readTemporalConfig(); const target = resolveTemporalTarget(temporal, options.targetId); const checks = policyChecks(temporal, target, renderManifest(temporal, target, null)); if (!checks.every((check) => check.ok)) return { ok: false, action: "platform-infra-temporal-apply", mode: "policy-blocked", policy: checks }; if (options.confirm && !options.wait) { const job = startJob( `platform_infra_temporal_apply_${target.id.toLowerCase()}`, ["bun", "scripts/cli.ts", "platform-infra", "temporal", "apply", "--target", target.id, "--confirm", "--wait"], `Apply ${target.id} Temporal manifests and initialize YAML-declared logical namespaces`, ); return { ok: true, action: "platform-infra-temporal-apply", mode: "async-job", mutation: true, job, statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, }; } const material = options.confirm ? readDatabaseMaterial(temporal) : null; const manifest = renderManifest(temporal, target, material); const script = options.dryRun ? dryRunScript(target, manifest) : applyScript(temporal, target, manifest); const result = await capture(config, target.route, ["sh"], script, { runtimeTimeoutMs: (temporal.runtime.rolloutTimeoutSeconds + 30) * 1000 }); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-temporal-apply", mode: options.dryRun ? "dry-run" : "confirmed", mutation: !options.dryRun && result.exitCode === 0, target: targetSummary(target), database: { configRef: temporal.database.configRef, sourceRef: temporal.database.sourceRef, secretName: temporal.database.secretName, fingerprint: material?.fingerprint ?? null, valuesPrinted: false, }, remote: parsed ?? compactCapture(result, { full: true }), next: { status: `bun scripts/cli.ts platform-infra temporal status --target ${target.id}`, validate: `bun scripts/cli.ts platform-infra temporal validate --target ${target.id}`, publicEdge: `bun scripts/cli.ts platform-infra public-edge status --target ${target.id}`, }, }; } async function observe( config: UniDeskConfig, action: "status" | "validate", options: ReturnType, ): Promise> { const temporal = readTemporalConfig(); const target = resolveTemporalTarget(temporal, options.targetId); const result = await capture(config, target.route, ["sh"], statusScript(temporal, target, action === "validate")); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, action: `platform-infra-temporal-${action}`, mutation: false, target: targetSummary(target), summary: parsed, remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }), ...(options.raw ? { raw: result } : {}), }; } function renderManifest(temporal: TemporalConfig, target: TemporalTarget, material: DatabaseMaterial | null): string { const server = temporal.runtime.server; const ui = temporal.runtime.ui; const secretData = material === null ? { user: "dry-run", password: "dry-run", database: "dry-run", visibilityDatabase: "dry-run" } : material; const encoded = (value: string) => Buffer.from(value, "utf8").toString("base64"); const namespace = target.namespace; return `apiVersion: v1 kind: Namespace metadata: name: ${namespace} labels: app.kubernetes.io/part-of: temporal --- apiVersion: v1 kind: Secret metadata: name: ${temporal.database.secretName} namespace: ${namespace} labels: app.kubernetes.io/part-of: temporal type: Opaque data: user: ${encoded(secretData.user)} password: ${encoded(secretData.password)} database: ${encoded(secretData.database)} visibilityDatabase: ${encoded(secretData.visibilityDatabase)} --- apiVersion: v1 kind: Service metadata: name: ${server.serviceName} namespace: ${namespace} spec: selector: app.kubernetes.io/name: temporal ports: - name: grpc port: ${server.frontendPort} targetPort: grpc --- apiVersion: apps/v1 kind: Deployment metadata: name: ${server.deploymentName} namespace: ${namespace} labels: app.kubernetes.io/name: temporal app.kubernetes.io/part-of: temporal spec: replicas: ${server.replicas} selector: matchLabels: app.kubernetes.io/name: temporal template: metadata: labels: app.kubernetes.io/name: temporal app.kubernetes.io/part-of: temporal spec: enableServiceLinks: ${server.enableServiceLinks} containers: - name: temporal image: ${temporal.images.server} imagePullPolicy: ${temporal.images.pullPolicy} ports: - name: grpc containerPort: ${server.frontendPort} env: - name: DB value: postgres12 - name: DB_PORT value: "${temporal.database.port}" - name: POSTGRES_SEEDS value: ${temporal.database.host} - name: POSTGRES_USER valueFrom: secretKeyRef: name: ${temporal.database.secretName} key: user - name: POSTGRES_PWD valueFrom: secretKeyRef: name: ${temporal.database.secretName} key: password - name: DBNAME valueFrom: secretKeyRef: name: ${temporal.database.secretName} key: database - name: VISIBILITY_DBNAME valueFrom: secretKeyRef: name: ${temporal.database.secretName} key: visibilityDatabase - name: POSTGRES_TLS_ENABLED value: "true" - name: POSTGRES_TLS_DISABLE_HOST_VERIFICATION value: "${temporal.database.disableHostVerification}" - name: SKIP_DB_CREATE value: "${temporal.database.skipCreate}" - name: DYNAMIC_CONFIG_FILE_PATH value: ${server.dynamicConfigFilePath} readinessProbe: tcpSocket: port: grpc initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: tcpSocket: port: grpc initialDelaySeconds: 30 periodSeconds: 10 --- apiVersion: v1 kind: Service metadata: name: ${ui.serviceName} namespace: ${namespace} spec: selector: app.kubernetes.io/name: temporal-ui ports: - name: http port: ${ui.port} targetPort: auth --- apiVersion: apps/v1 kind: Deployment metadata: name: ${ui.deploymentName} namespace: ${namespace} labels: app.kubernetes.io/name: temporal-ui app.kubernetes.io/part-of: temporal spec: replicas: ${ui.replicas} selector: matchLabels: app.kubernetes.io/name: temporal-ui template: metadata: labels: app.kubernetes.io/name: temporal-ui app.kubernetes.io/part-of: temporal spec: enableServiceLinks: ${ui.enableServiceLinks} containers: - name: temporal-ui image: ${temporal.images.ui} imagePullPolicy: ${temporal.images.pullPolicy} ports: - name: http containerPort: ${ui.port} env: - name: TEMPORAL_ADDRESS value: ${server.serviceName}:${server.frontendPort} - name: TEMPORAL_CORS_ORIGINS value: ${temporal.publicExposure.publicBaseUrl} readinessProbe: httpGet: path: / port: http initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: / port: http initialDelaySeconds: 20 periodSeconds: 10 - name: auth-proxy image: ${temporal.images.authProxy} imagePullPolicy: ${temporal.images.pullPolicy} command: - sh - -ec - | password="$(cat /run/secrets/admin-password)" hash="$(caddy hash-password --plaintext "$password")" cat >/tmp/Caddyfile </dev/null 2>&1; then kubectl apply --server-side --dry-run=server --field-manager=${fieldManager} -f "$manifest" >/dev/null else kubectl apply --dry-run=client --validate=false -f "$manifest" >/dev/null fi`); } function applyScript(temporal: TemporalConfig, target: TemporalTarget, manifest: string): string { const server = temporal.runtime.server; const ui = temporal.runtime.ui; const namespaceCommands = temporal.runtime.logicalNamespaces.map((item) => ` if ! kubectl -n ${target.namespace} exec deploy/${server.deploymentName} -- temporal operator namespace describe --address ${server.serviceName}:${server.frontendPort} --namespace ${item.name} >/dev/null 2>&1; then kubectl -n ${target.namespace} exec deploy/${server.deploymentName} -- temporal operator namespace create --address ${server.serviceName}:${server.frontendPort} --namespace ${item.name} --retention ${item.retention} >/dev/null fi`).join("\n"); const command = ` kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >/dev/null kubectl -n ${target.namespace} rollout status deployment/${server.deploymentName} --timeout=${temporal.runtime.rolloutTimeoutSeconds}s >/dev/null kubectl -n ${target.namespace} rollout status deployment/${ui.deploymentName} --timeout=${temporal.runtime.rolloutTimeoutSeconds}s >/dev/null ${namespaceCommands} `; return manifestScript(manifest, command); } function manifestScript(manifest: string, command: string): string { const encoded = Buffer.from(manifest, "utf8").toString("base64"); return ` set -eu tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT manifest="$tmp/temporal.yaml" printf '%s' ${shQuote(encoded)} | base64 -d >"$manifest" ${command} printf '%s\n' '{"ok":true,"valuesPrinted":false}' `; } function statusScript(temporal: TemporalConfig, target: TemporalTarget, validate: boolean): string { const server = temporal.runtime.server; const ui = temporal.runtime.ui; const logicalNames = temporal.runtime.logicalNamespaces.map((item) => item.name); const logicalArgs = logicalNames.map(shQuote).join(" "); return ` set -u server_ready="$(kubectl -n ${target.namespace} get deploy ${server.deploymentName} -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)" ui_ready="$(kubectl -n ${target.namespace} get deploy ${ui.deploymentName} -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)" endpoint="$(kubectl -n ${target.namespace} get endpoints ${server.serviceName} -o jsonpath='{.subsets[0].addresses[0].ip}' 2>/dev/null || true)" ui_endpoint="$(kubectl -n ${target.namespace} get endpoints ${ui.serviceName} -o jsonpath='{.subsets[0].addresses[0].ip}' 2>/dev/null || true)" secret="$(kubectl -n ${target.namespace} get secret ${temporal.database.secretName} -o name 2>/dev/null || true)" logical_present=0 for logical in ${logicalArgs}; do if kubectl -n ${target.namespace} exec deploy/${server.deploymentName} -- temporal operator namespace describe --address ${server.serviceName}:${server.frontendPort} --namespace "$logical" >/dev/null 2>&1; then logical_present=$((logical_present + 1)) fi done python3 - "$server_ready" "$ui_ready" "$endpoint" "$ui_endpoint" "$secret" "$logical_present" "${logicalNames.length}" "${validate}" <<'PY' import json, sys server_ready, ui_ready, endpoint, ui_endpoint, secret = sys.argv[1:6] logical_present, logical_expected = int(sys.argv[6]), int(sys.argv[7]) validate = sys.argv[8] == "true" ready = int(server_ready or 0) >= ${server.replicas} and int(ui_ready or 0) >= ${ui.replicas} ok = ready and bool(endpoint) and bool(ui_endpoint) and bool(secret) and logical_present == logical_expected print(json.dumps({ "ok": ok, "mode": "validate" if validate else "status", "namespace": "${target.namespace}", "server": {"readyReplicas": int(server_ready or 0), "endpointPresent": bool(endpoint), "serviceDns": "${server.serviceName}.${target.namespace}.svc.cluster.local:${server.frontendPort}"}, "ui": {"readyReplicas": int(ui_ready or 0), "endpointPresent": bool(ui_endpoint), "publicUrl": "${temporal.publicExposure.publicBaseUrl}"}, "database": {"mode": "nc01-host-native-postgresql", "host": "${temporal.database.host}", "secretPresent": bool(secret), "valuesPrinted": False}, "logicalNamespaces": {"expected": logical_expected, "present": logical_present, "names": ${JSON.stringify(logicalNames)}}, }, ensure_ascii=False)) sys.exit(0 if ok else 1) PY `; } function readDatabaseMaterial(temporal: TemporalConfig): DatabaseMaterial { const source = readEnvSourceFile({ root: secretRoot, sourceRef: temporal.database.sourceRef, missingMessage: () => `Temporal database source is missing; run platform-db postgres apply --config ${temporal.database.configRef} --confirm first`, }); const keys = temporal.database.sourceKeys; const material = { user: requiredEnvValue(source.values, keys.user, temporal.database.sourceRef), password: requiredEnvValue(source.values, keys.password, temporal.database.sourceRef), database: requiredEnvValue(source.values, keys.database, temporal.database.sourceRef), visibilityDatabase: requiredEnvValue(source.values, keys.visibilityDatabase, temporal.database.sourceRef), }; return { ...material, fingerprint: fingerprintSecretValues(material, Object.keys(material)) }; } export function readTemporalConfig(): TemporalConfig { const root = readYamlRecord(configPath, "platform-infra-temporal"); const defaults = recordField(root, "defaults", configLabel); const images = recordField(root, "images", configLabel); const database = recordField(root, "database", configLabel); const sourceKeys = recordField(database, "sourceKeys", `${configLabel}.database`); const runtime = recordField(root, "runtime", configLabel); const server = recordField(runtime, "server", `${configLabel}.runtime`); const ui = recordField(runtime, "ui", `${configLabel}.runtime`); const uiAuth = recordField(ui, "auth", `${configLabel}.runtime.ui`); const exposure = recordField(root, "publicExposure", configLabel); const webProbe = recordField(root, "webProbe", configLabel); const webProbeRunner = recordField(webProbe, "runner", `${configLabel}.webProbe`); const webProbeOrigin = recordField(webProbe, "origin", `${configLabel}.webProbe`); const webProbeAuth = recordField(webProbe, "authentication", `${configLabel}.webProbe`); const webProbeCredentials = recordField(webProbeAuth, "credentials", `${configLabel}.webProbe.authentication`); const webProbeProfiles = recordField(webProbe, "smokeProfiles", `${configLabel}.webProbe`); const smokeProfiles = Object.fromEntries(Object.entries(webProbeProfiles).map(([name, value]) => { const path = `${configLabel}.webProbe.smokeProfiles.${name}`; const profile = recordField({ value }, "value", path); const viewport = recordField(profile, "viewport", path); const limits = recordField(profile, "outputLimits", path); return [name, { path: stringField(profile, "path", path), readySelector: stringField(profile, "readySelector", path), minimumBodyTextBytes: integerField(profile, "minimumBodyTextBytes", path), screenshotName: stringField(profile, "screenshotName", path), viewport: { width: integerField(viewport, "width", `${path}.viewport`), height: integerField(viewport, "height", `${path}.viewport`) }, navigationTimeoutMs: integerField(profile, "navigationTimeoutMs", path), settleMs: integerField(profile, "settleMs", path), commandTimeoutSeconds: integerField(profile, "commandTimeoutSeconds", path), outputLimits: { failures: integerField(limits, "failures", `${path}.outputLimits`), network: integerField(limits, "network", `${path}.outputLimits`), console: integerField(limits, "console", `${path}.outputLimits`), }, } satisfies TemporalWebProbeSmokeProfile]; })); const defaultSmokeProfile = stringField(webProbe, "defaultSmokeProfile", `${configLabel}.webProbe`); if (smokeProfiles[defaultSmokeProfile] === undefined) throw new Error(`${configLabel}.webProbe.defaultSmokeProfile must reference smokeProfiles`); const webProbeMode = stringField(webProbeAuth, "mode", `${configLabel}.webProbe.authentication`); if (webProbeMode !== "basic-auth") throw new Error(`${configLabel}.webProbe.authentication.mode must be basic-auth`); const productId = stringField(webProbe, "productId", `${configLabel}.webProbe`); if (productId !== "temporal") throw new Error(`${configLabel}.webProbe.productId must be temporal`); const browserProxyMode = stringField(webProbeOrigin, "browserProxyMode", `${configLabel}.webProbe.origin`); if (browserProxyMode !== "auto" && browserProxyMode !== "direct") throw new Error(`${configLabel}.webProbe.origin.browserProxyMode must be auto or direct`); return { defaults: { targetId: stringField(defaults, "targetId", `${configLabel}.defaults`) }, images: { server: stringField(images, "server", `${configLabel}.images`), ui: stringField(images, "ui", `${configLabel}.images`), authProxy: stringField(images, "authProxy", `${configLabel}.images`), pullPolicy: stringField(images, "pullPolicy", `${configLabel}.images`), }, targets: arrayField(root, "targets", configLabel).map((item, index) => ({ id: stringField(item, "id", `${configLabel}.targets[${index}]`), route: stringField(item, "route", `${configLabel}.targets[${index}]`), namespace: stringField(item, "namespace", `${configLabel}.targets[${index}]`), createNamespace: booleanField(item, "createNamespace", `${configLabel}.targets[${index}]`), enabled: booleanField(item, "enabled", `${configLabel}.targets[${index}]`), })), database: { configRef: stringField(database, "configRef", `${configLabel}.database`), sourceRef: stringField(database, "sourceRef", `${configLabel}.database`), sourceKeys: { user: stringField(sourceKeys, "user", `${configLabel}.database.sourceKeys`), password: stringField(sourceKeys, "password", `${configLabel}.database.sourceKeys`), database: stringField(sourceKeys, "database", `${configLabel}.database.sourceKeys`), visibilityDatabase: stringField(sourceKeys, "visibilityDatabase", `${configLabel}.database.sourceKeys`), }, host: stringField(database, "host", `${configLabel}.database`), port: integerField(database, "port", `${configLabel}.database`), sslMode: stringField(database, "sslMode", `${configLabel}.database`), disableHostVerification: booleanField(database, "disableHostVerification", `${configLabel}.database`), skipCreate: booleanField(database, "skipCreate", `${configLabel}.database`), secretName: stringField(database, "secretName", `${configLabel}.database`), }, runtime: { server: { ...deploymentConfig(server, `${configLabel}.runtime.server`, "frontendPort"), dynamicConfigFilePath: stringField(server, "dynamicConfigFilePath", `${configLabel}.runtime.server`), enableServiceLinks: booleanField(server, "enableServiceLinks", `${configLabel}.runtime.server`), }, ui: { ...deploymentConfig(ui, `${configLabel}.runtime.ui`, "port"), enableServiceLinks: booleanField(ui, "enableServiceLinks", `${configLabel}.runtime.ui`), auth: { username: stringField(uiAuth, "username", `${configLabel}.runtime.ui.auth`), proxyPort: integerField(uiAuth, "proxyPort", `${configLabel}.runtime.ui.auth`), secretName: stringField(uiAuth, "secretName", `${configLabel}.runtime.ui.auth`), passwordKey: stringField(uiAuth, "passwordKey", `${configLabel}.runtime.ui.auth`), }, }, logicalNamespaces: arrayField(runtime, "logicalNamespaces", `${configLabel}.runtime`).map((item, index) => ({ name: stringField(item, "name", `${configLabel}.runtime.logicalNamespaces[${index}]`), retention: stringField(item, "retention", `${configLabel}.runtime.logicalNamespaces[${index}]`), })), rolloutTimeoutSeconds: integerField(runtime, "rolloutTimeoutSeconds", `${configLabel}.runtime`), }, publicExposure: { enabled: booleanField(exposure, "enabled", `${configLabel}.publicExposure`), publicBaseUrl: stringField(exposure, "publicBaseUrl", `${configLabel}.publicExposure`), hostname: stringField(exposure, "hostname", `${configLabel}.publicExposure`), upstream: stringField(exposure, "upstream", `${configLabel}.publicExposure`), healthPath: stringField(exposure, "healthPath", `${configLabel}.publicExposure`), expectedA: stringField(exposure, "expectedA", `${configLabel}.publicExposure`), }, webProbe: { enabled: booleanField(webProbe, "enabled", `${configLabel}.webProbe`), productId, runner: { node: stringField(webProbeRunner, "node", `${configLabel}.webProbe.runner`), lane: stringField(webProbeRunner, "lane", `${configLabel}.webProbe.runner`), }, origin: { browserProxyMode }, authentication: { mode: webProbeMode, path: stringField(webProbeAuth, "path", `${configLabel}.webProbe.authentication`), authenticatedSelector: stringField(webProbeAuth, "authenticatedSelector", `${configLabel}.webProbe.authentication`), credentials: { configRef: stringField(webProbeCredentials, "configRef", `${configLabel}.webProbe.authentication.credentials`), targetId: stringField(webProbeCredentials, "targetId", `${configLabel}.webProbe.authentication.credentials`), secretName: stringField(webProbeCredentials, "secretName", `${configLabel}.webProbe.authentication.credentials`), passwordTargetKey: stringField(webProbeCredentials, "passwordTargetKey", `${configLabel}.webProbe.authentication.credentials`), }, }, defaultSmokeProfile, smokeProfiles, }, }; } function deploymentConfig(record: Record, path: string, portKey: "frontendPort" | "port"): any { return { deploymentName: stringField(record, "deploymentName", path), serviceName: stringField(record, "serviceName", path), replicas: integerField(record, "replicas", path), [portKey]: integerField(record, portKey, path), }; } export function resolveTemporalTarget(temporal: TemporalConfig, requested: string | null): TemporalTarget { const id = requested ?? temporal.defaults.targetId; const target = temporal.targets.find((item) => item.id === id); if (target === undefined) throw new Error(`${configLabel}.targets does not contain ${id}`); if (!target.enabled) throw new Error(`${configLabel}.targets[id=${id}] is disabled`); return target; } function policyChecks(temporal: TemporalConfig, target: TemporalTarget, manifest: string): Array<{ name: string; ok: boolean; detail: string }> { return [ { name: "independent-namespace", ok: target.namespace === "temporal", detail: target.namespace }, { name: "native-postgres", ok: temporal.database.configRef === "config/platform-db/postgres-nc01.yaml", detail: temporal.database.configRef }, { name: "no-in-cluster-postgres", ok: !/kind: StatefulSet|PersistentVolumeClaim|image: .*postgres/iu.test(manifest), detail: "Temporal manifest contains no PostgreSQL workload or PVC." }, { name: "cluster-internal-grpc", ok: !/type: (?:NodePort|LoadBalancer)/u.test(manifest), detail: `${temporal.runtime.server.serviceName}.${target.namespace}.svc.cluster.local` }, { name: "public-ui-only", ok: temporal.publicExposure.publicBaseUrl === "https://temporal.hwpod.com", detail: temporal.publicExposure.publicBaseUrl }, { name: "unified-web-admin", ok: temporal.runtime.ui.auth.secretName === "temporal-web-admin", detail: "Web password is distributed from unified-admin-password.txt through the secrets CLI." }, ]; } function targetSummary(target: TemporalTarget): Record { return { id: target.id, route: target.route, namespace: target.namespace }; } function summary(temporal: TemporalConfig, target: TemporalTarget): Record { return { target: targetSummary(target), images: temporal.images, database: { mode: "host-native", configRef: temporal.database.configRef, sourceRef: temporal.database.sourceRef, host: temporal.database.host, sslMode: temporal.database.sslMode }, frontend: `${temporal.runtime.server.serviceName}.${target.namespace}.svc.cluster.local:${temporal.runtime.server.frontendPort}`, logicalNamespaces: temporal.runtime.logicalNamespaces, publicExposure: temporal.publicExposure, }; }