Files
caprover/app-backend/built/user/LoadBalancerManager.js
T

393 lines
16 KiB
JavaScript

"use strict";
const ejs = require("ejs");
const CaptainConstants = require("../utils/CaptainConstants");
const Logger = require("../utils/Logger");
const fs = require("fs-extra");
const uuid = require("uuid/v4");
const request = require("request");
const ApiStatusCodes = require("../api/ApiStatusCodes");
const defaultPageTemplate = fs
.readFileSync(__dirname + '/../../template/default-page.ejs')
.toString();
class LoadBalancerManager {
constructor(dockerApi, certbotManager, dataStore) {
this.dockerApi = dockerApi;
this.certbotManager = certbotManager;
this.dataStore = dataStore;
this.reloadInProcess = false;
this.requestedReloadPromises = [];
this.captainPublicRandomKey = uuid();
}
/**
* Reloads the configuation for NGINX.
* NOTE that this can return synchronously with UNDEFINED if there is already a process in the background.
* @param dataStoreToQueue
* @returns {Promise.<>}
*/
rePopulateNginxConfigFile(dataStoreToQueue) {
const self = this;
return new Promise(function (res, rej) {
self.requestedReloadPromises.push({
dataStore: dataStoreToQueue,
resolve: res,
reject: rej,
});
self.consumeQueueIfAnyInNginxReloadQueue();
});
}
consumeQueueIfAnyInNginxReloadQueue() {
const self = this;
const q = self.requestedReloadPromises.pop();
if (!q) {
return;
}
if (self.reloadInProcess) {
Logger.d('NGINX Reload already in process, Bouncing off...');
return;
}
Logger.d('Locking NGINX configuration reloading...');
self.reloadInProcess = true;
const dataStore = q.dataStore;
// This will resolve to something like: /captain/nginx/conf.d/captain
const configFilePathBase = CaptainConstants.perAppNginxConfigPathBase +
'/' +
dataStore.getNameSpace();
const FUTURE = configFilePathBase + '.fut';
const BACKUP = configFilePathBase + '.bak';
const CONFIG = configFilePathBase + '.conf';
let nginxConfigContent = '';
return Promise.resolve()
.then(function () {
return fs.remove(FUTURE);
})
.then(function () {
return dataStore.getServerList();
})
.then(function (servers) {
const promises = [];
if (servers && !!servers.length) {
for (let i = 0; i < servers.length; i++) {
const s = servers[i];
if (s.hasSsl) {
s.crtPath = self.getSslCertPath(s.publicDomain);
s.keyPath = self.getSslKeyPath(s.publicDomain);
}
s.staticWebRoot =
CaptainConstants.nginxStaticRootDir +
CaptainConstants.nginxDomainSpecificHtmlDir +
'/' +
s.publicDomain;
promises.push(Promise.resolve()
.then(function () {
return ejs.render(s.nginxConfigTemplate, {
s: s,
});
})
.then(function (rendered) {
nginxConfigContent += rendered;
}));
}
}
return Promise.all(promises);
})
.then(function () {
return fs.outputFile(FUTURE, nginxConfigContent);
})
.then(function () {
return fs.remove(BACKUP);
})
.then(function () {
return fs.ensureFile(CONFIG);
})
.then(function () {
return fs.renameSync(CONFIG, BACKUP); // sync method. It's really fast.
})
.then(function () {
return fs.renameSync(FUTURE, CONFIG); // sync method. It's really fast.
})
.then(function () {
return self.createRootConfFile(dataStore);
})
.then(function () {
Logger.d('SUCCESS: UNLocking NGINX configuration reloading...');
self.reloadInProcess = false;
q.resolve();
self.consumeQueueIfAnyInNginxReloadQueue();
})
.catch(function (error) {
Logger.e(error);
Logger.d('Error: UNLocking NGINX configuration reloading...');
self.reloadInProcess = false;
q.reject(error);
self.consumeQueueIfAnyInNginxReloadQueue();
});
}
sendReloadSignal() {
return this.dockerApi.sendSingleContainerKillHUP(CaptainConstants.nginxServiceName);
}
getCaptainPublicRandomKey() {
return this.captainPublicRandomKey;
}
getSslCertPath(domainName) {
const self = this;
return (CaptainConstants.letsEncryptEtcPathOnNginx +
self.certbotManager.getCertRelativePathForDomain(domainName));
}
getSslKeyPath(domainName) {
const self = this;
return (CaptainConstants.letsEncryptEtcPathOnNginx +
self.certbotManager.getKeyRelativePathForDomain(domainName));
}
getInfo() {
return new Promise(function (resolve, reject) {
const url = 'http://' + CaptainConstants.nginxServiceName + '/nginx_status';
request(url, function (error, response, body) {
if (error || !body) {
Logger.e('Error ' + error);
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Request to nginx Failed.'));
return;
}
try {
const data = new LoadBalancerInfo();
const lines = body.split('\n');
data.activeConnections = Number(lines[0].split(' ')[2].trim());
data.accepted = Number(lines[2].split(' ')[1].trim());
data.handled = Number(lines[2].split(' ')[2].trim());
data.total = Number(lines[2].split(' ')[3].trim());
data.reading = Number(lines[3].split(' ')[1].trim());
data.writing = Number(lines[3].split(' ')[3].trim());
data.waiting = Number(lines[3].split(' ')[5].trim());
resolve(data);
}
catch (error) {
Logger.e('Cannot parse ' + body);
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Parser Failed. See internal logs...'));
}
});
});
}
createRootConfFile(dataStore) {
const self = this;
const captainDomain = CaptainConstants.captainSubDomain + '.' + dataStore.getRootDomain();
const registryDomain = CaptainConstants.registrySubDomain + '.' + dataStore.getRootDomain();
let hasRootSsl = false;
const FUTURE = CaptainConstants.rootNginxConfigPath + '.fut';
const BACKUP = CaptainConstants.rootNginxConfigPath + '.bak';
const CONFIG = CaptainConstants.rootNginxConfigPath + '.conf';
let rootNginxTemplate = undefined;
return Promise.resolve()
.then(function () {
return dataStore.getNginxConfig();
})
.then(function (nginxConfig) {
rootNginxTemplate =
nginxConfig.captainConfig.customValue ||
nginxConfig.captainConfig.byDefault;
return dataStore.getHasRootSsl();
})
.then(function (hasSsl) {
hasRootSsl = hasSsl;
return dataStore.getHasRegistrySsl();
})
.then(function (hasRegistrySsl) {
return ejs.render(rootNginxTemplate, {
captain: {
crtPath: self.getSslCertPath(captainDomain),
keyPath: self.getSslKeyPath(captainDomain),
hasRootSsl: hasRootSsl,
serviceName: CaptainConstants.captainServiceName,
domain: captainDomain,
serviceExposedPort: CaptainConstants.captainServiceExposedPort,
defaultHtmlDir: CaptainConstants.nginxStaticRootDir +
CaptainConstants.nginxDefaultHtmlDir,
staticWebRoot: CaptainConstants.nginxStaticRootDir +
CaptainConstants.nginxDomainSpecificHtmlDir +
'/' +
captainDomain,
},
registry: {
crtPath: self.getSslCertPath(registryDomain),
keyPath: self.getSslKeyPath(registryDomain),
hasRootSsl: hasRegistrySsl,
domain: registryDomain,
staticWebRoot: CaptainConstants.nginxStaticRootDir +
CaptainConstants.nginxDomainSpecificHtmlDir +
'/' +
registryDomain,
},
});
})
.then(function (rootNginxConfContent) {
return fs.outputFile(FUTURE, rootNginxConfContent);
})
.then(function () {
return fs.remove(BACKUP);
})
.then(function () {
return fs.ensureFile(CONFIG);
})
.then(function () {
return fs.renameSync(CONFIG, BACKUP); // sync method. It's really fast.
})
.then(function () {
return fs.renameSync(FUTURE, CONFIG); // sync method. It's really fast.
});
}
ensureBaseNginxConf() {
const self = this;
return Promise.resolve()
.then(function () {
return self.dataStore.getNginxConfig();
})
.then(function (captainConfig) {
const baseConfigTemplate = captainConfig.baseConfig.customValue ||
captainConfig.baseConfig.byDefault;
return ejs.render(baseConfigTemplate, {});
})
.then(function (baseNginxConfFileContent) {
return fs.outputFile(CaptainConstants.baseNginxConfigPath, baseNginxConfFileContent);
});
}
init(myNodeId, dataStore) {
const dockerApi = this.dockerApi;
const self = this;
function createNginxServiceOnNode(nodeId) {
Logger.d('No Captain Nginx service is running. Creating one on captain node...');
return dockerApi
.createServiceOnNodeId(CaptainConstants.nginxImageName, CaptainConstants.nginxServiceName, [
{
protocol: 'tcp',
publishMode: 'host',
containerPort: 80,
hostPort: CaptainConstants.nginxPortNumber,
},
{
protocol: 'tcp',
publishMode: 'host',
containerPort: 443,
hostPort: 443,
},
], nodeId, undefined, undefined, {
Reservation: {
MemoryBytes: 30 * 1024 * 1024,
},
})
.then(function () {
const waitTimeInMillis = 5000;
Logger.d('Waiting for ' +
waitTimeInMillis / 1000 +
' seconds for nginx to start up');
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(true);
}, waitTimeInMillis);
});
});
}
return fs
.outputFile(CaptainConstants.captainStaticFilesDir +
CaptainConstants.nginxDefaultHtmlDir +
CaptainConstants.captainConfirmationPath, self.getCaptainPublicRandomKey())
.then(function () {
return ejs.render(defaultPageTemplate, {
message: 'Nothing here yet :/',
});
})
.then(function (staticPageContent) {
return fs.outputFile(CaptainConstants.captainStaticFilesDir +
CaptainConstants.nginxDefaultHtmlDir +
'/index.html', staticPageContent);
})
.then(function () {
return ejs.render(defaultPageTemplate, {
message: 'An Error Occurred :/',
});
})
.then(function (errorPageContent) {
return fs.outputFile(CaptainConstants.captainStaticFilesDir +
CaptainConstants.nginxDefaultHtmlDir +
'/error.html', errorPageContent);
})
.then(function () {
Logger.d('Setting up NGINX conf file...');
return self.ensureBaseNginxConf();
})
.then(function () {
return self.rePopulateNginxConfigFile(dataStore);
})
.then(function () {
return fs.ensureDir(CaptainConstants.letsEncryptEtcPath);
})
.then(function () {
return fs.ensureDir(CaptainConstants.nginxSharedPathOnHost);
})
.then(function () {
return dockerApi.isServiceRunningByName(CaptainConstants.nginxServiceName);
})
.then(function (isRunning) {
if (isRunning) {
Logger.d('Captain Nginx is already running.. ');
return dockerApi.getNodeIdByServiceName(CaptainConstants.nginxServiceName, 0);
}
else {
return createNginxServiceOnNode(myNodeId).then(function () {
return myNodeId;
});
}
})
.then(function (nodeId) {
if (nodeId !== myNodeId) {
Logger.d('Captain Nginx is running on a different node. Removing...');
return dockerApi
.removeServiceByName(CaptainConstants.nginxServiceName)
.then(function () {
return createNginxServiceOnNode(myNodeId).then(function () {
return true;
});
});
}
else {
return true;
}
})
.then(function () {
Logger.d('Updating NGINX service...');
return dockerApi.updateService(CaptainConstants.nginxServiceName, undefined, [
{
containerPath: CaptainConstants.nginxStaticRootDir,
hostPath: CaptainConstants.captainStaticFilesDir,
},
{
containerPath: '/etc/nginx/nginx.conf',
hostPath: CaptainConstants.baseNginxConfigPath,
},
{
containerPath: '/etc/nginx/conf.d',
hostPath: CaptainConstants.perAppNginxConfigPathBase,
},
{
containerPath: CaptainConstants.letsEncryptEtcPathOnNginx,
hostPath: CaptainConstants.letsEncryptEtcPath,
},
{
containerPath: CaptainConstants.nginxSharedPathOnNginx,
hostPath: CaptainConstants.nginxSharedPathOnHost,
},
], [CaptainConstants.captainNetworkName], undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
})
.then(function () {
const waitTimeInMillis = 5000;
Logger.d('Waiting for ' +
waitTimeInMillis / 1000 +
' seconds for nginx reload to take into effect');
return new Promise(function (resolve, reject) {
setTimeout(function () {
Logger.d('NGINX is fully set up and working...');
resolve(true);
}, waitTimeInMillis);
});
});
}
}
module.exports = LoadBalancerManager;
//# sourceMappingURL=LoadBalancerManager.js.map