@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
275 lines (274 loc) • 12.9 kB
JavaScript
import { logInfo, logSuccess } from "../../utils/log.js";
import { LOCAL_ENV, LOCAL_AWS_CONFIG, registerTenantEnv } from "./seed.js";
import { api, readSeederPat, PLATFORM_ORG, ensureZitadelProject, searchZitadelApp } from "./seed-zitadel.js";
import { upsertLocalSecret } from "./helpers.js";
export function authSecretPath(tenant, app, service) {
return `mesh/${tenant}/${LOCAL_ENV}/apps/${app}/zitadel/${service}`;
}
async function ensureOrg(pat, name) {
const search = await api(pat, "POST", "/admin/v1/orgs/_search", {
queries: [{ nameQuery: { name, method: "TEXT_QUERY_METHOD_EQUALS" } }],
});
const existing = search?.result?.[0]?.id;
if (existing)
return existing;
const created = await api(pat, "POST", "/v2/organizations", { name });
logSuccess(`Created Zitadel org '${name}' (app tenant)`);
return created.organizationId;
}
const ensureProject = (pat, orgId, name) => ensureZitadelProject(pat, name, { orgId, describe: "app" });
async function ensureServiceApp(pat, orgId, projectId, service) {
const existing = await searchZitadelApp(pat, projectId, service, orgId);
if (existing) {
return { created: false, appId: existing.id, clientId: existing?.apiConfig?.clientId };
}
const created = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/api`, { name: service, authMethodType: "API_AUTH_METHOD_TYPE_BASIC" }, orgId);
logSuccess(`Created Zitadel application '${service}' (service)`);
return { created: true, clientId: created.clientId, clientSecret: created.clientSecret };
}
async function ensureProjectRoles(pat, orgId, projectId, roles) {
if (roles.length === 0)
return;
const existing = await api(pat, "POST", `/management/v1/projects/${projectId}/roles/_search`, {}, orgId);
const have = new Set((existing?.result ?? []).map((r) => r.key));
for (const key of roles) {
if (have.has(key))
continue;
await api(pat, "POST", `/management/v1/projects/${projectId}/roles`, { roleKey: key, displayName: key }, orgId);
logSuccess(`Created project role '${key}'`);
}
}
async function ensureM2mCaller(pat, tenant, app, orgId, projectId, roles) {
const userName = `${app}-m2m`;
const search = await api(pat, "POST", "/management/v1/users/_search", { queries: [{ userNameQuery: { userName, method: "TEXT_QUERY_METHOD_EQUALS" } }] }, orgId);
let userId = search?.result?.[0]?.id;
let createdUser = false;
if (!userId) {
const created = await api(pat, "POST", "/management/v1/users/machine", { userName, name: `${app} local M2M caller`, accessTokenType: "ACCESS_TOKEN_TYPE_BEARER" }, orgId);
userId = created.userId;
createdUser = true;
logSuccess(`Created machine user '${userName}' (local M2M caller)`);
}
try {
await api(pat, "POST", `/management/v1/users/${userId}/grants`, { projectId, roleKeys: roles }, orgId);
}
catch (err) {
const e = err;
if (e.status !== 409 && !/already exists/i.test(String(e.message)))
throw err;
const grants = await api(pat, "POST", "/management/v1/users/grants/_search", { queries: [{ userIdQuery: { userId } }, { projectIdQuery: { projectId } }] }, orgId);
const grantId = grants?.result?.[0]?.id;
if (grantId && roles.length > 0) {
try {
await api(pat, "PUT", `/management/v1/users/${userId}/grants/${grantId}`, { roleKeys: roles }, orgId);
}
catch (updateErr) {
const ue = updateErr;
if (!/has not been changed/i.test(String(ue.message)))
throw updateErr;
}
}
}
if (createdUser || !(await authSecretExists(tenant, app, "m2m"))) {
const secret = await api(pat, "PUT", `/management/v1/users/${userId}/secret`, {}, orgId);
await writeAuthSecret(tenant, app, "m2m", {
clientId: secret.clientId,
clientSecret: secret.clientSecret,
issuer: "http://localhost:8080",
orgId,
projectId,
});
return true;
}
return false;
}
async function authSecretExists(tenant, app, service) {
const { SecretsManagerClient, GetSecretValueCommand } = await import("@aws-sdk/client-secrets-manager");
const sm = new SecretsManagerClient(LOCAL_AWS_CONFIG);
return sm.send(new GetSecretValueCommand({ SecretId: authSecretPath(tenant, app, service) })).then(() => true, () => false);
}
async function writeAuthSecret(tenant, app, service, value) {
const name = authSecretPath(tenant, app, service);
await upsertLocalSecret(name, value);
logSuccess(`Stored service credentials → ${name}`);
}
export async function registerLocalApp(args) {
const { SSMClient, PutParameterCommand } = await import("@aws-sdk/client-ssm");
const ssm = new SSMClient(LOCAL_AWS_CONFIG);
const put = (name, value, description) => ssm.send(new PutParameterCommand({ Name: name, Type: "String", Overwrite: true, Value: JSON.stringify(value), Description: description }));
await registerTenantEnv(args.tenant);
const namespace = `${args.tenant}-${LOCAL_ENV}-${args.app}`;
const base = `/mesh-platform/${args.tenant}/${LOCAL_ENV}/apps/${args.app}/stacks/local`;
await put(base, {
name: args.app,
tenant: args.tenant,
stack: "local",
namespace,
platform: { name: "local", env: LOCAL_ENV },
}, `Local app registration for ${args.tenant}/${args.app} (mesh dev --local)`);
for (const service of args.services) {
const port = args.ports?.[service];
const isMock = service.startsWith("mock-");
await put(`${base}/services/${service}`, {
...(port ? { url: `http://localhost:${port}`, port } : {}),
replicas: 1,
tier: "core",
...(!isMock && args.links?.length ? { links: args.links } : {}),
}, `Local service registration (${service})`);
}
for (const service of args.services) {
if (args.kinds?.[service] !== "dagster")
continue;
const port = args.ports?.[service];
if (!port)
continue;
await put(`${base}/dagster`, {
kind: "dagster",
graphqlUrl: `http://host.docker.internal:${port}/graphql`,
uiUrl: `http://localhost:${port}`,
}, `Local Dagster discovery record (${service})`);
}
for (const service of args.services) {
if (!/worker/i.test(service))
continue;
await put(`${base}/workers/${service}`, { temporalNamespace: namespace, taskQueue: args.app }, `Local worker registration (${service})`);
}
await put(`${base}/meta`, {
runtime: "mesh-dev-local",
services: args.services,
ports: args.ports ?? {},
...(args.version ? { version: args.version } : {}),
deployedAt: new Date().toISOString(),
}, "Local run metadata (mesh dev --local)");
logSuccess(`Registered app in the local registry → ${base}`);
return base;
}
export async function reconcileRegistryFromZitadel() {
const pat = readSeederPat();
const { SSMClient, GetParameterCommand, PutParameterCommand } = await import("@aws-sdk/client-ssm");
const ssm = new SSMClient(LOCAL_AWS_CONFIG);
const orgs = await api(pat, "POST", "/admin/v1/orgs/_search", { query: { limit: 200 } });
const tenants = [];
let apps = 0;
for (const org of orgs?.result ?? []) {
const tenant = org?.name;
if (!tenant || tenant === PLATFORM_ORG)
continue;
await registerTenantEnv(tenant);
tenants.push(tenant);
const projects = await api(pat, "POST", "/management/v1/projects/_search", { query: { limit: 200 } }, org.id);
for (const project of projects?.result ?? []) {
const app = project?.name;
if (!app)
continue;
apps++;
const base = `/mesh-platform/${tenant}/${LOCAL_ENV}/apps/${app}/stacks/local`;
const exists = await ssm.send(new GetParameterCommand({ Name: base })).then(() => true, () => false);
if (exists)
continue;
await ssm.send(new PutParameterCommand({
Name: base,
Type: "String",
Overwrite: true,
Value: JSON.stringify({
name: app,
tenant,
stack: "local",
namespace: `${tenant}-${LOCAL_ENV}-${app}`,
platform: { name: "local", env: LOCAL_ENV },
}),
Description: `App registration reconciled from Zitadel (${tenant}/${app})`,
}));
const applications = await api(pat, "POST", `/management/v1/projects/${project.id}/apps/_search`, { query: { limit: 100 } }, org.id);
for (const application of applications?.result ?? []) {
const service = application?.name;
if (!service)
continue;
await ssm.send(new PutParameterCommand({
Name: `${base}/services/${service}`,
Type: "String",
Overwrite: true,
Value: JSON.stringify({ replicas: 1, tier: "core" }),
Description: `Service registration reconciled from Zitadel (${service})`,
}));
}
}
}
return { tenants, apps };
}
export async function ensureSignInApp(args) {
const pat = readSeederPat();
const orgId = await ensureOrg(pat, args.tenant);
const projectId = await ensureProject(pat, orgId, args.app);
const name = `${args.service}-web`;
const base = args.baseUrl.replace(/\/+$/, "");
const redirectUris = [`${base}/oauth2/callback`];
const postLogoutRedirectUris = [base, `${base}/`];
const config = {
redirectUris,
postLogoutRedirectUris,
responseTypes: ["OIDC_RESPONSE_TYPE_CODE"],
grantTypes: ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN"],
appType: "OIDC_APP_TYPE_WEB",
authMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC",
accessTokenType: "OIDC_TOKEN_TYPE_JWT",
accessTokenRoleAssertion: true,
idTokenRoleAssertion: true,
idTokenUserinfoAssertion: true,
devMode: true,
};
const existing = await searchZitadelApp(pat, projectId, name, orgId);
if (existing) {
await api(pat, "PUT", `/management/v1/projects/${projectId}/apps/${existing.id}/oidc_config`, config, orgId);
return;
}
const created = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/oidc`, { name, ...config }, orgId);
await writeAuthSecret(args.tenant, args.app, name, {
clientId: created.clientId,
clientSecret: created.clientSecret,
issuer: "http://localhost:8080",
orgId,
projectId,
});
logSuccess(`Created Zitadel application '${name}' (browser sign-in → ${base}/oauth2/callback)`);
}
export async function ensureAppTenantAuth(args) {
const pat = readSeederPat();
const orgId = await ensureOrg(pat, args.tenant);
const projectId = await ensureProject(pat, orgId, args.app);
const roles = args.roles ?? [];
await ensureProjectRoles(pat, orgId, projectId, roles);
const created = [];
for (const service of args.services) {
const result = await ensureServiceApp(pat, orgId, projectId, service);
if (result.created) {
await writeAuthSecret(args.tenant, args.app, service, {
clientId: result.clientId,
clientSecret: result.clientSecret,
issuer: "http://localhost:8080",
orgId,
projectId,
});
created.push(service);
}
else if (!(await authSecretExists(args.tenant, args.app, service))) {
const regenerated = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/${result.appId}/api_config/_generate_client_secret`, {}, orgId);
await writeAuthSecret(args.tenant, args.app, service, {
clientId: result.clientId ?? "",
clientSecret: regenerated.clientSecret,
issuer: "http://localhost:8080",
orgId,
projectId,
});
logSuccess(`Regenerated credentials for '${service}' (local secret store was reset)`);
created.push(service);
}
}
if (await ensureM2mCaller(pat, args.tenant, args.app, orgId, projectId, roles)) {
created.push("m2m");
}
if (created.length === 0) {
logInfo(`Auth config for ${args.tenant}/${args.app} already provisioned`);
}
return { orgId, projectId, created };
}