import { readFileSync } from "node:fs"; import { runCommand } from "./command"; import { repoRoot, rootPath } from "./config"; const defaultManifest = "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml"; const devNamespace = "unidesk-dev"; const prodNamespace = "unidesk"; const requiredKinds = new Set([ "Namespace/unidesk-dev", "Secret/unidesk-dev-runtime-secrets", "ConfigMap/unidesk-dev-runtime-config", "ConfigMap/unidesk-dev-db-guard", "ConfigMap/unidesk-dev-db-init", "Service/postgres-dev", "StatefulSet/postgres-dev", "Job/unidesk-dev-db-migrate", ]); interface ManifestDocument { index: number; raw: string; kind: string; name: string; namespace: string | null; } interface DevEnvOptions { manifestPath: string; kubectlDryRun: boolean; } function isHelpArg(arg: string | undefined): boolean { return arg === "help" || arg === "--help" || arg === "-h"; } function parseOptions(args: string[]): DevEnvOptions { const options: DevEnvOptions = { manifestPath: defaultManifest, kubectlDryRun: false }; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--manifest") { const value = args[index + 1]; if (value === undefined || value.length === 0) throw new Error("--manifest requires a path"); options.manifestPath = value; index += 1; } else if (arg === "--kubectl-dry-run") { options.kubectlDryRun = true; } else if (!isHelpArg(arg)) { throw new Error(`unknown dev-env option: ${arg}`); } } return options; } function scalarAfter(text: string, key: string): string | null { const match = text.match(new RegExp(`^\\s*${key}:\\s*"?([^"\\n#]+)"?\\s*(?:#.*)?$`, "mu")); return match?.[1]?.trim() ?? null; } function namespaceFromDoc(text: string): string | null { const metadataIndex = text.search(/^metadata:\s*$/mu); if (metadataIndex < 0) return null; const metadataBlock = text.slice(metadataIndex); const match = metadataBlock.match(/^ {2}namespace:\s*"?([^"\n#]+)"?\s*(?:#.*)?$/mu); return match?.[1]?.trim() ?? null; } function parseManifestDocuments(text: string): ManifestDocument[] { return text.split(/^---\s*$/mu) .map((raw, index) => ({ raw: raw.trim(), index })) .filter((doc) => doc.raw.length > 0) .map(({ raw, index }) => { const kind = scalarAfter(raw, "kind") ?? ""; const name = (() => { const metadataIndex = raw.search(/^metadata:\s*$/mu); if (metadataIndex < 0) return ""; const metadataBlock = raw.slice(metadataIndex); const match = metadataBlock.match(/^ {2}name:\s*"?([^"\n#]+)"?\s*(?:#.*)?$/mu); return match?.[1]?.trim() ?? ""; })(); return { index, raw, kind, name, namespace: namespaceFromDoc(raw) }; }); } function databaseUrls(text: string): string[] { const urls: string[] = []; const pattern = /postgres(?:ql)?:\/\/[^\s"']+/gu; for (const match of text.matchAll(pattern)) urls.push(match[0] ?? ""); return urls.filter((url) => url.length > 0 && !url.includes("*") && !url.includes("$")); } function validateDatabaseUrl(url: string): { ok: boolean; url: string; reason: string | null } { if (url.includes("d601-tcp-egress-gateway") || url.includes("74.48.78.17:15432") || url.includes("database:5432/unidesk")) { return { ok: false, url, reason: "matches production database route" }; } let parsed: URL; try { parsed = new URL(url); } catch { return { ok: false, url, reason: "invalid URL" }; } const hostOk = [ "postgres-dev", "postgres-dev.unidesk-dev", "postgres-dev.unidesk-dev.svc", "postgres-dev.unidesk-dev.svc.cluster.local", ].includes(parsed.hostname); const database = parsed.pathname.replace(/^\/+/u, ""); if (!hostOk) return { ok: false, url, reason: `host ${parsed.hostname} is not postgres-dev` }; if (database !== "unidesk_dev") return { ok: false, url, reason: `database ${database} is not unidesk_dev` }; return { ok: true, url, reason: null }; } function kubectlDryRun(manifestPath: string): unknown { const result = runCommand(["kubectl", "apply", "--dry-run=client", "--validate=false", "-f", manifestPath], repoRoot, { timeoutMs: 60_000 }); return { command: result.command, exitCode: result.exitCode, signal: result.signal, timedOut: result.timedOut, ok: result.exitCode === 0, stdoutTail: result.stdout.slice(-4000), stderrTail: result.stderr.slice(-4000), }; } function devEnvHelp(): Record { return { ok: true, command: "dev-env validate", usage: "bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]", defaultManifest, checks: [ "all namespaced resources must target unidesk-dev", "required dev namespace, postgres-dev, secret/config, guard, migration resources must exist", "dev DATABASE_URL values must target postgres-dev/unidesk_dev and not production routes", "--kubectl-dry-run optionally asks kubectl to client-dry-run the manifest without applying it", ], }; } export function runDevEnvCommand(args: string[]): unknown { const action = args[0]; if (action === undefined || isHelpArg(action)) return devEnvHelp(); if (action !== "validate") throw new Error("dev-env usage: bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]"); const options = parseOptions(args.slice(1)); const manifestPath = rootPath(options.manifestPath); const manifestText = readFileSync(manifestPath, "utf8"); const docs = parseManifestDocuments(manifestText); const resources = docs.map((doc) => `${doc.kind}/${doc.name}`); const namespacedViolations = docs .filter((doc) => doc.kind !== "Namespace") .filter((doc) => doc.namespace !== devNamespace) .map((doc) => ({ index: doc.index, kind: doc.kind, name: doc.name, namespace: doc.namespace })); const namespaceObjectViolations = docs .filter((doc) => doc.kind === "Namespace") .filter((doc) => doc.name !== devNamespace) .map((doc) => ({ index: doc.index, kind: doc.kind, name: doc.name })); const productionNamespaceTouches = docs .filter((doc) => doc.namespace === prodNamespace || (doc.kind === "Namespace" && doc.name === prodNamespace)) .map((doc) => ({ kind: doc.kind, name: doc.name })); const missingRequiredResources = Array.from(requiredKinds).filter((resource) => !resources.includes(resource)); const urlChecks = databaseUrls(manifestText).map(validateDatabaseUrl); const badUrls = urlChecks.filter((check) => !check.ok); const forbiddenProductionTextHits = [ "namespace: unidesk\n", "d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk", "74.48.78.17:15432/unidesk", ].filter((needle) => manifestText.includes(needle)); const staticOk = namespacedViolations.length === 0 && namespaceObjectViolations.length === 0 && productionNamespaceTouches.length === 0 && missingRequiredResources.length === 0 && badUrls.length === 0 && forbiddenProductionTextHits.length === 0; const kubectl = options.kubectlDryRun ? kubectlDryRun(manifestPath) : { skipped: true, enableWith: "--kubectl-dry-run" }; const kubectlOk = typeof kubectl === "object" && kubectl !== null && "ok" in kubectl ? (kubectl as { ok: boolean }).ok : true; return { ok: staticOk && kubectlOk, manifest: options.manifestPath, namespace: devNamespace, staticChecks: { ok: staticOk, resources, namespacedViolations, namespaceObjectViolations, productionNamespaceTouches, missingRequiredResources, databaseUrlChecks: urlChecks, forbiddenProductionTextHits, }, kubectlDryRun: kubectl, }; }