/** * Single source of truth for every URL, namespace, credential, and path this * suite touches. Nothing else in the suite may read process.env directly. * * Defaults target the live orchard9-k3sf deployment described in * docs/runbooks/deploy-verification.md. Override any value via env var to point * the same suite at another cluster. */ function required(name: string, value: string | undefined): string { if (!value || value.trim() === '') { throw new Error( `${name} is required but empty.\n` + `Run 'source scripts/verify-env.sh' or export it manually.\n` + `See docs/runbooks/deploy-verification.md section 0.`, ); } return value.trim(); } /** Public hostname under test. */ export const PUBLIC_HOST = process.env.E2E_PUBLIC_HOST ?? 'tidaldb.threesix.ai'; /** Public base URL. */ export const PUBLIC_BASE_URL = `https://${PUBLIC_HOST}`; /** * Node IP used to pin the TLS connection when the local resolver lags behind * public DNS (split-DNS resolvers such as Tailscale MagicDNS do this). * Certificate validation still applies — only name resolution is bypassed. */ export const RESOLVE_IP = process.env.E2E_RESOLVE_IP ?? '208.122.204.172'; /** All node IPs that should answer for PUBLIC_HOST. */ export const EXPECTED_NODE_IPS = ( process.env.E2E_NODE_IPS ?? '208.122.204.172,208.122.204.173,208.122.204.174' ) .split(',') .map((ip) => ip.trim()) .filter(Boolean); /** Kubernetes namespace holding the cluster StatefulSet. */ export const NAMESPACE = process.env.E2E_NAMESPACE ?? 'tidaldb-cluster'; /** Namespace holding Grafana / vmagent / vmsingle / Alertmanager. */ export const OBS_NAMESPACE = process.env.E2E_OBS_NAMESPACE ?? 'observability'; /** Namespace holding Velero. */ export const BACKUP_NAMESPACE = process.env.E2E_BACKUP_NAMESPACE ?? 'backup-system'; /** Velero schedule whose freshness the fleet alert watches. */ export const BACKUP_SCHEDULE = process.env.E2E_BACKUP_SCHEDULE ?? 'velero-fleet-daily'; /** StatefulSet pod names, in ordinal order. */ export const POD_NAMES = (process.env.E2E_POD_NAMES ?? 'tidaldb-0,tidaldb-1,tidaldb-2') .split(',') .map((p) => p.trim()) .filter(Boolean); /** Namespace + pod used to prove the metrics port is NOT reachable cluster-wide. */ export const FOREIGN_NAMESPACE = process.env.E2E_FOREIGN_NAMESPACE ?? 'threesix'; export const FOREIGN_POD = process.env.E2E_FOREIGN_POD ?? 'gitea-0'; /** Grafana dashboard uid under test. */ export const DASHBOARD_UID = process.env.E2E_DASHBOARD_UID ?? 'tidaldb-overview'; /** Ports inside the tidalDB container. */ export const PORT_CLIENT = 9500; export const PORT_METRICS = 9091; /** kubeconfig used for every kubectl invocation. */ export const KUBECONFIG = process.env.KUBECONFIG ?? `${process.env.HOME}/.kube/orchard9-k3sf.yaml`; /** Path to the built tidalctl binary. */ export const TIDALCTL_BIN = process.env.E2E_TIDALCTL_BIN ?? 'target/debug/tidalctl'; /** Revision recorded in capture metadata. */ export const BUILD_REVISION = process.env.E2E_BUILD_REVISION ?? 'unset'; /** Correlates this run across browser, CLI, and cluster evidence. */ export const RUN_ID = process.env.E2E_RUN_ID ?? `local-${Date.now()}`; /** * Data-plane bearer. Read lazily: a test that does not need it must not fail * at import time, and the value must never be written into an artifact. */ export function apiKey(): string { return required('E2E_TIDAL_API_KEY', process.env.E2E_TIDAL_API_KEY); } /** Operator bearer. Absent until the admin-key image is rolled (section 9.2). */ export function adminKey(): string | undefined { const value = process.env.E2E_TIDAL_ADMIN_KEY; return value && value.trim() !== '' ? value.trim() : undefined; } /** Grafana admin password, read lazily for the same reason as apiKey(). */ export function grafanaPassword(): string { return required('E2E_GRAFANA_PASSWORD', process.env.E2E_GRAFANA_PASSWORD); } /** * Every secret this suite can hold, for redaction before anything is written to * an attachment, log line, or capture. Order matters: longest first, so a key * that contains another as a substring is masked completely. */ export function secretsForRedaction(): string[] { return [ process.env.E2E_TIDAL_API_KEY, process.env.E2E_TIDAL_ADMIN_KEY, process.env.E2E_GRAFANA_PASSWORD, ] .filter((v): v is string => !!v && v.trim().length >= 8) .map((v) => v.trim()) .sort((a, b) => b.length - a.length); } /** Replace every known secret with a stable placeholder. */ export function redact(text: string): string { let out = text; for (const secret of secretsForRedaction()) { out = out.split(secret).join('«redacted»'); } return out; }