mirror of
https://github.com/caprover/caprover
synced 2026-08-10 01:30:59 +00:00
480 lines
19 KiB
JavaScript
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
|