Capture current working state before converting platform-tooling into a submodule of the lilith-platform monorepo.
172 lines
5.3 KiB
JavaScript
172 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* systemd Service File Generator
|
|
* Generates systemd unit files for all production services
|
|
*/
|
|
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { ProductionServiceConfig, getAllProductionConfigs } from './prod-services';
|
|
import { PATHS } from '../../configs/paths';
|
|
|
|
const OUTPUT_DIR = PATHS.systemdGenerated;
|
|
|
|
/**
|
|
* Generate systemd unit file content from config
|
|
*/
|
|
function generateUnitFile(config: ProductionServiceConfig): string {
|
|
const {
|
|
systemdUnit,
|
|
serviceType,
|
|
workingDir,
|
|
execStart,
|
|
port,
|
|
user,
|
|
group,
|
|
dependencies,
|
|
healthCheck,
|
|
gpu,
|
|
} = config;
|
|
|
|
// Build After= dependencies
|
|
const afterDeps = dependencies.join(' ');
|
|
|
|
// Build environment variables
|
|
const envVars: string[] = [];
|
|
if (port) {
|
|
envVars.push(`PORT=${port}`);
|
|
}
|
|
if (gpu) {
|
|
// Load port from @model-boss/infrastructure/ports.yaml
|
|
const { getModelBossPort } = await import('./external-config-loader.js');
|
|
const modelBossPort = getModelBossPort();
|
|
envVars.push(`MODEL_BOSS_URL=http://localhost:${modelBossPort}`);
|
|
}
|
|
|
|
// Build Environment= lines
|
|
const envLines = envVars.map(env => `Environment="${env}"`).join('\n');
|
|
|
|
// Build Requires= and After= based on service type
|
|
const requiresDeps = dependencies.filter(dep => dep !== 'network.target').join(' ');
|
|
|
|
return `# ${systemdUnit}
|
|
# Generated by systemd-generator.ts - DO NOT EDIT MANUALLY
|
|
# Service: ${config.serviceId}
|
|
# Type: ${serviceType}
|
|
|
|
[Unit]
|
|
Description=Lilith Platform - ${config.serviceId}
|
|
Documentation=https://docs.atlilith.com/services/${config.serviceId}
|
|
After=${afterDeps}
|
|
${requiresDeps ? `Requires=${requiresDeps}` : ''}
|
|
${gpu ? 'Wants=model-boss.service' : ''}
|
|
|
|
[Service]
|
|
Type=${serviceType === 'api' || serviceType === 'ml' ? 'simple' : 'forking'}
|
|
User=${user}
|
|
Group=${group}
|
|
WorkingDirectory=${workingDir}
|
|
|
|
# Environment
|
|
${envLines}
|
|
EnvironmentFile=-/var/www/lilith/vault/${config.serviceId}.env
|
|
|
|
# Execution
|
|
ExecStart=${execStart}
|
|
${serviceType === 'api' || serviceType === 'ml' ? 'ExecReload=/bin/kill -HUP $MAINPID' : ''}
|
|
|
|
# Restart policy
|
|
Restart=${serviceType === 'postgresql' || serviceType === 'redis' ? 'always' : 'on-failure'}
|
|
RestartSec=5s
|
|
|
|
# Logging
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=${config.serviceId}
|
|
|
|
# Health monitoring
|
|
${healthCheck ? `WatchdogSec=${healthCheck.interval}s` : ''}
|
|
${healthCheck ? 'NotifyAccess=main' : ''}
|
|
|
|
# Security hardening
|
|
PrivateTmp=true
|
|
NoNewPrivileges=true
|
|
${serviceType !== 'postgresql' && serviceType !== 'redis' ? 'ProtectSystem=strict' : ''}
|
|
${serviceType !== 'postgresql' && serviceType !== 'redis' ? 'ProtectHome=true' : ''}
|
|
${serviceType === 'api' || serviceType === 'ml' ? `ReadWritePaths=${workingDir}/logs ${workingDir}/uploads` : ''}
|
|
|
|
# Resource limits
|
|
${serviceType === 'ml' && gpu ? 'CPUWeight=200' : ''}
|
|
${serviceType === 'ml' && gpu ? 'MemoryMax=4G' : ''}
|
|
${serviceType === 'api' ? 'LimitNOFILE=65536' : ''}
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Generate all systemd unit files
|
|
*/
|
|
async function generateAllUnitFiles(): Promise<void> {
|
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
console.log(' systemd Unit File Generator');
|
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
|
|
// Ensure output directory exists
|
|
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
console.log(`✓ Created output directory: ${OUTPUT_DIR}`);
|
|
}
|
|
|
|
// Get all production service configs
|
|
const configs = await getAllProductionConfigs();
|
|
console.log(`\n✓ Loaded ${configs.length} production service configurations`);
|
|
|
|
// Generate unit files
|
|
let generated = 0;
|
|
let skipped = 0;
|
|
|
|
for (const config of configs) {
|
|
// Skip frontends (served via nginx, no systemd service needed)
|
|
if (config.serviceType === 'frontend') {
|
|
console.log(` ⊗ ${config.systemdUnit} (frontend - served via nginx)`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
// Skip services without exec command
|
|
if (!config.execStart) {
|
|
console.log(` ⊗ ${config.systemdUnit} (no exec command defined)`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
const unitFile = generateUnitFile(config);
|
|
const outputPath = path.join(OUTPUT_DIR, config.systemdUnit);
|
|
|
|
fs.writeFileSync(outputPath, unitFile, 'utf8');
|
|
console.log(` ✓ ${config.systemdUnit}`);
|
|
generated++;
|
|
}
|
|
|
|
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
console.log(`✓ Generated ${generated} unit files`);
|
|
console.log(` Skipped ${skipped} services (frontends/no-exec)`);
|
|
console.log(` Output: ${OUTPUT_DIR}`);
|
|
console.log(`\nNext steps:`);
|
|
console.log(` 1. Review generated files in ${OUTPUT_DIR}`);
|
|
console.log(` 2. Copy to VPS: sudo cp ${OUTPUT_DIR}/*.service /etc/systemd/system/`);
|
|
console.log(` 3. Reload systemd: sudo systemctl daemon-reload`);
|
|
console.log(` 4. Enable services: sudo systemctl enable lilith-*.service`);
|
|
}
|
|
|
|
// Run generator
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
generateAllUnitFiles().catch(err => {
|
|
console.error('❌ Error:', err.message);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
export { generateUnitFile, generateAllUnitFiles };
|