Files
pikasTech-unidesk/scripts/src/platform-infra-temporal.ts
T
2026-07-18 17:59:04 +02:00

718 lines
32 KiB
TypeScript

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<string, TemporalWebProbeSmokeProfile>;
}
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<string, unknown> {
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<Record<string, unknown>> {
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<typeof parseOpsCommonOptions>): Record<string, unknown> {
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<typeof parseOpsApplyOptions>): Promise<Record<string, unknown>> {
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<typeof parseOpsCommonOptions>,
): Promise<Record<string, unknown>> {
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 <<EOF
:${ui.auth.proxyPort} {
handle /healthz {
respond 200
}
handle {
basic_auth {
${ui.auth.username} $hash
}
reverse_proxy 127.0.0.1:${ui.port}
}
}
EOF
exec caddy run --config /tmp/Caddyfile --adapter caddyfile
ports:
- name: auth
containerPort: ${ui.auth.proxyPort}
readinessProbe:
tcpSocket:
port: auth
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
tcpSocket:
port: auth
initialDelaySeconds: 15
periodSeconds: 10
volumeMounts:
- name: web-admin-password
mountPath: /run/secrets/admin-password
subPath: ${ui.auth.passwordKey}
readOnly: true
volumes:
- name: web-admin-password
secret:
secretName: ${ui.auth.secretName}
`;
}
function dryRunScript(target: TemporalTarget, manifest: string): string {
return manifestScript(manifest, `
if kubectl get namespace ${target.namespace} >/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<string, unknown>, 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<string, unknown> {
return { id: target.id, route: target.route, namespace: target.namespace };
}
function summary(temporal: TemporalConfig, target: TemporalTarget): Record<string, unknown> {
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,
};
}