From c33f858dd0dbe9ce861090f617f7bcf4434b13b7 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 17 Aug 2025 19:53:09 -0700 Subject: [PATCH 01/20] Updated getServiceName --- src/datastore/AppsDataStore.ts | 8 +++-- src/models/AppDefinition.ts | 4 +++ src/user/ServiceManager.ts | 41 ++++++++++++++++---------- src/user/system/LoadBalancerManager.ts | 2 +- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/datastore/AppsDataStore.ts b/src/datastore/AppsDataStore.ts index dd4bf37..ff8725c 100644 --- a/src/datastore/AppsDataStore.ts +++ b/src/datastore/AppsDataStore.ts @@ -269,8 +269,12 @@ class AppsDataStore { }) } - getServiceName(appName: string) { - return `srv-${this.namepace}--${appName}` + getServiceName(appName: string, isLegacyAppName: boolean) { + if (isLegacyAppName) { + return `srv-${this.namepace}--${appName}` + } + + return `${appName}` } getVolumeName(volumeName: string) { diff --git a/src/models/AppDefinition.ts b/src/models/AppDefinition.ts index 332059c..49322e4 100644 --- a/src/models/AppDefinition.ts +++ b/src/models/AppDefinition.ts @@ -84,6 +84,10 @@ export interface IAppDefinitionBase { envVars: IAppEnvVar[] versions: IAppVersion[] appDeployTokenConfig?: AppDeployTokenConfig + + // True for apps created before v1.15.0 + // non-existent for apps created on or after v1.15.0 + isLegacyAppName?: boolean } export interface IHttpAuth { diff --git a/src/user/ServiceManager.ts b/src/user/ServiceManager.ts index 569b1e3..b5acd78 100644 --- a/src/user/ServiceManager.ts +++ b/src/user/ServiceManager.ts @@ -423,13 +423,11 @@ class ServiceManager { Logger.d(`Renaming app: ${oldAppName}`) const self = this - const oldServiceName = this.dataStore - .getAppsDataStore() - .getServiceName(oldAppName) const dockerApi = this.dockerApi const dataStore = this.dataStore let defaultSslOn = false + let oldServiceName: string return Promise.resolve() .then(function () { @@ -437,6 +435,9 @@ class ServiceManager { }) .then(function (appDef) { defaultSslOn = !!appDef.hasDefaultSubDomainSsl + oldServiceName = dataStore + .getAppsDataStore() + .getServiceName(oldAppName, !!appDef.isLegacyAppName) dataStore.getAppsDataStore().nameAllowedOrThrow(newAppName) @@ -473,14 +474,20 @@ class ServiceManager { const self = this const removeAppPromise = function (appName: string) { - const serviceName = self.dataStore - .getAppsDataStore() - .getServiceName(appName) const dockerApi = self.dockerApi const dataStore = self.dataStore + let serviceName: string return Promise.resolve() .then(function () { + return dataStore + .getAppsDataStore() + .getAppDefinition(appName) + }) + .then(function (appDef) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!appDef.isLegacyAppName) return self.ensureNotBuilding(appName) }) .then(function () { @@ -664,7 +671,7 @@ class ServiceManager { .then(function (app) { serviceName = dataStore .getAppsDataStore() - .getServiceName(appName) + .getServiceName(appName, !!app.isLegacyAppName) // After leaving this block, nodeId will be guaranteed to be NonNull if (app.hasPersistentData) { @@ -869,14 +876,18 @@ class ServiceManager { } getAppLogs(appName: string, encoding: string) { - const serviceName = this.dataStore - .getAppsDataStore() - .getServiceName(appName) - const dockerApi = this.dockerApi + const dataStore = this.dataStore + let serviceName: string return Promise.resolve() // .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName) + }) + .then(function (appDef) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!appDef.isLegacyAppName) return dockerApi.getLogForService( serviceName, CaptainConstants.configs.appLogSize, @@ -889,15 +900,12 @@ class ServiceManager { Logger.d(`Ensure service inited and Updated for: ${appName}`) const self = this - const serviceName = this.dataStore - .getAppsDataStore() - .getServiceName(appName) - let imageName: string | undefined const dockerApi = this.dockerApi const dataStore = this.dataStore let app: IAppDef let dockerAuthObject: DockerAuthObj | undefined + let serviceName: string return Promise.resolve() // .then(function () { @@ -905,6 +913,9 @@ class ServiceManager { }) .then(function (appFound) { app = appFound + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!app.isLegacyAppName) Logger.d(`Check if service is running: ${serviceName}`) return dockerApi.isServiceRunningByName(serviceName) diff --git a/src/user/system/LoadBalancerManager.ts b/src/user/system/LoadBalancerManager.ts index bb596ef..70ea1ad 100644 --- a/src/user/system/LoadBalancerManager.ts +++ b/src/user/system/LoadBalancerManager.ts @@ -308,7 +308,7 @@ class LoadBalancerManager { const localDomain = dataStore .getAppsDataStore() - .getServiceName(appName) + .getServiceName(appName, !!webApp.isLegacyAppName) const forceSsl = !!webApp.forceSsl const websocketSupport = !!webApp.websocketSupport const nginxConfigTemplate = From ea885fc405568f6ecacbf689fc3b367bd00626c7 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 17 Aug 2025 21:32:44 -0700 Subject: [PATCH 02/20] Added migration --- src/datastore/DataStore.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/datastore/DataStore.ts b/src/datastore/DataStore.ts index 96efef8..5a5487b 100644 --- a/src/datastore/DataStore.ts +++ b/src/datastore/DataStore.ts @@ -3,6 +3,7 @@ */ import Configstore = require('configstore') import fs = require('fs-extra') +import { IAppDefSaved } from '../models/AppDefinition' import { AutomatedCleanupConfigsCleaner, IAutomatedCleanupConfigs, @@ -77,6 +78,20 @@ class DataStore { } ) + // running migrations + if (data.get('schemaVersion') < 2) { + const appDefinitions = data.get('appDefinitions') + if (appDefinitions) { + Object.keys(appDefinitions).forEach((appName) => { + const appDef = appDefinitions[appName] as IAppDefSaved + appDef.isLegacyAppName = true + }) + } + + data.set('appDefinitions', appDefinitions) + data.set('schemaVersion', 2) + } + this.data = data this.namespace = namespace this.data.set(NAMESPACE, namespace) From 80e1a601fe853c2c9819fb8f1480f80988222ae3 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Wed, 20 Aug 2025 23:09:57 -0700 Subject: [PATCH 03/20] Added volume legacy logic --- src/datastore/AppsDataStore.ts | 4 ---- src/docker/DockerApi.ts | 5 ++-- .../apps/appdefinition/AppDefinitionRouter.ts | 24 ++++++++++++++++++- src/user/ServiceManager.ts | 4 ++-- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/datastore/AppsDataStore.ts b/src/datastore/AppsDataStore.ts index ff8725c..ef58fbd 100644 --- a/src/datastore/AppsDataStore.ts +++ b/src/datastore/AppsDataStore.ts @@ -277,10 +277,6 @@ class AppsDataStore { return `${appName}` } - getVolumeName(volumeName: string) { - return `${this.namepace}--${volumeName}` - } - getAppDefinitions() { const self = this return new Promise(function (resolve, reject) { diff --git a/src/docker/DockerApi.ts b/src/docker/DockerApi.ts index c3ec5d4..0ac9d8c 100644 --- a/src/docker/DockerApi.ts +++ b/src/docker/DockerApi.ts @@ -1438,8 +1438,9 @@ class DockerApi { // /var/lib/docker/volumes/YOUR_VOLUME_NAME/_data mts.push({ Source: - (namespace ? namespace + '--' : '') + - v.volumeName, + (namespace && appObject?.isLegacyAppName + ? namespace + '--' + : '') + v.volumeName, Target: v.containerPath, Type: VolumesTypes.VOLUME, ReadOnly: false, diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 6cd0650..b6fc2ca 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -3,6 +3,7 @@ import ApiStatusCodes from '../../../../api/ApiStatusCodes' import BaseApi from '../../../../api/BaseApi' import InjectionExtractor from '../../../../injection/InjectionExtractor' import { AppDeployTokenConfig, IAppDef } from '../../../../models/AppDefinition' +import { IHashMapGeneric } from '../../../../models/ICacheGeneric' import { ICaptainDefinition } from '../../../../models/ICaptainDefinition' import { CaptainError } from '../../../../models/OtherTypes' import CaptainManager from '../../../../user/system/CaptainManager' @@ -260,6 +261,8 @@ router.post('/register/', function (req, res, next) { router.post('/delete/', function (req, res, next) { const serviceManager = InjectionExtractor.extractUserFromInjected(res).user.serviceManager + const dataStore = + InjectionExtractor.extractUserFromInjected(res).user.dataStore const appName: string = req.body.appName const volumes: string[] = req.body.volumes || [] @@ -267,6 +270,7 @@ router.post('/delete/', function (req, res, next) { const appsToDelete: string[] = appNames.length ? appNames : [appName] Logger.d(`Deleting app started: ${appName}`) + const legacyVolumes: IHashMapGeneric = {} return Promise.resolve() .then(function () { @@ -277,6 +281,24 @@ router.post('/delete/', function (req, res, next) { ) } }) + .then(function () { + return dataStore.getAppsDataStore().getAppDefinitions() + }) + .then(function (apps) { + Object.keys(apps).forEach((appName) => { + const app = apps[appName] + if (app.isLegacyAppName) { + const volumesForApp = app.volumes.map( + (vol) => vol.volumeName + ) + volumesForApp.forEach((volumeName) => { + if (volumeName) { + legacyVolumes[volumeName] = true + } + }) + } + }) + }) .then(function () { return serviceManager.removeApps(appsToDelete) }) @@ -284,7 +306,7 @@ router.post('/delete/', function (req, res, next) { return Utils.getDelayedPromise(volumes.length ? 12000 : 0) }) .then(function () { - return serviceManager.removeVolsSafe(volumes) + return serviceManager.removeVolsSafe(volumes, legacyVolumes) }) .then(function (failedVolsToRemoved) { Logger.d(`Successfully deleted: ${appsToDelete.join(', ')}`) diff --git a/src/user/ServiceManager.ts b/src/user/ServiceManager.ts index b5acd78..80b0dab 100644 --- a/src/user/ServiceManager.ts +++ b/src/user/ServiceManager.ts @@ -522,7 +522,7 @@ class ServiceManager { return Promise.all(promises) } - removeVolsSafe(volumes: string[]) { + removeVolsSafe(volumes: string[], legacyVolumes: IHashMapGeneric) { const dockerApi = this.dockerApi const dataStore = this.dataStore @@ -552,7 +552,7 @@ class ServiceManager { volumes.forEach((v) => { if (!volsFailedToDelete[v]) { volumesTryToDelete.push( - dataStore.getAppsDataStore().getVolumeName(v) + legacyVolumes[v] ? `captain--${v}` : v ) } }) From 7c7d2b33e20b22f3e5a17b158b5edac1826db927 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:27:18 -0700 Subject: [PATCH 04/20] Harden service and volume naming migration --- src/datastore/DataStore.ts | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/datastore/DataStore.ts b/src/datastore/DataStore.ts index 5a5487b..e3dda7c 100644 --- a/src/datastore/DataStore.ts +++ b/src/datastore/DataStore.ts @@ -60,6 +60,25 @@ const DEFAULT_NGINX_CONFIG_FOR_APP = fs .readFileSync(DEFAULT_NGINX_CONFIG_FOR_APP_PATH) .toString() +export function runDataStoreMigrations(data: Configstore) { + const schemaVersion = data.get('schemaVersion') as number | undefined + + if (schemaVersion && schemaVersion >= 2) { + return + } + + const appDefinitions = data.get('appDefinitions') + if (appDefinitions) { + Object.keys(appDefinitions).forEach((appName) => { + const appDef = appDefinitions[appName] as IAppDefSaved + appDef.isLegacyAppName = true + }) + data.set('appDefinitions', appDefinitions) + } + + data.set('schemaVersion', 2) +} + class DataStore { private encryptor: CaptainEncryptor private namespace: string @@ -78,19 +97,7 @@ class DataStore { } ) - // running migrations - if (data.get('schemaVersion') < 2) { - const appDefinitions = data.get('appDefinitions') - if (appDefinitions) { - Object.keys(appDefinitions).forEach((appName) => { - const appDef = appDefinitions[appName] as IAppDefSaved - appDef.isLegacyAppName = true - }) - } - - data.set('appDefinitions', appDefinitions) - data.set('schemaVersion', 2) - } + runDataStoreMigrations(data) this.data = data this.namespace = namespace From e80dc6dd60643908e1049ca0fe7587422397099a Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:27:22 -0700 Subject: [PATCH 05/20] Harden service and volume naming migration --- src/datastore/AppsDataStore.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/datastore/AppsDataStore.ts b/src/datastore/AppsDataStore.ts index 654c03a..6351fee 100644 --- a/src/datastore/AppsDataStore.ts +++ b/src/datastore/AppsDataStore.ts @@ -26,7 +26,7 @@ import isValidPath = require('is-valid-path') const APP_DEFINITIONS = 'appDefinitions' -function isNameAllowed(name: string) { +export function isNameAllowed(name: string) { const isNameFormattingOk = !!name && name.length < 50 && @@ -34,7 +34,11 @@ function isNameAllowed(name: string) { /[a-z0-9]$/.test(name) && /^[a-z0-9\-]+$/.test(name) && name.indexOf('--') < 0 - return isNameFormattingOk && ['captain', 'registry'].indexOf(name) < 0 + return ( + isNameFormattingOk && + ['captain', 'registry'].indexOf(name) < 0 && + !name.startsWith('captain-') + ) } /** @@ -302,6 +306,14 @@ class AppsDataStore { return `${appName}` } + getVolumeName(volumeName: string, isLegacyVolumeName: boolean) { + if (isLegacyVolumeName) { + return `${this.namepace}--${volumeName}` + } + + return volumeName + } + getAppDefinitions() { const self = this return new Promise(function (resolve, reject) { From c7bd1b90266994e2efe5ad721af7791c009de78b Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:27:29 -0700 Subject: [PATCH 06/20] Harden service and volume naming migration --- .../apps/appdefinition/AppDefinitionRouter.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 562c72a..069d5ee 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -191,7 +191,7 @@ router.post('/delete/', function (req, res, next) { const appsToDelete: string[] = appNames.length ? appNames : [appName] Logger.d(`Deleting app started: ${appName}`) - const legacyVolumes: IHashMapGeneric = {} + const volumesToDelete: IHashMapGeneric = {} return Promise.resolve() .then(function () { @@ -206,18 +206,27 @@ router.post('/delete/', function (req, res, next) { return dataStore.getAppsDataStore().getAppDefinitions() }) .then(function (apps) { - Object.keys(apps).forEach((appName) => { - const app = apps[appName] - if (app.isLegacyAppName) { - const volumesForApp = app.volumes.map( - (vol) => vol.volumeName - ) - volumesForApp.forEach((volumeName) => { - if (volumeName) { - legacyVolumes[volumeName] = true - } - }) + appsToDelete.forEach((appNameToDelete) => { + const app = apps[appNameToDelete] + if (!app) { + return } + + const volumesForApp = app.volumes || [] + volumesForApp.forEach((volume) => { + const volumeName = volume.volumeName + if (!volumeName || volumes.indexOf(volumeName) < 0) { + return + } + + const physicalVolumeName = dataStore + .getAppsDataStore() + .getVolumeName( + volumeName, + !!app.isLegacyAppName + ) + volumesToDelete[physicalVolumeName] = volumeName + }) }) }) .then(function () { @@ -227,7 +236,7 @@ router.post('/delete/', function (req, res, next) { return Utils.getDelayedPromise(volumes.length ? 12000 : 0) }) .then(function () { - return serviceManager.removeVolsSafe(volumes, legacyVolumes) + return serviceManager.removeVolsSafe(volumesToDelete) }) .then(function (failedVolsToRemoved) { Logger.d(`Successfully deleted: ${appsToDelete.join(', ')}`) From 8716607074a3e372fab19617c9a185bd59abb593 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:27:35 -0700 Subject: [PATCH 07/20] Harden service and volume naming migration --- src/user/ServiceManager.ts | 40 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/user/ServiceManager.ts b/src/user/ServiceManager.ts index 80b0dab..d2c9026 100644 --- a/src/user/ServiceManager.ts +++ b/src/user/ServiceManager.ts @@ -522,7 +522,7 @@ class ServiceManager { return Promise.all(promises) } - removeVolsSafe(volumes: string[], legacyVolumes: IHashMapGeneric) { + removeVolsSafe(volumes: IHashMapGeneric) { const dockerApi = this.dockerApi const dataStore = this.dataStore @@ -533,35 +533,45 @@ class ServiceManager { return dataStore.getAppsDataStore().getAppDefinitions() }) .then(function (apps) { - // Don't even try deleting volumes which are present in other app definitions + const physicalVolumesInUse: IHashMapGeneric = {} + Object.keys(apps).forEach((appName) => { const app = apps[appName] const volsInApp = app.volumes || [] - volsInApp.forEach((v) => { - const volName = v.volumeName - if (!volName) return - if (volumes.indexOf(volName) >= 0) { - volsFailedToDelete[volName] = true + volsInApp.forEach((volume) => { + const volumeName = volume.volumeName + if (!volumeName) { + return } + + const physicalVolumeName = dataStore + .getAppsDataStore() + .getVolumeName( + volumeName, + !!app.isLegacyAppName + ) + physicalVolumesInUse[physicalVolumeName] = true }) }) const volumesTryToDelete: string[] = [] - - volumes.forEach((v) => { - if (!volsFailedToDelete[v]) { - volumesTryToDelete.push( - legacyVolumes[v] ? `captain--${v}` : v - ) + Object.keys(volumes).forEach((physicalVolumeName) => { + const logicalVolumeName = volumes[physicalVolumeName] + if (physicalVolumesInUse[physicalVolumeName]) { + volsFailedToDelete[logicalVolumeName] = true + } else { + volumesTryToDelete.push(physicalVolumeName) } }) return dockerApi.deleteVols(volumesTryToDelete) }) .then(function (failedVols) { - failedVols.forEach((v) => { - volsFailedToDelete[v] = true + failedVols.forEach((physicalVolumeName) => { + const logicalVolumeName = + volumes[physicalVolumeName] || physicalVolumeName + volsFailedToDelete[logicalVolumeName] = true }) return Object.keys(volsFailedToDelete) From e301b900392a5a076c7f71446571bca622759235 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:27:38 -0700 Subject: [PATCH 08/20] Add naming migration regression tests --- tests/ServiceNamingMigration.test.ts | 120 +++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/ServiceNamingMigration.test.ts diff --git a/tests/ServiceNamingMigration.test.ts b/tests/ServiceNamingMigration.test.ts new file mode 100644 index 0000000..f4896f2 --- /dev/null +++ b/tests/ServiceNamingMigration.test.ts @@ -0,0 +1,120 @@ +import configstore = require('configstore') +import AppsDataStore, { + isNameAllowed, +} from '../src/datastore/AppsDataStore' +import { runDataStoreMigrations } from '../src/datastore/DataStore' +import ServiceManager from '../src/user/ServiceManager' + +function createConfigStore(initialData: { [key: string]: any }) { + const data = { ...initialData } + return { + get: jest.fn((key: string) => data[key]), + set: jest.fn((key: string, value: any) => { + data[key] = value + }), + } as unknown as configstore +} + +describe('service and volume naming migration', () => { + test('marks existing apps as legacy when schemaVersion is missing', () => { + const appDefinitions = { + existingApp: {}, + } + const data = createConfigStore({ appDefinitions }) + + runDataStoreMigrations(data) + + expect(appDefinitions.existingApp).toEqual({ + isLegacyAppName: true, + }) + expect(data.set).toHaveBeenCalledWith('schemaVersion', 2) + }) + + test('does not mark apps created after schema version 2 as legacy', () => { + const appDefinitions = { + newApp: {}, + } + const data = createConfigStore({ + schemaVersion: 2, + appDefinitions, + }) + + runDataStoreMigrations(data) + + expect(appDefinitions.newApp).toEqual({}) + expect(data.set).not.toHaveBeenCalled() + }) + + test('reserves the captain service-name prefix', () => { + expect(isNameAllowed('captain-nginx')).toBe(false) + expect(isNameAllowed('captain-custom')).toBe(false) + expect(isNameAllowed('my-app')).toBe(true) + }) + + test('keeps legacy and new physical volumes distinct during deletion', async () => { + const appsDataStore = new AppsDataStore( + createConfigStore({}), + 'captain' + ) + jest.spyOn(appsDataStore, 'getAppDefinitions').mockResolvedValue({ + remainingLegacyApp: { + isLegacyAppName: true, + volumes: [ + { + volumeName: 'data', + containerPath: '/data', + }, + ], + } as any, + }) + + const deleteVols = jest.fn().mockResolvedValue([]) + const serviceManager = Object.create( + ServiceManager.prototype + ) as ServiceManager + ;(serviceManager as any).dataStore = { + getAppsDataStore: () => appsDataStore, + } + ;(serviceManager as any).dockerApi = { + deleteVols, + } + + const failedVolumes = await serviceManager.removeVolsSafe({ + 'captain--data': 'data', + data: 'data', + }) + + expect(deleteVols).toHaveBeenCalledWith(['data']) + expect(failedVolumes).toEqual(['data']) + }) + + test('deletes both physical volumes when neither remains in use', async () => { + const appsDataStore = new AppsDataStore( + createConfigStore({}), + 'captain' + ) + jest.spyOn(appsDataStore, 'getAppDefinitions').mockResolvedValue({}) + + const deleteVols = jest.fn().mockResolvedValue([]) + const serviceManager = Object.create( + ServiceManager.prototype + ) as ServiceManager + ;(serviceManager as any).dataStore = { + getAppsDataStore: () => appsDataStore, + } + ;(serviceManager as any).dockerApi = { + deleteVols, + } + + const failedVolumes = await serviceManager.removeVolsSafe({ + 'captain--data': 'data', + data: 'data', + }) + + expect(deleteVols).toHaveBeenCalledWith([ + 'captain--data', + 'data', + ]) + expect(failedVolumes).toEqual([]) + }) +}) From a5fd6879d160d60bb4a8dd61545f03c3f772e23c Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:31:05 -0700 Subject: [PATCH 09/20] Format naming migration changes --- .../apps/appdefinition/AppDefinitionRouter.ts | 596 +++++++++--------- 1 file changed, 292 insertions(+), 304 deletions(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 069d5ee..256b081 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -1,348 +1,336 @@ -import express = require('express') -import ApiStatusCodes from '../../../../api/ApiStatusCodes' -import BaseApi from '../../../../api/BaseApi' +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. +import express = require("express"); +import ApiStatusCodes from "../../../../api/ApiStatusCodes"; +import BaseApi from "../../../../api/BaseApi"; import { - getAllAppDefinitions, - registerAppDefinition, - updateAppDefinition, -} from '../../../../handlers/users/apps/appdefinition/AppDefinitionHandler' -import InjectionExtractor from '../../../../injection/InjectionExtractor' -import { AppDeployTokenConfig } from '../../../../models/AppDefinition' -import { IHashMapGeneric } from '../../../../models/ICacheGeneric' -import CaptainManager from '../../../../user/system/CaptainManager' -import Logger from '../../../../utils/Logger' -import Utils from '../../../../utils/Utils' + getAllAppDefinitions, + registerAppDefinition, + updateAppDefinition, +} from "../../../../handlers/users/apps/appdefinition/AppDefinitionHandler"; +import InjectionExtractor from "../../../../injection/InjectionExtractor"; +import { AppDeployTokenConfig } from "../../../../models/AppDefinition"; +import { IHashMapGeneric } from "../../../../models/ICacheGeneric"; +import CaptainManager from "../../../../user/system/CaptainManager"; +import Logger from "../../../../utils/Logger"; +import Utils from "../../../../utils/Utils"; -const router = express.Router() +const router = express.Router(); // unused images -router.get('/unusedImages', function (req, res, next) { - return Promise.resolve() - .then(function () { - const mostRecentLimit = Number(req.query.mostRecentLimit || '0') - return CaptainManager.get() - .getDiskCleanupManager() - .getUnusedImages(mostRecentLimit) - }) - .then(function (unusedImages) { - const baseApi = new BaseApi( - ApiStatusCodes.STATUS_OK, - 'Unused images retrieved.' - ) - baseApi.data = {} - baseApi.data.unusedImages = unusedImages +router.get("/unusedImages", function (req, res, next) { + return Promise.resolve() + .then(function () { + const mostRecentLimit = Number(req.query.mostRecentLimit || "0"); + return CaptainManager.get() + .getDiskCleanupManager() + .getUnusedImages(mostRecentLimit); + }) + .then(function (unusedImages) { + const baseApi = new BaseApi( + ApiStatusCodes.STATUS_OK, + "Unused images retrieved.", + ); + baseApi.data = {}; + baseApi.data.unusedImages = unusedImages; - res.send(baseApi) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + res.send(baseApi); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); // delete images -router.post('/deleteImages', function (req, res, next) { - const imageIds = req.body.imageIds || [] +router.post("/deleteImages", function (req, res, next) { + const imageIds = req.body.imageIds || []; - return Promise.resolve() - .then(function () { - return CaptainManager.get() - .getDiskCleanupManager() - .deleteImages(imageIds) - }) - .then(function () { - const baseApi = new BaseApi( - ApiStatusCodes.STATUS_OK, - 'Images Deleted.' - ) - res.send(baseApi) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return Promise.resolve() + .then(function () { + return CaptainManager.get() + .getDiskCleanupManager() + .deleteImages(imageIds); + }) + .then(function () { + const baseApi = new BaseApi(ApiStatusCodes.STATUS_OK, "Images Deleted."); + res.send(baseApi); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); // Get All App Definitions -router.get('/', function (req, res, next) { - const dataStore = - InjectionExtractor.extractUserFromInjected(res).user.dataStore - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.get("/", function (req, res, next) { + const dataStore = + InjectionExtractor.extractUserFromInjected(res).user.dataStore; + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - return getAllAppDefinitions(dataStore, serviceManager) - .then(function (result) { - const baseApi = new BaseApi( - ApiStatusCodes.STATUS_OK, - result.message - ) - baseApi.data = result.data - res.send(baseApi) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return getAllAppDefinitions(dataStore, serviceManager) + .then(function (result) { + const baseApi = new BaseApi(ApiStatusCodes.STATUS_OK, result.message); + baseApi.data = result.data; + res.send(baseApi); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -router.post('/enablebasedomainssl/', function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.post("/enablebasedomainssl/", function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const appName = req.body.appName + const appName = req.body.appName; - return Promise.resolve() - .then(function () { - return serviceManager.enableSslForApp(appName) - }) - .then(function () { - const msg = `General SSL is enabled for: ${appName}` - Logger.d(msg) - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return Promise.resolve() + .then(function () { + return serviceManager.enableSslForApp(appName); + }) + .then(function () { + const msg = `General SSL is enabled for: ${appName}`; + Logger.d(msg); + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -router.post('/customdomain/', function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.post("/customdomain/", function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const appName = req.body.appName - const customDomain = (req.body.customDomain || '').toLowerCase().trim() + const appName = req.body.appName; + const customDomain = (req.body.customDomain || "").toLowerCase().trim(); - // verify customdomain.com going through the default NGINX - // Add customdomain.com to app in Data Store + // verify customdomain.com going through the default NGINX + // Add customdomain.com to app in Data Store - return Promise.resolve() - .then(function () { - return serviceManager.addCustomDomain(appName, customDomain) - }) - .then(function () { - const msg = `Custom domain is enabled for: ${appName} at ${customDomain}` - Logger.d(msg) - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return Promise.resolve() + .then(function () { + return serviceManager.addCustomDomain(appName, customDomain); + }) + .then(function () { + const msg = `Custom domain is enabled for: ${appName} at ${customDomain}`; + Logger.d(msg); + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -router.post('/removecustomdomain/', function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.post("/removecustomdomain/", function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const appName = req.body.appName - const customDomain = (req.body.customDomain || '').toLowerCase() + const appName = req.body.appName; + const customDomain = (req.body.customDomain || "").toLowerCase(); - return Promise.resolve() - .then(function () { - return serviceManager.removeCustomDomain(appName, customDomain) - }) - .then(function () { - const msg = `Custom domain is removed for: ${appName} at ${customDomain}` - Logger.d(msg) - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return Promise.resolve() + .then(function () { + return serviceManager.removeCustomDomain(appName, customDomain); + }) + .then(function () { + const msg = `Custom domain is removed for: ${appName} at ${customDomain}`; + Logger.d(msg); + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -router.post('/enablecustomdomainssl/', function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.post("/enablecustomdomainssl/", function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const appName = req.body.appName - const customDomain = (req.body.customDomain || '').toLowerCase() + const appName = req.body.appName; + const customDomain = (req.body.customDomain || "").toLowerCase(); - // Check if customdomain is already associated with app. If not, error out. - // Verify customdomain.com is served from /customdomain.com/ + // Check if customdomain is already associated with app. If not, error out. + // Verify customdomain.com is served from /customdomain.com/ - return Promise.resolve() - .then(function () { - return serviceManager.enableCustomDomainSsl(appName, customDomain) - }) - .then(function () { - const msg = `Custom domain SSL is enabled for: ${appName} at ${customDomain} ` - Logger.d(msg) - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return Promise.resolve() + .then(function () { + return serviceManager.enableCustomDomainSsl(appName, customDomain); + }) + .then(function () { + const msg = `Custom domain SSL is enabled for: ${appName} at ${customDomain} `; + Logger.d(msg); + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -router.post('/register/', function (req, res, next) { - const dataStore = - InjectionExtractor.extractUserFromInjected(res).user.dataStore - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.post("/register/", function (req, res, next) { + const dataStore = + InjectionExtractor.extractUserFromInjected(res).user.dataStore; + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const appName = req.body.appName as string - const projectId = `${req.body.projectId || ''}` - const hasPersistentData = !!req.body.hasPersistentData - const isDetachedBuild = !!req.query.detached + const appName = req.body.appName as string; + const projectId = `${req.body.projectId || ""}`; + const hasPersistentData = !!req.body.hasPersistentData; + const isDetachedBuild = !!req.query.detached; - return registerAppDefinition( - { appName, projectId, hasPersistentData, isDetachedBuild }, - dataStore, - serviceManager - ) - .then(function (result) { - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return registerAppDefinition( + { appName, projectId, hasPersistentData, isDetachedBuild }, + dataStore, + serviceManager, + ) + .then(function (result) { + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -router.post('/delete/', function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const dataStore = - InjectionExtractor.extractUserFromInjected(res).user.dataStore +router.post("/delete/", function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; + const dataStore = + InjectionExtractor.extractUserFromInjected(res).user.dataStore; - const appName: string = req.body.appName - const volumes: string[] = req.body.volumes || [] - const appNames: string[] = req.body.appNames || [] - const appsToDelete: string[] = appNames.length ? appNames : [appName] + const appName: string = req.body.appName; + const volumes: string[] = req.body.volumes || []; + const appNames: string[] = req.body.appNames || []; + const appsToDelete: string[] = appNames.length ? appNames : [appName]; - Logger.d(`Deleting app started: ${appName}`) - const volumesToDelete: IHashMapGeneric = {} + Logger.d(`Deleting app started: ${appName}`); + const volumesToDelete: IHashMapGeneric = {}; - return Promise.resolve() - .then(function () { - if (appNames.length > 0 && appName) { - throw ApiStatusCodes.createError( - ApiStatusCodes.ILLEGAL_OPERATION, - 'Either appName or appNames should be provided' - ) - } - }) - .then(function () { - return dataStore.getAppsDataStore().getAppDefinitions() - }) - .then(function (apps) { - appsToDelete.forEach((appNameToDelete) => { - const app = apps[appNameToDelete] - if (!app) { - return - } + return Promise.resolve() + .then(function () { + if (appNames.length > 0 && appName) { + throw ApiStatusCodes.createError( + ApiStatusCodes.ILLEGAL_OPERATION, + "Either appName or appNames should be provided", + ); + } + }) + .then(function () { + return dataStore.getAppsDataStore().getAppDefinitions(); + }) + .then(function (apps) { + appsToDelete.forEach((appNameToDelete) => { + const app = apps[appNameToDelete]; + if (!app) { + return; + } - const volumesForApp = app.volumes || [] - volumesForApp.forEach((volume) => { - const volumeName = volume.volumeName - if (!volumeName || volumes.indexOf(volumeName) < 0) { - return - } + const volumesForApp = app.volumes || []; + volumesForApp.forEach((volume) => { + const volumeName = volume.volumeName; + if (!volumeName || volumes.indexOf(volumeName) < 0) { + return; + } - const physicalVolumeName = dataStore - .getAppsDataStore() - .getVolumeName( - volumeName, - !!app.isLegacyAppName - ) - volumesToDelete[physicalVolumeName] = volumeName - }) - }) - }) - .then(function () { - return serviceManager.removeApps(appsToDelete) - }) - .then(function () { - return Utils.getDelayedPromise(volumes.length ? 12000 : 0) - }) - .then(function () { - return serviceManager.removeVolsSafe(volumesToDelete) - }) - .then(function (failedVolsToRemoved) { - Logger.d(`Successfully deleted: ${appsToDelete.join(', ')}`) + const physicalVolumeName = dataStore + .getAppsDataStore() + .getVolumeName(volumeName, !!app.isLegacyAppName); + volumesToDelete[physicalVolumeName] = volumeName; + }); + }); + }) + .then(function () { + return serviceManager.removeApps(appsToDelete); + }) + .then(function () { + return Utils.getDelayedPromise(volumes.length ? 12000 : 0); + }) + .then(function () { + return serviceManager.removeVolsSafe(volumesToDelete); + }) + .then(function (failedVolsToRemoved) { + Logger.d(`Successfully deleted: ${appsToDelete.join(", ")}`); - if (failedVolsToRemoved.length) { - const returnVal = new BaseApi( - ApiStatusCodes.STATUS_OK_PARTIALLY, - `App is deleted. Some volumes were not safe to delete. Delete skipped for: ${failedVolsToRemoved.join( - ' , ' - )}` - ) - returnVal.data = { volumesFailedToDelete: failedVolsToRemoved } - res.send(returnVal) - } else { - res.send( - new BaseApi(ApiStatusCodes.STATUS_OK, 'App is deleted') - ) - } - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + if (failedVolsToRemoved.length) { + const returnVal = new BaseApi( + ApiStatusCodes.STATUS_OK_PARTIALLY, + `App is deleted. Some volumes were not safe to delete. Delete skipped for: ${failedVolsToRemoved.join( + " , ", + )}`, + ); + returnVal.data = { volumesFailedToDelete: failedVolsToRemoved }; + res.send(returnVal); + } else { + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, "App is deleted")); + } + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -router.post('/rename/', function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.post("/rename/", function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const oldAppName = req.body.oldAppName + '' - const newAppName = req.body.newAppName + '' + const oldAppName = req.body.oldAppName + ""; + const newAppName = req.body.newAppName + ""; - Logger.d(`Renaming app started: From ${oldAppName} To ${newAppName} `) + Logger.d(`Renaming app started: From ${oldAppName} To ${newAppName} `); - return Promise.resolve() - .then(function () { - return serviceManager.renameApp(oldAppName, newAppName) - }) - .then(function () { - Logger.d('AppName is renamed') - res.send( - new BaseApi(ApiStatusCodes.STATUS_OK, 'AppName is renamed') - ) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return Promise.resolve() + .then(function () { + return serviceManager.renameApp(oldAppName, newAppName); + }) + .then(function () { + Logger.d("AppName is renamed"); + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, "AppName is renamed")); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); // Update app configs -router.post('/update/', function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager +router.post("/update/", function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const appName = req.body.appName - const projectId = req.body.projectId - const nodeId = req.body.nodeId - const captainDefinitionRelativeFilePath = - req.body.captainDefinitionRelativeFilePath - const notExposeAsWebApp = req.body.notExposeAsWebApp - const tags = req.body.tags - const customNginxConfig = req.body.customNginxConfig - const forceSsl = req.body.forceSsl - const websocketSupport = req.body.websocketSupport - const repoInfo = req.body.appPushWebhook - ? req.body.appPushWebhook.repoInfo - : undefined - const envVars = req.body.envVars - const volumes = req.body.volumes - const ports = req.body.ports - const instanceCount = req.body.instanceCount - const redirectDomain = req.body.redirectDomain - const preDeployFunction = req.body.preDeployFunction - const serviceUpdateOverride = req.body.serviceUpdateOverride - const containerHttpPort = req.body.containerHttpPort - const httpAuth = req.body.httpAuth - const appDeployTokenConfig = req.body.appDeployTokenConfig as - | AppDeployTokenConfig - | undefined - const description = req.body.description + const appName = req.body.appName; + const projectId = req.body.projectId; + const nodeId = req.body.nodeId; + const captainDefinitionRelativeFilePath = + req.body.captainDefinitionRelativeFilePath; + const notExposeAsWebApp = req.body.notExposeAsWebApp; + const tags = req.body.tags; + const customNginxConfig = req.body.customNginxConfig; + const forceSsl = req.body.forceSsl; + const websocketSupport = req.body.websocketSupport; + const repoInfo = req.body.appPushWebhook + ? req.body.appPushWebhook.repoInfo + : undefined; + const envVars = req.body.envVars; + const volumes = req.body.volumes; + const ports = req.body.ports; + const instanceCount = req.body.instanceCount; + const redirectDomain = req.body.redirectDomain; + const preDeployFunction = req.body.preDeployFunction; + const serviceUpdateOverride = req.body.serviceUpdateOverride; + const containerHttpPort = req.body.containerHttpPort; + const httpAuth = req.body.httpAuth; + const appDeployTokenConfig = req.body.appDeployTokenConfig as + | AppDeployTokenConfig + | undefined; + const description = req.body.description; - return updateAppDefinition( - { - appName, - projectId, - description, - instanceCount, - captainDefinitionRelativeFilePath, - envVars, - volumes, - tags, - nodeId, - notExposeAsWebApp, - containerHttpPort, - httpAuth, - forceSsl, - ports, - repoInfo, - customNginxConfig, - redirectDomain, - preDeployFunction, - serviceUpdateOverride, - websocketSupport, - appDeployTokenConfig, - }, - serviceManager - ) - .then(function (result) { - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)) - }) - .catch(ApiStatusCodes.createCatcher(res)) -}) + return updateAppDefinition( + { + appName, + projectId, + description, + instanceCount, + captainDefinitionRelativeFilePath, + envVars, + volumes, + tags, + nodeId, + notExposeAsWebApp, + containerHttpPort, + httpAuth, + forceSsl, + ports, + repoInfo, + customNginxConfig, + redirectDomain, + preDeployFunction, + serviceUpdateOverride, + websocketSupport, + appDeployTokenConfig, + }, + serviceManager, + ) + .then(function (result) { + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)); + }) + .catch(ApiStatusCodes.createCatcher(res)); +}); -export default router +export default router; From b94a34ce8633a9054ff13e09e7e13f64a537aaee Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:31:14 -0700 Subject: [PATCH 10/20] Format naming migration changes --- src/user/ServiceManager.ts | 1874 ++++++++++++++++++------------------ 1 file changed, 922 insertions(+), 952 deletions(-) diff --git a/src/user/ServiceManager.ts b/src/user/ServiceManager.ts index d2c9026..42c4d5d 100644 --- a/src/user/ServiceManager.ts +++ b/src/user/ServiceManager.ts @@ -1,593 +1,572 @@ -import ApiStatusCodes from '../api/ApiStatusCodes' -import DataStore from '../datastore/DataStore' -import DockerApi, { IDockerUpdateOrders } from '../docker/DockerApi' +npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. +import ApiStatusCodes from "../api/ApiStatusCodes"; +import DataStore from "../datastore/DataStore"; +import DockerApi, { IDockerUpdateOrders } from "../docker/DockerApi"; import { - AppDeployTokenConfig, - IAppDef, - IAppEnvVar, - IAppPort, - IAppTag, - IAppVolume, - IHttpAuth, - RepoInfo, -} from '../models/AppDefinition' -import { DockerAuthObj } from '../models/DockerAuthObj' -import { IHashMapGeneric } from '../models/ICacheGeneric' -import { IImageSource } from '../models/IImageSource' -import { PreDeployFunction } from '../models/OtherTypes' -import CaptainConstants from '../utils/CaptainConstants' -import Logger from '../utils/Logger' -import Utils from '../utils/Utils' -import Authenticator from './Authenticator' -import DockerRegistryHelper from './DockerRegistryHelper' -import ImageMaker, { BuildLogsManager } from './ImageMaker' -import { EventLogger } from './events/EventLogger' + AppDeployTokenConfig, + IAppDef, + IAppEnvVar, + IAppPort, + IAppTag, + IAppVolume, + IHttpAuth, + RepoInfo, +} from "../models/AppDefinition"; +import { DockerAuthObj } from "../models/DockerAuthObj"; +import { IHashMapGeneric } from "../models/ICacheGeneric"; +import { IImageSource } from "../models/IImageSource"; +import { PreDeployFunction } from "../models/OtherTypes"; +import CaptainConstants from "../utils/CaptainConstants"; +import Logger from "../utils/Logger"; +import Utils from "../utils/Utils"; +import Authenticator from "./Authenticator"; +import DockerRegistryHelper from "./DockerRegistryHelper"; +import ImageMaker, { BuildLogsManager } from "./ImageMaker"; +import { EventLogger } from "./events/EventLogger"; import { - CapRoverEventFactory, - CapRoverEventType, -} from './events/ICapRoverEvent' -import DomainResolveChecker from './system/DomainResolveChecker' -import LoadBalancerManager from './system/LoadBalancerManager' -import requireFromString = require('require-from-string') + CapRoverEventFactory, + CapRoverEventType, +} from "./events/ICapRoverEvent"; +import DomainResolveChecker from "./system/DomainResolveChecker"; +import LoadBalancerManager from "./system/LoadBalancerManager"; +import requireFromString = require("require-from-string"); const ERROR_FIRST_ENABLE_ROOT_SSL = - 'You have to first enable SSL for your root domain' + "You have to first enable SSL for your root domain"; -const serviceMangerCache = {} as IHashMapGeneric +const serviceMangerCache = {} as IHashMapGeneric; interface QueuedPromise { - resolve: undefined | ((reason?: unknown) => void) - reject: undefined | ((reason?: any) => void) - promise: undefined | Promise + resolve: undefined | ((reason?: unknown) => void); + reject: undefined | ((reason?: any) => void); + promise: undefined | Promise; } interface QueuedBuild { - appName: string - source: IImageSource - promiseToSave: QueuedPromise + appName: string; + source: IImageSource; + promiseToSave: QueuedPromise; } class ServiceManager { - static get( - namespace: string, - authenticator: Authenticator, - dataStore: DataStore, - dockerApi: DockerApi, - loadBalancerManager: LoadBalancerManager, - eventLogger: EventLogger, - domainResolveChecker: DomainResolveChecker - ) { - if (!serviceMangerCache[namespace]) { - serviceMangerCache[namespace] = new ServiceManager( - dataStore, - authenticator, - dockerApi, - loadBalancerManager, - eventLogger, - domainResolveChecker - ) - } - return serviceMangerCache[namespace] + static get( + namespace: string, + authenticator: Authenticator, + dataStore: DataStore, + dockerApi: DockerApi, + loadBalancerManager: LoadBalancerManager, + eventLogger: EventLogger, + domainResolveChecker: DomainResolveChecker, + ) { + if (!serviceMangerCache[namespace]) { + serviceMangerCache[namespace] = new ServiceManager( + dataStore, + authenticator, + dockerApi, + loadBalancerManager, + eventLogger, + domainResolveChecker, + ); + } + return serviceMangerCache[namespace]; + } + + private activeOrScheduledBuilds: IHashMapGeneric; + private buildLogsManager: BuildLogsManager; + private queuedBuilds: QueuedBuild[]; + private isReady: boolean; + private imageMaker: ImageMaker; + private dockerRegistryHelper: DockerRegistryHelper; + + constructor( + private dataStore: DataStore, + private authenticator: Authenticator, + private dockerApi: DockerApi, + private loadBalancerManager: LoadBalancerManager, + private eventLogger: EventLogger, + private domainResolveChecker: DomainResolveChecker, + ) { + this.activeOrScheduledBuilds = {}; + this.queuedBuilds = []; + this.buildLogsManager = new BuildLogsManager(); + this.isReady = true; + this.dockerRegistryHelper = new DockerRegistryHelper( + this.dataStore, + this.dockerApi, + ); + this.imageMaker = new ImageMaker( + this.dockerRegistryHelper, + this.dockerApi, + this.dataStore.getNameSpace(), + this.buildLogsManager, + ); + } + + getRegistryHelper() { + return this.dockerRegistryHelper; + } + + isInited() { + return this.isReady; + } + + scheduleDeployNewVersion(appName: string, source: IImageSource) { + const self = this; + + const activeBuildAppName = self.isAnyBuildRunning(); + this.activeOrScheduledBuilds[appName] = true; + + self.buildLogsManager.getAppBuildLogs(appName).clear(); + + if (activeBuildAppName) { + const existingBuildForTheSameApp = self.queuedBuilds.find( + (v) => v.appName === appName, + ); + + if (existingBuildForTheSameApp) { + self.buildLogsManager + .getAppBuildLogs(appName) + .log( + `A build for ${appName} was queued, it's now being replaced with a new build...`, + ); + + // replacing the new source! + existingBuildForTheSameApp.source = source; + + const existingPromise = + existingBuildForTheSameApp.promiseToSave.promise; + + if (!existingPromise) + throw new Error("Existing promise for the queued app is NULL!!"); + + return existingPromise; + } + + self.buildLogsManager + .getAppBuildLogs(appName) + .log( + `An active build (${activeBuildAppName}) is in progress. This build is queued...`, + ); + + const promiseToSave: QueuedPromise = { + resolve: undefined, + reject: undefined, + promise: undefined, + }; + + const promise = new Promise(function (resolve, reject) { + promiseToSave.resolve = resolve; + promiseToSave.reject = reject; + }); + + promiseToSave.promise = promise; + + self.queuedBuilds.push({ appName, source, promiseToSave }); + + // This should only return when the build is finished, + // somehow we need save the promise in queue - for "attached builds" + return promise; } - private activeOrScheduledBuilds: IHashMapGeneric - private buildLogsManager: BuildLogsManager - private queuedBuilds: QueuedBuild[] - private isReady: boolean - private imageMaker: ImageMaker - private dockerRegistryHelper: DockerRegistryHelper + return this.startDeployingNewVersion(appName, source); + } - constructor( - private dataStore: DataStore, - private authenticator: Authenticator, - private dockerApi: DockerApi, - private loadBalancerManager: LoadBalancerManager, - private eventLogger: EventLogger, - private domainResolveChecker: DomainResolveChecker - ) { - this.activeOrScheduledBuilds = {} - this.queuedBuilds = [] - this.buildLogsManager = new BuildLogsManager() - this.isReady = true - this.dockerRegistryHelper = new DockerRegistryHelper( - this.dataStore, - this.dockerApi - ) - this.imageMaker = new ImageMaker( - this.dockerRegistryHelper, - this.dockerApi, - this.dataStore.getNameSpace(), - this.buildLogsManager - ) - } + startDeployingNewVersion(appName: string, source: IImageSource) { + const self = this; + const dataStore = this.dataStore; + let deployedVersion: number; - getRegistryHelper() { - return this.dockerRegistryHelper - } + return Promise.resolve() // + .then(function () { + return dataStore.getAppsDataStore().createNewVersion(appName); + }) + .then(function (appVersion) { + deployedVersion = appVersion; + return dataStore + .getAppsDataStore() + .getAppDefinition(appName) + .then(function (app) { + const envVars = app.envVars || []; - isInited() { - return this.isReady - } + return self.imageMaker.ensureImage( + source, + appName, + app.captainDefinitionRelativeFilePath, + appVersion, + envVars, + ); + }); + }) + .then(function (builtImage) { + return dataStore + .getAppsDataStore() + .setDeployedVersionAndImage(appName, deployedVersion, builtImage); + }) + .then(function () { + self.onBuildFinished(appName); - scheduleDeployNewVersion(appName: string, source: IImageSource) { - const self = this + self.eventLogger.trackEvent( + CapRoverEventFactory.create(CapRoverEventType.AppBuildSuccessful, { + appName, + }), + ); - const activeBuildAppName = self.isAnyBuildRunning() - this.activeOrScheduledBuilds[appName] = true + return self.ensureServiceInitedAndUpdated(appName); + }) + .catch(function (error) { + self.onBuildFinished(appName); + return new Promise(function (resolve, reject) { + self.logBuildFailed(appName, error); + reject(error); + }); + }); + } - self.buildLogsManager.getAppBuildLogs(appName).clear() + onBuildFinished(appName: string) { + const self = this; + self.activeOrScheduledBuilds[appName] = false; - if (activeBuildAppName) { - const existingBuildForTheSameApp = self.queuedBuilds.find( - (v) => v.appName === appName - ) + Promise.resolve().then(function () { + const newBuild = self.queuedBuilds.shift(); + if (newBuild) + self.startDeployingNewVersion(newBuild.appName, newBuild.source); + }); + } - if (existingBuildForTheSameApp) { - self.buildLogsManager - .getAppBuildLogs(appName) - .log( - `A build for ${appName} was queued, it's now being replaced with a new build...` - ) + enableCustomDomainSsl(appName: string, customDomain: string) { + const self = this; - // replacing the new source! - existingBuildForTheSameApp.source = source - - const existingPromise = - existingBuildForTheSameApp.promiseToSave.promise - - if (!existingPromise) - throw new Error( - 'Existing promise for the queued app is NULL!!' - ) - - return existingPromise - } - - self.buildLogsManager - .getAppBuildLogs(appName) - .log( - `An active build (${activeBuildAppName}) is in progress. This build is queued...` - ) - - const promiseToSave: QueuedPromise = { - resolve: undefined, - reject: undefined, - promise: undefined, - } - - const promise = new Promise(function (resolve, reject) { - promiseToSave.resolve = resolve - promiseToSave.reject = reject - }) - - promiseToSave.promise = promise - - self.queuedBuilds.push({ appName, source, promiseToSave }) - - // This should only return when the build is finished, - // somehow we need save the promise in queue - for "attached builds" - return promise + return Promise.resolve() + .then(function () { + return self.dataStore.getHasRootSsl(); + }) + .then(function (rootHasSsl) { + if (!rootHasSsl) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + ERROR_FIRST_ENABLE_ROOT_SSL, + ); } - return this.startDeployingNewVersion(appName, source) - } + Logger.d(`Verifying Captain owns domain: ${customDomain}`); - startDeployingNewVersion(appName: string, source: IImageSource) { - const self = this - const dataStore = this.dataStore - let deployedVersion: number + return self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( + customDomain, + undefined, + ); + }) + .then(function () { + Logger.d(`Enabling SSL for: ${appName} on ${customDomain}`); - return Promise.resolve() // - .then(function () { - return dataStore.getAppsDataStore().createNewVersion(appName) - }) - .then(function (appVersion) { - deployedVersion = appVersion - return dataStore - .getAppsDataStore() - .getAppDefinition(appName) - .then(function (app) { - const envVars = app.envVars || [] + return self.dataStore + .getAppsDataStore() + .verifyCustomDomainBelongsToApp(appName, customDomain); + }) + .then(function () { + return self.domainResolveChecker.requestCertificateForDomain( + customDomain, + ); + }) + .then(function () { + return self.dataStore + .getAppsDataStore() + .enableCustomDomainSsl(appName, customDomain); + }) + .then(function () { + return self.reloadLoadBalancer(); + }); + } - return self.imageMaker.ensureImage( - source, - appName, - app.captainDefinitionRelativeFilePath, - appVersion, - envVars - ) - }) - }) - .then(function (builtImage) { - return dataStore - .getAppsDataStore() - .setDeployedVersionAndImage( - appName, - deployedVersion, - builtImage - ) - }) - .then(function () { - self.onBuildFinished(appName) + addCustomDomain(appName: string, customDomain: string) { + const self = this; - self.eventLogger.trackEvent( - CapRoverEventFactory.create( - CapRoverEventType.AppBuildSuccessful, - { - appName, - } - ) - ) + return Promise.resolve() + .then(function () { + const rootDomain = self.dataStore.getRootDomain(); - return self.ensureServiceInitedAndUpdated(appName) - }) - .catch(function (error) { - self.onBuildFinished(appName) - return new Promise(function (resolve, reject) { - self.logBuildFailed(appName, error) - reject(error) - }) - }) - } + try { + Utils.checkCustomDomain(customDomain, appName, rootDomain); + } catch (error) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_BAD_NAME, + error, + ); + } + }) + .then(function () { + return self.domainResolveChecker.verifyDomainResolvesToDefaultServerOnHost( + customDomain, + ); + }) + .then(function () { + Logger.d(`Enabling custom domain for: ${appName}`); - onBuildFinished(appName: string) { - const self = this - self.activeOrScheduledBuilds[appName] = false + return self.dataStore + .getAppsDataStore() + .addCustomDomainForApp(appName, customDomain); + }) + .then(function () { + return self.reloadLoadBalancer(); + }); + } - Promise.resolve().then(function () { - const newBuild = self.queuedBuilds.shift() - if (newBuild) - self.startDeployingNewVersion(newBuild.appName, newBuild.source) + removeCustomDomain(appName: string, customDomain: string) { + 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: string) { + const self = this; + + let rootDomain: string; + + return Promise.resolve() + .then(function () { + return self.dataStore.getHasRootSsl(); + }) + .then(function (rootHasSsl) { + if (!rootHasSsl) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + ERROR_FIRST_ENABLE_ROOT_SSL, + ); + } + 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 self.domainResolveChecker.requestCertificateForDomain( + domainName, + ); + }) + .then(function () { + return self.dataStore + .getAppsDataStore() + .setSslForDefaultSubDomain(appName, true); + }) + .then(function () { + return self.reloadLoadBalancer(); + }); + } + + verifyCaptainOwnsGenericSubDomain(appName: string) { + const self = this; + + let rootDomain: string; + + 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 self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( + domainName, + undefined, + ); + }); + } + + renameApp(oldAppName: string, newAppName: string) { + Logger.d(`Renaming app: ${oldAppName}`); + const self = this; + + const dockerApi = this.dockerApi; + const dataStore = this.dataStore; + + let defaultSslOn = false; + let oldServiceName: string; + + return Promise.resolve() + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(oldAppName); + }) + .then(function (appDef) { + defaultSslOn = !!appDef.hasDefaultSubDomainSsl; + oldServiceName = dataStore + .getAppsDataStore() + .getServiceName(oldAppName, !!appDef.isLegacyAppName); + + dataStore.getAppsDataStore().nameAllowedOrThrow(newAppName); + + return self.ensureNotBuilding(oldAppName); + }) + .then(function () { + Logger.d(`Check if service is running: ${oldServiceName}`); + return dockerApi.isServiceRunningByName(oldServiceName); + }) + .then(function (isRunning) { + if (!isRunning) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + "Service is not running!", + ); + } + return dockerApi.removeServiceByName(oldServiceName); + }) + .then(function () { + return dataStore + .getAppsDataStore() + .renameApp(self.authenticator, oldAppName, newAppName); + }) + .then(function () { + return self.ensureServiceInitedAndUpdated(newAppName); + }) + .then(function () { + if (defaultSslOn) return self.enableSslForApp(newAppName); + }); + } + + removeApps(appNames: string[]) { + Logger.d(`Removing service for: ${appNames.join(", ")}`); + const self = this; + + const removeAppPromise = function (appName: string) { + const dockerApi = self.dockerApi; + const dataStore = self.dataStore; + let serviceName: string; + + return Promise.resolve() + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName); }) + .then(function (appDef) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!appDef.isLegacyAppName); + return self.ensureNotBuilding(appName); + }) + .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(); + }); + }; + + const promises = []; + for (const appName of appNames) { + promises.push(removeAppPromise(appName)); } - enableCustomDomainSsl(appName: string, customDomain: string) { - const self = this + return Promise.all(promises); + } - return Promise.resolve() - .then(function () { - return self.dataStore.getHasRootSsl() - }) - .then(function (rootHasSsl) { - if (!rootHasSsl) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - ERROR_FIRST_ENABLE_ROOT_SSL - ) - } + removeVolsSafe(volumes: IHashMapGeneric) { + const dockerApi = this.dockerApi; + const dataStore = this.dataStore; - Logger.d(`Verifying Captain owns domain: ${customDomain}`) + const volsFailedToDelete: IHashMapGeneric = {}; - return self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( - customDomain, - undefined - ) - }) - .then(function () { - Logger.d(`Enabling SSL for: ${appName} on ${customDomain}`) + return Promise.resolve() + .then(function () { + return dataStore.getAppsDataStore().getAppDefinitions(); + }) + .then(function (apps) { + const physicalVolumesInUse: IHashMapGeneric = {}; - return self.dataStore - .getAppsDataStore() - .verifyCustomDomainBelongsToApp(appName, customDomain) - }) - .then(function () { - return self.domainResolveChecker.requestCertificateForDomain( - customDomain - ) - }) - .then(function () { - return self.dataStore - .getAppsDataStore() - .enableCustomDomainSsl(appName, customDomain) - }) - .then(function () { - return self.reloadLoadBalancer() - }) + Object.keys(apps).forEach((appName) => { + const app = apps[appName]; + const volsInApp = app.volumes || []; + + volsInApp.forEach((volume) => { + const volumeName = volume.volumeName; + if (!volumeName) { + return; + } + + const physicalVolumeName = dataStore + .getAppsDataStore() + .getVolumeName(volumeName, !!app.isLegacyAppName); + physicalVolumesInUse[physicalVolumeName] = true; + }); + }); + + const volumesTryToDelete: string[] = []; + Object.keys(volumes).forEach((physicalVolumeName) => { + const logicalVolumeName = volumes[physicalVolumeName]; + if (physicalVolumesInUse[physicalVolumeName]) { + volsFailedToDelete[logicalVolumeName] = true; + } else { + volumesTryToDelete.push(physicalVolumeName); + } + }); + + return dockerApi.deleteVols(volumesTryToDelete); + }) + .then(function (failedVols) { + failedVols.forEach((physicalVolumeName) => { + const logicalVolumeName = + volumes[physicalVolumeName] || physicalVolumeName; + volsFailedToDelete[logicalVolumeName] = true; + }); + + return Object.keys(volsFailedToDelete); + }); + } + + createPreDeployFunctionIfExist(app: IAppDef): PreDeployFunction | undefined { + let preDeployFunction = app.preDeployFunction; + + if (!preDeployFunction) { + return undefined; } - addCustomDomain(appName: string, customDomain: string) { - const self = this - - return Promise.resolve() - .then(function () { - const rootDomain = self.dataStore.getRootDomain() - - try { - Utils.checkCustomDomain(customDomain, appName, rootDomain) - } catch (error) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_BAD_NAME, - error - ) - } - }) - .then(function () { - return self.domainResolveChecker.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: string, customDomain: string) { - 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: string) { - const self = this - - let rootDomain: string - - return Promise.resolve() - .then(function () { - return self.dataStore.getHasRootSsl() - }) - .then(function (rootHasSsl) { - if (!rootHasSsl) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - ERROR_FIRST_ENABLE_ROOT_SSL - ) - } - 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 self.domainResolveChecker.requestCertificateForDomain( - domainName - ) - }) - .then(function () { - return self.dataStore - .getAppsDataStore() - .setSslForDefaultSubDomain(appName, true) - }) - .then(function () { - return self.reloadLoadBalancer() - }) - } - - verifyCaptainOwnsGenericSubDomain(appName: string) { - const self = this - - let rootDomain: string - - 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 self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( - domainName, - undefined - ) - }) - } - - renameApp(oldAppName: string, newAppName: string) { - Logger.d(`Renaming app: ${oldAppName}`) - const self = this - - const dockerApi = this.dockerApi - const dataStore = this.dataStore - - let defaultSslOn = false - let oldServiceName: string - - return Promise.resolve() - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(oldAppName) - }) - .then(function (appDef) { - defaultSslOn = !!appDef.hasDefaultSubDomainSsl - oldServiceName = dataStore - .getAppsDataStore() - .getServiceName(oldAppName, !!appDef.isLegacyAppName) - - dataStore.getAppsDataStore().nameAllowedOrThrow(newAppName) - - return self.ensureNotBuilding(oldAppName) - }) - .then(function () { - Logger.d(`Check if service is running: ${oldServiceName}`) - return dockerApi.isServiceRunningByName(oldServiceName) - }) - .then(function (isRunning) { - if (!isRunning) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - 'Service is not running!' - ) - } - return dockerApi.removeServiceByName(oldServiceName) - }) - .then(function () { - return dataStore - .getAppsDataStore() - .renameApp(self.authenticator, oldAppName, newAppName) - }) - .then(function () { - return self.ensureServiceInitedAndUpdated(newAppName) - }) - .then(function () { - if (defaultSslOn) return self.enableSslForApp(newAppName) - }) - } - - removeApps(appNames: string[]) { - Logger.d(`Removing service for: ${appNames.join(', ')}`) - const self = this - - const removeAppPromise = function (appName: string) { - const dockerApi = self.dockerApi - const dataStore = self.dataStore - let serviceName: string - - return Promise.resolve() - .then(function () { - return dataStore - .getAppsDataStore() - .getAppDefinition(appName) - }) - .then(function (appDef) { - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!appDef.isLegacyAppName) - return self.ensureNotBuilding(appName) - }) - .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() - }) - } - - const promises = [] - for (const appName of appNames) { - promises.push(removeAppPromise(appName)) - } - - return Promise.all(promises) - } - - removeVolsSafe(volumes: IHashMapGeneric) { - const dockerApi = this.dockerApi - const dataStore = this.dataStore - - const volsFailedToDelete: IHashMapGeneric = {} - - return Promise.resolve() - .then(function () { - return dataStore.getAppsDataStore().getAppDefinitions() - }) - .then(function (apps) { - const physicalVolumesInUse: IHashMapGeneric = {} - - Object.keys(apps).forEach((appName) => { - const app = apps[appName] - const volsInApp = app.volumes || [] - - volsInApp.forEach((volume) => { - const volumeName = volume.volumeName - if (!volumeName) { - return - } - - const physicalVolumeName = dataStore - .getAppsDataStore() - .getVolumeName( - volumeName, - !!app.isLegacyAppName - ) - physicalVolumesInUse[physicalVolumeName] = true - }) - }) - - const volumesTryToDelete: string[] = [] - Object.keys(volumes).forEach((physicalVolumeName) => { - const logicalVolumeName = volumes[physicalVolumeName] - if (physicalVolumesInUse[physicalVolumeName]) { - volsFailedToDelete[logicalVolumeName] = true - } else { - volumesTryToDelete.push(physicalVolumeName) - } - }) - - return dockerApi.deleteVols(volumesTryToDelete) - }) - .then(function (failedVols) { - failedVols.forEach((physicalVolumeName) => { - const logicalVolumeName = - volumes[physicalVolumeName] || physicalVolumeName - volsFailedToDelete[logicalVolumeName] = true - }) - - return Object.keys(volsFailedToDelete) - }) - } - - createPreDeployFunctionIfExist( - app: IAppDef - ): PreDeployFunction | undefined { - let preDeployFunction = app.preDeployFunction - - if (!preDeployFunction) { - return undefined - } - - /* + /* ////////////////////////////////// Expected content of the file ////////////////////////// console.log('-------------------------------'+new Date()); @@ -601,424 +580,415 @@ class ServiceManager { }; */ - preDeployFunction = - preDeployFunction + '\n\n module.exports = preDeployFunction' + preDeployFunction = + preDeployFunction + "\n\n module.exports = preDeployFunction"; - return requireFromString(preDeployFunction) - } + return requireFromString(preDeployFunction); + } - ensureNotBuilding(appName: string) { - if (this.activeOrScheduledBuilds[appName]) - throw ApiStatusCodes.createError( - ApiStatusCodes.ILLEGAL_OPERATION, - `Build in-progress for ${appName}. Please wait...` - ) - } + ensureNotBuilding(appName: string) { + if (this.activeOrScheduledBuilds[appName]) + throw ApiStatusCodes.createError( + ApiStatusCodes.ILLEGAL_OPERATION, + `Build in-progress for ${appName}. Please wait...`, + ); + } - updateAppDefinition( - appName: string, - projectId: string, - description: string, - instanceCount: number, - captainDefinitionRelativeFilePath: string, - envVars: IAppEnvVar[], - volumes: IAppVolume[], - tags: IAppTag[], - nodeId: string, - notExposeAsWebApp: boolean, - containerHttpPort: number, - httpAuth: IHttpAuth, - forceSsl: boolean, - ports: IAppPort[], - repoInfo: RepoInfo, - customNginxConfig: string, - redirectDomain: string, - preDeployFunction: string, - serviceUpdateOverride: string, - websocketSupport: boolean, - appDeployTokenConfig: AppDeployTokenConfig - ) { - const self = this - const dataStore = this.dataStore - const dockerApi = this.dockerApi + updateAppDefinition( + appName: string, + projectId: string, + description: string, + instanceCount: number, + captainDefinitionRelativeFilePath: string, + envVars: IAppEnvVar[], + volumes: IAppVolume[], + tags: IAppTag[], + nodeId: string, + notExposeAsWebApp: boolean, + containerHttpPort: number, + httpAuth: IHttpAuth, + forceSsl: boolean, + ports: IAppPort[], + repoInfo: RepoInfo, + customNginxConfig: string, + redirectDomain: string, + preDeployFunction: string, + serviceUpdateOverride: string, + websocketSupport: boolean, + appDeployTokenConfig: AppDeployTokenConfig, + ) { + const self = this; + const dataStore = this.dataStore; + const dockerApi = this.dockerApi; - let serviceName: string + let serviceName: string; - let existingAppDefinition: IAppDef + let existingAppDefinition: IAppDef; - const checkIfNodeIdExists = function (nodeIdToCheck: string) { - 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 is not part of the swarm cluster: ${nodeIdToCheck}` - ) - }) + const checkIfNodeIdExists = function (nodeIdToCheck: string) { + return dockerApi.getNodesInfo().then(function (nodeInfo) { + for (let i = 0; i < nodeInfo.length; i++) { + if (nodeIdToCheck === nodeInfo[i].nodeId) { + return; + } } - return Promise.resolve() - .then(function () { - projectId = `${projectId || ''}`.trim() - if (projectId) { - return dataStore - .getProjectsDataStore() - .getProject(projectId) + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + `Node ID you requested is not part of the swarm cluster: ${nodeIdToCheck}`, + ); + }); + }; - // if project is not found, it will throw an error - } - }) - .then(function () { - return self.ensureNotBuilding(appName) - }) - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName) - }) - .then(function (app) { - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!app.isLegacyAppName) + return Promise.resolve() + .then(function () { + projectId = `${projectId || ""}`.trim(); + if (projectId) { + return dataStore.getProjectsDataStore().getProject(projectId); - // 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: boolean) { - 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: string) { - if (!nodeIdRunningService) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - 'No NodeId was found. Try again in a minute...' - ) - } + // if project is not found, it will throw an error + } + }) + .then(function () { + return self.ensureNotBuilding(appName); + }) + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName); + }) + .then(function (app) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!app.isLegacyAppName); - 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 () { - serviceUpdateOverride = serviceUpdateOverride - ? `${serviceUpdateOverride}`.trim() - : '' - if (!serviceUpdateOverride) { - // no override! - return - } - - if (!Utils.convertYamlOrJsonToObject(serviceUpdateOverride)) { + // 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: boolean) { + if (!isRunning) { throw ApiStatusCodes.createError( - ApiStatusCodes.ILLEGAL_PARAMETER, - 'serviceUpdateOverride must be either a valid JSON object starting with { or an equivalent yaml' - ) - } - }) - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName) - }) - .then(function (appDef) { - existingAppDefinition = appDef - - return dataStore - .getAppsDataStore() - .updateAppDefinitionInDb( - appName, - projectId, - description, - instanceCount, - captainDefinitionRelativeFilePath, - envVars, - volumes, - tags, - nodeId, - notExposeAsWebApp, - containerHttpPort, - httpAuth, - forceSsl, - ports, - repoInfo, - self.authenticator, - customNginxConfig, - redirectDomain, - preDeployFunction, - serviceUpdateOverride, - websocketSupport, - appDeployTokenConfig - ) - }) - .then(function () { - return self.ensureServiceInitedAndUpdated(appName) - }) - .catch(function (error) { - if ( - error && - error.captainErrorType === - ApiStatusCodes.STATUS_ERROR_NGINX_VALIDATION_FAILED - ) { - // Revert back to the old definition because the nginx config is invalid - if (existingAppDefinition) { - Logger.d( - `nginx validation failed, reverting configs for: ${appName}` - ) - return dataStore - .getAppsDataStore() - .updateAppDefinitionInDb( - appName, - existingAppDefinition.projectId, - existingAppDefinition.description, - existingAppDefinition.instanceCount, - existingAppDefinition.captainDefinitionRelativeFilePath, - existingAppDefinition.envVars, - existingAppDefinition.volumes, - existingAppDefinition.tags || [], - existingAppDefinition.nodeId || '', - existingAppDefinition.notExposeAsWebApp, - existingAppDefinition.containerHttpPort || 80, - existingAppDefinition.httpAuth, - existingAppDefinition.forceSsl, - existingAppDefinition.ports, - existingAppDefinition.appPushWebhook - ?.repoInfo || { - repo: '', - branch: '', - user: '', - password: '', - }, - self.authenticator, - existingAppDefinition.customNginxConfig || '', - existingAppDefinition.redirectDomain || '', - existingAppDefinition.preDeployFunction || '', - existingAppDefinition.serviceUpdateOverride || - '', - existingAppDefinition.websocketSupport, - existingAppDefinition.appDeployTokenConfig || { - enabled: false, - } - ) - .then(function () { - self.reloadLoadBalancer() - }) - .then(function () { - throw error - }) - } - } - - throw error - }) - } - - isAppBuilding(appName: string) { - return !!this.activeOrScheduledBuilds[appName] - } - - /** - * - * @returns the active build that it finds - */ - isAnyBuildRunning() { - const activeBuilds = this.activeOrScheduledBuilds - - for (const appName in activeBuilds) { - if (activeBuilds[appName]) { - return appName - } - } - - return undefined - } - - getBuildStatus(appName: string) { - const self = this - - return { - isAppBuilding: self.isAppBuilding(appName), - logs: self.buildLogsManager.getAppBuildLogs(appName).getLogs(), - isBuildFailed: - self.buildLogsManager.getAppBuildLogs(appName).isBuildFailed, - } - } - - logBuildFailed(appName: string, error: string) { - const self = this - error = (error || '') + '' - - self.eventLogger.trackEvent( - CapRoverEventFactory.create(CapRoverEventType.AppBuildFailed, { - appName, - error: error.substring(0, 1000), - }) - ) - this.buildLogsManager.getAppBuildLogs(appName).onBuildFailed(error) - } - - getAppLogs(appName: string, encoding: string) { - const dockerApi = this.dockerApi - const dataStore = this.dataStore - let serviceName: string - - return Promise.resolve() // - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName) - }) - .then(function (appDef) { - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!appDef.isLegacyAppName) - return dockerApi.getLogForService( - serviceName, - CaptainConstants.configs.appLogSize, - encoding - ) - }) - } - - ensureServiceInitedAndUpdated(appName: string) { - Logger.d(`Ensure service inited and Updated for: ${appName}`) - const self = this - - let imageName: string | undefined - const dockerApi = this.dockerApi - const dataStore = this.dataStore - let app: IAppDef - let dockerAuthObject: DockerAuthObj | undefined - let serviceName: string - - return Promise.resolve() // - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName) - }) - .then(function (appFound) { - app = appFound - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!app.isLegacyAppName) - - Logger.d(`Check if service is running: ${serviceName}`) - return dockerApi.isServiceRunningByName(serviceName) - }) - .then(function (isRunning) { - 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!' - ) - } - - if (isRunning) { - Logger.d(`Service is already running: ${serviceName}`) - return true - } else { - 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.configs.appPlaceholderImageName, - serviceName, - undefined, - undefined, - undefined, - undefined, - undefined - ) - .then(() => true) - } - }) - .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, - IDockerUpdateOrders.AUTO, - Utils.convertYamlOrJsonToObject(app.serviceUpdateOverride), - preDeployFunction - ) - }) - .then(function () { - return new Promise(function (resolve) { - // Waiting 2 extra seconds for docker DNS to pickup the service name - setTimeout(resolve, 2000) + ApiStatusCodes.STATUS_ERROR_GENERIC, + "Cannot find the service. Try again in a minute...", + ); + } + return dockerApi.getNodeIdByServiceName(serviceName, 0); }) - }) - .then(function () { - return self.reloadLoadBalancer() - }) + .then(function (nodeIdRunningService: string) { + 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 () { + serviceUpdateOverride = serviceUpdateOverride + ? `${serviceUpdateOverride}`.trim() + : ""; + if (!serviceUpdateOverride) { + // no override! + return; + } + + if (!Utils.convertYamlOrJsonToObject(serviceUpdateOverride)) { + throw ApiStatusCodes.createError( + ApiStatusCodes.ILLEGAL_PARAMETER, + "serviceUpdateOverride must be either a valid JSON object starting with { or an equivalent yaml", + ); + } + }) + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName); + }) + .then(function (appDef) { + existingAppDefinition = appDef; + + return dataStore + .getAppsDataStore() + .updateAppDefinitionInDb( + appName, + projectId, + description, + instanceCount, + captainDefinitionRelativeFilePath, + envVars, + volumes, + tags, + nodeId, + notExposeAsWebApp, + containerHttpPort, + httpAuth, + forceSsl, + ports, + repoInfo, + self.authenticator, + customNginxConfig, + redirectDomain, + preDeployFunction, + serviceUpdateOverride, + websocketSupport, + appDeployTokenConfig, + ); + }) + .then(function () { + return self.ensureServiceInitedAndUpdated(appName); + }) + .catch(function (error) { + if ( + error && + error.captainErrorType === + ApiStatusCodes.STATUS_ERROR_NGINX_VALIDATION_FAILED + ) { + // Revert back to the old definition because the nginx config is invalid + if (existingAppDefinition) { + Logger.d( + `nginx validation failed, reverting configs for: ${appName}`, + ); + return dataStore + .getAppsDataStore() + .updateAppDefinitionInDb( + appName, + existingAppDefinition.projectId, + existingAppDefinition.description, + existingAppDefinition.instanceCount, + existingAppDefinition.captainDefinitionRelativeFilePath, + existingAppDefinition.envVars, + existingAppDefinition.volumes, + existingAppDefinition.tags || [], + existingAppDefinition.nodeId || "", + existingAppDefinition.notExposeAsWebApp, + existingAppDefinition.containerHttpPort || 80, + existingAppDefinition.httpAuth, + existingAppDefinition.forceSsl, + existingAppDefinition.ports, + existingAppDefinition.appPushWebhook?.repoInfo || { + repo: "", + branch: "", + user: "", + password: "", + }, + self.authenticator, + existingAppDefinition.customNginxConfig || "", + existingAppDefinition.redirectDomain || "", + existingAppDefinition.preDeployFunction || "", + existingAppDefinition.serviceUpdateOverride || "", + existingAppDefinition.websocketSupport, + existingAppDefinition.appDeployTokenConfig || { + enabled: false, + }, + ) + .then(function () { + self.reloadLoadBalancer(); + }) + .then(function () { + throw error; + }); + } + } + + throw error; + }); + } + + isAppBuilding(appName: string) { + return !!this.activeOrScheduledBuilds[appName]; + } + + /** + * + * @returns the active build that it finds + */ + isAnyBuildRunning() { + const activeBuilds = this.activeOrScheduledBuilds; + + for (const appName in activeBuilds) { + if (activeBuilds[appName]) { + return appName; + } } - reloadLoadBalancer() { - Logger.d('Updating Load Balancer - ServiceManager') - const self = this - return self.loadBalancerManager.rePopulateNginxConfigFile() - } + return undefined; + } + + getBuildStatus(appName: string) { + const self = this; + + return { + isAppBuilding: self.isAppBuilding(appName), + logs: self.buildLogsManager.getAppBuildLogs(appName).getLogs(), + isBuildFailed: + self.buildLogsManager.getAppBuildLogs(appName).isBuildFailed, + }; + } + + logBuildFailed(appName: string, error: string) { + const self = this; + error = (error || "") + ""; + + self.eventLogger.trackEvent( + CapRoverEventFactory.create(CapRoverEventType.AppBuildFailed, { + appName, + error: error.substring(0, 1000), + }), + ); + this.buildLogsManager.getAppBuildLogs(appName).onBuildFailed(error); + } + + getAppLogs(appName: string, encoding: string) { + const dockerApi = this.dockerApi; + const dataStore = this.dataStore; + let serviceName: string; + + return Promise.resolve() // + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName); + }) + .then(function (appDef) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!appDef.isLegacyAppName); + return dockerApi.getLogForService( + serviceName, + CaptainConstants.configs.appLogSize, + encoding, + ); + }); + } + + ensureServiceInitedAndUpdated(appName: string) { + Logger.d(`Ensure service inited and Updated for: ${appName}`); + const self = this; + + let imageName: string | undefined; + const dockerApi = this.dockerApi; + const dataStore = this.dataStore; + let app: IAppDef; + let dockerAuthObject: DockerAuthObj | undefined; + let serviceName: string; + + return Promise.resolve() // + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName); + }) + .then(function (appFound) { + app = appFound; + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!app.isLegacyAppName); + + Logger.d(`Check if service is running: ${serviceName}`); + return dockerApi.isServiceRunningByName(serviceName); + }) + .then(function (isRunning) { + 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!", + ); + } + + if (isRunning) { + Logger.d(`Service is already running: ${serviceName}`); + return true; + } else { + 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.configs.appPlaceholderImageName, + serviceName, + undefined, + undefined, + undefined, + undefined, + undefined, + ) + .then(() => true); + } + }) + .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, + IDockerUpdateOrders.AUTO, + Utils.convertYamlOrJsonToObject(app.serviceUpdateOverride), + 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 - ServiceManager"); + const self = this; + return self.loadBalancerManager.rePopulateNginxConfigFile(); + } } -export default ServiceManager +export default ServiceManager; From ab46c51d66e0f97d7e29dcef72fbb896a4ed3835 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:31:53 -0700 Subject: [PATCH 11/20] Restore scoped formatting --- .../apps/appdefinition/AppDefinitionRouter.ts | 592 +++++++++--------- 1 file changed, 301 insertions(+), 291 deletions(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 256b081..ddc0099 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -1,336 +1,346 @@ npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. -import express = require("express"); -import ApiStatusCodes from "../../../../api/ApiStatusCodes"; -import BaseApi from "../../../../api/BaseApi"; +import express = require('express') +import ApiStatusCodes from '../../../../api/ApiStatusCodes' +import BaseApi from '../../../../api/BaseApi' import { - getAllAppDefinitions, - registerAppDefinition, - updateAppDefinition, -} from "../../../../handlers/users/apps/appdefinition/AppDefinitionHandler"; -import InjectionExtractor from "../../../../injection/InjectionExtractor"; -import { AppDeployTokenConfig } from "../../../../models/AppDefinition"; -import { IHashMapGeneric } from "../../../../models/ICacheGeneric"; -import CaptainManager from "../../../../user/system/CaptainManager"; -import Logger from "../../../../utils/Logger"; -import Utils from "../../../../utils/Utils"; + getAllAppDefinitions, + registerAppDefinition, + updateAppDefinition, +} from '../../../../handlers/users/apps/appdefinition/AppDefinitionHandler' +import InjectionExtractor from '../../../../injection/InjectionExtractor' +import { AppDeployTokenConfig } from '../../../../models/AppDefinition' +import { IHashMapGeneric } from '../../../../models/ICacheGeneric' +import CaptainManager from '../../../../user/system/CaptainManager' +import Logger from '../../../../utils/Logger' +import Utils from '../../../../utils/Utils' -const router = express.Router(); +const router = express.Router() // unused images -router.get("/unusedImages", function (req, res, next) { - return Promise.resolve() - .then(function () { - const mostRecentLimit = Number(req.query.mostRecentLimit || "0"); - return CaptainManager.get() - .getDiskCleanupManager() - .getUnusedImages(mostRecentLimit); - }) - .then(function (unusedImages) { - const baseApi = new BaseApi( - ApiStatusCodes.STATUS_OK, - "Unused images retrieved.", - ); - baseApi.data = {}; - baseApi.data.unusedImages = unusedImages; +router.get('/unusedImages', function (req, res, next) { + return Promise.resolve() + .then(function () { + const mostRecentLimit = Number(req.query.mostRecentLimit || '0') + return CaptainManager.get() + .getDiskCleanupManager() + .getUnusedImages(mostRecentLimit) + }) + .then(function (unusedImages) { + const baseApi = new BaseApi( + ApiStatusCodes.STATUS_OK, + 'Unused images retrieved.' + ) + baseApi.data = {} + baseApi.data.unusedImages = unusedImages - res.send(baseApi); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + res.send(baseApi) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) // delete images -router.post("/deleteImages", function (req, res, next) { - const imageIds = req.body.imageIds || []; +router.post('/deleteImages', function (req, res, next) { + const imageIds = req.body.imageIds || [] - return Promise.resolve() - .then(function () { - return CaptainManager.get() - .getDiskCleanupManager() - .deleteImages(imageIds); - }) - .then(function () { - const baseApi = new BaseApi(ApiStatusCodes.STATUS_OK, "Images Deleted."); - res.send(baseApi); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return Promise.resolve() + .then(function () { + return CaptainManager.get() + .getDiskCleanupManager() + .deleteImages(imageIds) + }) + .then(function () { + const baseApi = new BaseApi( + ApiStatusCodes.STATUS_OK, + 'Images Deleted.' + ) + res.send(baseApi) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) // Get All App Definitions -router.get("/", function (req, res, next) { - const dataStore = - InjectionExtractor.extractUserFromInjected(res).user.dataStore; - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.get('/', function (req, res, next) { + const dataStore = + InjectionExtractor.extractUserFromInjected(res).user.dataStore + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - return getAllAppDefinitions(dataStore, serviceManager) - .then(function (result) { - const baseApi = new BaseApi(ApiStatusCodes.STATUS_OK, result.message); - baseApi.data = result.data; - res.send(baseApi); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return getAllAppDefinitions(dataStore, serviceManager) + .then(function (result) { + const baseApi = new BaseApi( + ApiStatusCodes.STATUS_OK, + result.message + ) + baseApi.data = result.data + res.send(baseApi) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -router.post("/enablebasedomainssl/", function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.post('/enablebasedomainssl/', function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const appName = req.body.appName; + const appName = req.body.appName - return Promise.resolve() - .then(function () { - return serviceManager.enableSslForApp(appName); - }) - .then(function () { - const msg = `General SSL is enabled for: ${appName}`; - Logger.d(msg); - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return Promise.resolve() + .then(function () { + return serviceManager.enableSslForApp(appName) + }) + .then(function () { + const msg = `General SSL is enabled for: ${appName}` + Logger.d(msg) + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -router.post("/customdomain/", function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.post('/customdomain/', function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const appName = req.body.appName; - const customDomain = (req.body.customDomain || "").toLowerCase().trim(); + const appName = req.body.appName + const customDomain = (req.body.customDomain || '').toLowerCase().trim() - // verify customdomain.com going through the default NGINX - // Add customdomain.com to app in Data Store + // verify customdomain.com going through the default NGINX + // Add customdomain.com to app in Data Store - return Promise.resolve() - .then(function () { - return serviceManager.addCustomDomain(appName, customDomain); - }) - .then(function () { - const msg = `Custom domain is enabled for: ${appName} at ${customDomain}`; - Logger.d(msg); - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return Promise.resolve() + .then(function () { + return serviceManager.addCustomDomain(appName, customDomain) + }) + .then(function () { + const msg = `Custom domain is enabled for: ${appName} at ${customDomain}` + Logger.d(msg) + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -router.post("/removecustomdomain/", function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.post('/removecustomdomain/', function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const appName = req.body.appName; - const customDomain = (req.body.customDomain || "").toLowerCase(); + const appName = req.body.appName + const customDomain = (req.body.customDomain || '').toLowerCase() - return Promise.resolve() - .then(function () { - return serviceManager.removeCustomDomain(appName, customDomain); - }) - .then(function () { - const msg = `Custom domain is removed for: ${appName} at ${customDomain}`; - Logger.d(msg); - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return Promise.resolve() + .then(function () { + return serviceManager.removeCustomDomain(appName, customDomain) + }) + .then(function () { + const msg = `Custom domain is removed for: ${appName} at ${customDomain}` + Logger.d(msg) + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -router.post("/enablecustomdomainssl/", function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.post('/enablecustomdomainssl/', function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const appName = req.body.appName; - const customDomain = (req.body.customDomain || "").toLowerCase(); + const appName = req.body.appName + const customDomain = (req.body.customDomain || '').toLowerCase() - // Check if customdomain is already associated with app. If not, error out. - // Verify customdomain.com is served from /customdomain.com/ + // Check if customdomain is already associated with app. If not, error out. + // Verify customdomain.com is served from /customdomain.com/ - return Promise.resolve() - .then(function () { - return serviceManager.enableCustomDomainSsl(appName, customDomain); - }) - .then(function () { - const msg = `Custom domain SSL is enabled for: ${appName} at ${customDomain} `; - Logger.d(msg); - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return Promise.resolve() + .then(function () { + return serviceManager.enableCustomDomainSsl(appName, customDomain) + }) + .then(function () { + const msg = `Custom domain SSL is enabled for: ${appName} at ${customDomain} ` + Logger.d(msg) + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg)) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -router.post("/register/", function (req, res, next) { - const dataStore = - InjectionExtractor.extractUserFromInjected(res).user.dataStore; - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.post('/register/', function (req, res, next) { + const dataStore = + InjectionExtractor.extractUserFromInjected(res).user.dataStore + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const appName = req.body.appName as string; - const projectId = `${req.body.projectId || ""}`; - const hasPersistentData = !!req.body.hasPersistentData; - const isDetachedBuild = !!req.query.detached; + const appName = req.body.appName as string + const projectId = `${req.body.projectId || ''}` + const hasPersistentData = !!req.body.hasPersistentData + const isDetachedBuild = !!req.query.detached - return registerAppDefinition( - { appName, projectId, hasPersistentData, isDetachedBuild }, - dataStore, - serviceManager, - ) - .then(function (result) { - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return registerAppDefinition( + { appName, projectId, hasPersistentData, isDetachedBuild }, + dataStore, + serviceManager + ) + .then(function (result) { + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -router.post("/delete/", function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; - const dataStore = - InjectionExtractor.extractUserFromInjected(res).user.dataStore; +router.post('/delete/', function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager + const dataStore = + InjectionExtractor.extractUserFromInjected(res).user.dataStore - const appName: string = req.body.appName; - const volumes: string[] = req.body.volumes || []; - const appNames: string[] = req.body.appNames || []; - const appsToDelete: string[] = appNames.length ? appNames : [appName]; + const appName: string = req.body.appName + const volumes: string[] = req.body.volumes || [] + const appNames: string[] = req.body.appNames || [] + const appsToDelete: string[] = appNames.length ? appNames : [appName] - Logger.d(`Deleting app started: ${appName}`); - const volumesToDelete: IHashMapGeneric = {}; + Logger.d(`Deleting app started: ${appName}`) + const volumesToDelete: IHashMapGeneric = {} - return Promise.resolve() - .then(function () { - if (appNames.length > 0 && appName) { - throw ApiStatusCodes.createError( - ApiStatusCodes.ILLEGAL_OPERATION, - "Either appName or appNames should be provided", - ); - } - }) - .then(function () { - return dataStore.getAppsDataStore().getAppDefinitions(); - }) - .then(function (apps) { - appsToDelete.forEach((appNameToDelete) => { - const app = apps[appNameToDelete]; - if (!app) { - return; - } + return Promise.resolve() + .then(function () { + if (appNames.length > 0 && appName) { + throw ApiStatusCodes.createError( + ApiStatusCodes.ILLEGAL_OPERATION, + 'Either appName or appNames should be provided' + ) + } + }) + .then(function () { + return dataStore.getAppsDataStore().getAppDefinitions() + }) + .then(function (apps) { + appsToDelete.forEach((appNameToDelete) => { + const app = apps[appNameToDelete] + if (!app) { + return + } - const volumesForApp = app.volumes || []; - volumesForApp.forEach((volume) => { - const volumeName = volume.volumeName; - if (!volumeName || volumes.indexOf(volumeName) < 0) { - return; - } + const volumesForApp = app.volumes || [] + volumesForApp.forEach((volume) => { + const volumeName = volume.volumeName + if (!volumeName || volumes.indexOf(volumeName) < 0) { + return + } - const physicalVolumeName = dataStore - .getAppsDataStore() - .getVolumeName(volumeName, !!app.isLegacyAppName); - volumesToDelete[physicalVolumeName] = volumeName; - }); - }); - }) - .then(function () { - return serviceManager.removeApps(appsToDelete); - }) - .then(function () { - return Utils.getDelayedPromise(volumes.length ? 12000 : 0); - }) - .then(function () { - return serviceManager.removeVolsSafe(volumesToDelete); - }) - .then(function (failedVolsToRemoved) { - Logger.d(`Successfully deleted: ${appsToDelete.join(", ")}`); + const physicalVolumeName = dataStore + .getAppsDataStore() + .getVolumeName(volumeName, !!app.isLegacyAppName) + volumesToDelete[physicalVolumeName] = volumeName + }) + }) + }) + .then(function () { + return serviceManager.removeApps(appsToDelete) + }) + .then(function () { + return Utils.getDelayedPromise(volumes.length ? 12000 : 0) + }) + .then(function () { + return serviceManager.removeVolsSafe(volumesToDelete) + }) + .then(function (failedVolsToRemoved) { + Logger.d(`Successfully deleted: ${appsToDelete.join(', ')}`) - if (failedVolsToRemoved.length) { - const returnVal = new BaseApi( - ApiStatusCodes.STATUS_OK_PARTIALLY, - `App is deleted. Some volumes were not safe to delete. Delete skipped for: ${failedVolsToRemoved.join( - " , ", - )}`, - ); - returnVal.data = { volumesFailedToDelete: failedVolsToRemoved }; - res.send(returnVal); - } else { - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, "App is deleted")); - } - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + if (failedVolsToRemoved.length) { + const returnVal = new BaseApi( + ApiStatusCodes.STATUS_OK_PARTIALLY, + `App is deleted. Some volumes were not safe to delete. Delete skipped for: ${failedVolsToRemoved.join( + ' , ' + )}` + ) + returnVal.data = { volumesFailedToDelete: failedVolsToRemoved } + res.send(returnVal) + } else { + res.send( + new BaseApi(ApiStatusCodes.STATUS_OK, 'App is deleted') + ) + } + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -router.post("/rename/", function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.post('/rename/', function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const oldAppName = req.body.oldAppName + ""; - const newAppName = req.body.newAppName + ""; + const oldAppName = req.body.oldAppName + '' + const newAppName = req.body.newAppName + '' - Logger.d(`Renaming app started: From ${oldAppName} To ${newAppName} `); + Logger.d(`Renaming app started: From ${oldAppName} To ${newAppName} `) - return Promise.resolve() - .then(function () { - return serviceManager.renameApp(oldAppName, newAppName); - }) - .then(function () { - Logger.d("AppName is renamed"); - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, "AppName is renamed")); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return Promise.resolve() + .then(function () { + return serviceManager.renameApp(oldAppName, newAppName) + }) + .then(function () { + Logger.d('AppName is renamed') + res.send( + new BaseApi(ApiStatusCodes.STATUS_OK, 'AppName is renamed') + ) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) // Update app configs -router.post("/update/", function (req, res, next) { - const serviceManager = - InjectionExtractor.extractUserFromInjected(res).user.serviceManager; +router.post('/update/', function (req, res, next) { + const serviceManager = + InjectionExtractor.extractUserFromInjected(res).user.serviceManager - const appName = req.body.appName; - const projectId = req.body.projectId; - const nodeId = req.body.nodeId; - const captainDefinitionRelativeFilePath = - req.body.captainDefinitionRelativeFilePath; - const notExposeAsWebApp = req.body.notExposeAsWebApp; - const tags = req.body.tags; - const customNginxConfig = req.body.customNginxConfig; - const forceSsl = req.body.forceSsl; - const websocketSupport = req.body.websocketSupport; - const repoInfo = req.body.appPushWebhook - ? req.body.appPushWebhook.repoInfo - : undefined; - const envVars = req.body.envVars; - const volumes = req.body.volumes; - const ports = req.body.ports; - const instanceCount = req.body.instanceCount; - const redirectDomain = req.body.redirectDomain; - const preDeployFunction = req.body.preDeployFunction; - const serviceUpdateOverride = req.body.serviceUpdateOverride; - const containerHttpPort = req.body.containerHttpPort; - const httpAuth = req.body.httpAuth; - const appDeployTokenConfig = req.body.appDeployTokenConfig as - | AppDeployTokenConfig - | undefined; - const description = req.body.description; + const appName = req.body.appName + const projectId = req.body.projectId + const nodeId = req.body.nodeId + const captainDefinitionRelativeFilePath = + req.body.captainDefinitionRelativeFilePath + const notExposeAsWebApp = req.body.notExposeAsWebApp + const tags = req.body.tags + const customNginxConfig = req.body.customNginxConfig + const forceSsl = req.body.forceSsl + const websocketSupport = req.body.websocketSupport + const repoInfo = req.body.appPushWebhook + ? req.body.appPushWebhook.repoInfo + : undefined + const envVars = req.body.envVars + const volumes = req.body.volumes + const ports = req.body.ports + const instanceCount = req.body.instanceCount + const redirectDomain = req.body.redirectDomain + const preDeployFunction = req.body.preDeployFunction + const serviceUpdateOverride = req.body.serviceUpdateOverride + const containerHttpPort = req.body.containerHttpPort + const httpAuth = req.body.httpAuth + const appDeployTokenConfig = req.body.appDeployTokenConfig as + | AppDeployTokenConfig + | undefined + const description = req.body.description - return updateAppDefinition( - { - appName, - projectId, - description, - instanceCount, - captainDefinitionRelativeFilePath, - envVars, - volumes, - tags, - nodeId, - notExposeAsWebApp, - containerHttpPort, - httpAuth, - forceSsl, - ports, - repoInfo, - customNginxConfig, - redirectDomain, - preDeployFunction, - serviceUpdateOverride, - websocketSupport, - appDeployTokenConfig, - }, - serviceManager, - ) - .then(function (result) { - res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)); - }) - .catch(ApiStatusCodes.createCatcher(res)); -}); + return updateAppDefinition( + { + appName, + projectId, + description, + instanceCount, + captainDefinitionRelativeFilePath, + envVars, + volumes, + tags, + nodeId, + notExposeAsWebApp, + containerHttpPort, + httpAuth, + forceSsl, + ports, + repoInfo, + customNginxConfig, + redirectDomain, + preDeployFunction, + serviceUpdateOverride, + websocketSupport, + appDeployTokenConfig, + }, + serviceManager + ) + .then(function (result) { + res.send(new BaseApi(ApiStatusCodes.STATUS_OK, result.message)) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) -export default router; +export default router From f86744ab7374a3b51cd49bd0470bf84f2e47ad32 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:32:35 -0700 Subject: [PATCH 12/20] Restore scoped formatting --- src/routes/user/apps/appdefinition/AppDefinitionRouter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index ddc0099..282f361 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -1,4 +1,3 @@ -npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. import express = require('express') import ApiStatusCodes from '../../../../api/ApiStatusCodes' import BaseApi from '../../../../api/BaseApi' From 922ed3f20714d481daa97dc84f12514e668f5ef8 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sat, 18 Jul 2026 22:32:42 -0700 Subject: [PATCH 13/20] Restore scoped formatting --- src/user/ServiceManager.ts | 1907 ++++++++++++++++++------------------ 1 file changed, 967 insertions(+), 940 deletions(-) diff --git a/src/user/ServiceManager.ts b/src/user/ServiceManager.ts index 42c4d5d..a6e5eb4 100644 --- a/src/user/ServiceManager.ts +++ b/src/user/ServiceManager.ts @@ -1,572 +1,590 @@ -npm warn Unknown env config "http-proxy". This will stop working in the next major version of npm. -import ApiStatusCodes from "../api/ApiStatusCodes"; -import DataStore from "../datastore/DataStore"; -import DockerApi, { IDockerUpdateOrders } from "../docker/DockerApi"; +import ApiStatusCodes from '../api/ApiStatusCodes' +import DataStore from '../datastore/DataStore' +import DockerApi, { IDockerUpdateOrders } from '../docker/DockerApi' import { - AppDeployTokenConfig, - IAppDef, - IAppEnvVar, - IAppPort, - IAppTag, - IAppVolume, - IHttpAuth, - RepoInfo, -} from "../models/AppDefinition"; -import { DockerAuthObj } from "../models/DockerAuthObj"; -import { IHashMapGeneric } from "../models/ICacheGeneric"; -import { IImageSource } from "../models/IImageSource"; -import { PreDeployFunction } from "../models/OtherTypes"; -import CaptainConstants from "../utils/CaptainConstants"; -import Logger from "../utils/Logger"; -import Utils from "../utils/Utils"; -import Authenticator from "./Authenticator"; -import DockerRegistryHelper from "./DockerRegistryHelper"; -import ImageMaker, { BuildLogsManager } from "./ImageMaker"; -import { EventLogger } from "./events/EventLogger"; + AppDeployTokenConfig, + IAppDef, + IAppEnvVar, + IAppPort, + IAppTag, + IAppVolume, + IHttpAuth, + RepoInfo, +} from '../models/AppDefinition' +import { DockerAuthObj } from '../models/DockerAuthObj' +import { IHashMapGeneric } from '../models/ICacheGeneric' +import { IImageSource } from '../models/IImageSource' +import { PreDeployFunction } from '../models/OtherTypes' +import CaptainConstants from '../utils/CaptainConstants' +import Logger from '../utils/Logger' +import Utils from '../utils/Utils' +import Authenticator from './Authenticator' +import DockerRegistryHelper from './DockerRegistryHelper' +import ImageMaker, { BuildLogsManager } from './ImageMaker' +import { EventLogger } from './events/EventLogger' import { - CapRoverEventFactory, - CapRoverEventType, -} from "./events/ICapRoverEvent"; -import DomainResolveChecker from "./system/DomainResolveChecker"; -import LoadBalancerManager from "./system/LoadBalancerManager"; -import requireFromString = require("require-from-string"); + CapRoverEventFactory, + CapRoverEventType, +} from './events/ICapRoverEvent' +import DomainResolveChecker from './system/DomainResolveChecker' +import LoadBalancerManager from './system/LoadBalancerManager' +import requireFromString = require('require-from-string') const ERROR_FIRST_ENABLE_ROOT_SSL = - "You have to first enable SSL for your root domain"; + 'You have to first enable SSL for your root domain' -const serviceMangerCache = {} as IHashMapGeneric; +const serviceMangerCache = {} as IHashMapGeneric interface QueuedPromise { - resolve: undefined | ((reason?: unknown) => void); - reject: undefined | ((reason?: any) => void); - promise: undefined | Promise; + resolve: undefined | ((reason?: unknown) => void) + reject: undefined | ((reason?: any) => void) + promise: undefined | Promise } interface QueuedBuild { - appName: string; - source: IImageSource; - promiseToSave: QueuedPromise; + appName: string + source: IImageSource + promiseToSave: QueuedPromise } class ServiceManager { - static get( - namespace: string, - authenticator: Authenticator, - dataStore: DataStore, - dockerApi: DockerApi, - loadBalancerManager: LoadBalancerManager, - eventLogger: EventLogger, - domainResolveChecker: DomainResolveChecker, - ) { - if (!serviceMangerCache[namespace]) { - serviceMangerCache[namespace] = new ServiceManager( - dataStore, - authenticator, - dockerApi, - loadBalancerManager, - eventLogger, - domainResolveChecker, - ); - } - return serviceMangerCache[namespace]; - } - - private activeOrScheduledBuilds: IHashMapGeneric; - private buildLogsManager: BuildLogsManager; - private queuedBuilds: QueuedBuild[]; - private isReady: boolean; - private imageMaker: ImageMaker; - private dockerRegistryHelper: DockerRegistryHelper; - - constructor( - private dataStore: DataStore, - private authenticator: Authenticator, - private dockerApi: DockerApi, - private loadBalancerManager: LoadBalancerManager, - private eventLogger: EventLogger, - private domainResolveChecker: DomainResolveChecker, - ) { - this.activeOrScheduledBuilds = {}; - this.queuedBuilds = []; - this.buildLogsManager = new BuildLogsManager(); - this.isReady = true; - this.dockerRegistryHelper = new DockerRegistryHelper( - this.dataStore, - this.dockerApi, - ); - this.imageMaker = new ImageMaker( - this.dockerRegistryHelper, - this.dockerApi, - this.dataStore.getNameSpace(), - this.buildLogsManager, - ); - } - - getRegistryHelper() { - return this.dockerRegistryHelper; - } - - isInited() { - return this.isReady; - } - - scheduleDeployNewVersion(appName: string, source: IImageSource) { - const self = this; - - const activeBuildAppName = self.isAnyBuildRunning(); - this.activeOrScheduledBuilds[appName] = true; - - self.buildLogsManager.getAppBuildLogs(appName).clear(); - - if (activeBuildAppName) { - const existingBuildForTheSameApp = self.queuedBuilds.find( - (v) => v.appName === appName, - ); - - if (existingBuildForTheSameApp) { - self.buildLogsManager - .getAppBuildLogs(appName) - .log( - `A build for ${appName} was queued, it's now being replaced with a new build...`, - ); - - // replacing the new source! - existingBuildForTheSameApp.source = source; - - const existingPromise = - existingBuildForTheSameApp.promiseToSave.promise; - - if (!existingPromise) - throw new Error("Existing promise for the queued app is NULL!!"); - - return existingPromise; - } - - self.buildLogsManager - .getAppBuildLogs(appName) - .log( - `An active build (${activeBuildAppName}) is in progress. This build is queued...`, - ); - - const promiseToSave: QueuedPromise = { - resolve: undefined, - reject: undefined, - promise: undefined, - }; - - const promise = new Promise(function (resolve, reject) { - promiseToSave.resolve = resolve; - promiseToSave.reject = reject; - }); - - promiseToSave.promise = promise; - - self.queuedBuilds.push({ appName, source, promiseToSave }); - - // This should only return when the build is finished, - // somehow we need save the promise in queue - for "attached builds" - return promise; + static get( + namespace: string, + authenticator: Authenticator, + dataStore: DataStore, + dockerApi: DockerApi, + loadBalancerManager: LoadBalancerManager, + eventLogger: EventLogger, + domainResolveChecker: DomainResolveChecker + ) { + if (!serviceMangerCache[namespace]) { + serviceMangerCache[namespace] = new ServiceManager( + dataStore, + authenticator, + dockerApi, + loadBalancerManager, + eventLogger, + domainResolveChecker + ) + } + return serviceMangerCache[namespace] } - return this.startDeployingNewVersion(appName, source); - } + private activeOrScheduledBuilds: IHashMapGeneric + private buildLogsManager: BuildLogsManager + private queuedBuilds: QueuedBuild[] + private isReady: boolean + private imageMaker: ImageMaker + private dockerRegistryHelper: DockerRegistryHelper - startDeployingNewVersion(appName: string, source: IImageSource) { - const self = this; - const dataStore = this.dataStore; - let deployedVersion: number; - - return Promise.resolve() // - .then(function () { - return dataStore.getAppsDataStore().createNewVersion(appName); - }) - .then(function (appVersion) { - deployedVersion = appVersion; - return dataStore - .getAppsDataStore() - .getAppDefinition(appName) - .then(function (app) { - const envVars = app.envVars || []; - - return self.imageMaker.ensureImage( - source, - appName, - app.captainDefinitionRelativeFilePath, - appVersion, - envVars, - ); - }); - }) - .then(function (builtImage) { - return dataStore - .getAppsDataStore() - .setDeployedVersionAndImage(appName, deployedVersion, builtImage); - }) - .then(function () { - self.onBuildFinished(appName); - - self.eventLogger.trackEvent( - CapRoverEventFactory.create(CapRoverEventType.AppBuildSuccessful, { - appName, - }), - ); - - return self.ensureServiceInitedAndUpdated(appName); - }) - .catch(function (error) { - self.onBuildFinished(appName); - return new Promise(function (resolve, reject) { - self.logBuildFailed(appName, error); - reject(error); - }); - }); - } - - onBuildFinished(appName: string) { - const self = this; - self.activeOrScheduledBuilds[appName] = false; - - Promise.resolve().then(function () { - const newBuild = self.queuedBuilds.shift(); - if (newBuild) - self.startDeployingNewVersion(newBuild.appName, newBuild.source); - }); - } - - enableCustomDomainSsl(appName: string, customDomain: string) { - const self = this; - - return Promise.resolve() - .then(function () { - return self.dataStore.getHasRootSsl(); - }) - .then(function (rootHasSsl) { - if (!rootHasSsl) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - ERROR_FIRST_ENABLE_ROOT_SSL, - ); - } - - Logger.d(`Verifying Captain owns domain: ${customDomain}`); - - return self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( - customDomain, - undefined, - ); - }) - .then(function () { - Logger.d(`Enabling SSL for: ${appName} on ${customDomain}`); - - return self.dataStore - .getAppsDataStore() - .verifyCustomDomainBelongsToApp(appName, customDomain); - }) - .then(function () { - return self.domainResolveChecker.requestCertificateForDomain( - customDomain, - ); - }) - .then(function () { - return self.dataStore - .getAppsDataStore() - .enableCustomDomainSsl(appName, customDomain); - }) - .then(function () { - return self.reloadLoadBalancer(); - }); - } - - addCustomDomain(appName: string, customDomain: string) { - const self = this; - - return Promise.resolve() - .then(function () { - const rootDomain = self.dataStore.getRootDomain(); - - try { - Utils.checkCustomDomain(customDomain, appName, rootDomain); - } catch (error) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_BAD_NAME, - error, - ); - } - }) - .then(function () { - return self.domainResolveChecker.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: string, customDomain: string) { - 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: string) { - const self = this; - - let rootDomain: string; - - return Promise.resolve() - .then(function () { - return self.dataStore.getHasRootSsl(); - }) - .then(function (rootHasSsl) { - if (!rootHasSsl) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - ERROR_FIRST_ENABLE_ROOT_SSL, - ); - } - 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 self.domainResolveChecker.requestCertificateForDomain( - domainName, - ); - }) - .then(function () { - return self.dataStore - .getAppsDataStore() - .setSslForDefaultSubDomain(appName, true); - }) - .then(function () { - return self.reloadLoadBalancer(); - }); - } - - verifyCaptainOwnsGenericSubDomain(appName: string) { - const self = this; - - let rootDomain: string; - - 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 self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( - domainName, - undefined, - ); - }); - } - - renameApp(oldAppName: string, newAppName: string) { - Logger.d(`Renaming app: ${oldAppName}`); - const self = this; - - const dockerApi = this.dockerApi; - const dataStore = this.dataStore; - - let defaultSslOn = false; - let oldServiceName: string; - - return Promise.resolve() - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(oldAppName); - }) - .then(function (appDef) { - defaultSslOn = !!appDef.hasDefaultSubDomainSsl; - oldServiceName = dataStore - .getAppsDataStore() - .getServiceName(oldAppName, !!appDef.isLegacyAppName); - - dataStore.getAppsDataStore().nameAllowedOrThrow(newAppName); - - return self.ensureNotBuilding(oldAppName); - }) - .then(function () { - Logger.d(`Check if service is running: ${oldServiceName}`); - return dockerApi.isServiceRunningByName(oldServiceName); - }) - .then(function (isRunning) { - if (!isRunning) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - "Service is not running!", - ); - } - return dockerApi.removeServiceByName(oldServiceName); - }) - .then(function () { - return dataStore - .getAppsDataStore() - .renameApp(self.authenticator, oldAppName, newAppName); - }) - .then(function () { - return self.ensureServiceInitedAndUpdated(newAppName); - }) - .then(function () { - if (defaultSslOn) return self.enableSslForApp(newAppName); - }); - } - - removeApps(appNames: string[]) { - Logger.d(`Removing service for: ${appNames.join(", ")}`); - const self = this; - - const removeAppPromise = function (appName: string) { - const dockerApi = self.dockerApi; - const dataStore = self.dataStore; - let serviceName: string; - - return Promise.resolve() - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName); - }) - .then(function (appDef) { - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!appDef.isLegacyAppName); - return self.ensureNotBuilding(appName); - }) - .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(); - }); - }; - - const promises = []; - for (const appName of appNames) { - promises.push(removeAppPromise(appName)); + constructor( + private dataStore: DataStore, + private authenticator: Authenticator, + private dockerApi: DockerApi, + private loadBalancerManager: LoadBalancerManager, + private eventLogger: EventLogger, + private domainResolveChecker: DomainResolveChecker + ) { + this.activeOrScheduledBuilds = {} + this.queuedBuilds = [] + this.buildLogsManager = new BuildLogsManager() + this.isReady = true + this.dockerRegistryHelper = new DockerRegistryHelper( + this.dataStore, + this.dockerApi + ) + this.imageMaker = new ImageMaker( + this.dockerRegistryHelper, + this.dockerApi, + this.dataStore.getNameSpace(), + this.buildLogsManager + ) } - return Promise.all(promises); - } + getRegistryHelper() { + return this.dockerRegistryHelper + } - removeVolsSafe(volumes: IHashMapGeneric) { - const dockerApi = this.dockerApi; - const dataStore = this.dataStore; + isInited() { + return this.isReady + } - const volsFailedToDelete: IHashMapGeneric = {}; + scheduleDeployNewVersion(appName: string, source: IImageSource) { + const self = this - return Promise.resolve() - .then(function () { - return dataStore.getAppsDataStore().getAppDefinitions(); - }) - .then(function (apps) { - const physicalVolumesInUse: IHashMapGeneric = {}; + const activeBuildAppName = self.isAnyBuildRunning() + this.activeOrScheduledBuilds[appName] = true - Object.keys(apps).forEach((appName) => { - const app = apps[appName]; - const volsInApp = app.volumes || []; + self.buildLogsManager.getAppBuildLogs(appName).clear() - volsInApp.forEach((volume) => { - const volumeName = volume.volumeName; - if (!volumeName) { - return; + if (activeBuildAppName) { + const existingBuildForTheSameApp = self.queuedBuilds.find( + (v) => v.appName === appName + ) + + if (existingBuildForTheSameApp) { + self.buildLogsManager + .getAppBuildLogs(appName) + .log( + `A build for ${appName} was queued, it's now being replaced with a new build...` + ) + + // replacing the new source! + existingBuildForTheSameApp.source = source + + const existingPromise = + existingBuildForTheSameApp.promiseToSave.promise + + if (!existingPromise) + throw new Error( + 'Existing promise for the queued app is NULL!!' + ) + + return existingPromise } - const physicalVolumeName = dataStore - .getAppsDataStore() - .getVolumeName(volumeName, !!app.isLegacyAppName); - physicalVolumesInUse[physicalVolumeName] = true; - }); - }); + self.buildLogsManager + .getAppBuildLogs(appName) + .log( + `An active build (${activeBuildAppName}) is in progress. This build is queued...` + ) - const volumesTryToDelete: string[] = []; - Object.keys(volumes).forEach((physicalVolumeName) => { - const logicalVolumeName = volumes[physicalVolumeName]; - if (physicalVolumesInUse[physicalVolumeName]) { - volsFailedToDelete[logicalVolumeName] = true; - } else { - volumesTryToDelete.push(physicalVolumeName); - } - }); + const promiseToSave: QueuedPromise = { + resolve: undefined, + reject: undefined, + promise: undefined, + } - return dockerApi.deleteVols(volumesTryToDelete); - }) - .then(function (failedVols) { - failedVols.forEach((physicalVolumeName) => { - const logicalVolumeName = - volumes[physicalVolumeName] || physicalVolumeName; - volsFailedToDelete[logicalVolumeName] = true; - }); + const promise = new Promise(function (resolve, reject) { + promiseToSave.resolve = resolve + promiseToSave.reject = reject + }) - return Object.keys(volsFailedToDelete); - }); - } + promiseToSave.promise = promise - createPreDeployFunctionIfExist(app: IAppDef): PreDeployFunction | undefined { - let preDeployFunction = app.preDeployFunction; + self.queuedBuilds.push({ appName, source, promiseToSave }) - if (!preDeployFunction) { - return undefined; + // This should only return when the build is finished, + // somehow we need save the promise in queue - for "attached builds" + return promise + } + + return this.startDeployingNewVersion(appName, source) } - /* + startDeployingNewVersion(appName: string, source: IImageSource) { + const self = this + const dataStore = this.dataStore + let deployedVersion: number + + return Promise.resolve() // + .then(function () { + return dataStore.getAppsDataStore().createNewVersion(appName) + }) + .then(function (appVersion) { + deployedVersion = appVersion + return dataStore + .getAppsDataStore() + .getAppDefinition(appName) + .then(function (app) { + const envVars = app.envVars || [] + + return self.imageMaker.ensureImage( + source, + appName, + app.captainDefinitionRelativeFilePath, + appVersion, + envVars + ) + }) + }) + .then(function (builtImage) { + return dataStore + .getAppsDataStore() + .setDeployedVersionAndImage( + appName, + deployedVersion, + builtImage + ) + }) + .then(function () { + self.onBuildFinished(appName) + + self.eventLogger.trackEvent( + CapRoverEventFactory.create( + CapRoverEventType.AppBuildSuccessful, + { + appName, + } + ) + ) + + return self.ensureServiceInitedAndUpdated(appName) + }) + .catch(function (error) { + self.onBuildFinished(appName) + return new Promise(function (resolve, reject) { + self.logBuildFailed(appName, error) + reject(error) + }) + }) + } + + onBuildFinished(appName: string) { + const self = this + self.activeOrScheduledBuilds[appName] = false + + Promise.resolve().then(function () { + const newBuild = self.queuedBuilds.shift() + if (newBuild) + self.startDeployingNewVersion(newBuild.appName, newBuild.source) + }) + } + + enableCustomDomainSsl(appName: string, customDomain: string) { + const self = this + + return Promise.resolve() + .then(function () { + return self.dataStore.getHasRootSsl() + }) + .then(function (rootHasSsl) { + if (!rootHasSsl) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + ERROR_FIRST_ENABLE_ROOT_SSL + ) + } + + Logger.d(`Verifying Captain owns domain: ${customDomain}`) + + return self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( + customDomain, + undefined + ) + }) + .then(function () { + Logger.d(`Enabling SSL for: ${appName} on ${customDomain}`) + + return self.dataStore + .getAppsDataStore() + .verifyCustomDomainBelongsToApp(appName, customDomain) + }) + .then(function () { + return self.domainResolveChecker.requestCertificateForDomain( + customDomain + ) + }) + .then(function () { + return self.dataStore + .getAppsDataStore() + .enableCustomDomainSsl(appName, customDomain) + }) + .then(function () { + return self.reloadLoadBalancer() + }) + } + + addCustomDomain(appName: string, customDomain: string) { + const self = this + + return Promise.resolve() + .then(function () { + const rootDomain = self.dataStore.getRootDomain() + + try { + Utils.checkCustomDomain(customDomain, appName, rootDomain) + } catch (error) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_BAD_NAME, + error + ) + } + }) + .then(function () { + return self.domainResolveChecker.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: string, customDomain: string) { + 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: string) { + const self = this + + let rootDomain: string + + return Promise.resolve() + .then(function () { + return self.dataStore.getHasRootSsl() + }) + .then(function (rootHasSsl) { + if (!rootHasSsl) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + ERROR_FIRST_ENABLE_ROOT_SSL + ) + } + 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 self.domainResolveChecker.requestCertificateForDomain( + domainName + ) + }) + .then(function () { + return self.dataStore + .getAppsDataStore() + .setSslForDefaultSubDomain(appName, true) + }) + .then(function () { + return self.reloadLoadBalancer() + }) + } + + verifyCaptainOwnsGenericSubDomain(appName: string) { + const self = this + + let rootDomain: string + + 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 self.domainResolveChecker.verifyCaptainOwnsDomainOrThrow( + domainName, + undefined + ) + }) + } + + renameApp(oldAppName: string, newAppName: string) { + Logger.d(`Renaming app: ${oldAppName}`) + const self = this + + const dockerApi = this.dockerApi + const dataStore = this.dataStore + + let defaultSslOn = false + let oldServiceName: string + + return Promise.resolve() + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(oldAppName) + }) + .then(function (appDef) { + defaultSslOn = !!appDef.hasDefaultSubDomainSsl + oldServiceName = dataStore + .getAppsDataStore() + .getServiceName(oldAppName, !!appDef.isLegacyAppName) + + dataStore.getAppsDataStore().nameAllowedOrThrow(newAppName) + + return self.ensureNotBuilding(oldAppName) + }) + .then(function () { + Logger.d(`Check if service is running: ${oldServiceName}`) + return dockerApi.isServiceRunningByName(oldServiceName) + }) + .then(function (isRunning) { + if (!isRunning) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + 'Service is not running!' + ) + } + return dockerApi.removeServiceByName(oldServiceName) + }) + .then(function () { + return dataStore + .getAppsDataStore() + .renameApp(self.authenticator, oldAppName, newAppName) + }) + .then(function () { + return self.ensureServiceInitedAndUpdated(newAppName) + }) + .then(function () { + if (defaultSslOn) return self.enableSslForApp(newAppName) + }) + } + + removeApps(appNames: string[]) { + Logger.d(`Removing service for: ${appNames.join(', ')}`) + const self = this + + const removeAppPromise = function (appName: string) { + const dockerApi = self.dockerApi + const dataStore = self.dataStore + let serviceName: string + + return Promise.resolve() + .then(function () { + return dataStore + .getAppsDataStore() + .getAppDefinition(appName) + }) + .then(function (appDef) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!appDef.isLegacyAppName) + return self.ensureNotBuilding(appName) + }) + .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() + }) + } + + const promises = [] + for (const appName of appNames) { + promises.push(removeAppPromise(appName)) + } + + return Promise.all(promises) + } + + removeVolsSafe(volumes: IHashMapGeneric) { + const dockerApi = this.dockerApi + const dataStore = this.dataStore + + const volsFailedToDelete: IHashMapGeneric = {} + + return Promise.resolve() + .then(function () { + return dataStore.getAppsDataStore().getAppDefinitions() + }) + .then(function (apps) { + const physicalVolumesInUse: IHashMapGeneric = {} + + Object.keys(apps).forEach((appName) => { + const app = apps[appName] + const volsInApp = app.volumes || [] + + volsInApp.forEach((volume) => { + const volumeName = volume.volumeName + if (!volumeName) { + return + } + + const physicalVolumeName = dataStore + .getAppsDataStore() + .getVolumeName(volumeName, !!app.isLegacyAppName) + physicalVolumesInUse[physicalVolumeName] = true + }) + }) + + const volumesTryToDelete: string[] = [] + Object.keys(volumes).forEach((physicalVolumeName) => { + const logicalVolumeName = volumes[physicalVolumeName] + if (physicalVolumesInUse[physicalVolumeName]) { + volsFailedToDelete[logicalVolumeName] = true + } else { + volumesTryToDelete.push(physicalVolumeName) + } + }) + + return dockerApi.deleteVols(volumesTryToDelete) + }) + .then(function (failedVols) { + failedVols.forEach((physicalVolumeName) => { + const logicalVolumeName = + volumes[physicalVolumeName] || physicalVolumeName + volsFailedToDelete[logicalVolumeName] = true + }) + + return Object.keys(volsFailedToDelete) + }) + } + + createPreDeployFunctionIfExist( + app: IAppDef + ): PreDeployFunction | undefined { + let preDeployFunction = app.preDeployFunction + + if (!preDeployFunction) { + return undefined + } + + /* ////////////////////////////////// Expected content of the file ////////////////////////// console.log('-------------------------------'+new Date()); @@ -580,415 +598,424 @@ class ServiceManager { }; */ - preDeployFunction = - preDeployFunction + "\n\n module.exports = preDeployFunction"; + preDeployFunction = + preDeployFunction + '\n\n module.exports = preDeployFunction' - return requireFromString(preDeployFunction); - } - - ensureNotBuilding(appName: string) { - if (this.activeOrScheduledBuilds[appName]) - throw ApiStatusCodes.createError( - ApiStatusCodes.ILLEGAL_OPERATION, - `Build in-progress for ${appName}. Please wait...`, - ); - } - - updateAppDefinition( - appName: string, - projectId: string, - description: string, - instanceCount: number, - captainDefinitionRelativeFilePath: string, - envVars: IAppEnvVar[], - volumes: IAppVolume[], - tags: IAppTag[], - nodeId: string, - notExposeAsWebApp: boolean, - containerHttpPort: number, - httpAuth: IHttpAuth, - forceSsl: boolean, - ports: IAppPort[], - repoInfo: RepoInfo, - customNginxConfig: string, - redirectDomain: string, - preDeployFunction: string, - serviceUpdateOverride: string, - websocketSupport: boolean, - appDeployTokenConfig: AppDeployTokenConfig, - ) { - const self = this; - const dataStore = this.dataStore; - const dockerApi = this.dockerApi; - - let serviceName: string; - - let existingAppDefinition: IAppDef; - - const checkIfNodeIdExists = function (nodeIdToCheck: string) { - 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 is not part of the swarm cluster: ${nodeIdToCheck}`, - ); - }); - }; - - return Promise.resolve() - .then(function () { - projectId = `${projectId || ""}`.trim(); - if (projectId) { - return dataStore.getProjectsDataStore().getProject(projectId); - - // if project is not found, it will throw an error - } - }) - .then(function () { - return self.ensureNotBuilding(appName); - }) - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName); - }) - .then(function (app) { - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!app.isLegacyAppName); - - // 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: boolean) { - 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: string) { - 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 () { - serviceUpdateOverride = serviceUpdateOverride - ? `${serviceUpdateOverride}`.trim() - : ""; - if (!serviceUpdateOverride) { - // no override! - return; - } - - if (!Utils.convertYamlOrJsonToObject(serviceUpdateOverride)) { - throw ApiStatusCodes.createError( - ApiStatusCodes.ILLEGAL_PARAMETER, - "serviceUpdateOverride must be either a valid JSON object starting with { or an equivalent yaml", - ); - } - }) - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName); - }) - .then(function (appDef) { - existingAppDefinition = appDef; - - return dataStore - .getAppsDataStore() - .updateAppDefinitionInDb( - appName, - projectId, - description, - instanceCount, - captainDefinitionRelativeFilePath, - envVars, - volumes, - tags, - nodeId, - notExposeAsWebApp, - containerHttpPort, - httpAuth, - forceSsl, - ports, - repoInfo, - self.authenticator, - customNginxConfig, - redirectDomain, - preDeployFunction, - serviceUpdateOverride, - websocketSupport, - appDeployTokenConfig, - ); - }) - .then(function () { - return self.ensureServiceInitedAndUpdated(appName); - }) - .catch(function (error) { - if ( - error && - error.captainErrorType === - ApiStatusCodes.STATUS_ERROR_NGINX_VALIDATION_FAILED - ) { - // Revert back to the old definition because the nginx config is invalid - if (existingAppDefinition) { - Logger.d( - `nginx validation failed, reverting configs for: ${appName}`, - ); - return dataStore - .getAppsDataStore() - .updateAppDefinitionInDb( - appName, - existingAppDefinition.projectId, - existingAppDefinition.description, - existingAppDefinition.instanceCount, - existingAppDefinition.captainDefinitionRelativeFilePath, - existingAppDefinition.envVars, - existingAppDefinition.volumes, - existingAppDefinition.tags || [], - existingAppDefinition.nodeId || "", - existingAppDefinition.notExposeAsWebApp, - existingAppDefinition.containerHttpPort || 80, - existingAppDefinition.httpAuth, - existingAppDefinition.forceSsl, - existingAppDefinition.ports, - existingAppDefinition.appPushWebhook?.repoInfo || { - repo: "", - branch: "", - user: "", - password: "", - }, - self.authenticator, - existingAppDefinition.customNginxConfig || "", - existingAppDefinition.redirectDomain || "", - existingAppDefinition.preDeployFunction || "", - existingAppDefinition.serviceUpdateOverride || "", - existingAppDefinition.websocketSupport, - existingAppDefinition.appDeployTokenConfig || { - enabled: false, - }, - ) - .then(function () { - self.reloadLoadBalancer(); - }) - .then(function () { - throw error; - }); - } - } - - throw error; - }); - } - - isAppBuilding(appName: string) { - return !!this.activeOrScheduledBuilds[appName]; - } - - /** - * - * @returns the active build that it finds - */ - isAnyBuildRunning() { - const activeBuilds = this.activeOrScheduledBuilds; - - for (const appName in activeBuilds) { - if (activeBuilds[appName]) { - return appName; - } + return requireFromString(preDeployFunction) } - return undefined; - } - - getBuildStatus(appName: string) { - const self = this; - - return { - isAppBuilding: self.isAppBuilding(appName), - logs: self.buildLogsManager.getAppBuildLogs(appName).getLogs(), - isBuildFailed: - self.buildLogsManager.getAppBuildLogs(appName).isBuildFailed, - }; - } - - logBuildFailed(appName: string, error: string) { - const self = this; - error = (error || "") + ""; - - self.eventLogger.trackEvent( - CapRoverEventFactory.create(CapRoverEventType.AppBuildFailed, { - appName, - error: error.substring(0, 1000), - }), - ); - this.buildLogsManager.getAppBuildLogs(appName).onBuildFailed(error); - } - - getAppLogs(appName: string, encoding: string) { - const dockerApi = this.dockerApi; - const dataStore = this.dataStore; - let serviceName: string; - - return Promise.resolve() // - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName); - }) - .then(function (appDef) { - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!appDef.isLegacyAppName); - return dockerApi.getLogForService( - serviceName, - CaptainConstants.configs.appLogSize, - encoding, - ); - }); - } - - ensureServiceInitedAndUpdated(appName: string) { - Logger.d(`Ensure service inited and Updated for: ${appName}`); - const self = this; - - let imageName: string | undefined; - const dockerApi = this.dockerApi; - const dataStore = this.dataStore; - let app: IAppDef; - let dockerAuthObject: DockerAuthObj | undefined; - let serviceName: string; - - return Promise.resolve() // - .then(function () { - return dataStore.getAppsDataStore().getAppDefinition(appName); - }) - .then(function (appFound) { - app = appFound; - serviceName = dataStore - .getAppsDataStore() - .getServiceName(appName, !!app.isLegacyAppName); - - Logger.d(`Check if service is running: ${serviceName}`); - return dockerApi.isServiceRunningByName(serviceName); - }) - .then(function (isRunning) { - 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!", - ); - } - - if (isRunning) { - Logger.d(`Service is already running: ${serviceName}`); - return true; - } else { - 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.configs.appPlaceholderImageName, - serviceName, - undefined, - undefined, - undefined, - undefined, - undefined, + ensureNotBuilding(appName: string) { + if (this.activeOrScheduledBuilds[appName]) + throw ApiStatusCodes.createError( + ApiStatusCodes.ILLEGAL_OPERATION, + `Build in-progress for ${appName}. Please wait...` ) - .then(() => true); + } + + updateAppDefinition( + appName: string, + projectId: string, + description: string, + instanceCount: number, + captainDefinitionRelativeFilePath: string, + envVars: IAppEnvVar[], + volumes: IAppVolume[], + tags: IAppTag[], + nodeId: string, + notExposeAsWebApp: boolean, + containerHttpPort: number, + httpAuth: IHttpAuth, + forceSsl: boolean, + ports: IAppPort[], + repoInfo: RepoInfo, + customNginxConfig: string, + redirectDomain: string, + preDeployFunction: string, + serviceUpdateOverride: string, + websocketSupport: boolean, + appDeployTokenConfig: AppDeployTokenConfig + ) { + const self = this + const dataStore = this.dataStore + const dockerApi = this.dockerApi + + let serviceName: string + + let existingAppDefinition: IAppDef + + const checkIfNodeIdExists = function (nodeIdToCheck: string) { + 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 is not part of the swarm cluster: ${nodeIdToCheck}` + ) + }) } - }) - .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, - IDockerUpdateOrders.AUTO, - Utils.convertYamlOrJsonToObject(app.serviceUpdateOverride), - 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(); - }); - } + return Promise.resolve() + .then(function () { + projectId = `${projectId || ''}`.trim() + if (projectId) { + return dataStore + .getProjectsDataStore() + .getProject(projectId) - reloadLoadBalancer() { - Logger.d("Updating Load Balancer - ServiceManager"); - const self = this; - return self.loadBalancerManager.rePopulateNginxConfigFile(); - } + // if project is not found, it will throw an error + } + }) + .then(function () { + return self.ensureNotBuilding(appName) + }) + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName) + }) + .then(function (app) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!app.isLegacyAppName) + + // 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: boolean) { + 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: string) { + 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 () { + serviceUpdateOverride = serviceUpdateOverride + ? `${serviceUpdateOverride}`.trim() + : '' + if (!serviceUpdateOverride) { + // no override! + return + } + + if (!Utils.convertYamlOrJsonToObject(serviceUpdateOverride)) { + throw ApiStatusCodes.createError( + ApiStatusCodes.ILLEGAL_PARAMETER, + 'serviceUpdateOverride must be either a valid JSON object starting with { or an equivalent yaml' + ) + } + }) + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName) + }) + .then(function (appDef) { + existingAppDefinition = appDef + + return dataStore + .getAppsDataStore() + .updateAppDefinitionInDb( + appName, + projectId, + description, + instanceCount, + captainDefinitionRelativeFilePath, + envVars, + volumes, + tags, + nodeId, + notExposeAsWebApp, + containerHttpPort, + httpAuth, + forceSsl, + ports, + repoInfo, + self.authenticator, + customNginxConfig, + redirectDomain, + preDeployFunction, + serviceUpdateOverride, + websocketSupport, + appDeployTokenConfig + ) + }) + .then(function () { + return self.ensureServiceInitedAndUpdated(appName) + }) + .catch(function (error) { + if ( + error && + error.captainErrorType === + ApiStatusCodes.STATUS_ERROR_NGINX_VALIDATION_FAILED + ) { + // Revert back to the old definition because the nginx config is invalid + if (existingAppDefinition) { + Logger.d( + `nginx validation failed, reverting configs for: ${appName}` + ) + return dataStore + .getAppsDataStore() + .updateAppDefinitionInDb( + appName, + existingAppDefinition.projectId, + existingAppDefinition.description, + existingAppDefinition.instanceCount, + existingAppDefinition.captainDefinitionRelativeFilePath, + existingAppDefinition.envVars, + existingAppDefinition.volumes, + existingAppDefinition.tags || [], + existingAppDefinition.nodeId || '', + existingAppDefinition.notExposeAsWebApp, + existingAppDefinition.containerHttpPort || 80, + existingAppDefinition.httpAuth, + existingAppDefinition.forceSsl, + existingAppDefinition.ports, + existingAppDefinition.appPushWebhook + ?.repoInfo || { + repo: '', + branch: '', + user: '', + password: '', + }, + self.authenticator, + existingAppDefinition.customNginxConfig || '', + existingAppDefinition.redirectDomain || '', + existingAppDefinition.preDeployFunction || '', + existingAppDefinition.serviceUpdateOverride || + '', + existingAppDefinition.websocketSupport, + existingAppDefinition.appDeployTokenConfig || { + enabled: false, + } + ) + .then(function () { + self.reloadLoadBalancer() + }) + .then(function () { + throw error + }) + } + } + + throw error + }) + } + + isAppBuilding(appName: string) { + return !!this.activeOrScheduledBuilds[appName] + } + + /** + * + * @returns the active build that it finds + */ + isAnyBuildRunning() { + const activeBuilds = this.activeOrScheduledBuilds + + for (const appName in activeBuilds) { + if (activeBuilds[appName]) { + return appName + } + } + + return undefined + } + + getBuildStatus(appName: string) { + const self = this + + return { + isAppBuilding: self.isAppBuilding(appName), + logs: self.buildLogsManager.getAppBuildLogs(appName).getLogs(), + isBuildFailed: + self.buildLogsManager.getAppBuildLogs(appName).isBuildFailed, + } + } + + logBuildFailed(appName: string, error: string) { + const self = this + error = (error || '') + '' + + self.eventLogger.trackEvent( + CapRoverEventFactory.create(CapRoverEventType.AppBuildFailed, { + appName, + error: error.substring(0, 1000), + }) + ) + this.buildLogsManager.getAppBuildLogs(appName).onBuildFailed(error) + } + + getAppLogs(appName: string, encoding: string) { + const dockerApi = this.dockerApi + const dataStore = this.dataStore + let serviceName: string + + return Promise.resolve() // + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName) + }) + .then(function (appDef) { + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!appDef.isLegacyAppName) + return dockerApi.getLogForService( + serviceName, + CaptainConstants.configs.appLogSize, + encoding + ) + }) + } + + ensureServiceInitedAndUpdated(appName: string) { + Logger.d(`Ensure service inited and Updated for: ${appName}`) + const self = this + + let imageName: string | undefined + const dockerApi = this.dockerApi + const dataStore = this.dataStore + let app: IAppDef + let dockerAuthObject: DockerAuthObj | undefined + let serviceName: string + + return Promise.resolve() // + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName) + }) + .then(function (appFound) { + app = appFound + serviceName = dataStore + .getAppsDataStore() + .getServiceName(appName, !!app.isLegacyAppName) + + Logger.d(`Check if service is running: ${serviceName}`) + return dockerApi.isServiceRunningByName(serviceName) + }) + .then(function (isRunning) { + 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!' + ) + } + + if (isRunning) { + Logger.d(`Service is already running: ${serviceName}`) + return true + } else { + 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.configs.appPlaceholderImageName, + serviceName, + undefined, + undefined, + undefined, + undefined, + undefined + ) + .then(() => true) + } + }) + .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, + IDockerUpdateOrders.AUTO, + Utils.convertYamlOrJsonToObject(app.serviceUpdateOverride), + 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 - ServiceManager') + const self = this + return self.loadBalancerManager.rePopulateNginxConfigFile() + } } -export default ServiceManager; +export default ServiceManager From 05f56a9cef655a50fa1ba261726fa463d38a6de8 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 19 Jul 2026 15:02:16 -0700 Subject: [PATCH 14/20] Reject missing apps before bulk deletion --- src/routes/user/apps/appdefinition/AppDefinitionRouter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 282f361..74dac0c 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -209,7 +209,10 @@ router.post('/delete/', function (req, res, next) { appsToDelete.forEach((appNameToDelete) => { const app = apps[appNameToDelete] if (!app) { - return + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + `App (${appNameToDelete}) could not be found. Make sure that you have created the app.` + ) } const volumesForApp = app.volumes || [] From 43cb3ee0614e834f00bb0bb5e06439500d70c835 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 19 Jul 2026 15:03:04 -0700 Subject: [PATCH 15/20] Extract app deletion preflight validation --- .../apps/appdefinition/AppDefinitionRouter.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 74dac0c..c5c9827 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -15,6 +15,20 @@ import Utils from '../../../../utils/Utils' const router = express.Router() +export function ensureAppsExist( + appNames: string[], + apps: IHashMapGeneric +) { + appNames.forEach((appName) => { + if (!apps[appName]) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + `App (${appName}) could not be found. Make sure that you have created the app.` + ) + } + }) +} + // unused images router.get('/unusedImages', function (req, res, next) { return Promise.resolve() @@ -206,14 +220,10 @@ router.post('/delete/', function (req, res, next) { return dataStore.getAppsDataStore().getAppDefinitions() }) .then(function (apps) { + ensureAppsExist(appsToDelete, apps) + appsToDelete.forEach((appNameToDelete) => { const app = apps[appNameToDelete] - if (!app) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - `App (${appNameToDelete}) could not be found. Make sure that you have created the app.` - ) - } const volumesForApp = app.volumes || [] volumesForApp.forEach((volume) => { From fe2819ca82c7e382cb03886ae49fb23efbd95129 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 19 Jul 2026 15:03:14 -0700 Subject: [PATCH 16/20] Test bulk deletion missing-app validation --- tests/AppDeletion.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/AppDeletion.test.ts diff --git a/tests/AppDeletion.test.ts b/tests/AppDeletion.test.ts new file mode 100644 index 0000000..4635df5 --- /dev/null +++ b/tests/AppDeletion.test.ts @@ -0,0 +1,13 @@ +import { ensureAppsExist } from '../src/routes/user/apps/appdefinition/AppDefinitionRouter' + +describe('app deletion', () => { + test('rejects the entire request when any requested app is missing', () => { + expect(() => + ensureAppsExist(['existing-app', 'missing-app'], { + 'existing-app': {}, + }) + ).toThrow( + 'App (missing-app) could not be found. Make sure that you have created the app.' + ) + }) +}) From 67107ed026209224c44eec977800768f1d7459b4 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 19 Jul 2026 15:03:56 -0700 Subject: [PATCH 17/20] Format app deletion validation helper --- src/routes/user/apps/appdefinition/AppDefinitionRouter.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index c5c9827..8e272d4 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -15,10 +15,7 @@ import Utils from '../../../../utils/Utils' const router = express.Router() -export function ensureAppsExist( - appNames: string[], - apps: IHashMapGeneric -) { +export function ensureAppsExist(appNames: string[], apps: IHashMapGeneric) { appNames.forEach((appName) => { if (!apps[appName]) { throw ApiStatusCodes.createError( From 5deaedc5237838d09f0adf81a177403dccbce38f Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 19 Jul 2026 15:05:55 -0700 Subject: [PATCH 18/20] Apply Prettier formatting --- src/routes/user/apps/appdefinition/AppDefinitionRouter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 8e272d4..c5c9827 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -15,7 +15,10 @@ import Utils from '../../../../utils/Utils' const router = express.Router() -export function ensureAppsExist(appNames: string[], apps: IHashMapGeneric) { +export function ensureAppsExist( + appNames: string[], + apps: IHashMapGeneric +) { appNames.forEach((appName) => { if (!apps[appName]) { throw ApiStatusCodes.createError( From 3a6f967f8d3f983109108d345e1ef6279a09d2e8 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 19 Jul 2026 15:33:53 -0700 Subject: [PATCH 19/20] Harden app deletion preflight --- src/routes/user/apps/appdefinition/AppDefinitionRouter.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index c5c9827..b877518 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -20,7 +20,7 @@ export function ensureAppsExist( apps: IHashMapGeneric ) { appNames.forEach((appName) => { - if (!apps[appName]) { + if (!Object.prototype.hasOwnProperty.call(apps, appName)) { throw ApiStatusCodes.createError( ApiStatusCodes.STATUS_ERROR_GENERIC, `App (${appName}) could not be found. Make sure that you have created the app.` @@ -243,7 +243,9 @@ router.post('/delete/', function (req, res, next) { return serviceManager.removeApps(appsToDelete) }) .then(function () { - return Utils.getDelayedPromise(volumes.length ? 12000 : 0) + return Utils.getDelayedPromise( + Object.keys(volumesToDelete).length ? 12000 : 0 + ) }) .then(function () { return serviceManager.removeVolsSafe(volumesToDelete) From 18bff1f125e712b0b1553f0b3f0ec0fea1227a91 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 19 Jul 2026 15:34:03 -0700 Subject: [PATCH 20/20] Test inherited app-name rejection --- tests/AppDeletion.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/AppDeletion.test.ts b/tests/AppDeletion.test.ts index 4635df5..fa3ad4d 100644 --- a/tests/AppDeletion.test.ts +++ b/tests/AppDeletion.test.ts @@ -10,4 +10,10 @@ describe('app deletion', () => { 'App (missing-app) could not be found. Make sure that you have created the app.' ) }) + + test('does not treat inherited object properties as existing apps', () => { + expect(() => ensureAppsExist(['constructor'], {})).toThrow( + 'App (constructor) could not be found. Make sure that you have created the app.' + ) + }) })