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'; import * as crypto from 'node:crypto'; type TServiceVolumeConfig = { name?: string; source?: string; mountPath?: string; target?: string; containerFsPath?: string; driver?: string; readOnly?: boolean; backup?: boolean; options?: Record; }; export class ClusterManager { public coreflowRef: Coreflow; public configSubscription?: plugins.smartrx.rxjs.Subscription; public readyDeferred = plugins.smartpromise.defer(); public commonDockerData = { networkNames: { sznWebgateway: 'sznwebgateway', sznCorechat: 'szncorechat', }, }; constructor(coreflowRefArg: Coreflow) { this.coreflowRef = coreflowRefArg; } private async getDockerServiceByName(serviceNameArg: string): Promise { try { return await this.coreflowRef.dockerHost.getServiceByName(serviceNameArg); } catch (error) { if ((error as Error).message === `Service not found: ${serviceNameArg}`) { return null; } throw error; } } private getDockerSafeName(valueArg: string, maxLengthArg = 64) { const safeName = valueArg .replace(/[^a-zA-Z0-9_.-]+/g, '-') .replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, '') .slice(0, maxLengthArg) .replace(/[^a-zA-Z0-9]+$/g, ''); return safeName || 'resource'; } private getWorkloadSecretName(serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService) { const serviceName = this.getDockerSafeName(serviceArgFromCloudly.data.name, 36); const serviceId = this.getDockerSafeName(serviceArgFromCloudly.id, 20); return this.getDockerSafeName(`${serviceName}-${serviceId}-secret`); } private getWorkloadServiceDeploymentLabels( serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService, containerImageFromCloudly: plugins.servezoneInterfaces.data.IImage, secretHashArg = '', volumeHashArg = '', ) { const desiredImageVersion = serviceArgFromCloudly.data.imageVersion || serviceArgFromCloudly.data.registryTarget?.tag || 'latest'; const desiredImageVersionData = (containerImageFromCloudly.data.versions || []).find((versionArg) => { return versionArg.versionString === desiredImageVersion; }); const desiredRegistryDigest = desiredImageVersionData?.digest || ( containerImageFromCloudly.data.lastPushEvent?.tag === desiredImageVersion ? containerImageFromCloudly.data.lastPushEvent.digest : '' ); return { 'serve.zone.serviceId': serviceArgFromCloudly.id, 'serve.zone.imageId': serviceArgFromCloudly.data.imageId || '', 'serve.zone.imageVersion': desiredImageVersion, 'serve.zone.registryImageUrl': serviceArgFromCloudly.data.registryTarget?.imageUrl || '', 'serve.zone.registryDigest': desiredRegistryDigest || '', 'serve.zone.secretHash': secretHashArg, ...(volumeHashArg ? { 'serve.zone.volumeHash': volumeHashArg } : {}), }; } private stableStringify(valueArg: unknown): string { if (Array.isArray(valueArg)) { return `[${valueArg.map((itemArg) => this.stableStringify(itemArg)).join(',')}]`; } if (valueArg && typeof valueArg === 'object') { return `{${Object.keys(valueArg as Record) .sort() .map((keyArg) => `${JSON.stringify(keyArg)}:${this.stableStringify((valueArg as Record)[keyArg])}`) .join(',')}}`; } return JSON.stringify(valueArg); } private hashSecretObject(secretObjectArg: Record) { return crypto.createHash('sha256').update(this.stableStringify(secretObjectArg)).digest('hex'); } private hashStableValue(valueArg: unknown) { return crypto.createHash('sha256').update(this.stableStringify(valueArg)).digest('hex'); } private async pullRegistryTargetImage( registryTargetArg: plugins.servezoneInterfaces.data.IRegistryTarget, ): Promise { const registryImageName = `${registryTargetArg.registryHost}/${registryTargetArg.repository}`; const registryImageTag = registryTargetArg.tag || 'latest'; const registryImageRef = `${registryImageName}:${registryImageTag}`; const response = await this.coreflowRef.dockerHost.request( 'POST', `/images/create?fromImage=${encodeURIComponent(registryImageName)}&tag=${encodeURIComponent( registryImageTag, )}`, ); if (response.statusCode >= 300) { const existingImage = await this.coreflowRef.dockerHost.getImageByName(registryImageRef); if (existingImage) { logger.log( 'warn', `registry pull failed for ${registryImageRef}, using locally cached image`, ); return existingImage; } throw new Error(`Failed to pull registry image ${registryImageRef}`); } const localDockerImage = await this.coreflowRef.dockerHost.getImageByName(registryImageRef); if (!localDockerImage) { throw new Error(`Registry image ${registryImageRef} not found after pull`); } return localDockerImage; } private getServiceVolumeConfigs(serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService) { const serviceData = serviceArgFromCloudly.data as plugins.servezoneInterfaces.data.IService['data'] & { volumes?: TServiceVolumeConfig[]; }; return (serviceData.volumes || []).filter((volumeArg) => { return Boolean(volumeArg.mountPath || volumeArg.target || volumeArg.containerFsPath); }); } private getCoreStoreVolumeName( serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService, volumeArg: TServiceVolumeConfig, ) { const requestedName = volumeArg.source || volumeArg.name; if (requestedName) { return this.getDockerSafeName(requestedName, 120); } const mountPath = volumeArg.mountPath || volumeArg.target || volumeArg.containerFsPath || 'data'; const serviceName = this.getDockerSafeName(serviceArgFromCloudly.data.name, 36); const mountName = this.getDockerSafeName(mountPath.replace(/^\/+/, '').replace(/\/+$/g, ''), 28); const hash = crypto.createHash('sha1').update(`${serviceArgFromCloudly.id}:${mountPath}`).digest('hex').slice(0, 12); return this.getDockerSafeName(`sz-${serviceName}-${mountName}-${hash}`, 120); } private getServiceDockerMounts(serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService) { const mounts: Array> = []; const resources = serviceArgFromCloudly.data.resources as (plugins.servezoneInterfaces.data.IService['data']['resources'] & { volumeMounts?: Array<{ hostFsPath: string; containerFsPath: string }>; }) | undefined; for (const volumeMount of resources?.volumeMounts || []) { mounts.push({ Target: volumeMount.containerFsPath, Source: volumeMount.hostFsPath, Consistency: 'default', ReadOnly: false, Type: 'bind', }); } for (const volume of this.getServiceVolumeConfigs(serviceArgFromCloudly)) { const target = volume.mountPath || volume.target || volume.containerFsPath; if (!target) { continue; } const driver = volume.driver || 'corestore'; const source = this.getCoreStoreVolumeName(serviceArgFromCloudly, volume); const backup = volume.backup !== false; const driverOptions: Record = { ...(volume.options || {}), serviceId: serviceArgFromCloudly.id, serviceName: serviceArgFromCloudly.data.name, mountPath: target, backup: String(backup), }; mounts.push({ Target: target, Source: source, Type: 'volume', ReadOnly: Boolean(volume.readOnly), VolumeOptions: { DriverConfig: { Name: driver, Options: driverOptions, }, Labels: { 'serve.zone.serviceId': serviceArgFromCloudly.id, 'serve.zone.serviceName': serviceArgFromCloudly.data.name, 'serve.zone.mountPath': target, 'serve.zone.backup': String(backup), }, }, }); } return mounts; } private getServiceVolumeHash(serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService) { const volumeConfigs = this.getServiceVolumeConfigs(serviceArgFromCloudly); if (volumeConfigs.length === 0) { return ''; } const volumeSpecs = volumeConfigs.map((volumeArg) => ({ ...volumeArg, source: this.getCoreStoreVolumeName(serviceArgFromCloudly, volumeArg), driver: volumeArg.driver || 'corestore', backup: volumeArg.backup !== false, })); return this.hashStableValue(volumeSpecs); } private async createWorkloadDockerService(argsArg: { service: plugins.servezoneInterfaces.data.IService; image: plugins.docker.DockerImage; network: plugins.docker.DockerNetwork; secret: plugins.docker.DockerSecret; labels: Record; }) { const image = argsArg.image as unknown as { RepoTags?: string[] }; const imageRef = image.RepoTags?.[0]; if (!imageRef) { throw new Error(`Docker image for ${argsArg.service.data.name} has no tag`); } const ports: Array<{ Protocol: string; PublishedPort: number; TargetPort: number }> = []; const resources = argsArg.service.data.resources as (plugins.servezoneInterfaces.data.IService['data']['resources'] & { memorySizeMB?: number; }) | undefined; const memoryLimitMB = resources?.memorySizeMB || resources?.memorySizeLimitMB || 1000; const replicas = Math.max(1, Number(argsArg.service.data.scaleFactor || 1)); const response = await this.coreflowRef.dockerHost.request('POST', '/services/create', { Name: argsArg.service.data.name, Labels: argsArg.labels, TaskTemplate: { ContainerSpec: { Image: imageRef, Labels: argsArg.labels, Secrets: [ { File: { Name: 'secret.json', UID: '33', GID: '33', Mode: 384, }, SecretID: argsArg.secret.ID, SecretName: argsArg.secret.Spec.Name, }, ], Mounts: this.getServiceDockerMounts(argsArg.service), }, UpdateConfig: { Parallelism: 0, Delay: 0, FailureAction: 'pause', Monitor: 15000000000, MaxFailureRatio: 0.15, }, ForceUpdate: 1, Resources: { Limits: { MemoryBytes: memoryLimitMB * 1000000, }, }, Networks: [ { Target: argsArg.network.Name, Aliases: [argsArg.service.data.name], }, ], LogDriver: { Name: 'json-file', Options: { 'max-file': '3', 'max-size': '10M', }, }, }, Mode: { Replicated: { Replicas: replicas, }, }, EndpointSpec: { Ports: ports, }, }); if (response.statusCode >= 300) { throw new Error(`Failed to create workload service ${argsArg.service.data.name}: ${JSON.stringify(response.body)}`); } return this.getDockerServiceByName(argsArg.service.data.name); } private async createCorestoreGlobalService( corestoreImageArg: plugins.docker.DockerImage, networksArg: Array, ) { const corestoreImage = corestoreImageArg as unknown as { RepoTags?: string[]; Labels?: Record; }; const imageRef = corestoreImage.RepoTags?.[0] || 'code.foss.global/serve.zone/corestore:latest'; const corestoreEnv = [ 'CORESTORE_DATA_DIR=/data/corestore', 'CORESTORE_PUBLIC_HOST=corestore', 'CORESTORE_VOLUME_PLUGIN_SOCKET=/run/docker/plugins/corestore.sock', ...(process.env.CORESTORE_API_TOKEN ? [`CORESTORE_API_TOKEN=${process.env.CORESTORE_API_TOKEN}`] : []), ]; const response = await this.coreflowRef.dockerHost.request('POST', '/services/create', { Name: 'corestore', Labels: { version: corestoreImage.Labels?.version || '', 'serve.zone.serviceCategory': 'base', 'serve.zone.provides': 'database,objectstorage,volume', }, TaskTemplate: { ContainerSpec: { Image: imageRef, Labels: { 'serve.zone.serviceCategory': 'base', 'serve.zone.provides': 'database,objectstorage,volume', }, Env: corestoreEnv, Mounts: [ { Target: '/data/corestore', Source: '/var/lib/serve.zone/corestore', Type: 'bind', ReadOnly: false, Consistency: 'default', }, { Target: '/run/docker/plugins', Source: '/run/docker/plugins', Type: 'bind', ReadOnly: false, Consistency: 'default', }, ], }, Networks: networksArg.map((networkArg) => ({ Target: networkArg.Name, Aliases: ['corestore'], })), RestartPolicy: { Condition: 'any', }, Resources: { Limits: { MemoryBytes: 700 * 1000000, }, }, }, Mode: { Global: {}, }, }); if (response.statusCode >= 300) { throw new Error(`Failed to create corestore service: ${JSON.stringify(response.body)}`); } return this.getDockerServiceByName('corestore'); } /** * starts the cluster manager */ public async start() { const config = await this.coreflowRef.cloudlyConnector.getConfigFromCloudly(); this.readyDeferred.resolve(); // subscriptions // this subscription is the start point for most updates on the cluster this.configSubscription = this.coreflowRef.cloudlyConnector.cloudlyApiClient.configUpdateSubject.subscribe( async (dataArg) => { this.coreflowRef.taskManager.updateBaseServicesTask.trigger(); }, ); } /** * stops the clustermanager */ public async stop() { this.configSubscription ? this.configSubscription.unsubscribe() : null; } /** * provisions base services */ public async provisionBaseServices() { // swarm should be enabled by lower level serverconfig package // get current situation const networks = await this.coreflowRef.dockerHost.listNetworks(); logger.log('info', 'There are currently ' + networks.length + ' networks'); for (const network of networks) { logger.log('info', 'Network: ' + network.Name); } // make sure there is a network for the webgateway let sznWebgatewayNetwork = await this.coreflowRef.dockerHost.getNetworkByName( this.commonDockerData.networkNames.sznWebgateway, ); if (!sznWebgatewayNetwork) { logger.log('info', 'Creating network: ' + this.commonDockerData.networkNames.sznWebgateway); sznWebgatewayNetwork = await this.coreflowRef.dockerHost.createNetwork({ Name: this.commonDockerData.networkNames.sznWebgateway, }); } else { logger.log('ok', 'sznWebgateway is already present'); } // corechat network so base services can talk to each other let sznCorechatNetwork = await this.coreflowRef.dockerHost.getNetworkByName( this.commonDockerData.networkNames.sznCorechat, ); if (!sznCorechatNetwork) { sznCorechatNetwork = await this.coreflowRef.dockerHost.createNetwork({ Name: this.commonDockerData.networkNames.sznCorechat, }); } else { logger.log('ok', 'sznCorechat is already present'); } logger.log('info', `Waiting for networks to be ready...`); await plugins.smartdelay.delayFor(10000); logger.log('success', `Successfully created networks for servezone`); // Images logger.log('info', `now updating docker images of base services...`); const coretrafficImage = await this.coreflowRef.dockerHost.createImageFromRegistry({ imageUrl: 'code.foss.global/serve.zone/coretraffic', }); const corelogImage = await this.coreflowRef.dockerHost.createImageFromRegistry({ imageUrl: 'code.foss.global/serve.zone/corelog', }); const corestoreImage = await this.coreflowRef.dockerHost.createImageFromRegistry({ imageUrl: 'code.foss.global/serve.zone/corestore', }); // SERVICES // lets deploy the base services // coretraffic let coretrafficService: plugins.docker.DockerService | null; coretrafficService = await this.getDockerServiceByName( 'coretraffic', ); if (coretrafficService && (await coretrafficService.needsUpdate())) { await coretrafficService.remove(); await plugins.smartdelay.delayFor(5000); coretrafficService = null; } else { logger.log('ok', `coretraffic service is up to date`); } if (!coretrafficService) { coretrafficService = await this.coreflowRef.dockerHost.createService({ image: coretrafficImage, labels: {}, name: 'coretraffic', networks: [sznCorechatNetwork, sznWebgatewayNetwork], networkAlias: 'coretraffic', ports: ['80:7999', '443:8000'], secrets: [], resources: { memorySizeMB: 1100, volumeMounts: [], }, }); } else { logger.log('ok', 'coretraffic service is already present'); } logger.log('info', 'wait for coretraffic to be up and running'); await plugins.smartdelay.delayFor(10000); // corelog let corelogService: plugins.docker.DockerService | null; corelogService = await this.getDockerServiceByName( 'corelog', ); if (corelogService && (await corelogService.needsUpdate())) { await corelogService.remove(); corelogService = null; } else { logger.log('ok', `corelog service is up to date`); } if (!corelogService) { corelogService = await this.coreflowRef.dockerHost.createService({ image: corelogImage, labels: {}, name: 'corelog', networks: [sznCorechatNetwork], networkAlias: 'corelog', ports: [], secrets: [], resources: { memorySizeMB: 120, volumeMounts: [], }, }); } else { logger.log('ok', 'corelog service is already present'); } logger.log('info', 'waiting for corelog to be up and running'); await plugins.smartdelay.delayFor(10000); // corestore let corestoreService: plugins.docker.DockerService | null; corestoreService = await this.getDockerServiceByName('corestore'); if ( corestoreService && (((corestoreService.Spec as any).Mode && !(corestoreService.Spec as any).Mode.Global) || (await corestoreService.needsUpdate())) ) { await corestoreService.remove(); corestoreService = null; } else { logger.log('ok', `corestore service is up to date`); } if (!corestoreService) { corestoreService = await this.createCorestoreGlobalService(corestoreImage, [ sznCorechatNetwork, sznWebgatewayNetwork, ]); } else { logger.log('ok', 'corestore service is already present'); } logger.log('info', 'waiting for corestore to be up and running'); await plugins.smartdelay.delayFor(10000); } public async provisionWorkloadService( serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService, ) { logger.log( 'info', `deploying service ${serviceArgFromCloudly.data.name}@${serviceArgFromCloudly.data.imageVersion}...`, ); // get the image from cloudly logger.log( 'info', `getting image for ${serviceArgFromCloudly.data.name}@${serviceArgFromCloudly.data.imageVersion}`, ); const containerImageFromCloudly = await this.coreflowRef.cloudlyConnector.cloudlyApiClient.image.getImageById( serviceArgFromCloudly.data.imageId, ); let localDockerImage: plugins.docker.DockerImage; // lets get the docker image for the service if (serviceArgFromCloudly.data.registryTarget) { await this.coreflowRef.dockerHost.auth({ username: this.coreflowRef.cloudlyConnector.identity.name, password: this.coreflowRef.cloudlyConnector.coreflowJumpCode, serveraddress: serviceArgFromCloudly.data.registryTarget.registryHost, }); localDockerImage = await this.pullRegistryTargetImage(serviceArgFromCloudly.data.registryTarget); } else if (containerImageFromCloudly.data.location?.internal) { const imageStream = await containerImageFromCloudly.pullImageVersion( serviceArgFromCloudly.data.imageVersion, ); localDockerImage = await this.coreflowRef.dockerHost.createImageFromTarStream( plugins.smartstream.nodewebhelpers.convertWebReadableToNodeReadable(imageStream), { imageUrl: containerImageFromCloudly.id, imageTag: serviceArgFromCloudly.data.imageVersion, }, ); } else if ( containerImageFromCloudly.data.location?.externalRegistryId && containerImageFromCloudly.data.location?.externalImageTag ) { const externalRegistry = await this.coreflowRef.cloudlyConnector.cloudlyApiClient.externalRegistry.getRegistryById( containerImageFromCloudly.data.location.externalRegistryId, ); // Lets authenticate against the external registry // TODO: deduplicate this, check wether we are already authenticated if (!externalRegistry.data.username || !externalRegistry.data.password) { throw new Error(`External registry ${externalRegistry.id} is missing credentials`); } await this.coreflowRef.dockerHost.auth({ username: externalRegistry.data.username, password: externalRegistry.data.password, serveraddress: externalRegistry.data.url, }); localDockerImage = await this.coreflowRef.dockerHost.createImageFromRegistry({ imageUrl: containerImageFromCloudly.id, imageTag: serviceArgFromCloudly.data.imageVersion, }); await localDockerImage.pullLatestImageFromRegistry(); } else { throw new Error('Invalid image location'); } let containerService: plugins.docker.DockerService | null = await this.getDockerServiceByName( serviceArgFromCloudly.data.name, ); this.coreflowRef.cloudlyConnector.cloudlyApiClient; const dockerSecretName = this.getWorkloadSecretName(serviceArgFromCloudly); let containerSecret: plugins.docker.DockerSecret | undefined | null = await this.coreflowRef.dockerHost.getSecretByName( dockerSecretName, ); const secretBundle = await this.coreflowRef.cloudlyConnector.cloudlyApiClient.secretbundle.getSecretBundleById( serviceArgFromCloudly.data.secretBundleId, ); const platformEnvObject = await this.coreflowRef.platformManager.provisionBindingsForService( serviceArgFromCloudly, ); const secretObject = { ...platformEnvObject, ...(await secretBundle.getFlatKeyValueObjectForEnvironment()), }; const secretHash = this.hashSecretObject(secretObject); const volumeHash = this.getServiceVolumeHash(serviceArgFromCloudly); const deploymentLabels = this.getWorkloadServiceDeploymentLabels( serviceArgFromCloudly, containerImageFromCloudly, secretHash, volumeHash, ); // existing network to connect to const webGatewayNetwork = await this.coreflowRef.dockerHost.getNetworkByName( this.commonDockerData.networkNames.sznWebgateway, ); if (!webGatewayNetwork) { throw new Error(`Missing required Docker network ${this.commonDockerData.networkNames.sznWebgateway}`); } if (containerService) { const existingLabels = containerService.Spec.Labels || {}; const cloudlyDeploymentLabelsChanged = Object.entries(deploymentLabels).some(([key, value]) => { return existingLabels[key] !== value; }); const dockerImageNeedsUpdate = serviceArgFromCloudly.data.registryTarget ? false : await containerService.needsUpdate(); if (cloudlyDeploymentLabelsChanged || dockerImageNeedsUpdate) { logger.log('info', `service ${serviceArgFromCloudly.data.name} desired state changed, recreating`); await containerService.remove(); if (containerSecret) { await containerSecret.remove(); } containerService = null; containerSecret = null; } } if (!containerService) { containerSecret = await this.coreflowRef.dockerHost.getSecretByName( dockerSecretName, ); if (containerSecret) { await containerSecret.remove(); } // lets create the relevant stuff on the docker side containerSecret = await this.coreflowRef.dockerHost.createSecret({ name: dockerSecretName, contentArg: JSON.stringify(secretObject), labels: {}, version: serviceArgFromCloudly.data.imageVersion, }); containerService = await this.createWorkloadDockerService({ service: serviceArgFromCloudly, image: localDockerImage, network: webGatewayNetwork, secret: containerSecret, labels: deploymentLabels, }); } } public async provisionWorkloadServices( _clusterConfigArg: plugins.servezoneInterfaces.data.ICluster, ) { const services = (await this.coreflowRef.cloudlyConnector.cloudlyApiClient.services.getServices()) as unknown as plugins.servezoneInterfaces.data.IService[]; for (const service of services) { if (service.data.serviceCategory && service.data.serviceCategory !== 'workload') { continue; } await this.provisionWorkloadService(service); } } /** * update traffic routing */ public async updateTrafficRouting( 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, ); if (!webGatewayNetwork) { throw new Error(`Missing required Docker network ${this.commonDockerData.networkNames.sznWebgateway}`); } const reverseProxyConfigs: plugins.servezoneInterfaces.data.IReverseProxyConfig[] = []; const pushProxyConfig = async ( workloadServiceArg: plugins.servezoneInterfaces.data.IService, hostNameArg: string, containerDestinationIp: string, webDestinationPort: string, ) => { logger.log('ok', `trying to obtain a certificate for ${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)], hostName: hostNameArg, privateKey: certificate.privateKey, publicKey: certificate.publicKey, }); logger.log( 'success', `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!`); const workloadServices = (await this.coreflowRef.cloudlyConnector.cloudlyApiClient.services.getServices()) as unknown as plugins.servezoneInterfaces.data.IService[]; for (const service of services) { let workloadConfig: plugins.servezoneInterfaces.data.IService | undefined; for (const workloadServiceConfig of workloadServices) { if (service.Spec.Name === workloadServiceConfig.data.name) { workloadConfig = workloadServiceConfig; break; } } if (workloadConfig) { logger.log('ok', `found workload service ${service.Spec.Name}`); const containersOfServicesOnNetwork = ( await webGatewayNetwork.getContainersOnNetworkForService(service) ).filter((container) => container.Name !== service.Spec.Name); // TODO: make this multi container ready if (!containersOfServicesOnNetwork[0]) { logger.log( 'error', `There seems to be no container available for service ${service.Spec.Name}`, ); continue; } const containerDestinationIp = containersOfServicesOnNetwork[0].IPv4Address.split('/')[0]; const hostNames = (workloadConfig.data.domains || []).map((domainEntry) => domainEntry.name); // lets route the web port const webDestinationPort: string = workloadConfig.data.ports.web.toString(); for (const hostName of hostNames) { await pushProxyConfig( workloadConfig, hostName, containerDestinationIp, webDestinationPort, ); } // lets route custom ports if (workloadConfig.data.ports.custom) { const customDomainKeys = Object.keys(workloadConfig.data.ports.custom); for (const customDomainKey of customDomainKeys) { await pushProxyConfig( workloadConfig, customDomainKey, containerDestinationIp, workloadConfig.data.ports.custom[customDomainKey], ); } } } else { logger.log( 'ok', `service ${service.Spec.Name} is not a workload service and won't receive traffic`, ); } } console.log(reverseProxyConfigs); await this.coreflowRef.corechatConnector.setReverseConfigs(reverseProxyConfigs); } }