@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
484 lines (483 loc) • 20.6 kB
JavaScript
import { execFileSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { logInfo, logSuccess, logWarn } from "../../utils/log.js";
import { MeshCliError } from "../../utils/errors.js";
import { LOCAL_ENV, LOCAL_AWS_CONFIG } from "./seed.js";
import { probeTcp, upsertLocalSecret } from "./helpers.js";
import { localProbesDir } from "./stack.js";
const EXTERNAL_MODES = ["mock", "local", "remote"];
const DECL_DOCS = { docs: 'package.json → "mesh": { "externals": { … } }' };
export function externalMode(name, decl) {
const mode = decl.mode ??
(decl.compose
? "local"
: decl.openapi || decl.src
? "mock"
: decl.sandbox || decl.credentials || decl.remote
? "remote"
: undefined);
if (!mode || !EXTERNAL_MODES.includes(mode)) {
throw new MeshCliError(`External '${name}': cannot determine mode — set "mode" to mock | local | remote (or declare openapi/src, compose, or credentials).`, { remediation: DECL_DOCS });
}
if (mode === "local") {
if (!decl.compose) {
throw new MeshCliError(`External '${name}': local mode requires "compose" — the docker compose file running the local version of the service.`, { remediation: DECL_DOCS });
}
if (typeof decl.port !== "number") {
throw new MeshCliError(`External '${name}': local (compose) mode requires "port" — the port the compose file publishes on localhost.`, { remediation: DECL_DOCS });
}
}
if (mode === "mock" && !decl.openapi && !decl.src) {
throw new MeshCliError(`External '${name}': mock mode emulates the service — declare "openapi" (spec → Prism mock) or "src" (mock process).`, { remediation: DECL_DOCS });
}
if (mode === "remote" && !remoteInlineCredentials(decl) && !decl.external) {
throw new MeshCliError(`External '${name}': remote mode needs a credential source — inline "credentials", "remote": { "env": … }, or "external" (pulls that environment's configured secret).`, { remediation: DECL_DOCS });
}
return mode;
}
export function parseExternalsSelection(input) {
const names = [];
const overrides = new Map();
for (const entry of input.split(",").map((s) => s.trim()).filter(Boolean)) {
const [rawName, mode, ...rest] = entry.split("=").map((s) => s.trim());
const name = rawName ?? "";
if (!name) {
throw new MeshCliError(`Bad --externals entry '${entry}' — expected name or name=mode with mode ∈ ${EXTERNAL_MODES.join(" | ")}.`, { remediation: { command: "mesh dev --externals plaid-db=remote,plaid" } });
}
if (mode !== undefined) {
if (rest.length > 0 || !EXTERNAL_MODES.includes(mode)) {
throw new MeshCliError(`Bad --externals entry '${entry}' — expected name or name=mode with mode ∈ ${EXTERNAL_MODES.join(" | ")}.`, { remediation: { command: "mesh dev --externals plaid-db=remote,plaid" } });
}
overrides.set(name, mode);
}
if (!names.includes(name))
names.push(name);
}
return { names, overrides };
}
export function remoteInlineCredentials(decl) {
if (decl.remote?.credentials)
return decl.remote.credentials;
if (decl.credentials && !decl.compose && !decl.openapi && !decl.src)
return decl.credentials;
return undefined;
}
export function isServiceMode(name, decl) {
return externalMode(name, decl) === "mock";
}
export function interpolateCredentialValue(raw, endpoint, env = process.env) {
let value = raw.replace(/\{\{env:([A-Za-z_][A-Za-z0-9_]*)\}\}/g, (_, name) => {
const resolved = env[name];
if (resolved === undefined) {
throw new MeshCliError(`Credential value references {{env:${name}}} but ${name} is not set in your environment.`, { remediation: { command: `export ${name}=… # then re-run mesh dev` } });
}
return resolved;
});
if (endpoint) {
value = value
.replaceAll("{{url}}", endpoint.url)
.replaceAll("{{host}}", endpoint.host)
.replaceAll("{{port}}", String(endpoint.port));
}
return value;
}
export function openapiMockCommand(decl) {
return (decl.command ?? [
"npx",
"-y",
"@stoplight/prism-cli@5",
"mock",
"-p",
"$PORT",
"-h",
"0.0.0.0",
decl.openapi,
]);
}
export function composeProjectName(sessionName, mockName) {
return `mesh-ext-${sessionName}-${mockName}`.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
}
async function waitForPort(port, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await probeTcp(port, { timeoutMs: 2000 }))
return true;
await new Promise((r) => setTimeout(r, 2000));
}
return false;
}
export async function planComposeExternal(composeFile, project, port, probes) {
if (probes.ownsRunning(composeFile, project))
return { action: "up" };
if (await probes.portServed(port)) {
const publisher = probes.portPublisher(composeFile, port);
if (!publisher || !publisher.definedByFile) {
return { action: "conflict", ...(publisher ? { container: publisher.container } : {}) };
}
return { action: "adopt-served", container: publisher.container };
}
const foreign = probes.foreignPinned(composeFile, project);
if (foreign)
return { action: "adopt-stopped", container: foreign };
return { action: "up" };
}
const dockerProbes = {
ownsRunning: ownsRunningRealization,
portServed: (port) => probeTcp(port, { timeoutMs: 2000 }),
foreignPinned: foreignPinnedContainer,
portPublisher: composeFilePortPublisher,
};
export async function composeExternalUp(appRoot, sessionName, name, decl, probes = dockerProbes) {
const composeFile = path.resolve(appRoot, decl.compose);
if (!fs.existsSync(composeFile)) {
throw new MeshCliError(`External '${name}': compose file not found at ${composeFile}.`, {
remediation: { docs: 'package.json → "mesh": { "externals": { … } }' },
});
}
const project = composeProjectName(sessionName, name);
const plan = await planComposeExternal(composeFile, project, decl.port, probes);
if (plan.action === "conflict") {
throw new MeshCliError(`External '${name}': localhost:${decl.port} is already served by ${plan.container ? `container '${plan.container}'` : "a process outside docker"}, which ${decl.compose} does not define — refusing to seed '${name}' credentials against it.`, {
remediation: {
command: `docker ps --filter publish=${decl.port} # stop it, or change the declared port`,
},
});
}
if (plan.action === "adopt-served") {
logInfo(`External '${name}' already served on localhost:${decl.port} by container '${plan.container}' — adopting it (started outside this checkout; \`mesh dev --kill\` won't touch it).`);
return undefined;
}
if (plan.action === "adopt-stopped") {
const { container } = plan;
logInfo(`External '${name}': container '${container}' exists from another checkout — starting and adopting it.`);
execFileSync("docker", ["start", container], { stdio: ["ignore", "ignore", "inherit"] });
if (!(await waitForPort(decl.port, 300_000))) {
throw new MeshCliError(`External '${name}': adopted container '${container}' never served localhost:${decl.port}.`, { remediation: { command: `docker logs ${container} # then: docker rm -f ${container} and re-run` } });
}
return undefined;
}
execFileSync("docker", ["compose", "-p", project, "-f", composeFile, "up", "-d", "--wait", "--wait-timeout", "300"], { stdio: ["ignore", "inherit", "inherit"] });
return { name, composeFile, project };
}
function ownsRunningRealization(composeFile, project) {
try {
const out = execFileSync("docker", ["compose", "-p", project, "-f", composeFile, "ps", "--format", "json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
return out.length > 0 && out !== "[]";
}
catch {
return false;
}
}
function composeServices(composeFile) {
try {
const config = JSON.parse(execFileSync("docker", ["compose", "-f", composeFile, "config", "--format", "json"], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
}));
const services = config?.services;
return services && typeof services === "object" ? services : undefined;
}
catch {
return undefined;
}
}
function portPublisher(port) {
try {
const out = execFileSync("docker", [
"ps",
"--filter",
`publish=${port}`,
"--format",
'{{.Names}}\t{{.Image}}\t{{.Label "com.docker.compose.project"}}\t{{.Label "com.docker.compose.service"}}',
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
const line = out
.split("\n")
.map((l) => l.trim())
.filter(Boolean)[0];
if (!line)
return undefined;
const [container, image, project, service] = line.split("\t");
if (!container)
return undefined;
return {
container,
image: image || undefined,
project: project || undefined,
service: service || undefined,
};
}
catch {
return undefined;
}
}
function composeFileOwns(composeFile, publisher) {
if (!publisher)
return false;
const services = composeServices(composeFile);
if (!services)
return false;
for (const [key, svc] of Object.entries(services)) {
if (svc?.container_name && svc.container_name === publisher.container)
return true;
if (publisher.service && publisher.service === key) {
if (!svc?.image || !publisher.image || svc.image === publisher.image)
return true;
}
}
return false;
}
function composeFilePortPublisher(composeFile, port) {
const publisher = portPublisher(port);
if (!publisher)
return undefined;
return { container: publisher.container, definedByFile: composeFileOwns(composeFile, publisher) };
}
function foreignPinnedContainer(composeFile, project) {
for (const svc of Object.values(composeServices(composeFile) ?? {})) {
const pinned = svc?.container_name;
if (!pinned)
continue;
try {
const owner = execFileSync("docker", ["inspect", pinned, "--format", '{{ index .Config.Labels "com.docker.compose.project" }}'], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
if (owner !== project)
return pinned;
}
catch {
}
}
return undefined;
}
export function composeExternalsDown(refs) {
for (const ref of refs ?? []) {
try {
execFileSync("docker", ["compose", "-p", ref.project, "-f", ref.composeFile, "down", "--remove-orphans"], {
stdio: ["ignore", "inherit", "inherit"],
});
}
catch {
}
}
}
const SECRET_KEY_RE = /key|secret|password|token/i;
export function readLocalMocks(appRoot) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf-8"));
const mocks = pkg?.mesh?.mocks;
const externals = pkg?.mesh?.externals;
return {
...(mocks && typeof mocks === "object" ? mocks : {}),
...(externals && typeof externals === "object" ? externals : {}),
};
}
catch {
return {};
}
}
export function externalSecretPath(tenant, external) {
return `mesh/${tenant}/${LOCAL_ENV}/external/${external}`;
}
export async function fetchRemoteExternalCredentials(tenant, env, external, profile) {
const secretId = `mesh/${tenant}/${env}/external/${external}`;
const region = process.env.MESH_PLATFORM_REGION ?? process.env.AWS_REGION ?? "us-east-2";
const fallbackProfile = !process.env.AWS_ACCESS_KEY_ID && !process.env.AWS_PROFILE
? (process.env.MESH_AWS_PROFILE ?? profile)
: undefined;
try {
const { SecretsManagerClient, GetSecretValueCommand } = await import("@aws-sdk/client-secrets-manager");
if (fallbackProfile)
process.env.AWS_PROFILE = fallbackProfile;
const sm = new SecretsManagerClient({ region });
let res;
try {
res = await sm.send(new GetSecretValueCommand({ SecretId: secretId }));
}
finally {
if (fallbackProfile)
delete process.env.AWS_PROFILE;
}
const parsed = JSON.parse(res.SecretString ?? "{}");
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("secret value is not a JSON object");
}
return Object.fromEntries(Object.entries(parsed).map(([key, v]) => [key, String(v)]));
}
catch (err) {
throw new MeshCliError(`External '${external}': could not pull remote credentials from the ${env} environment (${secretId}, region ${region}): ${err instanceof Error ? err.message : err}`, {
remediation: {
command: `AWS_PROFILE=<${tenant}-${env} profile> mesh dev … # or set "remote": { "profile": … } on the declaration / MESH_AWS_PROFILE`,
},
});
}
}
function hostForProber(target) {
return target.replace(/\b(?:localhost|127\.0\.0\.1)\b/, "host.docker.internal");
}
function httpProbeUrl(raw) {
if (!raw)
return undefined;
let parsed;
try {
parsed = new URL(raw);
}
catch {
return undefined;
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
return undefined;
parsed.username = "";
parsed.password = "";
return hostForProber(parsed.toString());
}
function derivedTcpTarget(creds) {
if (creds.host && creds.port)
return `${hostForProber(creds.host)}:${creds.port}`;
if (creds.bucket)
return `${creds.bucket}.s3.${creds.region || "us-east-1"}.amazonaws.com:443`;
return undefined;
}
export function resolveProbeTarget(mode, decl, endpoint, creds) {
if (mode === "remote") {
const url = httpProbeUrl(creds.endpoint ?? creds.url ?? creds.baseUrl);
const tcpTarget = derivedTcpTarget(creds);
if (decl.probe === "tcp" || (!url && tcpTarget)) {
if (!tcpTarget && decl.probe === "tcp") {
logWarn(`External '${decl.external ?? ""}': probe: "tcp" declared but the remote credentials carry no host/port (or bucket) to probe — no uptime probe registered.`);
}
return tcpTarget ? { target: tcpTarget, module: "tcp_connect" } : undefined;
}
return url ? { target: url.replace(/\/+$/, "") + (decl.healthPath ?? "") } : undefined;
}
if (!endpoint)
return undefined;
if (decl.probe === "tcp") {
return { target: `host.docker.internal:${endpoint.port}`, module: "tcp_connect" };
}
return {
target: hostForProber(endpoint.url).replace(/\/+$/, "") + (decl.healthPath ?? "/health"),
};
}
export function externalDisplayName(name, decl) {
return (decl.displayName ??
name
.split(/[-_\s]+/)
.filter(Boolean)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" "));
}
export function externalDocs(decl) {
if (!decl.docs)
return undefined;
return typeof decl.docs === "string" ? { url: decl.docs } : decl.docs;
}
export function localProbeFile(tenant, app, external) {
return path.join(localProbesDir(), `${tenant}-${app}-${external}.json`);
}
export function localProbesRemove(files) {
for (const file of files ?? []) {
try {
fs.rmSync(file, { force: true });
}
catch {
}
}
}
export async function seedLocalMock(args) {
const { tenant, app, name, decl, endpoint } = args;
if (!decl.external)
return undefined;
const mode = externalMode(name, decl);
let value;
if (mode === "remote") {
const inline = remoteInlineCredentials(decl);
value = inline
? Object.fromEntries(Object.entries(inline).map(([key, raw]) => [
key,
interpolateCredentialValue(raw, undefined),
]))
: await fetchRemoteExternalCredentials(tenant, decl.remote?.env ?? "dev", decl.external, decl.remote?.profile);
}
else {
if (!decl.credentials)
return undefined;
value = Object.fromEntries(Object.entries(decl.credentials).map(([key, raw]) => [
key,
interpolateCredentialValue(raw, endpoint),
]));
}
const secretName = externalSecretPath(tenant, decl.external);
await upsertLocalSecret(secretName, value);
await upsertLocalSecret(`${secretName}/.config`, Object.fromEntries(Object.entries(value).filter(([key]) => !SECRET_KEY_RE.test(key))));
const { SSMClient, PutParameterCommand } = await import("@aws-sdk/client-ssm");
const ssm = new SSMClient(LOCAL_AWS_CONFIG);
const base = `/mesh-platform/${tenant}/${LOCAL_ENV}/apps/${app}/stacks/local/external-services/${decl.external}`;
await ssm.send(new PutParameterCommand({
Name: `${base}/meta`,
Type: "String",
Overwrite: true,
Value: JSON.stringify({
name: decl.external,
displayName: externalDisplayName(name, decl),
type: decl.type ?? "other",
description: mode === "local"
? `${externalDisplayName(name, decl)} — local replica via mesh dev (docker compose)`
: mode === "remote"
? remoteInlineCredentials(decl)
? `${externalDisplayName(name, decl)} — remote service (vendor credentials)`
: `${externalDisplayName(name, decl)} — remote service (${decl.remote?.env ?? "dev"} environment credentials)`
: `${externalDisplayName(name, decl)} — emulated by mesh dev (mock)`,
secretPrefix: secretName,
...(externalDocs(decl) ? { docs: externalDocs(decl) } : {}),
}),
Description: `External service registration (local mock ${name})`,
}));
await ssm.send(new PutParameterCommand({
Name: `${base}/credentials`,
Type: "String",
Overwrite: true,
Value: JSON.stringify({
fields: Object.fromEntries(Object.keys(value).map((key) => [
key,
{ type: "field", name: key, description: "", secret: SECRET_KEY_RE.test(key), optional: false },
])),
keyedBy: null,
}),
Description: `External service credential schema (local mock ${name})`,
}));
const probe = resolveProbeTarget(mode, decl, endpoint, value);
await ssm.send(new PutParameterCommand({
Name: `${base}/healthCheck`,
Type: "String",
Overwrite: true,
Value: JSON.stringify({ intervalSeconds: 30, timeoutSeconds: 10, hasCustomCheck: false, hasProbe: !!probe }),
Description: `External service health check config (local mock ${name})`,
}));
const probeFile = localProbeFile(tenant, app, decl.external);
if (probe) {
fs.writeFileSync(probeFile, JSON.stringify([
{
targets: [probe.target],
labels: {
type: "external-service",
tenant,
env: LOCAL_ENV,
app,
external_service: decl.external,
target: decl.external,
...(probe.module ? { __probe_module: probe.module } : {}),
},
},
], null, 2));
}
else {
localProbesRemove([probeFile]);
}
const endpointShown = endpoint?.url ??
value.endpoint ??
value.url ??
value.baseUrl ??
(value.host ? `${value.host}${value.port ? `:${value.port}` : ""}` : "credentials");
logSuccess(`External '${name}' wired (${mode}): ${secretName} → ${endpointShown}` +
` (Hub registration${probe ? " + uptime probe" : ""})`);
return probe ? probeFile : undefined;
}