@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
187 lines (186 loc) • 8.25 kB
JavaScript
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);
}
});
}