@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
119 lines (118 loc) • 5.56 kB
JavaScript
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.`);
}
}