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

480 lines
19 KiB
JavaScript

"use strict";
const Logger = require("../utils/Logger");
const CaptainConstants = require("../utils/CaptainConstants");
const CaptainManager = require("./CaptainManager");
const ApiStatusCodes = require("../api/ApiStatusCodes");
const Authenticator = require("./Authenticator");
const requireFromString = require("require-from-string");
const BuildLog = require("./BuildLog");
const ImageMaker = require("./ImageMaker");
const DockerRegistryHelper = require("./DockerRegistryHelper");
class ServiceManager {
constructor(dataStore, dockerApi, loadBalancerManager) {
this.dataStore = dataStore;
this.dockerApi = dockerApi;
this.loadBalancerManager = loadBalancerManager;
this.activeBuilds = {};
this.buildLogs = {};
this.isReady = true;
this.dockerRegistryHelper = new DockerRegistryHelper(this.dataStore, this.dockerApi);
this.imageMaker = new ImageMaker(this.dockerRegistryHelper, this.dockerApi, this.dataStore, this.buildLogs, this.activeBuilds);
}
getRegistryHelper() {
return this.dockerRegistryHelper;
}
isInited() {
return this.isReady;
}
deployNewVersion(appName, source, gitHash) {
const self = this;
const dataStore = this.dataStore;
let deployedVersion;
return Promise.resolve() //
.then(function () {
return dataStore.getAppsDataStore().createNewVersion(appName);
})
.then(function (appVersion) {
deployedVersion = appVersion;
return self.imageMaker.ensureImage(source, appName, appVersion);
})
.then(function (imageName) {
return dataStore
.getAppsDataStore()
.setDeployedVersionAndImage(appName, deployedVersion, imageName);
})
.then(function () {
return self.ensureServiceInitedAndUpdated(appName);
})
.catch(function (error) {
return new Promise(function (resolve, reject) {
self.logBuildFailed(appName, error);
reject(error);
});
});
}
enableCustomDomainSsl(appName, customDomain) {
const self = this;
return Promise.resolve()
.then(function () {
Logger.d('Verifying Captain owns domain: ' + customDomain);
return CaptainManager.get().verifyCaptainOwnsDomainOrThrow(customDomain, undefined);
})
.then(function () {
Logger.d('Enabling SSL for: ' + appName + ' on ' + customDomain);
return self.dataStore
.getAppsDataStore()
.verifyCustomDomainBelongsToApp(appName, customDomain);
})
.then(function () {
return CaptainManager.get().requestCertificateForDomain(customDomain);
})
.then(function () {
return self.dataStore
.getAppsDataStore()
.enableCustomDomainSsl(appName, customDomain);
})
.then(function () {
return self.reloadLoadBalancer();
});
}
addCustomDomain(appName, customDomain) {
const self = this;
return Promise.resolve()
.then(function () {
const rootDomain = self.dataStore.getRootDomain();
const dotRootDomain = '.' + rootDomain;
if (!customDomain || !/^[a-z0-9\-\.]+$/.test(customDomain)) {
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_BAD_NAME, 'Domain name is not accepted. Please use alphanumerical domains such as myapp.google123.ca');
}
if (customDomain.length > 80) {
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_BAD_NAME, 'Domain name is not accepted. Please use alphanumerical domains less than 80 characters in length.');
}
if (customDomain.indexOf('..') >= 0) {
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_BAD_NAME, 'Domain name is not accepted. You cannot have two consecutive periods ".." inside a domain name. Please use alphanumerical domains such as myapp.google123.ca');
}
if (customDomain.indexOf(dotRootDomain) >= 0 &&
customDomain.indexOf(dotRootDomain) +
dotRootDomain.length ===
customDomain.length) {
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_BAD_NAME, 'Domain name is not accepted. Custom domain cannot be subdomain of root domain.');
}
})
.then(function () {
return CaptainManager.get().verifyDomainResolvesToDefaultServerOnHost(customDomain);
})
.then(function () {
Logger.d('Enabling custom domain for: ' + appName);
return self.dataStore
.getAppsDataStore()
.addCustomDomainForApp(appName, customDomain);
})
.then(function () {
return self.reloadLoadBalancer();
});
}
removeCustomDomain(appName, customDomain) {
const self = this;
return Promise.resolve()
.then(function () {
Logger.d('Removing custom domain for: ' + appName);
return self.dataStore
.getAppsDataStore()
.removeCustomDomainForApp(appName, customDomain);
})
.then(function () {
return self.reloadLoadBalancer();
});
}
enableSslForApp(appName) {
const self = this;
let rootDomain;
return Promise.resolve()
.then(function () {
return self.verifyCaptainOwnsGenericSubDomain(appName);
})
.then(function () {
Logger.d('Enabling SSL for: ' + appName);
return self.dataStore.getRootDomain();
})
.then(function (val) {
rootDomain = val;
if (!rootDomain) {
throw new Error('No rootDomain! Cannot verify domain');
}
})
.then(function () {
// it will ensure that the app exists, otherwise it throws an exception
return self.dataStore
.getAppsDataStore()
.getAppDefinition(appName);
})
.then(function () {
return appName + '.' + rootDomain;
})
.then(function (domainName) {
return CaptainManager.get().requestCertificateForDomain(domainName);
})
.then(function () {
return self.dataStore
.getAppsDataStore()
.enableSslForDefaultSubDomain(appName);
})
.then(function () {
return self.reloadLoadBalancer();
});
}
verifyCaptainOwnsGenericSubDomain(appName) {
const self = this;
let rootDomain;
return Promise.resolve()
.then(function () {
return self.dataStore.getRootDomain();
})
.then(function (val) {
rootDomain = val;
})
.then(function () {
// it will ensure that the app exists, otherwise it throws an exception
return self.dataStore
.getAppsDataStore()
.getAppDefinition(appName);
})
.then(function () {
return appName + '.' + rootDomain;
})
.then(function (domainName) {
Logger.d('Verifying Captain owns domain: ' + domainName);
return CaptainManager.get().verifyCaptainOwnsDomainOrThrow(domainName, undefined);
});
}
removeApp(appName) {
Logger.d('Removing service for: ' + appName);
const self = this;
const serviceName = this.dataStore
.getAppsDataStore()
.getServiceName(appName);
const dockerApi = this.dockerApi;
const dataStore = this.dataStore;
return Promise.resolve()
.then(function () {
Logger.d('Check if service is running: ' + serviceName);
return dockerApi.isServiceRunningByName(serviceName);
})
.then(function (isRunning) {
if (isRunning) {
return dockerApi.removeServiceByName(serviceName);
}
else {
Logger.w('Cannot delete service... It is not running: ' +
serviceName);
return true;
}
})
.then(function () {
return dataStore.getAppsDataStore().deleteAppDefinition(appName);
})
.then(function () {
return self.reloadLoadBalancer();
});
}
getUnusedImages(mostRecentLimit) {
Logger.d('Getting unused images, excluding most recent ones: ' +
mostRecentLimit);
const self = this;
const dockerApi = this.dockerApi;
const dataStore = this.dataStore;
let allImages;
return Promise.resolve()
.then(function () {
return dockerApi.getImages();
})
.then(function (images) {
allImages = images;
return dataStore.getAppsDataStore().getAppDefinitions();
})
.then(function (apps) {
const unusedImages = [];
for (let i = 0; i < allImages.length; i++) {
const img = allImages[i];
let imageInUse = false;
if (img.RepoTags) {
for (let j = 0; j < img.RepoTags.length; j++) {
const repoTag = img.RepoTags[j];
Object.keys(apps).forEach(function (key, index) {
const app = apps[key];
const appName = key;
for (let k = 0; k < mostRecentLimit + 1; k++) {
if (repoTag.indexOf(dataStore.getImageNameAndTag(appName, Number(app.deployedVersion) - k)) >= 0) {
imageInUse = true;
}
}
});
}
}
if (!imageInUse) {
unusedImages.push({
id: img.Id,
description: img.RepoTags && img.RepoTags.length
? img.RepoTags[0]
: 'untagged',
});
}
}
return unusedImages;
});
}
deleteImages(imageIds) {
Logger.d('Deleting images...');
const self = this;
const dockerApi = this.dockerApi;
return Promise.resolve().then(function () {
return dockerApi.deleteImages(imageIds);
});
}
createPreDeployFunctionIfExist(app) {
let preDeployFunction = app.preDeployFunction;
if (!preDeployFunction) {
return undefined;
}
/*
////////////////////////////////// Expected content of the file //////////////////////////
const uuid = require('uuid/v4');
console.log('-------------------------------'+uuid());
preDeployFunction = function (captainAppObj, dockerUpdateObject) {
return Promise.resolve()
.then(function(){
console.log(JSON.stringify(dockerUpdateObject));
return dockerUpdateObject;
});
};
*/
preDeployFunction =
preDeployFunction + '\n\n module.exports = preDeployFunction';
return requireFromString(preDeployFunction);
}
updateAppDefinition(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, forceSsl, ports, repoInfo, customNginxConfig, preDeployFunction) {
const self = this;
const dataStore = this.dataStore;
const dockerApi = this.dockerApi;
let serviceName;
const checkIfNodeIdExists = function (nodeIdToCheck) {
return dockerApi.getNodesInfo().then(function (nodeInfo) {
for (let i = 0; i < nodeInfo.length; i++) {
if (nodeIdToCheck === nodeInfo[i].nodeId) {
return;
}
}
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Node ID you requested in not part of the swarm ' +
nodeIdToCheck);
});
};
return Promise.resolve()
.then(function () {
return dataStore.getAppsDataStore().getAppDefinition(appName);
})
.then(function (app) {
serviceName = dataStore
.getAppsDataStore()
.getServiceName(appName);
// After leaving this block, nodeId will be guaranteed to be NonNull
if (app.hasPersistentData) {
if (nodeId) {
return checkIfNodeIdExists(nodeId);
}
else {
if (app.nodeId) {
nodeId = app.nodeId;
}
else {
return dockerApi
.isServiceRunningByName(serviceName)
.then(function (isRunning) {
if (!isRunning) {
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Cannot find the service. Try again in a minute...');
}
return dockerApi.getNodeIdByServiceName(serviceName, 0);
})
.then(function (nodeIdRunningService) {
if (!nodeIdRunningService) {
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'No NodeId was found. Try again in a minute...');
}
nodeId = nodeIdRunningService;
});
}
}
}
else {
if (volumes && volumes.length) {
throw ApiStatusCodes.createError(ApiStatusCodes.ILLEGAL_OPERATION, 'Cannot set volumes for a non-persistent container!');
}
if (nodeId) {
return checkIfNodeIdExists(nodeId);
}
}
})
.then(function () {
return dataStore
.getAppsDataStore()
.updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, forceSsl, ports, repoInfo, Authenticator.get(dataStore.getNameSpace()), customNginxConfig, preDeployFunction);
})
.then(function () {
return self.ensureServiceInitedAndUpdated(appName);
})
.then(function () {
return self.reloadLoadBalancer();
});
}
isAppBuilding(appName) {
return !!this.activeBuilds[appName];
}
/**
*
* @returns the active build that it finds
*/
isAnyBuildRunning() {
const activeBuilds = this.activeBuilds;
for (const appName in activeBuilds) {
if (!!activeBuilds[appName]) {
return appName;
}
}
return undefined;
}
getBuildStatus(appName) {
const self = this;
this.buildLogs[appName] =
this.buildLogs[appName] ||
new BuildLog(CaptainConstants.configs.buildLogSize);
return {
isAppBuilding: self.isAppBuilding(appName),
logs: self.buildLogs[appName].getLogs(),
isBuildFailed: self.buildLogs[appName].isBuildFailed,
};
}
logBuildFailed(appName, error) {
error = (error || '') + '';
this.buildLogs[appName] =
this.buildLogs[appName] ||
new BuildLog(CaptainConstants.configs.buildLogSize);
this.buildLogs[appName].onBuildFailed(error);
}
ensureServiceInitedAndUpdated(appName) {
Logger.d('Ensure service inited and Updated for: ' + appName);
const self = this;
const serviceName = this.dataStore
.getAppsDataStore()
.getServiceName(appName);
let imageName;
const dockerApi = this.dockerApi;
const dataStore = this.dataStore;
let app;
let dockerAuthObject;
return Promise.resolve() //
.then(function () {
return dataStore.getAppsDataStore().getAppDefinition(appName);
})
.then(function (appFound) {
app = appFound;
Logger.d(`Check if service is running: ${serviceName}`);
return dockerApi.isServiceRunningByName(serviceName);
})
.then(function (isRunning) {
if (isRunning) {
Logger.d('Service is already running: ' + serviceName);
return true;
}
else {
for (let i = 0; i < app.versions.length; i++) {
const element = app.versions[i];
if (element.version == app.deployedVersion) {
imageName = element.deployedImageName;
break;
}
}
if (!imageName) {
throw ApiStatusCodes.createError(ApiStatusCodes.ILLEGAL_PARAMETER, 'ImageName for deployed version is not available, this version was probably failed due to an unsuccessful build!');
}
Logger.d(`Creating service ${serviceName} with default image, we will update image later`);
// if we pass in networks here. Almost always it results in a delayed update which causes
// update errors if they happen right away!
return dockerApi.createServiceOnNodeId(CaptainConstants.appPlaceholderImageName, serviceName, undefined, undefined, undefined, undefined, undefined);
}
})
.then(function () {
return self.dockerRegistryHelper.getDockerAuthObjectForImageName(imageName);
})
.then(function (data) {
dockerAuthObject = data;
})
.then(function () {
return self.createPreDeployFunctionIfExist(app);
})
.then(function (preDeployFunction) {
Logger.d(`Updating service ${serviceName} with image ${imageName}`);
return dockerApi.updateService(serviceName, imageName, app.volumes, app.networks, app.envVars, undefined, dockerAuthObject, Number(app.instanceCount), app.nodeId, dataStore.getNameSpace(), app.ports, app, preDeployFunction);
})
.then(function () {
return new Promise(function (resolve) {
// Waiting 2 extra seconds for docker DNS to pickup the service name
setTimeout(resolve, 2000);
});
})
.then(function () {
return self.reloadLoadBalancer();
});
}
reloadLoadBalancer() {
Logger.d('Updating Load Balancer');
const self = this;
return self.loadBalancerManager
.rePopulateNginxConfigFile(self.dataStore)
.then(function () {
Logger.d('sendReloadSignal...');
return self.loadBalancerManager.sendReloadSignal();
});
}
}
module.exports = ServiceManager;
//# sourceMappingURL=ServiceManager.js.map