Files
coreflow/ts/coreflow.classes.clustermanager.ts
T
2026-05-02 18:58:21 +00:00

846 lines
30 KiB
TypeScript

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<string, string>;
};
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<plugins.docker.DockerService | null> {
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<string, unknown>)
.sort()
.map((keyArg) => `${JSON.stringify(keyArg)}:${this.stableStringify((valueArg as Record<string, unknown>)[keyArg])}`)
.join(',')}}`;
}
return JSON.stringify(valueArg);
}
private hashSecretObject(secretObjectArg: Record<string, string>) {
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<plugins.docker.DockerImage> {
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<Record<string, unknown>> = [];
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<string, string> = {
...(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<string, string>;
}) {
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<plugins.docker.DockerNetwork>,
) {
const corestoreImage = corestoreImageArg as unknown as {
RepoTags?: string[];
Labels?: Record<string, string>;
};
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);
}
}