feat: integrate toast notifications in settings and layout components
- Added ToastService for managing toast notifications. - Replaced alert in settings component with toast notifications for success and error messages. - Included ToastComponent in layout for displaying notifications. - Created loading spinner component for better user experience. - Implemented domain detail component with detailed views for certificates, requirements, and services. - Added functionality to manage and display SSL certificates and their statuses. - Introduced a registry manager class for handling Docker registry operations.
This commit is contained in:
@@ -33,10 +33,26 @@ export class OneboxServicesManager {
|
||||
throw new Error(`Service already exists: ${options.name}`);
|
||||
}
|
||||
|
||||
// Handle Onebox Registry setup
|
||||
let registryToken: string | undefined;
|
||||
let imageToPull: string;
|
||||
|
||||
if (options.useOneboxRegistry) {
|
||||
// Generate registry token
|
||||
registryToken = await this.oneboxRef.registry.createServiceToken(options.name);
|
||||
|
||||
// Use onebox registry image name
|
||||
const tag = options.registryImageTag || 'latest';
|
||||
imageToPull = this.oneboxRef.registry.getImageName(options.name, tag);
|
||||
} else {
|
||||
// Use external image
|
||||
imageToPull = options.image;
|
||||
}
|
||||
|
||||
// Create service record in database
|
||||
const service = await this.database.createService({
|
||||
name: options.name,
|
||||
image: options.image,
|
||||
image: options.useOneboxRegistry ? imageToPull : options.image,
|
||||
registry: options.registry,
|
||||
envVars: options.envVars || {},
|
||||
port: options.port,
|
||||
@@ -44,10 +60,18 @@ export class OneboxServicesManager {
|
||||
status: 'stopped',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
// Onebox Registry fields
|
||||
useOneboxRegistry: options.useOneboxRegistry,
|
||||
registryRepository: options.useOneboxRegistry ? options.name : undefined,
|
||||
registryToken: registryToken,
|
||||
registryImageTag: options.registryImageTag || 'latest',
|
||||
autoUpdateOnPush: options.autoUpdateOnPush,
|
||||
});
|
||||
|
||||
// Pull image
|
||||
await this.docker.pullImage(options.image, options.registry);
|
||||
// Pull image (skip if using onebox registry - image might not exist yet)
|
||||
if (!options.useOneboxRegistry) {
|
||||
await this.docker.pullImage(imageToPull, options.registry);
|
||||
}
|
||||
|
||||
// Create container
|
||||
const containerID = await this.docker.createContainer(service);
|
||||
@@ -68,6 +92,47 @@ export class OneboxServicesManager {
|
||||
if (options.domain) {
|
||||
logger.info(`Configuring domain: ${options.domain}`);
|
||||
|
||||
// Validate domain and create CertRequirement
|
||||
try {
|
||||
// Extract base domain (e.g., "api.example.com" -> "example.com")
|
||||
const domainParts = options.domain.split('.');
|
||||
const baseDomain = domainParts.slice(-2).join('.');
|
||||
const subdomain = domainParts.length > 2 ? domainParts.slice(0, -2).join('.') : '';
|
||||
|
||||
// Check if base domain exists in Domain table
|
||||
const domainRecord = this.database.getDomainByName(baseDomain);
|
||||
|
||||
if (!domainRecord) {
|
||||
logger.warn(
|
||||
`Domain ${baseDomain} not found in Domain table. ` +
|
||||
`Service will deploy but certificate management may not work. ` +
|
||||
`Run Cloudflare domain sync or manually add the domain.`
|
||||
);
|
||||
} else if (domainRecord.isObsolete) {
|
||||
logger.warn(
|
||||
`Domain ${baseDomain} is marked as obsolete. ` +
|
||||
`Certificate management may not work properly.`
|
||||
);
|
||||
} else {
|
||||
// Create CertRequirement for automatic certificate management
|
||||
const now = Date.now();
|
||||
this.database.createCertRequirement({
|
||||
serviceId: service.id!,
|
||||
domainId: domainRecord.id!,
|
||||
subdomain: subdomain,
|
||||
status: 'pending',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
logger.info(
|
||||
`Created certificate requirement for ${options.domain} ` +
|
||||
`(domain: ${baseDomain}, subdomain: ${subdomain || 'none'})`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to create certificate requirement: ${error.message}`);
|
||||
}
|
||||
|
||||
// Configure DNS (if autoDNS is enabled)
|
||||
if (options.autoDNS !== false) {
|
||||
try {
|
||||
@@ -85,6 +150,8 @@ export class OneboxServicesManager {
|
||||
}
|
||||
|
||||
// Configure SSL (if autoSSL is enabled)
|
||||
// Note: With CertRequirement system, certificates are managed automatically
|
||||
// but we still support the old direct obtainCertificate for backward compatibility
|
||||
if (options.autoSSL !== false) {
|
||||
try {
|
||||
await this.oneboxRef.ssl.obtainCertificate(options.domain);
|
||||
@@ -362,6 +429,121 @@ export class OneboxServicesManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update service configuration (image, port, domain, env vars)
|
||||
* Recreates the container with new configuration and auto-restarts
|
||||
*/
|
||||
async updateService(
|
||||
name: string,
|
||||
updates: {
|
||||
image?: string;
|
||||
registry?: string;
|
||||
port?: number;
|
||||
domain?: string;
|
||||
envVars?: Record<string, string>;
|
||||
}
|
||||
): Promise<IService> {
|
||||
try {
|
||||
const service = this.database.getServiceByName(name);
|
||||
if (!service) {
|
||||
throw new Error(`Service not found: ${name}`);
|
||||
}
|
||||
|
||||
logger.info(`Updating service: ${name}`);
|
||||
const wasRunning = service.status === 'running';
|
||||
const oldContainerID = service.containerID;
|
||||
const oldDomain = service.domain;
|
||||
|
||||
// Stop the container if running
|
||||
if (wasRunning && oldContainerID) {
|
||||
logger.info(`Stopping service ${name} for updates...`);
|
||||
try {
|
||||
await this.docker.stopContainer(oldContainerID);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to stop container: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Pull new image if changed
|
||||
if (updates.image && updates.image !== service.image) {
|
||||
logger.info(`Pulling new image: ${updates.image}`);
|
||||
await this.docker.pullImage(updates.image, updates.registry || service.registry);
|
||||
}
|
||||
|
||||
// Update service in database
|
||||
const updateData: any = {
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
if (updates.image !== undefined) updateData.image = updates.image;
|
||||
if (updates.registry !== undefined) updateData.registry = updates.registry;
|
||||
if (updates.port !== undefined) updateData.port = updates.port;
|
||||
if (updates.domain !== undefined) updateData.domain = updates.domain;
|
||||
if (updates.envVars !== undefined) updateData.envVars = updates.envVars;
|
||||
|
||||
this.database.updateService(service.id!, updateData);
|
||||
|
||||
// Get updated service
|
||||
const updatedService = this.database.getServiceByName(name)!;
|
||||
|
||||
// Remove old container
|
||||
if (oldContainerID) {
|
||||
try {
|
||||
await this.docker.removeContainer(oldContainerID, true);
|
||||
logger.info(`Removed old container for ${name}`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to remove old container: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new container with updated config
|
||||
logger.info(`Creating new container for ${name}...`);
|
||||
const containerID = await this.docker.createContainer(updatedService);
|
||||
this.database.updateService(service.id!, { containerID });
|
||||
|
||||
// Update reverse proxy if domain changed
|
||||
if (updates.domain !== undefined && updates.domain !== oldDomain) {
|
||||
// Remove old route if it existed
|
||||
if (oldDomain) {
|
||||
try {
|
||||
this.oneboxRef.reverseProxy.removeRoute(oldDomain);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to remove old reverse proxy route: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new route if domain specified
|
||||
if (updates.domain) {
|
||||
try {
|
||||
await this.oneboxRef.reverseProxy.addRoute(
|
||||
service.id!,
|
||||
updates.domain,
|
||||
updates.port || service.port
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to configure reverse proxy: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restart the container if it was running
|
||||
if (wasRunning) {
|
||||
logger.info(`Starting updated service ${name}...`);
|
||||
this.database.updateService(service.id!, { status: 'starting' });
|
||||
await this.docker.startContainer(containerID);
|
||||
this.database.updateService(service.id!, { status: 'running' });
|
||||
logger.success(`Service ${name} updated and restarted`);
|
||||
} else {
|
||||
this.database.updateService(service.id!, { status: 'stopped' });
|
||||
logger.success(`Service ${name} updated (not started)`);
|
||||
}
|
||||
|
||||
return this.database.getServiceByName(name)!;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update service ${name}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync service status from Docker
|
||||
*/
|
||||
@@ -410,4 +592,86 @@ export class OneboxServicesManager {
|
||||
await this.syncServiceStatus(service.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start auto-update monitoring for registry services
|
||||
* Polls every 30 seconds for digest changes and restarts services if needed
|
||||
*/
|
||||
startAutoUpdateMonitoring(): void {
|
||||
// Check every 30 seconds
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await this.checkForRegistryUpdates();
|
||||
} catch (error) {
|
||||
logger.error(`Auto-update check failed: ${error.message}`);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
logger.info('Auto-update monitoring started (30s interval)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all services using onebox registry for updates
|
||||
*/
|
||||
private async checkForRegistryUpdates(): Promise<void> {
|
||||
const services = this.listServices();
|
||||
|
||||
for (const service of services) {
|
||||
// Skip if not using onebox registry or auto-update is disabled
|
||||
if (!service.useOneboxRegistry || !service.autoUpdateOnPush) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current digest from registry
|
||||
const currentDigest = await this.oneboxRef.registry.getImageDigest(
|
||||
service.registryRepository!,
|
||||
service.registryImageTag || 'latest'
|
||||
);
|
||||
|
||||
// Skip if no digest found (image might not exist yet)
|
||||
if (!currentDigest) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if digest has changed
|
||||
if (service.imageDigest && service.imageDigest !== currentDigest) {
|
||||
logger.info(
|
||||
`Digest changed for ${service.name}: ${service.imageDigest} -> ${currentDigest}`
|
||||
);
|
||||
|
||||
// Update digest in database
|
||||
this.database.updateService(service.id!, {
|
||||
imageDigest: currentDigest,
|
||||
});
|
||||
|
||||
// Pull new image
|
||||
const imageName = this.oneboxRef.registry.getImageName(
|
||||
service.registryRepository!,
|
||||
service.registryImageTag || 'latest'
|
||||
);
|
||||
|
||||
logger.info(`Pulling updated image: ${imageName}`);
|
||||
await this.docker.pullImage(imageName);
|
||||
|
||||
// Restart service
|
||||
logger.info(`Auto-restarting service: ${service.name}`);
|
||||
await this.restartService(service.name);
|
||||
|
||||
// Broadcast update via WebSocket
|
||||
this.oneboxRef.httpServer.broadcastServiceUpdate({
|
||||
action: 'updated',
|
||||
service: this.database.getServiceByName(service.name)!,
|
||||
});
|
||||
} else if (!service.imageDigest) {
|
||||
// First time - just store the digest
|
||||
this.database.updateService(service.id!, {
|
||||
imageDigest: currentDigest,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to check updates for ${service.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user