352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
|
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);
|
||
|
}
|
||
|
}
|