feat: sync workload routes to external gateway

This commit is contained in:
2026-04-29 15:29:27 +00:00
parent 8e3dd6f4f8
commit 0f2df05ec9
5 changed files with 173 additions and 10 deletions
+24 -7
View File
@@ -1,6 +1,7 @@
import * as plugins from './coreflow.plugins.js'; import * as plugins from './coreflow.plugins.js';
import { logger } from './coreflow.logging.js'; import { logger } from './coreflow.logging.js';
import { Coreflow } from './coreflow.classes.coreflow.js'; import { Coreflow } from './coreflow.classes.coreflow.js';
import type { IExternalGatewayConfig } from './coreflow.connector.externalgateway.js';
export class ClusterManager { export class ClusterManager {
public coreflowRef: Coreflow; public coreflowRef: Coreflow;
@@ -408,8 +409,11 @@ export class ClusterManager {
* update traffic routing * update traffic routing
*/ */
public async updateTrafficRouting( 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 services = await this.coreflowRef.dockerHost.listServices();
const webGatewayNetwork = await this.coreflowRef.dockerHost.getNetworkByName( const webGatewayNetwork = await this.coreflowRef.dockerHost.getNetworkByName(
this.commonDockerData.networkNames.sznWebgateway, this.commonDockerData.networkNames.sznWebgateway,
@@ -420,14 +424,20 @@ export class ClusterManager {
const reverseProxyConfigs: plugins.servezoneInterfaces.data.IReverseProxyConfig[] = []; const reverseProxyConfigs: plugins.servezoneInterfaces.data.IReverseProxyConfig[] = [];
const pushProxyConfig = async ( const pushProxyConfig = async (
serviceNameArg: string, workloadServiceArg: plugins.servezoneInterfaces.data.IService,
hostNameArg: string, hostNameArg: string,
containerDestinationIp: string, containerDestinationIp: string,
webDestinationPort: string, webDestinationPort: string,
) => { ) => {
logger.log('ok', `trying to obtain a certificate for ${hostNameArg}`); logger.log('ok', `trying to obtain a certificate for ${hostNameArg}`);
const certificate = let certificate = await this.coreflowRef.externalGatewayConnector.exportCertificateForDomain(
await this.coreflowRef.cloudlyConnector.getCertificateForDomainFromCloudly(hostNameArg); 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({ reverseProxyConfigs.push({
destinationIps: [containerDestinationIp], destinationIps: [containerDestinationIp],
destinationPorts: [Number(webDestinationPort)], destinationPorts: [Number(webDestinationPort)],
@@ -437,8 +447,15 @@ export class ClusterManager {
}); });
logger.log( logger.log(
'success', '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!`); logger.log('info', `Found ${services.length} services!`);
@@ -473,7 +490,7 @@ export class ClusterManager {
const webDestinationPort: string = workloadConfig.data.ports.web.toString(); const webDestinationPort: string = workloadConfig.data.ports.web.toString();
for (const hostName of hostNames) { for (const hostName of hostNames) {
await pushProxyConfig( await pushProxyConfig(
workloadConfig.data.name, workloadConfig,
hostName, hostName,
containerDestinationIp, containerDestinationIp,
webDestinationPort, webDestinationPort,
@@ -485,7 +502,7 @@ export class ClusterManager {
const customDomainKeys = Object.keys(workloadConfig.data.ports.custom); const customDomainKeys = Object.keys(workloadConfig.data.ports.custom);
for (const customDomainKey of customDomainKeys) { for (const customDomainKey of customDomainKeys) {
await pushProxyConfig( await pushProxyConfig(
workloadConfig.data.name, workloadConfig,
customDomainKey, customDomainKey,
containerDestinationIp, containerDestinationIp,
workloadConfig.data.ports.custom[customDomainKey], workloadConfig.data.ports.custom[customDomainKey],
+3
View File
@@ -4,6 +4,7 @@ import { CloudlyConnector } from './coreflow.connector.cloudlyconnector.js';
import { ClusterManager } from './coreflow.classes.clustermanager.js'; import { ClusterManager } from './coreflow.classes.clustermanager.js';
import { CoreflowTaskmanager } from './coreflow.classes.taskmanager.js'; import { CoreflowTaskmanager } from './coreflow.classes.taskmanager.js';
import { CoretrafficConnector } from './coreflow.connector.coretrafficconnector.js'; import { CoretrafficConnector } from './coreflow.connector.coretrafficconnector.js';
import { ExternalGatewayConnector } from './coreflow.connector.externalgateway.js';
import { InternalServer } from './coreflow.classes.internalserver.js'; import { InternalServer } from './coreflow.classes.internalserver.js';
import { PlatformManager } from './coreflow.classes.platformmanager.js'; import { PlatformManager } from './coreflow.classes.platformmanager.js';
@@ -18,6 +19,7 @@ export class Coreflow {
public dockerHost: plugins.docker.DockerHost; public dockerHost: plugins.docker.DockerHost;
public cloudlyConnector: CloudlyConnector; public cloudlyConnector: CloudlyConnector;
public corechatConnector: CoretrafficConnector; public corechatConnector: CoretrafficConnector;
public externalGatewayConnector: ExternalGatewayConnector;
public clusterManager: ClusterManager; public clusterManager: ClusterManager;
public platformManager: PlatformManager; public platformManager: PlatformManager;
public taskManager: CoreflowTaskmanager; public taskManager: CoreflowTaskmanager;
@@ -28,6 +30,7 @@ export class Coreflow {
this.internalServer = new InternalServer(this); this.internalServer = new InternalServer(this);
this.cloudlyConnector = new CloudlyConnector(this); this.cloudlyConnector = new CloudlyConnector(this);
this.corechatConnector = new CoretrafficConnector(this); this.corechatConnector = new CoretrafficConnector(this);
this.externalGatewayConnector = new ExternalGatewayConnector(this);
this.clusterManager = new ClusterManager(this); this.clusterManager = new ClusterManager(this);
this.platformManager = new PlatformManager(this); this.platformManager = new PlatformManager(this);
this.taskManager = new CoreflowTaskmanager(this); this.taskManager = new CoreflowTaskmanager(this);
+1 -1
View File
@@ -55,7 +55,7 @@ export class CoreflowTaskmanager {
bufferMax: 1, bufferMax: 1,
taskFunction: async () => { taskFunction: async () => {
logger.log('info', 'now updating traffic routing'); 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); await this.coreflowRef.clusterManager.updateTrafficRouting(config);
logger.log('success', 'traffic routing completed!'); logger.log('success', 'traffic routing completed!');
}, },
+8 -2
View File
@@ -43,8 +43,14 @@ export class CloudlyConnector {
} }
public async getConfigFromCloudly(): Promise<plugins.servezoneInterfaces.data.ICluster> { public async getConfigFromCloudly(): Promise<plugins.servezoneInterfaces.data.ICluster> {
const config = await this.cloudlyApiClient.getClusterConfigFromCloudlyByIdentity(this.identity); const config = await this.getClusterConfigPayloadFromCloudly();
return config as unknown as plugins.servezoneInterfaces.data.ICluster; return config.configData as unknown as plugins.servezoneInterfaces.data.ICluster;
}
public async getClusterConfigPayloadFromCloudly(): Promise<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetClusterConfig['response'] & {
externalGateway?: import('./coreflow.connector.externalgateway.js').IExternalGatewayConfig;
}> {
return await this.cloudlyApiClient.getClusterConfigFromCloudlyByIdentity(this.identity) as any;
} }
public async getCertificateForDomainFromCloudly( public async getCertificateForDomainFromCloudly(
+137
View File
@@ -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<void> {
const config = optionsArg.config;
if (!this.isConfigured(config)) return;
const result = await this.fireDcRouterRequest<IWorkAppRouteSyncResult>(
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<plugins.tsclass.network.ICert | undefined> {
if (!configArg?.url || !configArg.apiToken) return undefined;
const result = await this.fireDcRouterRequest<IDcRouterCertificateExport>(
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<TResponse>(
configArg: IExternalGatewayConfig,
methodArg: string,
requestDataArg: Record<string, unknown>,
): Promise<TResponse> {
const typedRequest = new plugins.typedrequest.TypedRequest<any>(
`${configArg.url.replace(/\/+$/, '')}/typedrequest`,
methodArg,
);
return await typedRequest.fire({
...requestDataArg,
apiToken: configArg.apiToken,
}) as TResponse;
}
}