mirror of
https://github.com/caprover/caprover
synced 2026-07-27 02:41:30 +00:00
873 lines
31 KiB
TypeScript
873 lines
31 KiB
TypeScript
import { v4 as uuid } from 'uuid'
|
|
import ApiStatusCodes from '../api/ApiStatusCodes'
|
|
import { IBuiltImage } from '../models/IBuiltImage'
|
|
import Authenticator from '../user/Authenticator'
|
|
import ApacheMd5 from '../utils/ApacheMd5'
|
|
import CaptainConstants from '../utils/CaptainConstants'
|
|
import CaptainEncryptor from '../utils/Encryptor'
|
|
import Utils from '../utils/Utils'
|
|
import configstore = require('configstore')
|
|
|
|
const isValidPath = require('is-valid-path')
|
|
|
|
const APP_DEFINITIONS = 'appDefinitions'
|
|
|
|
function isNameAllowed(name: string) {
|
|
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: number) {
|
|
return portNumber > 0 && portNumber < 65535
|
|
}
|
|
|
|
class AppsDataStore {
|
|
private encryptor: CaptainEncryptor
|
|
|
|
constructor(private data: configstore, private namepace: string) {}
|
|
|
|
setEncryptor(encryptor: CaptainEncryptor) {
|
|
this.encryptor = encryptor
|
|
}
|
|
|
|
private saveApp(appName: String, app: IAppDef) {
|
|
const self = this
|
|
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
if (!appName) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
'App Name should not be empty'
|
|
)
|
|
}
|
|
|
|
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.envVars) {
|
|
for (let i = 0; i < app.envVars.length; i++) {
|
|
const element = app.envVars[i]
|
|
if (!element.key) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
'Environmental Variable key is empty!'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
`Invalid ports: ${hostPort} or ${containerPort}`
|
|
)
|
|
}
|
|
} else {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
`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 ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
'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) {
|
|
if (!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 () {
|
|
let passwordToBeEncrypted = ''
|
|
let sshKeyToBeEncrypted = ''
|
|
let pushWebhook = app.appPushWebhook
|
|
if (
|
|
pushWebhook &&
|
|
pushWebhook.pushWebhookToken &&
|
|
pushWebhook.tokenVersion &&
|
|
pushWebhook.repoInfo &&
|
|
pushWebhook.repoInfo.repo
|
|
) {
|
|
// we have required info
|
|
passwordToBeEncrypted = pushWebhook.repoInfo.password
|
|
sshKeyToBeEncrypted = pushWebhook.repoInfo.sshKey || ''
|
|
pushWebhook.repoInfo.password = ''
|
|
pushWebhook.repoInfo.sshKey = ''
|
|
} else {
|
|
// some required data is missing. We drop the push data
|
|
pushWebhook = undefined
|
|
}
|
|
|
|
const appToSave: IAppDefSaved = <IAppDefSaved>app
|
|
|
|
if (passwordToBeEncrypted) {
|
|
appToSave.appPushWebhook!.repoInfo!.passwordEncrypted = self.encryptor.encrypt(
|
|
passwordToBeEncrypted
|
|
)
|
|
}
|
|
|
|
if (sshKeyToBeEncrypted) {
|
|
appToSave.appPushWebhook!.repoInfo!.sshKeyEncrypted = self.encryptor.encrypt(
|
|
sshKeyToBeEncrypted
|
|
)
|
|
}
|
|
|
|
return appToSave
|
|
})
|
|
.then(function (appToSave: IAppDefSaved) {
|
|
self.data.set(`${APP_DEFINITIONS}.${appName}`, appToSave)
|
|
})
|
|
}
|
|
|
|
nameAllowedOrThrow(appName: string) {
|
|
if (!isNameAllowed(appName)) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_BAD_NAME,
|
|
'App Name is not allowed. Only lowercase letters and single hyphens are allowed'
|
|
)
|
|
}
|
|
|
|
if (!!this.data.get(`${APP_DEFINITIONS}.${appName}`)) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_ALREADY_EXIST,
|
|
'App Name already exists. Please use a different name'
|
|
)
|
|
}
|
|
}
|
|
|
|
renameApp(
|
|
authenticator: Authenticator,
|
|
oldAppName: string,
|
|
newAppName: string
|
|
) {
|
|
const self = this
|
|
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
self.nameAllowedOrThrow(newAppName)
|
|
return self.getAppDefinition(oldAppName)
|
|
})
|
|
.then(function (appData) {
|
|
if (
|
|
appData.appPushWebhook &&
|
|
appData.appPushWebhook.pushWebhookToken
|
|
) {
|
|
const tokenVersion = uuid()
|
|
return authenticator
|
|
.getAppPushWebhookToken(newAppName, tokenVersion)
|
|
.then((val) => {
|
|
appData.appPushWebhook!.pushWebhookToken = val
|
|
appData.appPushWebhook!.tokenVersion = tokenVersion
|
|
return appData
|
|
})
|
|
}
|
|
|
|
return appData
|
|
})
|
|
.then(function (appData) {
|
|
if (appData.appName) appData.appName = newAppName
|
|
appData.hasDefaultSubDomainSsl = false
|
|
return self.saveApp(newAppName, appData)
|
|
})
|
|
.then(function () {
|
|
self.data.delete(`${APP_DEFINITIONS}.${oldAppName}`)
|
|
return Utils.getDelayedPromise(2000)
|
|
})
|
|
}
|
|
|
|
getServiceName(appName: string) {
|
|
return `srv-${this.namepace}--${appName}`
|
|
}
|
|
|
|
getVolumeName(volumeName: string) {
|
|
return `${this.namepace}--${volumeName}`
|
|
}
|
|
|
|
getAppDefinitions() {
|
|
const self = this
|
|
return new Promise<IAllAppDefinitions>(function (resolve, reject) {
|
|
let allApps = self.data.get(APP_DEFINITIONS) || {}
|
|
let allAppsUnencrypted: IAllAppDefinitions = {}
|
|
|
|
Object.keys(allApps).forEach(function (appName) {
|
|
allAppsUnencrypted[appName] = allApps[appName]
|
|
const appUnencrypted = allAppsUnencrypted[appName]
|
|
|
|
// captainDefinitionFilePath added in v1.2.0, we need to backfill if it doesn't exists.
|
|
appUnencrypted.captainDefinitionRelativeFilePath =
|
|
appUnencrypted.captainDefinitionRelativeFilePath ||
|
|
CaptainConstants.defaultCaptainDefinitionPath
|
|
|
|
const appSave = allApps[appName] as IAppDefSaved
|
|
|
|
if (
|
|
appSave.appPushWebhook &&
|
|
appSave.appPushWebhook.repoInfo &&
|
|
(appSave.appPushWebhook.repoInfo.passwordEncrypted ||
|
|
appSave.appPushWebhook.repoInfo.sshKeyEncrypted)
|
|
) {
|
|
const repo = appSave.appPushWebhook!.repoInfo
|
|
appUnencrypted.appPushWebhook = {
|
|
tokenVersion: appSave.appPushWebhook.tokenVersion,
|
|
pushWebhookToken:
|
|
appSave.appPushWebhook.pushWebhookToken,
|
|
repoInfo: {
|
|
repo: repo.repo,
|
|
user: repo.user,
|
|
password: repo.passwordEncrypted
|
|
? self.encryptor.decrypt(repo.passwordEncrypted)
|
|
: '',
|
|
sshKey: repo.sshKeyEncrypted
|
|
? self.encryptor.decrypt(repo.sshKeyEncrypted)
|
|
: '',
|
|
branch: repo.branch,
|
|
},
|
|
}
|
|
}
|
|
})
|
|
resolve(JSON.parse(JSON.stringify(allAppsUnencrypted)))
|
|
})
|
|
}
|
|
|
|
getAppDefinition(appName: string) {
|
|
return this.getAppDefinitions().then(function (allApps) {
|
|
if (!appName) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
'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
|
|
})
|
|
}
|
|
|
|
setSslForDefaultSubDomain(appName: string, isEnabled: boolean) {
|
|
const self = this
|
|
|
|
return this.getAppDefinition(appName).then(function (app) {
|
|
app.hasDefaultSubDomainSsl = !!isEnabled
|
|
return self.saveApp(appName, app)
|
|
})
|
|
}
|
|
|
|
ensureAllAppsSubDomainSslDisabled() {
|
|
const self = this
|
|
|
|
return this.getAppDefinitions().then(function (appDefinitions) {
|
|
const promises: (() => Promise<void>)[] = []
|
|
Object.keys(appDefinitions).forEach((appName) => {
|
|
const APP_NAME = appName
|
|
promises.push(function () {
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return self.getAppDefinition(APP_NAME)
|
|
})
|
|
.then(function (app) {
|
|
app.forceSsl = false
|
|
return self.saveApp(APP_NAME, app)
|
|
})
|
|
.then(function () {
|
|
return self.setSslForDefaultSubDomain(
|
|
APP_NAME,
|
|
false
|
|
)
|
|
})
|
|
})
|
|
})
|
|
return Utils.runPromises(promises)
|
|
})
|
|
}
|
|
|
|
enableCustomDomainSsl(appName: string, customDomain: string) {
|
|
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 ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
`customDomain: ${customDomain} is not attached to app ${appName}`
|
|
)
|
|
})
|
|
}
|
|
|
|
removeCustomDomainForApp(appName: string, customDomain: string) {
|
|
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: string, customDomain: string) {
|
|
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)
|
|
})
|
|
}
|
|
|
|
addCustomDomainForAppForMigration(
|
|
appName: string,
|
|
hasDefaultSubDomainSsl: boolean,
|
|
customDomains: any[]
|
|
) {
|
|
const self = this
|
|
|
|
return this.getAppDefinition(appName) //
|
|
.then(function (app) {
|
|
app.customDomain = app.customDomain || []
|
|
|
|
for (let idx = 0; idx < customDomains.length; idx++) {
|
|
app.customDomain.push({
|
|
publicDomain: customDomains[idx].publicDomain + '',
|
|
hasSsl: !!customDomains[idx].hasSsl,
|
|
})
|
|
}
|
|
|
|
app.hasDefaultSubDomainSsl = !!hasDefaultSubDomainSsl
|
|
|
|
return self.saveApp(appName, app)
|
|
})
|
|
}
|
|
|
|
verifyCustomDomainBelongsToApp(appName: string, customDomain: string) {
|
|
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: string,
|
|
deployedVersion: number,
|
|
builtImage: IBuiltImage
|
|
) {
|
|
if (!appName) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
'App Name should not be empty'
|
|
)
|
|
}
|
|
|
|
if (!builtImage || !builtImage.imageName) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
'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 = builtImage.imageName
|
|
element.gitHash = builtImage.gitHash
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
`Version trying to deploy not found ${deployedVersion}`
|
|
)
|
|
}
|
|
|
|
app.deployedVersion = deployedVersion
|
|
|
|
return self.saveApp(appName, app)
|
|
})
|
|
}
|
|
|
|
createNewVersion(appName: string) {
|
|
if (!appName) {
|
|
throw ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
|
'App Name should not be empty'
|
|
)
|
|
}
|
|
const self = this
|
|
|
|
return this.getAppDefinition(appName).then(function (app) {
|
|
// Drop older versions
|
|
app.versions = Utils.dropFirstElements(
|
|
app.versions,
|
|
CaptainConstants.configs.maxVersionHistory - 1
|
|
)
|
|
|
|
const versions = app.versions
|
|
|
|
let newVersionIndex = versions.length
|
|
|
|
// Just in case some versions from the db were deleted manually!!!
|
|
for (let index = 0; index < versions.length; index++) {
|
|
const element = versions[index]
|
|
if (newVersionIndex <= element.version) {
|
|
newVersionIndex = element.version + 1
|
|
}
|
|
|
|
// In Ver <= v1.1.0, timeStamp was set to new Date().toString() which creates an inconsistent date format.
|
|
element.timeStamp = element.timeStamp
|
|
? new Date(element.timeStamp + '').toISOString()
|
|
: new Date().toISOString()
|
|
}
|
|
|
|
versions.push({
|
|
version: newVersionIndex,
|
|
gitHash: undefined,
|
|
timeStamp: new Date().toISOString(),
|
|
})
|
|
|
|
return self.saveApp(appName, app).then(function () {
|
|
return newVersionIndex
|
|
})
|
|
})
|
|
}
|
|
|
|
setVersionsForMigration(
|
|
appName: string,
|
|
vers: IAppVersion[],
|
|
deployedVersion: number
|
|
) {
|
|
const self = this
|
|
return Promise.resolve() //
|
|
.then(function () {
|
|
return self.getAppDefinition(appName)
|
|
})
|
|
.then(function (appLoaded) {
|
|
appLoaded.deployedVersion = deployedVersion
|
|
appLoaded.versions = vers
|
|
return self.saveApp(appName, appLoaded)
|
|
})
|
|
}
|
|
|
|
updateAppDefinitionInDb(
|
|
appName: string,
|
|
description: string,
|
|
instanceCount: number,
|
|
captainDefinitionRelativeFilePath: string,
|
|
envVars: IAppEnvVar[],
|
|
volumes: IAppVolume[],
|
|
nodeId: string,
|
|
notExposeAsWebApp: boolean,
|
|
containerHttpPort: number,
|
|
httpAuth: IHttpAuth | undefined,
|
|
forceSsl: boolean,
|
|
ports: IAppPort[],
|
|
repoInfo: RepoInfo,
|
|
authenticator: Authenticator,
|
|
customNginxConfig: string,
|
|
preDeployFunction: string,
|
|
serviceUpdateOverride: string,
|
|
websocketSupport: boolean
|
|
) {
|
|
const self = this
|
|
let appObj: IAppDef
|
|
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return self.getAppDefinition(appName)
|
|
})
|
|
.then(function (appLoaded) {
|
|
appObj = appLoaded
|
|
|
|
if (
|
|
repoInfo &&
|
|
repoInfo.repo &&
|
|
repoInfo.branch &&
|
|
((repoInfo.user && repoInfo.password) || repoInfo.sshKey)
|
|
) {
|
|
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,
|
|
sshKey: repoInfo.sshKey,
|
|
},
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if (captainDefinitionRelativeFilePath) {
|
|
appObj.captainDefinitionRelativeFilePath =
|
|
captainDefinitionRelativeFilePath + ''
|
|
}
|
|
|
|
appObj.notExposeAsWebApp = !!notExposeAsWebApp
|
|
appObj.containerHttpPort = containerHttpPort
|
|
appObj.forceSsl = !!forceSsl
|
|
appObj.websocketSupport = !!websocketSupport
|
|
appObj.nodeId = nodeId
|
|
appObj.customNginxConfig = customNginxConfig
|
|
appObj.preDeployFunction = preDeployFunction
|
|
appObj.serviceUpdateOverride = serviceUpdateOverride
|
|
appObj.description = description
|
|
|
|
if (httpAuth && httpAuth.user) {
|
|
const newAuth: IHttpAuth = {
|
|
user: httpAuth.user + '',
|
|
passwordHashed: httpAuth.passwordHashed + '',
|
|
}
|
|
|
|
if (httpAuth.password) {
|
|
newAuth.passwordHashed = ApacheMd5.createApacheHash(
|
|
httpAuth.password + ''
|
|
)
|
|
}
|
|
|
|
appObj.httpAuth = newAuth
|
|
} else {
|
|
appObj.httpAuth = undefined
|
|
}
|
|
|
|
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)
|
|
|
|
if (!containerPort && !hostPort) {
|
|
// Empty entry... Skipping...
|
|
continue
|
|
}
|
|
|
|
appObj.ports.push({
|
|
hostPort: hostPort,
|
|
containerPort: containerPort,
|
|
})
|
|
}
|
|
}
|
|
|
|
if (envVars) {
|
|
appObj.envVars = []
|
|
for (let i = 0; i < envVars.length; i++) {
|
|
const obj = envVars[i]
|
|
obj.key = (obj.key || '').trim()
|
|
obj.value = obj.value || ''
|
|
|
|
if (!obj.key && !obj.value) {
|
|
// Empty entry... Skipping...
|
|
continue
|
|
}
|
|
|
|
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 || '').trim(),
|
|
} as IAppVolume
|
|
|
|
if (obj.hostPath) {
|
|
newVol.hostPath = (obj.hostPath || '').trim()
|
|
} else {
|
|
newVol.volumeName = (obj.volumeName || '').trim()
|
|
}
|
|
|
|
if (
|
|
!newVol.containerPath &&
|
|
!newVol.hostPath &&
|
|
!newVol.volumeName
|
|
) {
|
|
// Empty entry... Skipping...
|
|
continue
|
|
}
|
|
|
|
appObj.volumes.push(newVol)
|
|
}
|
|
}
|
|
})
|
|
.then(function () {
|
|
return self.saveApp(appName, appObj)
|
|
})
|
|
}
|
|
|
|
deleteAppDefinition(appName: string) {
|
|
const self = this
|
|
|
|
return new Promise<void>(function (resolve, reject) {
|
|
if (!isNameAllowed(appName)) {
|
|
reject(
|
|
ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_BAD_NAME,
|
|
'App Name is not allowed. Only lowercase letters and single hyphens are allowed'
|
|
)
|
|
)
|
|
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()
|
|
}).then(function () {
|
|
return Utils.getDelayedPromise(2000)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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: string, hasPersistentData: boolean) {
|
|
const self = this
|
|
|
|
return new Promise<IAppDef>(function (resolve, reject) {
|
|
if (!isNameAllowed(appName)) {
|
|
reject(
|
|
ApiStatusCodes.createError(
|
|
ApiStatusCodes.STATUS_ERROR_BAD_NAME,
|
|
'App Name is not allowed. Only lowercase letters and single hyphens are allowed'
|
|
)
|
|
)
|
|
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: IAppDef = {
|
|
hasPersistentData: !!hasPersistentData,
|
|
description: '',
|
|
instanceCount: 1,
|
|
captainDefinitionRelativeFilePath:
|
|
CaptainConstants.defaultCaptainDefinitionPath,
|
|
networks: [CaptainConstants.captainNetworkName],
|
|
envVars: [],
|
|
volumes: [],
|
|
ports: [],
|
|
versions: [],
|
|
deployedVersion: 0,
|
|
notExposeAsWebApp: false,
|
|
customDomain: [],
|
|
hasDefaultSubDomainSsl: false,
|
|
forceSsl: false,
|
|
websocketSupport: false,
|
|
}
|
|
|
|
resolve(defaultAppDefinition)
|
|
}).then(function (app) {
|
|
return self.saveApp(appName, app)
|
|
})
|
|
}
|
|
}
|
|
|
|
export default AppsDataStore
|