UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

387 lines (386 loc) 18.7 kB
import { execFileSync } from "child_process"; import * as path from "path"; import * as fs from "fs"; import { parse as parseYaml } from "yaml"; import { logError, logInfo, logSuccess, logWarn } from "../utils/log.js"; import { findAppRoot, findStackConfigs, getCurrentStack, readStackConfig } from "../utils/pulumi.js"; import { resolveWorktreeIdentity, worktreeStackName } from "../utils/worktree-identity.js"; import { resolvePulumiEnv } from "../utils/pulumi-run.js"; function readTopLevelYamlKey(appRoot, stack, key) { const configFile = path.join(appRoot, `Pulumi.${stack}.yaml`); if (!fs.existsSync(configFile)) return null; const content = fs.readFileSync(configFile, "utf-8"); const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m"); const match = content.match(pattern); if (!match) return null; return match[1].trim().replace(/^["']|["']$/g, ""); } function readConfigBlockKey(appRoot, stack, key) { const configFile = path.join(appRoot, `Pulumi.${stack}.yaml`); if (!fs.existsSync(configFile)) return null; const content = fs.readFileSync(configFile, "utf-8"); const pattern = new RegExp(`^\\s{2}${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*(.+)$`, "m"); const match = content.match(pattern); if (!match) return null; return match[1].trim().replace(/^["']|["']$/g, ""); } async function checkParentZoneDelegation(parentZone, awsEnv) { let publicNs = []; try { const { Resolver } = await import("node:dns/promises"); const resolver = new Resolver(); resolver.setServers(["1.1.1.1", "8.8.8.8"]); publicNs = (await resolver.resolveNs(parentZone)).map((n) => n.replace(/\.$/, "").toLowerCase()); } catch { } let zoneNs = []; try { const { Route53Client, ListHostedZonesByNameCommand, GetHostedZoneCommand } = await import("@aws-sdk/client-route-53"); const client = new Route53Client({ credentials: awsEnv.AWS_ACCESS_KEY_ID ? { accessKeyId: awsEnv.AWS_ACCESS_KEY_ID, secretAccessKey: awsEnv.AWS_SECRET_ACCESS_KEY ?? "", sessionToken: awsEnv.AWS_SESSION_TOKEN, } : undefined, }); const byName = await client.send(new ListHostedZonesByNameCommand({ DNSName: parentZone, MaxItems: 1 })); const zone = byName.HostedZones?.[0]; if (zone?.Id && zone.Name === `${parentZone}.`) { const detail = await client.send(new GetHostedZoneCommand({ Id: zone.Id })); zoneNs = (detail.DelegationSet?.NameServers ?? []).map((n) => n.toLowerCase()); } else { logWarn(`Parent zone '${parentZone}' does not exist in Route53 yet — the platform deploy expects it (it hosts the env-zone delegations). Create it first (one-time, operator step).`); return; } } catch { } if (publicNs.length === 0) { const records = zoneNs.length > 0 ? zoneNs.map((ns) => ` ${parentZone.split(".")[0]} NS ${ns}`).join("\n") : ` (run this command again with AWS credentials to print the exact nameservers)`; logWarn(`Parent zone '${parentZone}' has NO public NS delegation — hub/identity/temporal hostnames for this stack will NOT resolve and ACM certs stay pending until it lands.\n` + ` One-time step: at the DNS host for '${parentZone.split(".").slice(1).join(".")}' (e.g. Cloudflare), add:\n` + `${records}\n` + ` The deploy itself can proceed now (certs validate automatically once delegation propagates).`); } else if (zoneNs.length > 0 && !publicNs.some((ns) => zoneNs.includes(ns))) { logWarn(`Parent zone '${parentZone}' is publicly delegated to DIFFERENT nameservers than the Route53 zone in this account:\n` + ` public: ${publicNs.join(", ")}\n` + ` zone: ${zoneNs.join(", ")}\n` + ` The env zone would be created in a zone the internet never consults — fix the delegation before deploying.`); } else { logSuccess(`Parent zone delegation OK: ${parentZone} resolves publicly.`); } } function readBaseConfigFromYaml(appRoot, stack) { const file = path.join(appRoot, `Pulumi.${stack}.yaml`); if (!fs.existsSync(file)) return {}; let doc; try { doc = parseYaml(fs.readFileSync(file, "utf-8")); } catch { return {}; } const cfg = doc?.config ?? {}; const out = {}; for (const [key, val] of Object.entries(cfg)) { out[key] = val !== null && typeof val === "object" ? { value: "", objectValue: val } : { value: String(val) }; } return out; } function getGitHubUsername() { try { const result = execFileSync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); const login = result.trim(); if (login) return login; } catch { } try { const result = execFileSync("git", ["config", "user.email"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); const email = result.trim(); const match = email.match(/^(\d+\+)?([^@]+)@/); if (match) return match[2]; } catch { } return null; } export function registerStackCommand(program) { const stack = program .command("stack") .description("Manage personal Pulumi stacks"); stack .command("init") .description("Create a personal dev stack (dev-{github-username})") .option("--from <stack>", "Base stack to copy config from") .option("--name <name>", "Override stack name (default: dev-{username})") .option("--adopt", "resume an EXISTING stack with this name (skips the availability check)", false) .option("--worktree", "Isolate this stack per git worktree — append the worktree token to the name so concurrent worktrees deploy to distinct stacks (no SSM/namespace collisions)") .option("--platform", "Personal PLATFORM stack: a fully isolated platform env (Zitadel, Hub, Temporal, tenant envs) on the SHARED core — sets mesh:coreEnv to the base env, mesh:clusterOwner false, disables org-wide singletons (package registry, VPN), and keeps deploy ON. Run from a platform Pulumi program.", false) .option("--parent-zone <zone>", "With --platform: parent DNS zone for the env zone (mesh:dns.public.parentZone) — platform hostnames become {sub}.{env}.{parentZone} (e.g. hub.<env>.<parentZone>), so personal stacks never collide on DNS") .action(async (opts) => { const appRoot = findAppRoot(process.cwd()); if (!appRoot) { logError("No Pulumi.yaml found. Run from within a Pulumi app directory."); process.exit(1); } const username = getGitHubUsername(); if (!username) { logError("Could not determine GitHub username.\n" + "Install gh CLI (https://cli.github.com) and run: gh auth login"); process.exit(1); } let baseStack = opts.from; if (!baseStack) { const stacks = findStackConfigs(appRoot); if (stacks.includes("dev")) { baseStack = "dev"; } else if (stacks.length === 1) { baseStack = stacks[0]; } else if (stacks.length > 1) { logError(`Multiple stacks found: ${stacks.join(", ")}\n` + "Specify which to base on: mesh stack init --from <stack>"); process.exit(1); } else { logError("No stack configs found (Pulumi.<stack>.yaml)."); process.exit(1); } } const baseConfigPath = `${appRoot}/Pulumi.${baseStack}.yaml`; if (!fs.existsSync(baseConfigPath)) { logError(`Stack config not found: Pulumi.${baseStack}.yaml`); process.exit(1); } const baseTenant = readConfigBlockKey(appRoot, baseStack, "mesh:tenant"); if (opts.platform && !baseTenant) { logError(`--platform: could not read mesh:tenant from Pulumi.${baseStack}.yaml — is this a platform program?`); process.exit(1); } const baseName = opts.name ?? (opts.platform ? `${baseTenant}-${username}-dev` : `dev-${username}`); let newStack = baseName; if (opts.worktree) { const wt = resolveWorktreeIdentity(appRoot); newStack = worktreeStackName(baseName, wt); if (wt.token) { logInfo(`--worktree: isolating as "${newStack}" (linked worktree "${wt.slug}")`); } else { logInfo(`--worktree: primary checkout has no worktree token — using "${baseName}" (isolation not needed here).`); } } const newConfigPath = path.join(appRoot, `Pulumi.${newStack}.yaml`); const configExists = fs.existsSync(newConfigPath); const secretsProvider = readTopLevelYamlKey(appRoot, baseStack, "secretsprovider"); const credEnv = await resolvePulumiEnv({ appRoot, stack: baseStack }); const pulumiEnv = { ...process.env, ...credEnv }; delete pulumiEnv.AWS_PROFILE; if (!opts.adopt) { let existing = []; try { const raw = execFileSync("pulumi", ["stack", "ls", "--json"], { cwd: appRoot, encoding: "utf-8", env: pulumiEnv, stdio: ["pipe", "pipe", "pipe"], }); existing = JSON.parse(raw).map((s) => s.name); } catch { } if (existing.includes(newStack) || configExists) { logError(`Personal stack name '${newStack}' is not available` + (configExists ? ` (Pulumi.${newStack}.yaml already exists here)` : " (a stack with this name exists in the backend)") + "."); logInfo(` Pick another name: mesh stack init --name ${newStack}-2`); logInfo(` Resume YOUR OWN stack: mesh stack init --name ${newStack} --adopt`); process.exit(1); } } const initArgs = ["stack", "init", newStack]; if (secretsProvider) { initArgs.push("--secrets-provider", secretsProvider); logInfo(`Using KMS secrets provider: ${secretsProvider}`); } try { execFileSync("pulumi", initArgs, { cwd: appRoot, env: pulumiEnv, stdio: "inherit", }); logSuccess(`Stack initialized: ${newStack}${secretsProvider ? " (KMS secrets)" : ""}`); } catch (err) { if (err.stderr?.includes("already exists") && opts.adopt) { logWarn(`Stack ${newStack} already exists — adopting (--adopt).`); } else if (err.stderr?.includes("already exists")) { logError(`Stack ${newStack} already exists. Re-run with --adopt if it is yours.`); process.exit(1); } else { process.exit(err.status ?? 1); } } try { execFileSync("pulumi", ["stack", "select", newStack], { cwd: appRoot, env: pulumiEnv, stdio: ["pipe", "pipe", "pipe"], }); } catch { } if (!configExists) { let baseConfig = {}; try { const raw = execFileSync("pulumi", ["config", "--json", "--stack", baseStack], { cwd: appRoot, env: pulumiEnv, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); baseConfig = JSON.parse(raw); } catch { baseConfig = readBaseConfigFromYaml(appRoot, baseStack); if (Object.keys(baseConfig).length === 0) { logWarn(`Could not read base config from the backend or Pulumi.${baseStack}.yaml — ` + `the new stack will need manual config.`); } } for (const [key, entry] of Object.entries(baseConfig)) { if (key === "mesh:deploy") continue; try { if (entry.objectValue !== undefined) { execFileSync("pulumi", ["config", "set", key, JSON.stringify(entry.objectValue)], { cwd: appRoot, env: pulumiEnv, stdio: ["pipe", "pipe", "pipe"] }); } else if (entry.value === "true" || entry.value === "false") { execFileSync("pulumi", ["config", "set", "--type", "bool", key, entry.value], { cwd: appRoot, env: pulumiEnv, stdio: ["pipe", "pipe", "pipe"] }); } else { execFileSync("pulumi", ["config", "set", key, entry.value], { cwd: appRoot, env: pulumiEnv, stdio: ["pipe", "pipe", "pipe"] }); } } catch { } } if (opts.platform) { const baseEnv = readConfigBlockKey(appRoot, baseStack, "mesh:coreEnv") ?? (baseTenant && baseStack.startsWith(`${baseTenant}-`) ? baseStack.slice(baseTenant.length + 1) : baseStack); const setCfg = (args) => { try { execFileSync("pulumi", ["config", "set", ...args], { cwd: appRoot, env: pulumiEnv, stdio: ["pipe", "pipe", "pipe"], }); } catch { } }; setCfg(["mesh:coreEnv", baseEnv]); setCfg(["--type", "bool", "mesh:clusterOwner", "false"]); setCfg(["--type", "bool", "mesh:packageRegistry", "false"]); setCfg(["--type", "bool", "mesh:headscale", "false"]); setCfg(["--type", "bool", "mesh:subnetRouter", "false"]); setCfg(["--type", "bool", "mesh:devbox", "false"]); setCfg(["--type", "bool", "mesh:stableUrl", "true"]); try { const tenantsRaw = readConfigBlockKey(appRoot, newStack, "mesh:tenants"); if (tenantsRaw) { const tenants = JSON.parse(tenantsRaw); const envSegment = baseTenant && newStack.startsWith(`${baseTenant}-`) ? newStack.slice(baseTenant.length + 1) : newStack; for (const [tName, tCfg] of Object.entries(tenants)) { if (!tCfg.pulumiStateBucket) { tCfg.pulumiStateBucket = `${baseTenant}-tenant-${tName}-${envSegment}-pulumi-state`; } } setCfg(["mesh:tenants", JSON.stringify(tenants)]); } } catch { logWarn("Could not env-scope tenant pulumiStateBucket overrides — check mesh:tenants by hand (bucket names collide across envs otherwise)."); } if (opts.parentZone) { setCfg([ "mesh:dns", JSON.stringify({ public: { parentZone: opts.parentZone, waitForValidation: false }, }), ]); } else { logWarn("No --parent-zone given: platform hostnames will derive FLAT from the core zone and can collide with the primary platform. Set it now or later with: mesh deploy config set mesh:dns '{\"public\":{\"parentZone\":\"<zone>\"}}'"); } if (opts.parentZone) { await checkParentZoneDelegation(opts.parentZone, pulumiEnv); } logSuccess(`Configured Pulumi.${newStack}.yaml (personal platform env on core '${baseEnv}'${opts.parentZone ? `, DNS zone ${newStack.startsWith(`${baseTenant}-`) ? newStack.slice(baseTenant.length + 1) : newStack}.${opts.parentZone}` : ""})`); } else { execFileSync("pulumi", ["config", "set", "--type", "bool", "mesh:deploy", "false"], { cwd: appRoot, env: pulumiEnv, stdio: ["pipe", "pipe", "pipe"], }); logSuccess(`Configured Pulumi.${newStack}.yaml (based on ${baseStack}, deploy: false)`); } } console.log(""); logInfo(opts.platform ? `Your personal platform stack is ready.` : `Your personal stack is ready.`); logInfo(`Run: mesh deploy up --stack ${newStack} --yes`); logInfo(opts.platform ? `Then point app stacks at it: mesh:platformEnv ${newStack.startsWith(`${baseTenant}-`) ? newStack.slice(baseTenant.length + 1) : newStack}` : `Then: mesh dev --stage ${newStack}`); }); stack .command("rm <name>") .description("Remove a personal Pulumi stack (backend state + local config)") .option("--yes", "Skip the confirmation prompt") .action(async (name, opts) => { const appRoot = findAppRoot(process.cwd()); if (!appRoot) { logError("No Pulumi.yaml found. Run from within a Pulumi app directory."); process.exit(1); } const credStack = readStackConfig(appRoot, name, "mesh:deployerRole") ? name : [getCurrentStack(appRoot), "dev", ...findStackConfigs(appRoot)].find((s) => !!s && s !== name && !!readStackConfig(appRoot, s, "mesh:deployerRole")) ?? name; const credEnv = await resolvePulumiEnv({ appRoot, stack: credStack }); const pulumiEnv = { ...process.env, ...credEnv }; delete pulumiEnv.AWS_PROFILE; const args = ["stack", "rm", name]; if (opts.yes) args.push("--yes"); try { execFileSync("pulumi", args, { cwd: appRoot, env: pulumiEnv, stdio: "inherit" }); logSuccess(`Removed stack ${name}`); } catch (err) { process.exit(err.status ?? 1); } }); }