UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

246 lines (245 loc) 9.78 kB
import * as fs from "fs"; import * as path from "path"; import { isServiceMode, openapiMockCommand } from "./mocks.js"; import { probeTcp } from "./helpers.js"; import { MeshCliError } from "../../utils/errors.js"; import { localAwsEnv, LOCAL_ENV } from "./seed.js"; import { TEMPORAL_ADDRESS } from "./seed.js"; import { LOGIN_CONTEXT, ZITADEL_ISSUER } from "./seed-zitadel.js"; import { authSecretPath } from "./auth-provision.js"; import { findPackageRoot } from "./stack.js"; import { getContextConfig } from "../login.js"; import { findMeshJson } from "../../utils/mesh-json.js"; export const LOCAL_STACK_NAME = "local"; const BASE_PORT = 3000; export function localAppNamespace(tenant, appName) { return `${tenant}-${LOCAL_ENV}-${appName}`; } export function logShipperPath() { return path.join(findPackageRoot(), "assets", "log-shipper.mjs"); } export function autoInstrumentationEnv(appRoot, src) { const pkgDir = path.join("node_modules", "@opentelemetry", "auto-instrumentations-node"); const present = fs.existsSync(path.join(appRoot, src, pkgDir)) || fs.existsSync(path.join(appRoot, pkgDir)); if (!present) return {}; return { NODE_OPTIONS: { value: "--import @opentelemetry/auto-instrumentations-node/register" }, OTEL_TRACES_EXPORTER: { value: "otlp" }, OTEL_METRICS_EXPORTER: { value: "none" }, OTEL_LOGS_EXPORTER: { value: "none" }, OTEL_NODE_DISABLED_INSTRUMENTATIONS: { value: "fs,dns,net" }, }; } export function otelEnv(tenant, appName, serviceName) { const namespace = localAppNamespace(tenant, appName); return { OTEL_EXPORTER_OTLP_ENDPOINT: { value: "http://localhost:4318" }, OTEL_SERVICE_NAME: { value: serviceName }, OTEL_RESOURCE_ATTRIBUTES: { value: `k8s.namespace.name=${namespace},k8s.deployment.name=${serviceName},k8s.container.name=${serviceName},service.namespace=${appName}`, }, }; } export function localPlatformEnv(tenant, appName, serviceName) { const namespace = localAppNamespace(tenant, appName); const env = { TEMPORAL_ADDRESS: { value: TEMPORAL_ADDRESS }, TEMPORAL_NAMESPACE: { value: namespace }, TEMPORAL_TASK_QUEUE: { value: appName }, ...otelEnv(tenant, appName, serviceName), DATABASE_URL: { value: "postgres://postgres:postgres@localhost:5433/app" }, ZITADEL_ISSUER: { value: ZITADEL_ISSUER }, ZITADEL_SECRET_PREFIX: { value: authSecretPath(tenant, appName, serviceName) }, SPICEDB_ENDPOINT: { value: "localhost:50051" }, SPICEDB_HTTP_ENDPOINT: { value: "http://localhost:8443" }, SPICEDB_PRESHARED_KEY: { value: "local-dev-key" }, MEMCACHED_SERVERS: { value: "localhost:11211" }, MESH_LOCAL: { value: "1" }, MESH_SERVICE: { value: serviceName }, }; const cliClientId = getContextConfig(LOGIN_CONTEXT)?.clientId; if (cliClientId) { env.AUTH_AUDIENCE = { value: cliClientId }; } for (const [key, value] of Object.entries(localAwsEnv())) { env[key] = { value }; } return env; } export function isDagsterWorkspaceRoot(appRoot) { if (fs.existsSync(path.join(appRoot, "dg.toml"))) return true; const pyproject = path.join(appRoot, "pyproject.toml"); if (!fs.existsSync(pyproject)) return false; try { return /^\s*\[tool\.dg[\].]/m.test(fs.readFileSync(pyproject, "utf-8")); } catch { return false; } } export function resolveDgBinary(appRoot) { for (const candidate of ["deployments/local/.venv/bin/dg", ".venv/bin/dg"]) { if (fs.existsSync(path.join(appRoot, candidate))) return candidate; } return "dg"; } function hasDevScript(dir) { const pkgPath = path.join(dir, "package.json"); if (!fs.existsSync(pkgPath)) return false; try { const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); return typeof pkg?.scripts?.dev === "string"; } catch { return false; } } export function detectLocalServices(appRoot) { const services = {}; for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) { if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue; if (hasDevScript(path.join(appRoot, entry.name))) { services[entry.name] = { src: entry.name }; } } if (Object.keys(services).length === 0 && hasDevScript(appRoot)) { services[path.basename(appRoot)] = { src: "." }; } return services; } export function buildLocalDevOutput(appRoot, tenant, opts = {}) { const detected = detectLocalServices(appRoot); const names = Object.keys(detected); const isDagsterWorkspace = isDagsterWorkspaceRoot(appRoot); if (names.length === 0 && !isDagsterWorkspace) { throw new MeshCliError(`No runnable services found under ${appRoot} — expected subdirectories (api/, worker/, …) with a package.json 'dev' script, or a Dagster workspace (dg.toml, or [tool.dg] in pyproject.toml).`, { remediation: { command: "mesh create-app" } }); } let appName = path.basename(appRoot); try { const pkg = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf-8")); if (typeof pkg?.name === "string" && pkg.name) appName = pkg.name.replace(/^@[^/]+\//, ""); } catch { } const pulumiYaml = path.join(appRoot, "Pulumi.yaml"); if (fs.existsSync(pulumiYaml)) { const match = fs.readFileSync(pulumiYaml, "utf-8").match(/^name:\s*(.+)$/m); if (match?.[1]?.trim()) appName = match[1].trim(); } const services = {}; const mocks = opts.mocks ?? {}; const mockNames = Object.keys(mocks).filter((name) => isServiceMode(name, mocks[name])); const externalEnv = {}; for (const decl of Object.values(mocks)) { if (!decl.external) continue; const prefix = decl.external.toUpperCase().replace(/-/g, "_"); externalEnv[`${prefix}_SECRET_PREFIX`] = { value: `mesh/${tenant}/${LOCAL_ENV}/external/${decl.external}`, }; } names.forEach((name, i) => { const port = BASE_PORT + i; services[name] = { src: detected[name].src, port, command: ["pnpm", "dev"], env: { PORT: { value: String(port) }, ...localPlatformEnv(tenant, appName, name), ...autoInstrumentationEnv(appRoot, detected[name].src), ...externalEnv, }, }; }); for (const name of names) { const key = `SERVICE_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`; const url = `http://localhost:${services[name].port}`; for (const other of names) { services[other].env[key] = { value: url }; } } if (isDagsterWorkspace) { const port = BASE_PORT + names.length + mockNames.length; services["dagster"] = { src: appRoot, kind: "dagster", port, command: [resolveDgBinary(appRoot), "dev", "-h", "127.0.0.1", "-p", '"$PORT"'], env: { PORT: { value: String(port) }, DAGSTER_HOME: { value: path.join(appRoot, ".dagster") }, ...localPlatformEnv(tenant, appName, "dagster"), }, }; } mockNames.forEach((name, i) => { const decl = mocks[name]; const serviceName = `mock-${name}`; const port = decl.port ?? BASE_PORT + names.length + i; services[serviceName] = { src: decl.src ?? ".", port, command: decl.openapi ? openapiMockCommand(decl) : (decl.command ?? ["pnpm", "dev"]), env: { PORT: { value: String(port) }, ...otelEnv(tenant, appName, serviceName), ...Object.fromEntries(Object.entries(decl.env ?? {}).map(([key, value]) => [key, { value }])), }, }; }); return { platform: { tenant: "local", env: "dev" }, app: appName, stack: LOCAL_STACK_NAME, services, tunnels: {}, }; } export function hasStackBacking(appRoot) { const hasOwn = fs.existsSync(path.join(appRoot, "Pulumi.yaml")) || fs.readdirSync(appRoot).some((f) => /^Pulumi\..+\.yaml$/.test(f)); if (hasOwn) return true; let dir = path.dirname(appRoot); while (true) { if (fs.existsSync(path.join(dir, "Pulumi.yaml"))) return true; if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) { return fs.existsSync(path.join(dir, "Pulumi.yaml")); } const parent = path.dirname(dir); if (parent === dir) return false; dir = parent; } } export function detectLocalTenant(appRoot) { for (const file of fs.readdirSync(appRoot)) { if (!/^Pulumi\..*\.yaml$/.test(file)) continue; const match = fs .readFileSync(path.join(appRoot, file), "utf-8") .match(/^\s*mesh:tenant:\s*["']?([A-Za-z0-9-]+)["']?\s*$/m); if (match) return match[1]; } const recorded = findMeshJson(appRoot); if (recorded) return recorded.data.tenant; return "local"; } export async function ensureLocalPlatformRunning() { const [temporalUp, fabricUp] = await Promise.all([probeTcp(7233), probeTcp(4566)]); if (!temporalUp || !fabricUp) { throw new MeshCliError("The local Mesh platform is not running (mesh dev local mode wires services to it).", { remediation: { command: "mesh start" } }); } }