UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

187 lines (186 loc) 8.25 kB
import { spawnSync } from "node:child_process"; import { logError, logInfo } from "../utils/log.js"; import { findAppRoot, findStackConfigs, getCurrentStack, pulumiStackOutput, readStackConfig, } from "../utils/pulumi.js"; import { resolveAwsCredentials, derivePlatformContext } from "../utils/aws-auth.js"; import { ensureKubeconfig, resolveHubPlatformName, } from "../utils/kubeconfig.js"; import { probeCredentials } from "./login.js"; import { credProbeToPreflightError } from "../utils/pulumi-run.js"; async function resolveClusterAccess(opts) { const appRoot = findAppRoot(process.cwd()); if (!appRoot) { throw new Error("Not in a Mesh app directory (no Pulumi.yaml found). Run from an app directory."); } const stack = opts.stack ?? getCurrentStack(appRoot); if (!stack) { const stacks = findStackConfigs(appRoot); throw new Error("No Pulumi stack selected.\n" + (stacks.length > 0 ? `Available: ${stacks.join(", ")}\nUse --stack <name> (the deployed stack, e.g. dev).` : "No stack configs found in this directory.")); } const sa = ["--stack", stack]; const deployerRole = readStackConfig(appRoot, stack, "mesh:deployerRole"); const ctx = derivePlatformContext(appRoot, stack); const resolved = deployerRole ? await resolveAwsCredentials(deployerRole, appRoot, stack) : null; if (deployerRole && !resolved && !process.env.AWS_ACCESS_KEY_ID) { const pf = ctx ? credProbeToPreflightError(await probeCredentials(ctx, deployerRole), ctx) : null; throw new Error(pf?.message ?? `Couldn't resolve AWS credentials. Run: mesh login ${ctx ?? "mesh.dev"} --device`); } const awsEnv = resolved?.env; const region = readStackConfig(appRoot, stack, "aws:region"); if (awsEnv && region && !awsEnv.AWS_REGION) awsEnv.AWS_REGION = region; if (awsEnv) Object.assign(process.env, awsEnv); let appOutput; let devOutput; try { appOutput = JSON.parse(pulumiStackOutput(appRoot, "app", sa, awsEnv)); devOutput = appOutput.dev ?? appOutput; } catch { try { devOutput = JSON.parse(pulumiStackOutput(appRoot, "dev", sa, awsEnv)); appOutput = devOutput; } catch { throw new Error(`Could not read stack output for '${stack}'. Ensure the app is deployed: mesh deploy up`); } } const platformEnv = devOutput?.platform?.env ?? "dev"; const platformName = resolveHubPlatformName(devOutput?.platform); const namespace = appOutput?.namespace ?? devOutput?.namespace; if (!namespace) { throw new Error(`No namespace found in stack output for '${stack}'.`); } let failure; const kubeconfigPath = await ensureKubeconfig(platformName, platformEnv, `mesh-cluster-${platformName}-${platformEnv}-${stack}`, { onError: (f) => (failure = f) }); if (!kubeconfigPath) { throw new Error(clusterResolveErrorMessage(platformName, platformEnv, failure)); } return { kubeconfigPath, namespace, stack }; } export function clusterResolveErrorMessage(platformName, platformEnv, failure) { const parameter = failure?.parameter ?? `/mesh-platform/${platformName}/${platformEnv}/core/eks`; const err = failure?.error; const detail = err instanceof Error ? `${err.name}: ${err.message}` : err !== undefined ? String(err) : "no cluster data returned"; return (`Could not resolve the EKS cluster for platform '${platformName}/${platformEnv}' from SSM.\n` + ` Parameter tried: ${parameter}\n` + ` Failure: ${detail}\n` + "If this platform name is wrong, check the app's `mesh:platform` stack config " + "(the hub platform name, e.g. `trabian`) — an AccessDeniedException here usually " + "means the WRONG platform path, not missing SSM permissions."); } const NS_FLAGS = new Set(["-n", "--namespace", "-A", "--all-namespaces"]); export function hasNamespaceFlag(args) { return args.some((a) => NS_FLAGS.has(a) || a.startsWith("--namespace=")); } function runKubectl(access, args, opts = {}) { const finalArgs = opts.defaultNamespace !== false && !hasNamespaceFlag(args) ? ["-n", access.namespace, ...args] : args; const res = spawnSync("kubectl", finalArgs, { stdio: "inherit", env: { ...process.env, KUBECONFIG: access.kubeconfigPath }, }); if (res.error) { const e = res.error; if (e.code === "ENOENT") { logError("kubectl not found on PATH. Install kubectl to use mesh cluster commands."); } else { logError(`Failed to run kubectl: ${e.message}`); } return 1; } return res.status ?? 0; } export function targetToSelector(target) { if (target.includes("/")) return [target]; if (target.includes("=")) return ["-l", target]; return ["-l", `app=${target}`]; } export function registerClusterCommands(program) { program .command("kubectl") .description("Run kubectl against the app's cluster (deployer role + SSM kubeconfig)") .option("--stack <stack>", "Deployed Pulumi stack (default: current selection)") .allowUnknownOption(true) .allowExcessArguments(true) .helpOption(false) .action(async (opts, cmd) => { try { const access = await resolveClusterAccess({ stack: opts.stack }); process.stderr.write(`→ kubectl · ns ${access.namespace} · stack ${access.stack}\n`); process.exit(runKubectl(access, cmd.args)); } catch (e) { logError(e.message); process.exit(1); } }); program .command("logs [target]") .description("Tail logs from the app's pods (kubectl logs; target = service, deployment/x, or k=v)") .option("--stack <stack>", "Deployed Pulumi stack (default: current selection)") .option("-f, --follow", "Stream new logs") .option("--tail <n>", "Lines from the end of the logs", "200") .option("-c, --container <name>", "Container name") .option("--previous", "Logs from the previous container instance (crash debugging)") .action(async (target, opts) => { try { const access = await resolveClusterAccess({ stack: opts.stack }); if (!target) { logInfo(`Which service? Pods in ${access.namespace}:\n` + "Then: mesh logs <service> (e.g. mesh logs demo-agent-worker)\n"); process.exit(runKubectl(access, ["get", "pods"])); } const kargs = ["logs", ...targetToSelector(target), "--prefix", "--tail", opts.tail ?? "200"]; if (opts.follow) kargs.push("-f"); if (opts.container) kargs.push("-c", opts.container); if (opts.previous) kargs.push("--previous"); process.exit(runKubectl(access, kargs)); } catch (e) { logError(e.message); process.exit(1); } }); program .command("exec <target> [cmd...]") .description("Exec a command in the app's pod (kubectl exec; target = service or deployment/x)") .option("--stack <stack>", "Deployed Pulumi stack (default: current selection)") .option("-c, --container <name>", "Container name") .action(async (target, cmdParts, opts) => { try { const access = await resolveClusterAccess({ stack: opts.stack }); const podRef = target.includes("/") ? target : `deployment/${target}`; const command = cmdParts.length > 0 ? cmdParts : ["sh"]; const kargs = ["exec"]; kargs.push(process.stdout.isTTY ? "-it" : "-i"); kargs.push(podRef); if (opts.container) kargs.push("-c", opts.container); kargs.push("--", ...command); process.exit(runKubectl(access, kargs)); } catch (e) { logError(e.message); process.exit(1); } }); }