UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

119 lines (118 loc) 5.56 kB
import { SSMClient } from "@aws-sdk/client-ssm"; import { SecretsManagerClient, ListSecretsCommand, GetSecretValueCommand, PutSecretValueCommand, CreateSecretCommand, } from "@aws-sdk/client-secrets-manager"; import { buildInstanceIndexValue, instanceIndexSecretId, instanceKeyFromSecretName, parseInstanceIndex, } from "@mesh-tech/secrets"; import { detectContext } from "../../utils/context.js"; import { logInfo, logSuccess, logError } from "../../utils/log.js"; import { resolveStackOption } from "../../utils/stack-flag.js"; import { discoverExternalServices, ensureAwsCredentialsForStack, isResourceNotFound, } from "./set.js"; export async function scanInstanceKeys(client, secretPrefix) { const keys = []; let nextToken; do { const response = await client.send(new ListSecretsCommand({ Filters: [{ Key: "name", Values: [`${secretPrefix}/`] }], NextToken: nextToken, })); for (const secret of response.SecretList ?? []) { const key = secret.Name ? instanceKeyFromSecretName(secretPrefix, secret.Name) : null; if (key) keys.push(key); } nextToken = response.NextToken; } while (nextToken); return keys; } export function indexAlreadyMatches(missing, stale, indexExists, malformed) { return missing.length === 0 && stale.length === 0 && indexExists && !malformed; } export async function reindexCommand(servicePath, opts) { const stackOpt = resolveStackOption(opts); const context = detectContext(stackOpt); const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2"; await ensureAwsCredentialsForStack(stackOpt ?? context.stage); const ssmClient = new SSMClient({ region }); const smClient = new SecretsManagerClient({ region }); const services = await discoverExternalServices(ssmClient, context.tenant, context.platformEnv); const serviceName = servicePath?.replace(/^external\//, ""); if (!serviceName || !services.has(serviceName)) { if (serviceName) { logError(`External service "${serviceName}" not found.`); } const keyed = [...services.entries()].filter(([, s]) => s.schema?.keyedBy); console.error("\nMulti-instance external services:"); for (const [name, svc] of keyed) { console.error(` ${name} (keyed by ${svc.schema.keyedBy})`); } if (keyed.length === 0) console.error(" (none)"); console.error(`\nUsage: mesh secrets reindex external/<name>`); process.exit(serviceName ? 1 : 0); } const svc = services.get(serviceName); if (!svc.schema?.keyedBy) { logError(`"${serviceName}" is single-instance (no keyedBy) — it has no instance index.`); process.exit(1); } const secretPrefix = svc.meta.secretPrefix; const indexId = instanceIndexSecretId(secretPrefix); const actual = [...new Set(await scanInstanceKeys(smClient, secretPrefix))].sort(); let listed = []; let indexExists = true; let malformed = false; try { const res = await smClient.send(new GetSecretValueCommand({ SecretId: indexId })); try { listed = parseInstanceIndex(res.SecretString ?? ""); } catch { malformed = true; logInfo(`Existing index at ${indexId} is malformed — rebuilding from scratch.`); } } catch (err) { if (!isResourceNotFound(err)) throw err; indexExists = false; logInfo(`No index at ${indexId} yet — it will be created.`); } const missing = actual.filter((k) => !listed.includes(k)); const stale = listed.filter((k) => !actual.includes(k)); logInfo(`Instances in Secrets Manager: ${actual.length ? actual.join(", ") : "(none)"}`); if (missing.length > 0) logInfo(`Missing from index: ${missing.join(", ")}`); if (stale.length > 0) logInfo(`Stale in index (secret gone): ${stale.join(", ")}`); if (indexAlreadyMatches(missing, stale, indexExists, malformed)) { logSuccess(`Index at ${indexId} already matches — nothing to do.`); return; } if (opts.dryRun) { logInfo(`Dry run — would write ${indexId} with ${actual.length} instance(s).`); return; } await writeInstanceIndex(smClient, secretPrefix, serviceName, actual); logSuccess(`Index rebuilt: ${indexId} now lists ${actual.length} instance(s).`); } export async function writeInstanceIndex(client, secretPrefix, serviceName, keys) { const indexId = instanceIndexSecretId(secretPrefix); const secretString = buildInstanceIndexValue(keys); try { await client.send(new PutSecretValueCommand({ SecretId: indexId, SecretString: secretString })); } catch (err) { if (!isResourceNotFound(err)) throw err; await client.send(new CreateSecretCommand({ Name: indexId, Description: `Instance-key index for ${serviceName} (read by listInstances(); maintained by mesh secrets set/reindex and the Hub)`, SecretString: secretString, Tags: [ { Key: "mesh:type", Value: "external-service-index" }, { Key: "mesh:service", Value: serviceName }, ], })); logInfo(`Created ${indexId} OUT OF BAND — the declaring stack does not manage it. ` + `Deploy the declaring stack BEFORE reindex next time, or import this ` + `secret into the stack before its next deploy.`); } }