From 0f2df05ec90603024a50189fc030bbfd5219665e Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 29 Apr 2026 15:29:27 +0000 Subject: [PATCH] feat: sync workload routes to external gateway --- ts/coreflow.classes.clustermanager.ts | 31 +++-- ts/coreflow.classes.coreflow.ts | 3 + ts/coreflow.classes.taskmanager.ts | 2 +- ts/coreflow.connector.cloudlyconnector.ts | 10 +- ts/coreflow.connector.externalgateway.ts | 137 ++++++++++++++++++++++ 5 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 ts/coreflow.connector.externalgateway.ts diff --git a/ts/coreflow.classes.clustermanager.ts b/ts/coreflow.classes.clustermanager.ts index 9d6708e..48be5ec 100644 --- a/ts/coreflow.classes.clustermanager.ts +++ b/ts/coreflow.classes.clustermanager.ts @@ -1,6 +1,7 @@ import * as plugins from './coreflow.plugins.js'; import { logger } from './coreflow.logging.js'; import { Coreflow } from './coreflow.classes.coreflow.js'; +import type { IExternalGatewayConfig } from './coreflow.connector.externalgateway.js'; export class ClusterManager { public coreflowRef: Coreflow; @@ -408,8 +409,11 @@ export class ClusterManager { * update traffic routing */ public async updateTrafficRouting( - _clusterConfigArg: plugins.servezoneInterfaces.data.ICluster, + clusterConfigArg: plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetClusterConfig['response'] & { + externalGateway?: IExternalGatewayConfig; + }, ) { + const externalGatewayConfig = clusterConfigArg.externalGateway; const services = await this.coreflowRef.dockerHost.listServices(); const webGatewayNetwork = await this.coreflowRef.dockerHost.getNetworkByName( this.commonDockerData.networkNames.sznWebgateway, @@ -420,14 +424,20 @@ export class ClusterManager { const reverseProxyConfigs: plugins.servezoneInterfaces.data.IReverseProxyConfig[] = []; const pushProxyConfig = async ( - serviceNameArg: string, + workloadServiceArg: plugins.servezoneInterfaces.data.IService, hostNameArg: string, containerDestinationIp: string, webDestinationPort: string, ) => { logger.log('ok', `trying to obtain a certificate for ${hostNameArg}`); - const certificate = - await this.coreflowRef.cloudlyConnector.getCertificateForDomainFromCloudly(hostNameArg); + let certificate = await this.coreflowRef.externalGatewayConnector.exportCertificateForDomain( + externalGatewayConfig, + hostNameArg, + ).catch((error) => { + logger.log('warn', `external gateway certificate export failed for ${hostNameArg}: ${(error as Error).message}`); + return undefined; + }); + certificate = certificate || await this.coreflowRef.cloudlyConnector.getCertificateForDomainFromCloudly(hostNameArg); reverseProxyConfigs.push({ destinationIps: [containerDestinationIp], destinationPorts: [Number(webDestinationPort)], @@ -437,8 +447,15 @@ export class ClusterManager { }); logger.log( 'success', - `pushed routing config for ${hostNameArg} on workload service ${serviceNameArg}`, + `pushed routing config for ${hostNameArg} on workload service ${workloadServiceArg.data.name}`, ); + await this.coreflowRef.externalGatewayConnector.syncWorkAppRoute({ + config: externalGatewayConfig, + service: workloadServiceArg, + hostname: hostNameArg, + }).catch((error) => { + logger.log('warn', `external gateway route sync failed for ${hostNameArg}: ${(error as Error).message}`); + }); }; logger.log('info', `Found ${services.length} services!`); @@ -473,7 +490,7 @@ export class ClusterManager { const webDestinationPort: string = workloadConfig.data.ports.web.toString(); for (const hostName of hostNames) { await pushProxyConfig( - workloadConfig.data.name, + workloadConfig, hostName, containerDestinationIp, webDestinationPort, @@ -485,7 +502,7 @@ export class ClusterManager { const customDomainKeys = Object.keys(workloadConfig.data.ports.custom); for (const customDomainKey of customDomainKeys) { await pushProxyConfig( - workloadConfig.data.name, + workloadConfig, customDomainKey, containerDestinationIp, workloadConfig.data.ports.custom[customDomainKey], diff --git a/ts/coreflow.classes.coreflow.ts b/ts/coreflow.classes.coreflow.ts index c72f421..88a77b4 100644 --- a/ts/coreflow.classes.coreflow.ts +++ b/ts/coreflow.classes.coreflow.ts @@ -4,6 +4,7 @@ import { CloudlyConnector } from './coreflow.connector.cloudlyconnector.js'; import { ClusterManager } from './coreflow.classes.clustermanager.js'; import { CoreflowTaskmanager } from './coreflow.classes.taskmanager.js'; import { CoretrafficConnector } from './coreflow.connector.coretrafficconnector.js'; +import { ExternalGatewayConnector } from './coreflow.connector.externalgateway.js'; import { InternalServer } from './coreflow.classes.internalserver.js'; import { PlatformManager } from './coreflow.classes.platformmanager.js'; @@ -18,6 +19,7 @@ export class Coreflow { public dockerHost: plugins.docker.DockerHost; public cloudlyConnector: CloudlyConnector; public corechatConnector: CoretrafficConnector; + public externalGatewayConnector: ExternalGatewayConnector; public clusterManager: ClusterManager; public platformManager: PlatformManager; public taskManager: CoreflowTaskmanager; @@ -28,6 +30,7 @@ export class Coreflow { this.internalServer = new InternalServer(this); this.cloudlyConnector = new CloudlyConnector(this); this.corechatConnector = new CoretrafficConnector(this); + this.externalGatewayConnector = new ExternalGatewayConnector(this); this.clusterManager = new ClusterManager(this); this.platformManager = new PlatformManager(this); this.taskManager = new CoreflowTaskmanager(this); diff --git a/ts/coreflow.classes.taskmanager.ts b/ts/coreflow.classes.taskmanager.ts index 28fb76a..0cee5f3 100644 --- a/ts/coreflow.classes.taskmanager.ts +++ b/ts/coreflow.classes.taskmanager.ts @@ -55,7 +55,7 @@ export class CoreflowTaskmanager { bufferMax: 1, taskFunction: async () => { logger.log('info', 'now updating traffic routing'); - const config = await this.coreflowRef.cloudlyConnector.getConfigFromCloudly(); + const config = await this.coreflowRef.cloudlyConnector.getClusterConfigPayloadFromCloudly(); await this.coreflowRef.clusterManager.updateTrafficRouting(config); logger.log('success', 'traffic routing completed!'); }, diff --git a/ts/coreflow.connector.cloudlyconnector.ts b/ts/coreflow.connector.cloudlyconnector.ts index 443969f..8431a56 100644 --- a/ts/coreflow.connector.cloudlyconnector.ts +++ b/ts/coreflow.connector.cloudlyconnector.ts @@ -43,8 +43,14 @@ export class CloudlyConnector { } public async getConfigFromCloudly(): Promise { - const config = await this.cloudlyApiClient.getClusterConfigFromCloudlyByIdentity(this.identity); - return config as unknown as plugins.servezoneInterfaces.data.ICluster; + const config = await this.getClusterConfigPayloadFromCloudly(); + return config.configData as unknown as plugins.servezoneInterfaces.data.ICluster; + } + + public async getClusterConfigPayloadFromCloudly(): Promise { + return await this.cloudlyApiClient.getClusterConfigFromCloudlyByIdentity(this.identity) as any; } public async getCertificateForDomainFromCloudly( diff --git a/ts/coreflow.connector.externalgateway.ts b/ts/coreflow.connector.externalgateway.ts new file mode 100644 index 0000000..1d45ef1 --- /dev/null +++ b/ts/coreflow.connector.externalgateway.ts @@ -0,0 +1,137 @@ +import * as plugins from './coreflow.plugins.js'; +import { Coreflow } from './coreflow.classes.coreflow.js'; +import { logger } from './coreflow.logging.js'; + +export interface IExternalGatewayConfig { + url: string; + apiToken: string; + workHosterType: 'cloudly'; + workHosterId: string; + targetHost?: string; + targetPort?: number; +} + +interface IWorkAppRouteSyncResult { + success: boolean; + action?: 'created' | 'updated' | 'deleted' | 'unchanged'; + routeId?: string; + message?: string; +} + +interface IDcRouterCertificateExport { + success: boolean; + cert?: { + id: string; + domainName: string; + created: number; + validUntil: number; + privateKey: string; + publicKey: string; + csr: string; + }; + message?: string; +} + +export class ExternalGatewayConnector { + constructor(public coreflowRef: Coreflow) {} + + public isConfigured(configArg?: IExternalGatewayConfig): configArg is IExternalGatewayConfig { + return Boolean( + configArg?.url + && configArg.apiToken + && configArg.workHosterId + && configArg.targetHost + && configArg.targetPort, + ); + } + + public async syncWorkAppRoute(optionsArg: { + config: IExternalGatewayConfig | undefined; + service: plugins.servezoneInterfaces.data.IService; + hostname: string; + }): Promise { + const config = optionsArg.config; + if (!this.isConfigured(config)) return; + + const result = await this.fireDcRouterRequest( + config, + 'syncWorkAppRoute', + { + ownership: { + workHosterType: 'cloudly', + workHosterId: config.workHosterId, + workAppId: optionsArg.service.id || optionsArg.service.data.name, + hostname: optionsArg.hostname, + }, + route: { + name: this.routeName(optionsArg.hostname), + match: { + ports: [443], + domains: [optionsArg.hostname], + }, + action: { + type: 'forward', + targets: [{ host: config.targetHost!, port: config.targetPort! }], + tls: { + mode: 'terminate', + certificate: 'auto', + }, + websocket: { + enabled: true, + }, + }, + }, + enabled: true, + }, + ); + + if (!result.success) { + throw new Error(result.message || `dcrouter route sync failed for ${optionsArg.hostname}`); + } + + logger.log('success', `external gateway route ${result.action || 'synced'} for ${optionsArg.hostname}`); + } + + public async exportCertificateForDomain( + configArg: IExternalGatewayConfig | undefined, + hostnameArg: string, + ): Promise { + if (!configArg?.url || !configArg.apiToken) return undefined; + + const result = await this.fireDcRouterRequest( + configArg, + 'exportCertificate', + { domain: hostnameArg }, + ); + + if (!result.success || !result.cert) return undefined; + return { + id: result.cert.id, + domainName: result.cert.domainName, + created: result.cert.created, + validUntil: result.cert.validUntil, + privateKey: result.cert.privateKey, + publicKey: result.cert.publicKey, + csr: result.cert.csr, + }; + } + + private routeName(hostnameArg: string): string { + return `cloudly-${hostnameArg.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '')}`; + } + + private async fireDcRouterRequest( + configArg: IExternalGatewayConfig, + methodArg: string, + requestDataArg: Record, + ): Promise { + const typedRequest = new plugins.typedrequest.TypedRequest( + `${configArg.url.replace(/\/+$/, '')}/typedrequest`, + methodArg, + ); + return await typedRequest.fire({ + ...requestDataArg, + apiToken: configArg.apiToken, + }) as TResponse; + } +}