From fd1da01a3fb9e924d554528956ca3479a45e0139 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 10 Sep 2025 15:38:42 +0000 Subject: [PATCH] feat(dns): Enhance DNS management with auto-generated entries and service activation --- .../classes.deploymentmanager.ts | 23 ++++- ts/manager.dns/classes.dnsmanager.ts | 89 +++++++++++++++++++ ts/manager.service/classes.service.ts | 42 +++++++++ ts/manager.service/classes.servicemanager.ts | 4 + ts_interfaces/data/dns.ts | 19 ++++ ts_interfaces/data/service.ts | 14 +++ 6 files changed, 190 insertions(+), 1 deletion(-) diff --git a/ts/manager.deployment/classes.deploymentmanager.ts b/ts/manager.deployment/classes.deploymentmanager.ts index 6928eae..83f7919 100644 --- a/ts/manager.deployment/classes.deploymentmanager.ts +++ b/ts/manager.deployment/classes.deploymentmanager.ts @@ -181,8 +181,16 @@ export class DeploymentManager { throw new Error('Deployment not found'); } + const serviceId = deployment.serviceId; await deployment.delete(); + // Check if this was the last deployment for the service + const remainingDeployments = await this.getDeploymentsForService(serviceId); + if (remainingDeployments.length === 0) { + // Deactivate DNS entries if no more deployments exist + await this.cloudlyRef.dnsManager.deactivateServiceDnsEntries(serviceId); + } + return { success: true, }; @@ -281,7 +289,7 @@ export class DeploymentManager { nodeId: string, version: string = 'latest' ): Promise { - return await Deployment.createDeployment({ + const deployment = await Deployment.createDeployment({ serviceId, nodeId, version, @@ -289,6 +297,19 @@ export class DeploymentManager { deployedAt: Date.now(), deploymentLog: [`Deployment created at ${new Date().toISOString()}`], }); + + // Activate DNS entries for the service + await this.cloudlyRef.dnsManager.activateServiceDnsEntries(serviceId); + + // Get the node's IP address and update DNS entries + const node = await this.cloudlyRef.nodeManager.CClusterNode.getInstance({ + id: nodeId, + }); + if (node && node.data.publicIp) { + await this.cloudlyRef.dnsManager.updateServiceDnsEntriesIp(serviceId, node.data.publicIp); + } + + return deployment; } public async start() { diff --git a/ts/manager.dns/classes.dnsmanager.ts b/ts/manager.dns/classes.dnsmanager.ts index 24e6e7c..ad4b496 100644 --- a/ts/manager.dns/classes.dnsmanager.ts +++ b/ts/manager.dns/classes.dnsmanager.ts @@ -156,6 +156,95 @@ export class DnsManager { ); } + /** + * Create a DNS entry for a service + * @param dnsEntryData The DNS entry data + */ + public async createServiceDnsEntry(dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data']) { + // If domainId is provided, get the domain and set the zone + if (dnsEntryData.domainId) { + const domain = await this.cloudlyRef.domainManager.CDomain.getInstance({ + id: dnsEntryData.domainId, + }); + if (domain) { + dnsEntryData.zone = domain.data.name; + } + } + + // Create the DNS entry + const dnsEntry = await this.CDnsEntry.createDnsEntry(dnsEntryData); + return dnsEntry; + } + + /** + * Activate DNS entries for a service when it's deployed + * @param serviceId The service ID + */ + public async activateServiceDnsEntries(serviceId: string) { + const dnsEntries = await this.CDnsEntry.getInstances({ + 'data.sourceServiceId': serviceId, + 'data.sourceType': 'service', + }); + + for (const entry of dnsEntries) { + entry.data.active = true; + entry.data.updatedAt = Date.now(); + await entry.save(); + } + } + + /** + * Deactivate DNS entries for a service when it's undeployed + * @param serviceId The service ID + */ + public async deactivateServiceDnsEntries(serviceId: string) { + const dnsEntries = await this.CDnsEntry.getInstances({ + 'data.sourceServiceId': serviceId, + 'data.sourceType': 'service', + }); + + for (const entry of dnsEntries) { + entry.data.active = false; + entry.data.updatedAt = Date.now(); + await entry.save(); + } + } + + /** + * Remove all DNS entries for a service + * @param serviceId The service ID + */ + public async removeServiceDnsEntries(serviceId: string) { + const dnsEntries = await this.CDnsEntry.getInstances({ + 'data.sourceServiceId': serviceId, + 'data.sourceType': 'service', + }); + + for (const entry of dnsEntries) { + await entry.delete(); + } + } + + /** + * Update DNS entry values when deployment happens + * @param serviceId The service ID + * @param ipAddress The IP address to set for the DNS entries + */ + public async updateServiceDnsEntriesIp(serviceId: string, ipAddress: string) { + const dnsEntries = await this.CDnsEntry.getInstances({ + 'data.sourceServiceId': serviceId, + 'data.sourceType': 'service', + }); + + for (const entry of dnsEntries) { + if (entry.data.type === 'A' || entry.data.type === 'AAAA') { + entry.data.value = ipAddress; + entry.data.updatedAt = Date.now(); + await entry.save(); + } + } + } + /** * Initialize the DNS manager */ diff --git a/ts/manager.service/classes.service.ts b/ts/manager.service/classes.service.ts index 6f45fe0..4207e8d 100644 --- a/ts/manager.service/classes.service.ts +++ b/ts/manager.service/classes.service.ts @@ -25,6 +25,12 @@ export class Service extends plugins.smartdata.SmartDataDbDoc< service.id = await Service.getNewId(); Object.assign(service, serviceDataArg); await service.save(); + + // Create DNS entries if service has web port and domains configured + if (service.data.ports?.web && service.data.domains?.length > 0) { + await service.createDnsEntries(); + } + return service; } @@ -54,4 +60,40 @@ export class Service extends plugins.smartdata.SmartDataDbDoc< } return finalFlatObject; } + + /** + * Creates DNS entries for this service (in inactive state) + * These will be activated when the service is deployed + */ + public async createDnsEntries() { + const dnsManager = this.manager.cloudlyRef.dnsManager; + + for (const domain of this.data.domains) { + const dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data'] = { + type: 'A', // Default to A record, could be made configurable + name: domain.name, + value: '0.0.0.0', // Placeholder, will be updated on deployment + ttl: 3600, + zone: '', // Will be set based on domainId + domainId: domain.domainId, + active: false, // Created as inactive + description: `Auto-generated DNS entry for service ${this.data.name}`, + createdAt: Date.now(), + isAutoGenerated: true, + sourceServiceId: this.id, + sourceType: 'service', + }; + + // Create the DNS entry + await dnsManager.createServiceDnsEntry(dnsEntryData); + } + } + + /** + * Removes DNS entries for this service + */ + public async removeDnsEntries() { + const dnsManager = this.manager.cloudlyRef.dnsManager; + await dnsManager.removeServiceDnsEntries(this.id); + } } diff --git a/ts/manager.service/classes.servicemanager.ts b/ts/manager.service/classes.servicemanager.ts index 6bb5001..3ab5197 100644 --- a/ts/manager.service/classes.servicemanager.ts +++ b/ts/manager.service/classes.servicemanager.ts @@ -91,6 +91,10 @@ export class ServiceManager { const service = await Service.getInstance({ id: dataArg.serviceId, }); + + // Remove DNS entries before deleting the service + await service.removeDnsEntries(); + await service.delete(); return { success: true, diff --git a/ts_interfaces/data/dns.ts b/ts_interfaces/data/dns.ts index 8b41b7c..ee5111a 100644 --- a/ts_interfaces/data/dns.ts +++ b/ts_interfaces/data/dns.ts @@ -77,5 +77,24 @@ export interface IDnsEntry { * Timestamp when the entry was last updated */ updatedAt?: number; + + /** + * Whether this DNS entry was auto-generated + */ + isAutoGenerated?: boolean; + + /** + * The service ID that created this DNS entry (for auto-generated entries) + */ + sourceServiceId?: string; + + /** + * The source type of this DNS entry + * - manual: Created by user through UI/API + * - service: Auto-generated from service configuration + * - system: Created by system processes + * - external: Synced from external DNS providers + */ + sourceType?: 'manual' | 'service' | 'system' | 'external'; }; } \ No newline at end of file diff --git a/ts_interfaces/data/service.ts b/ts_interfaces/data/service.ts index 557156c..4d51429 100644 --- a/ts_interfaces/data/service.ts +++ b/ts_interfaces/data/service.ts @@ -54,8 +54,22 @@ export interface IService { }; resources?: IServiceRessources; domains: { + /** + * Optional domain ID to specify which domain to use + * If not specified, will use the default domain or require manual configuration + */ + domainId?: string; + /** + * The subdomain name (e.g., 'api', 'www', '@' for root) + */ name: string; + /** + * The port to expose (defaults to ports.web if not specified) + */ port?: number; + /** + * The protocol for this domain entry + */ protocol?: 'http' | 'https' | 'ssh'; }[]; deploymentIds: string[];