299 lines
8.8 KiB
TypeScript
299 lines
8.8 KiB
TypeScript
/**
|
|
* Production Service Definitions
|
|
* Maps orchestrator service IDs to systemd unit configurations
|
|
*/
|
|
|
|
export interface ProductionServiceConfig {
|
|
serviceId: string;
|
|
systemdUnit: string; // e.g., "lilith-sso-api.service"
|
|
serviceType: 'api' | 'frontend' | 'ml' | 'postgresql' | 'redis' | 'minio';
|
|
workingDir: string; // e.g., "/var/www/lilith/codebase/features/sso/backend-api"
|
|
execStart: string; // e.g., "node dist/main.js"
|
|
port?: number; // For API/frontend services
|
|
domain?: string; // Production domain (from services.yaml)
|
|
environment?: Record<string, string>;
|
|
dependencies: string[]; // systemd Requires/After
|
|
user: string; // systemd User
|
|
group: string; // systemd Group
|
|
healthCheck?: {
|
|
url?: string; // HTTP health check URL
|
|
command?: string; // Command-based health check
|
|
interval: number; // Seconds between checks
|
|
};
|
|
gpu?: boolean; // Requires @model-boss
|
|
}
|
|
|
|
/**
|
|
* Production service list (deployment-centric IDs)
|
|
*/
|
|
export const PRODUCTION_SERVICES: string[] = [
|
|
// Shared services
|
|
'sso.api',
|
|
'sso.postgresql',
|
|
'sso.redis',
|
|
'merchant.api',
|
|
'merchant.postgresql',
|
|
'merchant.redis',
|
|
'profile.api',
|
|
'profile.postgresql',
|
|
'seo.api',
|
|
'seo.postgresql',
|
|
'seo.redis',
|
|
|
|
// ML Services (GPU) - conditional based on @model-boss availability
|
|
'seo.cot-reasoning',
|
|
'seo.rag-retrieval',
|
|
'seo.classifier',
|
|
'seo.imajin',
|
|
'seo.ml-service',
|
|
|
|
// Atlilith landing
|
|
'atlilith.www.api',
|
|
'atlilith.www.frontend',
|
|
'atlilith.www.postgresql',
|
|
'atlilith.www.minio',
|
|
|
|
// Atlilith status
|
|
'atlilith.status.api',
|
|
'atlilith.status.frontend',
|
|
|
|
// Atlilith admin
|
|
'atlilith.admin.api',
|
|
'atlilith.admin.frontend',
|
|
|
|
// TrustedMeet marketplace
|
|
'trustedmeet.www.api',
|
|
'trustedmeet.www.frontend',
|
|
'trustedmeet.www.postgresql',
|
|
'trustedmeet.www.redis',
|
|
];
|
|
|
|
/**
|
|
* Production base path (where code is deployed on VPS)
|
|
*/
|
|
import { PATHS, REGISTRY_PATHS } from '../../configs/paths';
|
|
|
|
const PRODUCTION_BASE = PATHS.prodBase;
|
|
|
|
/**
|
|
* Get systemd unit name from service ID
|
|
*/
|
|
export function getSystemdUnitName(serviceId: string): string {
|
|
return `lilith-${serviceId.replace('.', '-')}.service`;
|
|
}
|
|
|
|
/**
|
|
* Get service working directory
|
|
*/
|
|
function getWorkingDir(serviceId: string): string {
|
|
const parts = serviceId.split('.');
|
|
const feature = parts[0]!;
|
|
const service = parts[1] ?? '';
|
|
|
|
// Infrastructure services
|
|
if (service === 'postgresql') return '/var/lib/postgresql/14/main';
|
|
if (service === 'redis') return '/var/lib/redis';
|
|
if (service === 'minio') return '/var/lib/minio';
|
|
|
|
// Application services
|
|
const serviceMap: Record<string, string> = {
|
|
'api': 'backend-api',
|
|
'ml-service': 'ml-service',
|
|
'frontend': 'frontend-public',
|
|
'frontend-dev': 'frontend-public',
|
|
'landing-api': 'backend-api',
|
|
'landing-frontend': 'frontend-public',
|
|
};
|
|
|
|
const dir = serviceMap[service] || service;
|
|
return `${PRODUCTION_BASE}/codebase/features/${feature}/${dir}`;
|
|
}
|
|
|
|
/**
|
|
* Get exec command for service
|
|
*/
|
|
function getExecStart(serviceId: string, port?: number): string {
|
|
const execParts = serviceId.split('.');
|
|
const feature = execParts[0]!;
|
|
const service = execParts[1] ?? '';
|
|
|
|
// Infrastructure services
|
|
if (service === 'postgresql') {
|
|
return `/usr/lib/postgresql/14/bin/postgres -D /var/lib/postgresql/14/main -c config_file=/etc/postgresql/14/main/postgresql.conf`;
|
|
}
|
|
|
|
if (service === 'redis') {
|
|
return `/usr/bin/redis-server /etc/redis/redis-${feature}.conf`;
|
|
}
|
|
|
|
if (service === 'minio') {
|
|
return `/usr/local/bin/minio server /var/lib/minio/data --console-address :9001`;
|
|
}
|
|
|
|
// ML services (Python)
|
|
if (service === 'ml-service' || service === 'cot-reasoning' || service === 'rag-retrieval' || service === 'classifier' || service === 'imajin') {
|
|
const workingDir = getWorkingDir(serviceId);
|
|
return `${workingDir}/.venv/bin/python -m uvicorn ... --host 0.0.0.0 --port ${port}`;
|
|
}
|
|
|
|
// Node.js APIs
|
|
if (service === 'api' || (service && service.endsWith('-api'))) {
|
|
return `node dist/main.js`;
|
|
}
|
|
|
|
// Frontends (static builds served via nginx, no systemd service needed)
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get production domain from service ID
|
|
*/
|
|
function getProductionDomain(serviceId: string): string | undefined {
|
|
const domainMap: Record<string, string> = {
|
|
// Shared services
|
|
'sso.api': 'sso.atlilith.com',
|
|
'merchant.api': 'merchant.atlilith.com',
|
|
'profile.api': 'profile.atlilith.com',
|
|
'seo.api': 'seo.atlilith.com',
|
|
// Atlilith landing
|
|
'atlilith.www.api': 'www.atlilith.com',
|
|
'atlilith.www.frontend': 'www.atlilith.com',
|
|
// Atlilith status
|
|
'atlilith.status.api': 'status.atlilith.com',
|
|
'atlilith.status.frontend': 'status.atlilith.com',
|
|
// Atlilith admin
|
|
'atlilith.admin.api': 'admin.atlilith.com',
|
|
'atlilith.admin.frontend': 'admin.atlilith.com',
|
|
// TrustedMeet marketplace
|
|
'trustedmeet.www.api': 'www.trustedmeet.com',
|
|
'trustedmeet.www.frontend': 'www.trustedmeet.com',
|
|
};
|
|
|
|
return domainMap[serviceId];
|
|
}
|
|
|
|
/**
|
|
* Get service dependencies (for systemd After=/Requires=)
|
|
*/
|
|
function getServiceDependencies(serviceId: string): string[] {
|
|
const parts = serviceId.split('.');
|
|
const lastPart = parts[parts.length - 1];
|
|
const deploymentId = parts.slice(0, -1).join('.');
|
|
|
|
// Infrastructure services have no dependencies
|
|
if (lastPart === 'postgresql' || lastPart === 'redis' || lastPart === 'minio') {
|
|
return ['network.target'];
|
|
}
|
|
|
|
// APIs depend on their infrastructure
|
|
if (lastPart === 'api') {
|
|
const deps = ['network.target'];
|
|
|
|
// Deployment-specific database
|
|
if (PRODUCTION_SERVICES.includes(`${deploymentId}.postgresql`)) {
|
|
deps.push(getSystemdUnitName(`${deploymentId}.postgresql`));
|
|
}
|
|
|
|
if (PRODUCTION_SERVICES.includes(`${deploymentId}.redis`)) {
|
|
deps.push(getSystemdUnitName(`${deploymentId}.redis`));
|
|
}
|
|
|
|
// Service-specific dependencies
|
|
if (serviceId === 'merchant.api') {
|
|
deps.push(getSystemdUnitName('sso.api'));
|
|
}
|
|
|
|
if (serviceId === 'trustedmeet.www.api') {
|
|
deps.push(getSystemdUnitName('sso.api'));
|
|
deps.push(getSystemdUnitName('merchant.api'));
|
|
deps.push(getSystemdUnitName('profile.api'));
|
|
}
|
|
|
|
if (serviceId === 'atlilith.admin.api') {
|
|
deps.push(getSystemdUnitName('sso.api'));
|
|
}
|
|
|
|
return deps;
|
|
}
|
|
|
|
// ML services
|
|
if (serviceId === 'seo.classifier' || serviceId === 'seo.imajin') {
|
|
return [
|
|
'network.target',
|
|
getSystemdUnitName('seo.cot-reasoning'),
|
|
getSystemdUnitName('seo.rag-retrieval'),
|
|
];
|
|
}
|
|
|
|
if (serviceId === 'seo.rag-retrieval') {
|
|
return [
|
|
'network.target',
|
|
getSystemdUnitName('seo.redis'),
|
|
];
|
|
}
|
|
|
|
return ['network.target'];
|
|
}
|
|
|
|
/**
|
|
* Check if service requires GPU
|
|
*/
|
|
function isGpuService(serviceId: string): boolean {
|
|
const gpuPatterns = ['cot-reasoning', 'ml-service'];
|
|
return gpuPatterns.some(pattern => serviceId.includes(pattern));
|
|
}
|
|
|
|
/**
|
|
* Get production service configuration
|
|
*/
|
|
export function getProductionServiceConfig(
|
|
serviceId: string,
|
|
port?: number,
|
|
): ProductionServiceConfig {
|
|
const configParts = serviceId.split('.');
|
|
const feature = configParts[0]!;
|
|
const service = configParts[1] ?? '';
|
|
|
|
// Determine service type
|
|
let serviceType: ProductionServiceConfig['serviceType'];
|
|
if (service === 'postgresql') serviceType = 'postgresql';
|
|
else if (service === 'redis') serviceType = 'redis';
|
|
else if (service === 'minio') serviceType = 'minio';
|
|
else if (service === 'ml-service' || service.includes('cot-') || service.includes('rag-') || service.includes('classifier') || service.includes('imajin')) serviceType = 'ml';
|
|
else if (service.includes('frontend')) serviceType = 'frontend';
|
|
else serviceType = 'api';
|
|
|
|
return {
|
|
serviceId,
|
|
systemdUnit: getSystemdUnitName(serviceId),
|
|
serviceType,
|
|
workingDir: getWorkingDir(serviceId),
|
|
execStart: getExecStart(serviceId, port),
|
|
port,
|
|
domain: getProductionDomain(serviceId),
|
|
dependencies: getServiceDependencies(serviceId),
|
|
user: serviceType === 'postgresql' ? 'postgres' : 'lilith',
|
|
group: serviceType === 'postgresql' ? 'postgres' : 'lilith',
|
|
healthCheck: serviceType === 'api' || serviceType === 'ml' ? {
|
|
url: `http://localhost:${port}/health`,
|
|
interval: 30,
|
|
} : undefined,
|
|
gpu: isGpuService(serviceId),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all production service configs with ports from service registry
|
|
*/
|
|
export async function getAllProductionConfigs(): Promise<ProductionServiceConfig[]> {
|
|
const { buildDeploymentRegistry } = await import('@lilith/service-registry');
|
|
|
|
const registry = buildDeploymentRegistry(REGISTRY_PATHS);
|
|
|
|
return PRODUCTION_SERVICES.map(serviceId => {
|
|
const service = registry.services.get(serviceId);
|
|
const port = service?.port;
|
|
|
|
return getProductionServiceConfig(serviceId, port);
|
|
});
|
|
}
|