Initial commit: Onebox v1.0.0
- Complete Deno-based architecture following nupst/spark patterns - SQLite database with full schema - Docker container management - Service orchestration (Docker + Nginx + DNS + SSL) - Registry authentication - Nginx reverse proxy configuration - Cloudflare DNS integration - Let's Encrypt SSL automation - Background daemon with metrics collection - HTTP API server - Comprehensive CLI - Cross-platform compilation setup - NPM distribution wrapper - Shell installer script Core features: - Deploy containers with single command - Automatic domain configuration - Automatic SSL certificates - Multi-registry support - Metrics and logging - Systemd integration Ready for Angular UI implementation and testing.
This commit is contained in:
345
ts/onebox.classes.nginx.ts
Normal file
345
ts/onebox.classes.nginx.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Nginx Manager for Onebox
|
||||
*
|
||||
* Manages Nginx reverse proxy configurations for services
|
||||
*/
|
||||
|
||||
import * as plugins from './onebox.plugins.ts';
|
||||
import { logger } from './onebox.logging.ts';
|
||||
import { OneboxDatabase } from './onebox.classes.database.ts';
|
||||
|
||||
export class OneboxNginxManager {
|
||||
private oneboxRef: any;
|
||||
private database: OneboxDatabase;
|
||||
private configDir = '/etc/nginx/sites-available';
|
||||
private enabledDir = '/etc/nginx/sites-enabled';
|
||||
|
||||
constructor(oneboxRef: any) {
|
||||
this.oneboxRef = oneboxRef;
|
||||
this.database = oneboxRef.database;
|
||||
|
||||
// Allow custom nginx config directory
|
||||
const customDir = this.database.getSetting('nginxConfigDir');
|
||||
if (customDir) {
|
||||
this.configDir = customDir;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize nginx manager
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
// Ensure directories exist
|
||||
await Deno.mkdir(this.configDir, { recursive: true });
|
||||
await Deno.mkdir(this.enabledDir, { recursive: true });
|
||||
|
||||
logger.info('Nginx manager initialized');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize Nginx manager: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create nginx config for a service
|
||||
*/
|
||||
async createConfig(serviceId: number, domain: string, port: number): Promise<void> {
|
||||
try {
|
||||
logger.info(`Creating Nginx config for ${domain}`);
|
||||
|
||||
const service = this.database.getServiceByID(serviceId);
|
||||
if (!service) {
|
||||
throw new Error(`Service not found: ${serviceId}`);
|
||||
}
|
||||
|
||||
// Get container IP (or use container name for DNS resolution within Docker network)
|
||||
const containerName = `onebox-${service.name}`;
|
||||
|
||||
// Generate config
|
||||
const config = this.generateConfig(domain, containerName, port);
|
||||
|
||||
// Write config file
|
||||
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
|
||||
await Deno.writeTextFile(configPath, config);
|
||||
|
||||
// Create symlink in sites-enabled
|
||||
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
|
||||
try {
|
||||
await Deno.remove(enabledPath);
|
||||
} catch {
|
||||
// Ignore if doesn't exist
|
||||
}
|
||||
await Deno.symlink(configPath, enabledPath);
|
||||
|
||||
logger.success(`Nginx config created: ${domain}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create Nginx config: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate nginx configuration
|
||||
*/
|
||||
private generateConfig(domain: string, upstream: string, port: number): string {
|
||||
return `# Onebox-managed configuration for ${domain}
|
||||
# Generated at ${new Date().toISOString()}
|
||||
|
||||
upstream onebox_${domain.replace(/\./g, '_')} {
|
||||
server ${upstream}:${port};
|
||||
}
|
||||
|
||||
# HTTP server (redirect to HTTPS or serve directly)
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${domain};
|
||||
|
||||
# ACME challenge for Let's Encrypt
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Redirect to HTTPS (will be enabled after SSL is configured)
|
||||
# location / {
|
||||
# return 301 https://$server_name$request_uri;
|
||||
# }
|
||||
|
||||
# Proxy to container (remove after SSL is configured)
|
||||
location / {
|
||||
proxy_pass http://onebox_${domain.replace(/\./g, '_')};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS server (uncomment after SSL is configured)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# listen [::]:443 ssl http2;
|
||||
# server_name ${domain};
|
||||
#
|
||||
# ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
|
||||
#
|
||||
# # SSL configuration
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://onebox_${domain.replace(/\./g, '_')};
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
#
|
||||
# # WebSocket support
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
#
|
||||
# # Timeouts
|
||||
# proxy_connect_timeout 60s;
|
||||
# proxy_send_timeout 60s;
|
||||
# proxy_read_timeout 60s;
|
||||
# }
|
||||
# }
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update nginx config to enable SSL
|
||||
*/
|
||||
async enableSSL(domain: string): Promise<void> {
|
||||
try {
|
||||
logger.info(`Enabling SSL for ${domain}`);
|
||||
|
||||
// Find service by domain
|
||||
const services = this.database.getAllServices();
|
||||
const service = services.find((s) => s.domain === domain);
|
||||
|
||||
if (!service) {
|
||||
throw new Error(`Service not found for domain: ${domain}`);
|
||||
}
|
||||
|
||||
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
|
||||
let config = await Deno.readTextFile(configPath);
|
||||
|
||||
// Enable HTTPS redirect and HTTPS server block
|
||||
config = config.replace('# location / {\n # return 301', 'location / {\n return 301');
|
||||
config = config.replace('# }', '}');
|
||||
config = config.replace(/# server \{[\s\S]*?# \}/m, (match) =>
|
||||
match.replace(/# /g, '')
|
||||
);
|
||||
|
||||
// Comment out HTTP proxy location
|
||||
config = config.replace(
|
||||
/# Proxy to container \(remove after SSL is configured\)[\s\S]*?location \/ \{[\s\S]*?\n \}/,
|
||||
(match) => `# ${match.replace(/\n/g, '\n # ')}`
|
||||
);
|
||||
|
||||
await Deno.writeTextFile(configPath, config);
|
||||
|
||||
logger.success(`SSL enabled for ${domain}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to enable SSL for ${domain}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove nginx config for a service
|
||||
*/
|
||||
async removeConfig(serviceId: number): Promise<void> {
|
||||
try {
|
||||
const service = this.database.getServiceByID(serviceId);
|
||||
if (!service) {
|
||||
throw new Error(`Service not found: ${serviceId}`);
|
||||
}
|
||||
|
||||
logger.info(`Removing Nginx config for ${service.name}`);
|
||||
|
||||
// Remove symlink
|
||||
const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`;
|
||||
try {
|
||||
await Deno.remove(enabledPath);
|
||||
} catch {
|
||||
// Ignore if doesn't exist
|
||||
}
|
||||
|
||||
// Remove config file
|
||||
const configPath = `${this.configDir}/onebox-${service.name}.conf`;
|
||||
try {
|
||||
await Deno.remove(configPath);
|
||||
} catch {
|
||||
// Ignore if doesn't exist
|
||||
}
|
||||
|
||||
logger.success(`Nginx config removed for ${service.name}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to remove Nginx config: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test nginx configuration
|
||||
*/
|
||||
async test(): Promise<boolean> {
|
||||
try {
|
||||
const command = new Deno.Command('nginx', {
|
||||
args: ['-t'],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
const { code, stderr } = await command.output();
|
||||
|
||||
if (code !== 0) {
|
||||
const errorMsg = new TextDecoder().decode(stderr);
|
||||
logger.error(`Nginx config test failed: ${errorMsg}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.success('Nginx configuration is valid');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to test Nginx config: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload nginx
|
||||
*/
|
||||
async reload(): Promise<void> {
|
||||
try {
|
||||
// Test config first
|
||||
const isValid = await this.test();
|
||||
if (!isValid) {
|
||||
throw new Error('Nginx configuration is invalid');
|
||||
}
|
||||
|
||||
logger.info('Reloading Nginx...');
|
||||
|
||||
const command = new Deno.Command('systemctl', {
|
||||
args: ['reload', 'nginx'],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
const { code, stderr } = await command.output();
|
||||
|
||||
if (code !== 0) {
|
||||
const errorMsg = new TextDecoder().decode(stderr);
|
||||
throw new Error(`Nginx reload failed: ${errorMsg}`);
|
||||
}
|
||||
|
||||
logger.success('Nginx reloaded successfully');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to reload Nginx: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nginx status
|
||||
*/
|
||||
async getStatus(): Promise<string> {
|
||||
try {
|
||||
const command = new Deno.Command('systemctl', {
|
||||
args: ['status', 'nginx'],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
const { code, stdout } = await command.output();
|
||||
const output = new TextDecoder().decode(stdout);
|
||||
|
||||
if (code === 0 || output.includes('active (running)')) {
|
||||
return 'running';
|
||||
} else if (output.includes('inactive') || output.includes('dead')) {
|
||||
return 'stopped';
|
||||
} else if (output.includes('failed')) {
|
||||
return 'failed';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get Nginx status: ${error.message}`);
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if nginx is installed
|
||||
*/
|
||||
async isInstalled(): Promise<boolean> {
|
||||
try {
|
||||
const command = new Deno.Command('which', {
|
||||
args: ['nginx'],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
const { code } = await command.output();
|
||||
return code === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user