import * as plugins from './coreflow.plugins.js'; import { logger } from './coreflow.logging.js'; import { Coreflow } from './coreflow.classes.coreflow.js'; export class ClusterManager { public coreflowRef: Coreflow; public dockerHost: plugins.docker.DockerHost; public configSubscription: plugins.smartrx.rxjs.Subscription; public containerSubscription: plugins.smartrx.rxjs.Subscription; public containerVersionSubscription: plugins.smartrx.rxjs.Subscription; public readyDeferred = plugins.smartpromise.defer(); public commonDockerData = { networkNames: { sznWebgateway: 'sznwebgateway', sznCorechat: 'szncorechat', }, }; constructor(coreflowRefArg: Coreflow) { this.coreflowRef = coreflowRefArg; this.dockerHost = new plugins.docker.DockerHost(); } /** * starts the cluster manager */ public async start() { const config = await this.coreflowRef.cloudlyConnector.getConfigFromCloudly(); await this.setDockerAuth(config); this.readyDeferred.resolve(); // subscriptions this.configSubscription = this.coreflowRef.cloudlyConnector.cloudlyClient.configUpdateSubject.subscribe( async (dataArg) => { await this.setDockerAuth(dataArg.configData); this.coreflowRef.taskManager.updateBaseServicesTask.trigger(); } ); this.containerSubscription = this.coreflowRef.cloudlyConnector.cloudlyClient.containerUpdateSubject.subscribe( async (dataArg) => { this.coreflowRef.taskManager.updateBaseServicesTask.trigger(); } ); this.containerVersionSubscription = this.coreflowRef.cloudlyConnector.cloudlyClient.containerVersionUpdateSubject.subscribe( async (dataArg) => { console.log( `Got a container version update trigger for ${dataArg.dockerImageUrl}@${dataArg.dockerImageVersion}` ); this.coreflowRef.taskManager.updateBaseServicesTask.trigger(); } ); } /** * stops the clustermanager */ public async stop() { this.configSubscription ? this.configSubscription.unsubscribe() : null; } public async setDockerAuth(configArg: plugins.servezoneInterfaces.data.IClusterConfig) { await this.dockerHost.auth(configArg.data.registryInfo); } /** * provisions base services */ public async provisionBaseServices() { // swarm should be enabled by lower level serverconfig package // get current situation const networks = await this.dockerHost.getNetworks(); let sznWebgatewayNetwork = await plugins.docker.DockerNetwork.getNetworkByName( this.dockerHost, this.commonDockerData.networkNames.sznWebgateway ); if (!sznWebgatewayNetwork) { sznWebgatewayNetwork = await plugins.docker.DockerNetwork.createNetwork(this.dockerHost, { Name: this.commonDockerData.networkNames.sznWebgateway, }); } else { logger.log('ok', 'sznWebgateway is already present'); } // corechat network let sznCorechatNetwork = await plugins.docker.DockerNetwork.getNetworkByName( this.dockerHost, this.commonDockerData.networkNames.sznCorechat ); if (!sznCorechatNetwork) { sznCorechatNetwork = await plugins.docker.DockerNetwork.createNetwork(this.dockerHost, { 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 plugins.docker.DockerImage.createFromRegistry(this.dockerHost, { imageUrl: 'registry.gitlab.com/losslessone/services/servezone/coretraffic', }); const corelogImage = await plugins.docker.DockerImage.createFromRegistry(this.dockerHost, { imageUrl: 'registry.gitlab.com/losslessone/services/servezone/corelog', }); // SERVICES // lets deploy the base services // coretraffic let coretrafficService: plugins.docker.DockerService; coretrafficService = await plugins.docker.DockerService.getServiceByName( this.dockerHost, '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 plugins.docker.DockerService.createService(this.dockerHost, { 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; corelogService = await plugins.docker.DockerService.getServiceByName( this.dockerHost, '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 plugins.docker.DockerService.createService(this.dockerHost, { 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); } /** * provision services obtained from cloudly */ public async provisionWorkloadServices(configData: plugins.servezoneInterfaces.data.IClusterConfig) { for (const containerConfig of configData.data.containers) { await this.provisionSpecificWorkloadService(containerConfig); } logger.log('ok', 'Waiting for scheduled workload services to settle'); await plugins.smartdelay.delayFor(10000); } public async provisionSpecificWorkloadService( containerConfigArg: plugins.servezoneInterfaces.data.IClusterConfigContainer ) { const containerImage = await plugins.docker.DockerImage.createFromRegistry(this.dockerHost, { imageUrl: containerConfigArg.image, }); let containerService = await plugins.docker.DockerService.getServiceByName( this.dockerHost, containerConfigArg.name ); let containerSecret = await plugins.docker.DockerSecret.getSecretByName( this.dockerHost, `${containerConfigArg.name}Secret` ); // existing network to connect to const webGatewayNetwork = await plugins.docker.DockerNetwork.getNetworkByName( this.dockerHost, this.commonDockerData.networkNames.sznWebgateway ); if (containerService && (await containerService.needsUpdate())) { await containerService.remove(); if (containerSecret) { await containerSecret.remove(); } containerService = null; containerSecret = null; } if (!containerService) { containerSecret = await plugins.docker.DockerSecret.getSecretByName( this.dockerHost, `${containerConfigArg.name}Secret` ); if (containerSecret) { await containerSecret.remove(); } containerSecret = await plugins.docker.DockerSecret.createSecret(this.dockerHost, { name: `${containerConfigArg.name}Secret`, contentArg: JSON.stringify(containerConfigArg.secrets), labels: {}, version: await containerImage.getVersion(), }); containerService = await plugins.docker.DockerService.createService(this.dockerHost, { name: containerConfigArg.name, image: containerImage, networks: [webGatewayNetwork], secrets: [containerSecret], ports: [], labels: {}, resources: containerConfigArg.resources, networkAlias: containerConfigArg.name, }); } } /** * update traffic routing */ public async updateTrafficRouting(clusterConfigArg: plugins.servezoneInterfaces.data.IClusterConfig) { const services = await this.dockerHost.getServices(); const webGatewayNetwork = await plugins.docker.DockerNetwork.getNetworkByName( this.dockerHost, this.commonDockerData.networkNames.sznWebgateway ); const reverseProxyConfigs: plugins.servezoneInterfaces.data.IReverseProxyConfig[] = []; const pushProxyConfig = async ( serviceNameArg: string, hostNameArg: string, containerDestinationIp: string, webDestinationPort: string ) => { logger.log('ok', `trying to obtain a certificate for ${hostNameArg}`); const certificate = await this.coreflowRef.cloudlyConnector.getCertificateForDomainFromCloudly(hostNameArg); reverseProxyConfigs.push({ destinationIp: containerDestinationIp, destinationPort: webDestinationPort, hostName: hostNameArg, privateKey: certificate.privateKey, publicKey: certificate.publicKey, }); logger.log( 'success', `pushed routing config for ${hostNameArg} on workload service ${serviceNameArg}` ); }; logger.log('info', `Found ${services.length} services!`); for (const service of services) { let workloadConfig: plugins.servezoneInterfaces.data.IClusterConfigContainer; for (const workloadServiceConfig of clusterConfigArg.data.containers) { if (service.Spec.Name === workloadServiceConfig.name) { workloadConfig = workloadServiceConfig; break; } } if (workloadConfig) { logger.log('ok', `found workload service ${service.Spec.Name}`); const containersOfServicesOnNetwork = await webGatewayNetwork.getContainersOnNetworkForService(service); // 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: string[] = workloadConfig.domains; // lets route the web port const webDestinationPort: string = workloadConfig.ports.web.toString(); for (const hostName of hostNames) { await pushProxyConfig( workloadConfig.name, hostName, containerDestinationIp, webDestinationPort ); } // lets route custom ports if (workloadConfig.ports.custom) { const customDomainKeys = Object.keys(workloadConfig.ports.custom); for (const customDomainKey of customDomainKeys) { await pushProxyConfig( workloadConfig.name, customDomainKey, containerDestinationIp, workloadConfig.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); } }