UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

275 lines (274 loc) 12.9 kB
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 }; }