mirror of
https://github.com/caprover/caprover
synced 2026-08-14 03:31:09 +00:00
472 lines
20 KiB
JavaScript
472 lines
20 KiB
JavaScript
"use strict";
|
|
const uuid = require("uuid/v4");
|
|
const ApiStatusCodes = require("../api/ApiStatusCodes");
|
|
const CaptainConstants = require("../utils/CaptainConstants");
|
|
const isValidPath = require('is-valid-path');
|
|
const APP_DEFINITIONS = 'appDefinitions';
|
|
function isNameAllowed(name) {
|
|
const isNameFormattingOk = !!name &&
|
|
name.length < 50 &&
|
|
/^[a-z]/.test(name) &&
|
|
/[a-z0-9]$/.test(name) &&
|
|
/^[a-z0-9\-]+$/.test(name) &&
|
|
name.indexOf('--') < 0;
|
|
return isNameFormattingOk && ['captain', 'registry'].indexOf(name) < 0;
|
|
}
|
|
function isPortValid(portNumber) {
|
|
return portNumber > 0 && portNumber < 65535;
|
|
}
|
|
class AppsDataStore {
|
|
constructor(data, namepace) {
|
|
this.data = data;
|
|
this.namepace = namepace;
|
|
}
|
|
setEncryptor(encryptor) {
|
|
this.encryptor = encryptor;
|
|
}
|
|
saveApp(appName, app) {
|
|
const self = this;
|
|
let passwordToBeEncrypted = '';
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
if (!appName) {
|
|
throw new Error('App Name should not be empty');
|
|
}
|
|
let pushWebhook = app.appPushWebhook;
|
|
if (pushWebhook &&
|
|
pushWebhook.pushWebhookToken &&
|
|
pushWebhook.tokenVersion &&
|
|
pushWebhook.repoInfo &&
|
|
pushWebhook.repoInfo.repo) {
|
|
// we have required info
|
|
passwordToBeEncrypted = pushWebhook.repoInfo.password;
|
|
pushWebhook.repoInfo.password = '';
|
|
}
|
|
else {
|
|
// some required data is missing. We drop the push data
|
|
pushWebhook = undefined;
|
|
}
|
|
if (app.forceSsl) {
|
|
let hasAtLeastOneSslDomain = app.hasDefaultSubDomainSsl;
|
|
const customDomainArray = app.customDomain;
|
|
if (customDomainArray && customDomainArray.length > 0) {
|
|
for (let idx = 0; idx < customDomainArray.length; idx++) {
|
|
if (customDomainArray[idx].hasSsl) {
|
|
hasAtLeastOneSslDomain = true;
|
|
}
|
|
}
|
|
}
|
|
if (!hasAtLeastOneSslDomain) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.ILLEGAL_OPERATION, 'Cannot force SSL without at least one SSL-enabled domain!');
|
|
}
|
|
}
|
|
if (app.ports) {
|
|
for (let i = 0; i < app.ports.length; i++) {
|
|
const obj = app.ports[i];
|
|
if (obj.containerPort && obj.hostPort) {
|
|
const containerPort = Number(obj.containerPort);
|
|
const hostPort = Number(obj.hostPort);
|
|
if (!isPortValid(containerPort) ||
|
|
!isPortValid(hostPort)) {
|
|
throw new Error(`Invalid ports: ${hostPort} or ${containerPort}`);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error('Host or container port is missing');
|
|
}
|
|
}
|
|
}
|
|
if (app.volumes) {
|
|
for (let i = 0; i < app.volumes.length; i++) {
|
|
const obj = app.volumes[i];
|
|
if (!obj.containerPath ||
|
|
!(obj.volumeName || obj.hostPath)) {
|
|
throw new Error('containerPath or the source paths (volume name or host path) are missing');
|
|
}
|
|
if (obj.volumeName && obj.hostPath) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Cannot define both host path and volume name!');
|
|
}
|
|
if (!isValidPath(obj.containerPath)) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Invalid containerPath: ' + obj.containerPath);
|
|
}
|
|
if (obj.hostPath && !isValidPath(obj.hostPath)) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Invalid volume host path: ' + obj.hostPath);
|
|
}
|
|
else {
|
|
if (!obj.volumeName ||
|
|
!isNameAllowed(obj.volumeName)) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'Invalid volume name: ' + obj.volumeName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.then(function () {
|
|
const appToSave = app;
|
|
if (passwordToBeEncrypted) {
|
|
appToSave.appPushWebhook.repoInfo.passwordEncrypted = self.encryptor.encrypt(passwordToBeEncrypted);
|
|
}
|
|
return appToSave;
|
|
})
|
|
.then(function (appToSave) {
|
|
self.data.set(APP_DEFINITIONS + '.' + appName, appToSave);
|
|
});
|
|
}
|
|
getServiceName(appName) {
|
|
return 'srv-' + this.namepace + '--' + appName;
|
|
}
|
|
getAppDefinitions() {
|
|
const self = this;
|
|
return new Promise(function (resolve, reject) {
|
|
let allApps = self.data.get(APP_DEFINITIONS) || {};
|
|
let allAppsUnencrypted = {};
|
|
Object.keys(allApps).forEach(function (appName) {
|
|
allAppsUnencrypted[appName] = allApps[appName];
|
|
const appUnencrypted = allAppsUnencrypted[appName];
|
|
const appSave = allApps[appName];
|
|
if (appSave.appPushWebhook &&
|
|
appSave.appPushWebhook.repoInfo &&
|
|
appSave.appPushWebhook.repoInfo.passwordEncrypted) {
|
|
const repo = appSave.appPushWebhook.repoInfo;
|
|
appUnencrypted.appPushWebhook = {
|
|
tokenVersion: appSave.appPushWebhook.tokenVersion,
|
|
pushWebhookToken: appSave.appPushWebhook.pushWebhookToken,
|
|
repoInfo: {
|
|
repo: repo.repo,
|
|
user: repo.user,
|
|
password: self.encryptor.decrypt(repo.passwordEncrypted),
|
|
branch: repo.branch,
|
|
},
|
|
};
|
|
}
|
|
});
|
|
resolve(allAppsUnencrypted);
|
|
});
|
|
}
|
|
getAppDefinition(appName) {
|
|
const self = this;
|
|
return this.getAppDefinitions().then(function (allApps) {
|
|
if (!appName) {
|
|
throw new Error('App Name should not be empty');
|
|
}
|
|
const app = allApps[appName];
|
|
if (!app) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, `App (${appName}) could not be found. Make sure that you have created the app.`);
|
|
}
|
|
return app;
|
|
});
|
|
}
|
|
enableSslForDefaultSubDomain(appName) {
|
|
const self = this;
|
|
return this.getAppDefinition(appName).then(function (app) {
|
|
app.hasDefaultSubDomainSsl = true;
|
|
return self.saveApp(appName, app);
|
|
});
|
|
}
|
|
enableCustomDomainSsl(appName, customDomain) {
|
|
const self = this;
|
|
return self.getAppDefinition(appName).then(function (app) {
|
|
app.customDomain = app.customDomain || [];
|
|
if (app.customDomain.length > 0) {
|
|
for (let idx = 0; idx < app.customDomain.length; idx++) {
|
|
if (app.customDomain[idx].publicDomain === customDomain) {
|
|
app.customDomain[idx].hasSsl = true;
|
|
return self.saveApp(appName, app);
|
|
}
|
|
}
|
|
}
|
|
throw new Error(`customDomain: ${customDomain} is not attached to app ${appName}`);
|
|
});
|
|
}
|
|
removeCustomDomainForApp(appName, customDomain) {
|
|
const self = this;
|
|
return this.getAppDefinition(appName).then(function (app) {
|
|
app.customDomain = app.customDomain || [];
|
|
const newDomains = [];
|
|
let removed = false;
|
|
for (let idx = 0; idx < app.customDomain.length; idx++) {
|
|
if (app.customDomain[idx].publicDomain === customDomain) {
|
|
removed = true;
|
|
}
|
|
else {
|
|
newDomains.push(app.customDomain[idx]);
|
|
}
|
|
}
|
|
if (!removed) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, `Custom domain ${customDomain} does not exist in ${appName}`);
|
|
}
|
|
app.customDomain = newDomains;
|
|
return self.saveApp(appName, app);
|
|
});
|
|
}
|
|
addCustomDomainForApp(appName, customDomain) {
|
|
const self = this;
|
|
return this.getAppDefinition(appName).then(function (app) {
|
|
app.customDomain = app.customDomain || [];
|
|
if (app.customDomain.length > 0) {
|
|
for (let idx = 0; idx < app.customDomain.length; idx++) {
|
|
if (app.customDomain[idx].publicDomain === customDomain) {
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.ILLEGAL_PARAMETER, `App already has customDomain: ${customDomain} attached to app ${appName}`);
|
|
}
|
|
}
|
|
}
|
|
app.customDomain.push({
|
|
publicDomain: customDomain,
|
|
hasSsl: false,
|
|
});
|
|
return self.saveApp(appName, app);
|
|
});
|
|
}
|
|
verifyCustomDomainBelongsToApp(appName, customDomain) {
|
|
const self = this;
|
|
return self.getAppDefinition(appName).then(function (app) {
|
|
app.customDomain = app.customDomain || [];
|
|
if (app.customDomain.length > 0) {
|
|
for (let idx = 0; idx < app.customDomain.length; idx++) {
|
|
if (app.customDomain[idx].publicDomain === customDomain) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
throw ApiStatusCodes.createError(ApiStatusCodes.ILLEGAL_PARAMETER, `customDomain ${customDomain} is not attached to app ${appName}`);
|
|
});
|
|
}
|
|
setDeployedVersionAndImage(appName, deployedVersion, imageName) {
|
|
if (!appName) {
|
|
throw new Error('App Name should not be empty');
|
|
}
|
|
if (!imageName) {
|
|
throw new Error('ImageName Name should not be empty');
|
|
}
|
|
const self = this;
|
|
return this.getAppDefinition(appName) //
|
|
.then(function (app) {
|
|
const versions = app.versions;
|
|
let found = false;
|
|
for (let i = 0; i < versions.length; i++) {
|
|
const element = versions[i];
|
|
if (element.version === deployedVersion) {
|
|
element.deployedImageName = imageName;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
throw new Error(`Version trying to deploy not found ${deployedVersion}`);
|
|
}
|
|
app.deployedVersion = deployedVersion;
|
|
return self.saveApp(appName, app);
|
|
});
|
|
}
|
|
createNewVersion(appName) {
|
|
if (!appName) {
|
|
throw new Error('App Name should not be empty');
|
|
}
|
|
const self = this;
|
|
return this.getAppDefinition(appName).then(function (app) {
|
|
const versions = app.versions;
|
|
const newVersionIndex = versions.length;
|
|
versions.push({
|
|
version: newVersionIndex,
|
|
gitHash: undefined,
|
|
timeStamp: new Date().toString(),
|
|
});
|
|
return self.saveApp(appName, app).then(function () {
|
|
return newVersionIndex;
|
|
});
|
|
});
|
|
}
|
|
updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, forceSsl, ports, repoInfo, authenticator, customNginxConfig, preDeployFunction) {
|
|
const self = this;
|
|
let appObj;
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return self.getAppDefinition(appName);
|
|
})
|
|
.then(function (appLoaded) {
|
|
appObj = appLoaded;
|
|
if (repoInfo &&
|
|
repoInfo.repo &&
|
|
repoInfo.branch &&
|
|
repoInfo.user &&
|
|
repoInfo.password) {
|
|
appObj.appPushWebhook = {
|
|
tokenVersion: appObj.appPushWebhook &&
|
|
appObj.appPushWebhook.tokenVersion
|
|
? appObj.appPushWebhook.tokenVersion
|
|
: uuid(),
|
|
pushWebhookToken: appObj.appPushWebhook
|
|
? appObj.appPushWebhook.pushWebhookToken
|
|
: '',
|
|
repoInfo: {
|
|
repo: repoInfo.repo,
|
|
user: repoInfo.user,
|
|
branch: repoInfo.branch,
|
|
password: repoInfo.password,
|
|
},
|
|
};
|
|
if (appObj.appPushWebhook.pushWebhookToken) {
|
|
return Promise.resolve(undefined);
|
|
}
|
|
return authenticator
|
|
.getAppPushWebhookToken(appName, appObj.appPushWebhook.tokenVersion)
|
|
.then(function (val) {
|
|
appObj.appPushWebhook.pushWebhookToken = val;
|
|
});
|
|
}
|
|
else {
|
|
appObj.appPushWebhook = undefined;
|
|
return Promise.resolve(undefined);
|
|
}
|
|
})
|
|
.then(function () {
|
|
instanceCount = Number(instanceCount);
|
|
if (instanceCount >= 0) {
|
|
appObj.instanceCount = instanceCount;
|
|
}
|
|
appObj.notExposeAsWebApp = !!notExposeAsWebApp;
|
|
appObj.forceSsl = !!forceSsl;
|
|
appObj.nodeId = nodeId;
|
|
appObj.customNginxConfig = customNginxConfig;
|
|
appObj.preDeployFunction = preDeployFunction;
|
|
if (ports) {
|
|
appObj.ports = [];
|
|
for (let i = 0; i < ports.length; i++) {
|
|
const obj = ports[i];
|
|
const containerPort = Number(obj.containerPort);
|
|
const hostPort = Number(obj.hostPort);
|
|
appObj.ports.push({
|
|
hostPort: hostPort,
|
|
containerPort: containerPort,
|
|
});
|
|
}
|
|
}
|
|
if (envVars) {
|
|
appObj.envVars = [];
|
|
for (let i = 0; i < envVars.length; i++) {
|
|
const obj = envVars[i];
|
|
if (obj.key && obj.value) {
|
|
appObj.envVars.push({
|
|
key: obj.key,
|
|
value: obj.value,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (volumes) {
|
|
appObj.volumes = [];
|
|
for (let i = 0; i < volumes.length; i++) {
|
|
const obj = volumes[i];
|
|
const newVol = {
|
|
containerPath: obj.containerPath,
|
|
};
|
|
if (obj.hostPath) {
|
|
newVol.hostPath = obj.hostPath;
|
|
newVol.type = 'bind';
|
|
}
|
|
else {
|
|
newVol.volumeName = obj.volumeName;
|
|
newVol.type = 'volume';
|
|
}
|
|
appObj.volumes.push(newVol);
|
|
}
|
|
}
|
|
})
|
|
.then(function () {
|
|
return self.saveApp(appName, appObj);
|
|
});
|
|
}
|
|
deleteAppDefinition(appName) {
|
|
const self = this;
|
|
return new Promise(function (resolve, reject) {
|
|
if (!isNameAllowed(appName)) {
|
|
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_BAD_NAME, 'App Name is not allow. Only lowercase letters and single hyphen is allow'));
|
|
return;
|
|
}
|
|
if (!self.data.get(APP_DEFINITIONS + '.' + appName)) {
|
|
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'App Name does not exist in Database! Cannot be deleted.'));
|
|
return;
|
|
}
|
|
self.data.delete(APP_DEFINITIONS + '.' + appName);
|
|
resolve();
|
|
});
|
|
}
|
|
/**
|
|
* Creates a new app definition.
|
|
*
|
|
* @param appName The appName you want to register
|
|
* @param hasPersistentData whether the app has persistent data, you can only run one instance of the app.
|
|
* @returns {Promise}
|
|
*/
|
|
registerAppDefinition(appName, hasPersistentData) {
|
|
const self = this;
|
|
return new Promise(function (resolve, reject) {
|
|
if (!isNameAllowed(appName)) {
|
|
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_BAD_NAME, 'App Name is not allow. Only lowercase letters and single hyphen is allow'));
|
|
return;
|
|
}
|
|
if (!!self.data.get(APP_DEFINITIONS + '.' + appName)) {
|
|
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_ALREADY_EXIST, 'App Name already exists. Please use a different name'));
|
|
return;
|
|
}
|
|
const defaultAppDefinition = {
|
|
hasPersistentData: !!hasPersistentData,
|
|
instanceCount: 1,
|
|
networks: [CaptainConstants.captainNetworkName],
|
|
envVars: [],
|
|
volumes: [],
|
|
ports: [],
|
|
versions: [],
|
|
deployedVersion: 0,
|
|
notExposeAsWebApp: false,
|
|
customDomain: [],
|
|
hasDefaultSubDomainSsl: false,
|
|
forceSsl: false,
|
|
};
|
|
resolve(defaultAppDefinition);
|
|
}).then(function (app) {
|
|
return self.saveApp(appName, app);
|
|
});
|
|
}
|
|
getAppsServerConfig(defaultAppNginxConfig, hasRootSsl, rootDomain) {
|
|
const self = this;
|
|
const servers = [];
|
|
return self.getAppDefinitions().then(function (apps) {
|
|
Object.keys(apps).forEach(function (appName) {
|
|
const webApp = apps[appName];
|
|
if (webApp.notExposeAsWebApp) {
|
|
return;
|
|
}
|
|
const localDomain = self.getServiceName(appName);
|
|
const forceSsl = !!webApp.forceSsl;
|
|
const nginxConfigTemplate = webApp.customNginxConfig || defaultAppNginxConfig;
|
|
const serverWithSubDomain = {};
|
|
serverWithSubDomain.hasSsl =
|
|
hasRootSsl && webApp.hasDefaultSubDomainSsl;
|
|
serverWithSubDomain.publicDomain = appName + '.' + rootDomain;
|
|
serverWithSubDomain.localDomain = localDomain;
|
|
serverWithSubDomain.forceSsl = forceSsl;
|
|
serverWithSubDomain.nginxConfigTemplate = nginxConfigTemplate;
|
|
servers.push(serverWithSubDomain);
|
|
// adding custom domains
|
|
const customDomainArray = webApp.customDomain;
|
|
if (customDomainArray && customDomainArray.length > 0) {
|
|
for (let idx = 0; idx < customDomainArray.length; idx++) {
|
|
const d = customDomainArray[idx];
|
|
servers.push({
|
|
hasSsl: d.hasSsl,
|
|
forceSsl: forceSsl,
|
|
publicDomain: d.publicDomain,
|
|
localDomain: localDomain,
|
|
nginxConfigTemplate: nginxConfigTemplate,
|
|
staticWebRoot: '',
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return servers;
|
|
});
|
|
}
|
|
}
|
|
module.exports = AppsDataStore;
|
|
//# sourceMappingURL=AppsDataStore.js.map
|