Files
smom-dbis-138/orchestration/portal/src/server.ts
2026-03-02 12:14:09 -08:00

728 lines
23 KiB
TypeScript

/**
* Enhanced Multi-Cloud Orchestration Portal
* TypeScript/Node.js Express server
*/
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import path from 'path';
import fs from 'fs';
import http from 'http';
import { Server as SocketIOServer } from 'socket.io';
import { ConfigManager } from './config';
import { DatabaseManager } from './database';
import { Environment, DeploymentRequest, Deployment, Alert, Cost } from './types';
import { requireAdmin, createSession, getClientIp, AuthRequest } from './middleware/auth';
import { MonitoringService } from './services/monitoring';
import { appendAudit } from './services/central-audit';
const app = express();
app.use(cors());
app.use(express.json());
// Serve static files from client build (production) or static directory
const isProduction = process.env.NODE_ENV === 'production';
if (isProduction) {
app.use(express.static(path.join(__dirname, '../client')));
} else {
app.use(express.static(path.join(__dirname, '../static')));
}
// Set up view engine (EJS for templates - fallback)
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../templates'));
// Initialize managers
const config = new ConfigManager();
const db = new DatabaseManager(config.getDbFile());
const monitoring = new MonitoringService(db);
// Helper function to group environments by provider
function groupByProvider(environments: Environment[]): Record<string, Environment[]> {
const grouped: Record<string, Environment[]> = {};
for (const env of environments) {
const provider = env.provider || 'unknown';
if (!grouped[provider]) {
grouped[provider] = [];
}
grouped[provider].push(env);
}
return grouped;
}
// ============================================
// ROUTES
// ============================================
// Main dashboard - serve Vue/React SPA in production, EJS template in development
app.get('/', (_req: Request, res: Response): void => {
if (isProduction) {
// Serve the built Vue/React app
res.sendFile(path.join(__dirname, '../client/index.html'));
} else {
// Fallback to EJS template for development
const environments = config.loadEnvironments();
const byProvider = groupByProvider(environments);
const envStatuses: Record<string, any> = {};
for (const env of environments) {
if (env.enabled) {
envStatuses[env.name] = config.getDeploymentStatus(env.name, db);
}
}
const alerts = db.getAlerts({ unacknowledged_only: true });
const recentDeployments = db.getDeployments({ limit: 10 });
const totalEnvironments = environments.length;
const enabledCount = environments.filter(e => e.enabled).length;
const totalProviders = Object.keys(byProvider).length;
res.render('dashboard', {
environments,
by_provider: byProvider,
env_statuses: envStatuses,
alerts,
recent_deployments: recentDeployments,
total_environments: totalEnvironments,
enabled_count: enabledCount,
total_providers: totalProviders
});
}
});
// API: Get all environments
app.get('/api/environments', (_req: Request, res: Response) => {
const environments = config.loadEnvironments();
res.json(environments);
});
// API: Get specific environment
app.get('/api/environments/:name', (req: Request, res: Response): void => {
const { name } = req.params;
const env = config.getEnvironmentByName(name);
if (!env) {
res.status(404).json({ error: 'Environment not found' });
return;
}
const status = config.getDeploymentStatus(name, db);
const metrics = db.getMetrics(name, 24);
const alerts = db.getAlerts({ environment: name, unacknowledged_only: true });
const costs = db.getCosts({ environment: name, days: 30 });
res.json({
config: env,
status,
metrics,
alerts,
costs
});
});
// API: Deploy to environment
app.post('/api/environments/:name/deploy', (req: Request, res: Response): void => {
const { name } = req.params;
const body: DeploymentRequest = req.body || {};
const env = config.getEnvironmentByName(name);
if (!env) {
res.status(404).json({ error: 'Environment not found' });
return;
}
if (!env.enabled) {
res.status(400).json({ error: 'Environment is disabled' });
return;
}
const strategy = body.strategy || 'blue-green';
const version = body.version || 'latest';
const now = new Date();
const deploymentId = `${name}-${now.toISOString().replace(/[-:T.]/g, '').slice(0, 14)}`;
const logFile = path.join(config.getDeploymentLogDir(), `${deploymentId}.log`);
// Log deployment request
const logContent = [
`Deployment requested for ${name} at ${now.toISOString()}`,
`Strategy: ${strategy}`,
`Version: ${version}`,
`Environment config: ${JSON.stringify(env, null, 2)}`
].join('\n');
fs.writeFileSync(logFile, logContent);
// Store in database
const deployment: Deployment = {
id: deploymentId,
environment: name,
status: 'queued',
started_at: now.toISOString(),
triggered_by: body.triggered_by || 'api',
strategy,
version,
logs_path: logFile
};
db.createDeployment(deployment);
res.json({
deployment_id: deploymentId,
status: 'queued',
environment: name,
strategy,
version,
message: 'Deployment queued successfully'
});
});
// API: Get environment status
app.get('/api/environments/:name/status', (req: Request, res: Response): void => {
const { name } = req.params;
const status = config.getDeploymentStatus(name, db);
res.json(status);
});
// API: Get environment metrics
app.get('/api/environments/:name/metrics', (req: Request, res: Response): void => {
const { name } = req.params;
const hours = parseInt(req.query.hours as string) || 24;
const metrics = db.getMetrics(name, hours);
res.json(metrics);
});
// API: Get environment alerts
app.get('/api/environments/:name/alerts', (req: Request, res: Response) => {
const { name } = req.params;
const unacknowledgedOnly = req.query.unacknowledged_only === 'true';
const alerts = db.getAlerts({ environment: name, unacknowledged_only: unacknowledgedOnly });
res.json(alerts);
});
// API: Acknowledge alert
app.post('/api/alerts/:id/acknowledge', (req: Request, res: Response) => {
const alertId = parseInt(req.params.id);
db.acknowledgeAlert(alertId);
res.json({ message: 'Alert acknowledged' });
});
// API: Get costs
app.get('/api/costs', (req: Request, res: Response): void => {
const environment = req.query.environment as string | undefined;
const days = parseInt(req.query.days as string) || 30;
const costs = db.getCosts({ environment, days });
res.json(costs);
});
// API: Get deployments
app.get('/api/deployments', (req: Request, res: Response): void => {
const environment = req.query.environment as string | undefined;
const status = req.query.status as string | undefined;
const limit = parseInt(req.query.limit as string) || 50;
const deployments = db.getDeployments({ environment, status, limit });
res.json(deployments);
});
// API: Get deployment logs
app.get('/api/deployments/:id/logs', (req: Request, res: Response): void => {
const { id } = req.params;
const logsPath = db.getDeploymentLogsPath(id);
if (!logsPath || !fs.existsSync(logsPath)) {
res.status(404).json({ error: 'Logs not found' });
return;
}
const logs = fs.readFileSync(logsPath, 'utf-8');
res.json({ logs });
});
// Environment detail page - serve SPA in production
app.get('/environment/:name', (req: Request, res: Response): void => {
if (isProduction) {
res.sendFile(path.join(__dirname, '../client/index.html'));
} else {
const { name } = req.params;
const env = config.getEnvironmentByName(name);
if (!env) {
res.status(404).send('Environment not found');
return;
}
const status = config.getDeploymentStatus(name, db);
const metrics = db.getMetrics(name, 168); // 7 days
const alerts = db.getAlerts({ environment: name });
const costs = db.getCosts({ environment: name, days: 30 });
const deployments = db.getDeployments({ environment: name, limit: 20 });
res.render('environment_detail', {
environment: env,
status,
metrics,
alerts,
costs,
deployments
});
}
});
// Health dashboard - serve SPA in production
app.get('/dashboard/health', (_req: Request, res: Response): void => {
if (isProduction) {
res.sendFile(path.join(__dirname, '../client/index.html'));
} else {
const environments = config.loadEnvironments();
const healthData: any[] = [];
for (const env of environments) {
if (env.enabled) {
const status = config.getDeploymentStatus(env.name, db);
healthData.push({
name: env.name,
provider: env.provider,
region: env.region,
status,
health: status.cluster_health
});
}
}
res.render('health_dashboard', { health_data: healthData });
}
});
// Cost dashboard - serve SPA in production
app.get('/dashboard/costs', (_req: Request, res: Response): void => {
if (isProduction) {
res.sendFile(path.join(__dirname, '../client/index.html'));
} else {
const costs = db.getCosts({ days: 90 });
// Aggregate by provider
const byProvider: Record<string, number> = {};
let totalCost = 0;
for (const cost of costs) {
const provider = cost.provider;
if (!byProvider[provider]) {
byProvider[provider] = 0;
}
byProvider[provider] += cost.cost;
totalCost += cost.cost;
}
res.render('cost_dashboard', {
costs,
by_provider: byProvider,
total_cost: totalCost
});
}
});
// Seed sample data
function seedSampleData(): void {
if (db.getMetricsCount() > 0) {
return; // Data already seeded
}
const environments = config.loadEnvironments();
const now = new Date();
for (const env of environments.slice(0, 3)) {
const envName = env.name;
// Generate sample metrics (24 hours)
for (let i = 0; i < 24; i++) {
const timestamp = new Date(now.getTime() - (24 - i) * 60 * 60 * 1000).toISOString();
db.insertMetric(envName, 'cpu_usage', Math.random() * 60 + 20, timestamp);
db.insertMetric(envName, 'memory_usage', Math.random() * 55 + 30, timestamp);
}
// Generate sample alerts (30% chance)
if (Math.random() > 0.7) {
const alert: Omit<Alert, 'id'> = {
environment: envName,
severity: Math.random() > 0.5 ? 'warning' : 'error',
message: `Sample alert for ${envName}`,
timestamp: now.toISOString()
};
db.createAlert(alert);
}
// Generate sample costs (30 days)
for (let i = 0; i < 30; i++) {
const periodStart = new Date(now.getTime() - (30 - i) * 24 * 60 * 60 * 1000).toISOString();
const periodEnd = new Date(now.getTime() - (29 - i) * 24 * 60 * 60 * 1000).toISOString();
const cost: Cost = {
environment: envName,
provider: env.provider || 'azure',
cost: Math.random() * 490 + 10,
currency: 'USD',
period_start: periodStart,
period_end: periodEnd,
resource_type: 'compute'
};
db.insertCost(cost);
}
}
}
// ============================================
// ADMIN API ROUTES
// ============================================
// Admin login (simple for now - enhance with proper auth later)
app.post('/api/admin/login', (req: Request, res: Response): void => {
const { username, password } = req.body;
// Simple hardcoded admin for now (replace with proper auth)
if (username === 'admin' && (password === process.env.ADMIN_PASSWORD || password === 'admin')) {
const token = createSession(username);
res.json({ token, username });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
// Get all service configurations
app.get('/api/admin/services', requireAdmin, (_req: AuthRequest, res: Response): void => {
try {
const services = db.getAllServiceConfigs();
res.json(services);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch services' });
}
});
// Get specific service configuration
app.get('/api/admin/services/:name', requireAdmin, (req: AuthRequest, res: Response): void => {
try {
const { name } = req.params;
const service = db.getServiceConfig(name);
if (!service) {
res.status(404).json({ error: 'Service not found' });
return;
}
res.json({ service_name: name, ...service });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch service' });
}
});
// Update service configuration
app.put('/api/admin/services/:name', requireAdmin, (req: AuthRequest, res: Response): void => {
try {
const { name } = req.params;
const { enabled, config } = req.body;
const adminUser = req.adminUser || 'unknown';
const ipAddress = getClientIp(req);
db.setServiceConfig(name, enabled !== false, config || null, adminUser);
db.logAdminAction(adminUser, 'update_service', 'service', name, JSON.stringify({ enabled, config }), ipAddress);
appendAudit({ employeeId: adminUser, action: 'update_service', permission: 'admin:action', resourceType: 'service', resourceId: name, ipAddress, userAgent: req.headers['user-agent'] as string }).catch(() => {});
// Broadcast real-time update
broadcastAdminUpdate('service-updated', { service_name: name, enabled, updated_by: adminUser });
res.json({ success: true, message: `Service ${name} ${enabled ? 'enabled' : 'disabled'}` });
} catch (error) {
res.status(500).json({ error: 'Failed to update service' });
}
});
// Get all provider configurations
app.get('/api/admin/providers', requireAdmin, (_req: AuthRequest, res: Response): void => {
try {
const providers = db.getAllProviderConfigs();
res.json(providers);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch providers' });
}
});
// Get specific provider configuration
app.get('/api/admin/providers/:name', requireAdmin, (req: AuthRequest, res: Response): void => {
try {
const { name } = req.params;
const provider = db.getProviderConfig(name);
if (!provider) {
res.status(404).json({ error: 'Provider not found' });
return;
}
res.json({ provider_name: name, ...provider });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch provider' });
}
});
// Update provider configuration
app.put('/api/admin/providers/:name', requireAdmin, (req: AuthRequest, res: Response): void => {
try {
const { name } = req.params;
const { enabled, config } = req.body;
const adminUser = req.adminUser || 'unknown';
const ipAddress = getClientIp(req);
db.setProviderConfig(name, enabled !== false, config || null, adminUser);
db.logAdminAction(adminUser, 'update_provider', 'provider', name, JSON.stringify({ enabled, config }), ipAddress);
appendAudit({ employeeId: adminUser, action: 'update_provider', permission: 'admin:action', resourceType: 'provider', resourceId: name, ipAddress, userAgent: req.headers['user-agent'] as string }).catch(() => {});
// Broadcast real-time update
broadcastAdminUpdate('provider-updated', { provider_name: name, enabled, updated_by: adminUser });
res.json({ success: true, message: `Provider ${name} ${enabled ? 'enabled' : 'disabled'}` });
} catch (error) {
res.status(500).json({ error: 'Failed to update provider' });
}
});
// Get audit logs
app.get('/api/admin/audit-logs', requireAdmin, (req: AuthRequest, res: Response): void => {
try {
const limit = parseInt(req.query.limit as string) || 100;
const logs = db.getAuditLogs(limit);
res.json(logs);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch audit logs' });
}
});
// Toggle environment enabled/disabled (updates YAML via config manager)
app.put('/api/admin/environments/:name/toggle', requireAdmin, (req: AuthRequest, res: Response): void => {
try {
const { name } = req.params;
const { enabled } = req.body;
const adminUser = req.adminUser || 'unknown';
const ipAddress = getClientIp(req);
const env = config.getEnvironmentByName(name);
if (!env) {
res.status(404).json({ error: 'Environment not found' });
return;
}
// Update environment in YAML file
config.updateEnvironmentEnabled(name, enabled !== false);
db.logAdminAction(adminUser, 'toggle_environment', 'environment', name, JSON.stringify({ enabled }), ipAddress);
appendAudit({ employeeId: adminUser, action: 'toggle_environment', permission: 'admin:action', resourceType: 'environment', resourceId: name, metadata: { enabled }, ipAddress, userAgent: req.headers['user-agent'] as string }).catch(() => {});
// Broadcast real-time update
broadcastAdminUpdate('environment-updated', { environment_name: name, enabled, updated_by: adminUser });
res.json({ success: true, message: `Environment ${name} ${enabled ? 'enabled' : 'disabled'}` });
} catch (error) {
res.status(500).json({ error: 'Failed to toggle environment' });
}
});
// ============================================
// MONITORING API ROUTES
// ============================================
// Get monitoring dashboard data
app.get('/api/monitoring/dashboard', (_req: Request, res: Response): void => {
try {
const besuHealth = db.getServiceHealthSummary('besu');
const cactiHealth = db.getServiceHealthSummary('cacti');
const fireflyHealth = db.getServiceHealthSummary('firefly');
const chainlinkHealth = db.getServiceHealthSummary('chainlink-ccip');
res.json({
besu: besuHealth,
cacti: cactiHealth,
firefly: fireflyHealth,
chainlinkCcip: chainlinkHealth,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch monitoring data' });
}
});
// Get Besu metrics
app.get('/api/monitoring/besu', (req: Request, res: Response): void => {
try {
const environment = req.query.environment as string;
const serviceName = req.query.service as string || 'besu-validator-1';
if (environment) {
monitoring.collectBesuMetrics(environment, serviceName).then(metrics => {
res.json(metrics);
}).catch(() => {
res.status(500).json({ error: 'Failed to collect Besu metrics' });
});
} else {
const metrics = db.getServiceMetrics({ serviceType: 'besu', hours: 1 });
res.json(metrics);
}
} catch (error) {
res.status(500).json({ error: 'Failed to fetch Besu metrics' });
}
});
// Get Cacti metrics
app.get('/api/monitoring/cacti', (req: Request, res: Response): void => {
try {
const environment = req.query.environment as string;
const serviceName = req.query.service as string || 'cacti-1';
if (environment) {
monitoring.collectCactiMetrics(environment, serviceName).then(metrics => {
res.json(metrics);
}).catch(() => {
res.status(500).json({ error: 'Failed to collect Cacti metrics' });
});
} else {
const metrics = db.getServiceMetrics({ serviceType: 'cacti', hours: 1 });
res.json(metrics);
}
} catch (error) {
res.status(500).json({ error: 'Failed to fetch Cacti metrics' });
}
});
// Get Firefly metrics
app.get('/api/monitoring/firefly', (req: Request, res: Response): void => {
try {
const environment = req.query.environment as string;
const serviceName = req.query.service as string || 'firefly-1';
if (environment) {
monitoring.collectFireflyMetrics(environment, serviceName).then(metrics => {
res.json(metrics);
}).catch(() => {
res.status(500).json({ error: 'Failed to collect Firefly metrics' });
});
} else {
const metrics = db.getServiceMetrics({ serviceType: 'firefly', hours: 1 });
res.json(metrics);
}
} catch (error) {
res.status(500).json({ error: 'Failed to fetch Firefly metrics' });
}
});
// Get Chainlink CCIP metrics
app.get('/api/monitoring/chainlink-ccip', (req: Request, res: Response): void => {
try {
const environment = req.query.environment as string;
const serviceName = req.query.service as string || 'chainlink-ccip-1';
if (environment) {
monitoring.collectChainlinkCCIPMetrics(environment, serviceName).then(metrics => {
res.json(metrics);
}).catch(() => {
res.status(500).json({ error: 'Failed to collect Chainlink CCIP metrics' });
});
} else {
const metrics = db.getServiceMetrics({ serviceType: 'chainlink-ccip', hours: 1 });
res.json(metrics);
}
} catch (error) {
res.status(500).json({ error: 'Failed to fetch Chainlink CCIP metrics' });
}
});
// Get service status
app.get('/api/monitoring/services/:type/:name/status', (req: Request, res: Response): void => {
try {
const { type, name } = req.params;
const status = db.getServiceStatus(name, type);
if (!status) {
res.status(404).json({ error: 'Service status not found' });
return;
}
res.json(status);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch service status' });
}
});
// Collect all metrics for an environment
app.post('/api/monitoring/environments/:name/collect', async (req: Request, res: Response): Promise<void> => {
try {
const { name } = req.params;
const results = await monitoring.collectAllMetrics(name);
// Broadcast update via WebSocket
broadcastAdminUpdate('monitoring-updated', { environment: name, services: results });
res.json({ success: true, data: results });
} catch (error) {
res.status(500).json({ error: 'Failed to collect metrics' });
}
});
// Error handling middleware
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error('Error:', err);
res.status(500).json({ error: 'Internal server error' });
});
// Start server
const PORT = process.env.PORT || 5000;
const HOST = process.env.HOST || '0.0.0.0';
// Create HTTP server for Socket.IO
const server = http.createServer(app);
// Initialize Socket.IO
const io = new SocketIOServer(server, {
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});
// Socket.IO connection handling
io.on('connection', (socket: import('socket.io').Socket) => {
console.log('Client connected:', socket.id);
// Join admin room for real-time updates
socket.on('join-admin', () => {
socket.join('admin');
console.log('Client joined admin room:', socket.id);
});
// Handle disconnection
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
// Helper function to broadcast admin updates
function broadcastAdminUpdate(type: string, data: any): void {
io.to('admin').emit('admin-update', { type, data, timestamp: new Date().toISOString() });
}
// Seed sample data on startup
seedSampleData();
server.listen(PORT, () => {
console.log('🚀 Enhanced Multi-Cloud Orchestration Portal starting...');
console.log(`📊 Access dashboard at: http://${HOST}:${PORT}`);
console.log(`🔍 Health dashboard at: http://${HOST}:${PORT}/dashboard/health`);
console.log(`💰 Cost dashboard at: http://${HOST}:${PORT}/dashboard/costs`);
console.log(`🔐 Admin panel at: http://${HOST}:${PORT}/admin`);
console.log(`🔍 Monitoring dashboard at: http://${HOST}:${PORT}/monitoring`);
console.log(`🔌 WebSocket server ready on port ${PORT}`);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down gracefully...');
db.close();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\nShutting down gracefully...');
db.close();
process.exit(0);
});