diff --git a/dashboard/src/components/app/Uninstall.vue b/dashboard/src/components/app/Uninstall.vue index 3fd093a11..afd675a30 100644 --- a/dashboard/src/components/app/Uninstall.vue +++ b/dashboard/src/components/app/Uninstall.vue @@ -5,7 +5,7 @@ const i18n = useI18n(); const t = i18n.t; import { ref, onMounted, useTemplateRef } from 'vue'; -import { Button, InputDialog, Spinner } from '@cloudron/pankow'; +import { Button, InputDialog } from '@cloudron/pankow'; import { prettyLongDate } from '@cloudron/pankow/utils'; import { APP_TYPES, RSTATES, ISTATES } from '../../constants.js'; import AppsModel from '../../models/AppsModel.js'; @@ -120,14 +120,14 @@ onMounted(async () => {

- +

- +

diff --git a/src/services.js b/src/services.js index 2530d4a09..78a9e0fcd 100644 --- a/src/services.js +++ b/src/services.js @@ -51,9 +51,6 @@ const MV_VOLUME_CMD = path.join(import.meta.dirname, 'scripts/mvvolume.sh'); // setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost // teardown is destructive. app data stored with the addon is lost // addons have 1-1 mapping with the manifest -// Lazily initialized to avoid circular dependency TDZ issues at module load time -// (docker, mailServer, sftp may not be fully initialized when this module first evaluates) -let _services; function requiresUpgrade(existingImageRef, currentImageRef) { const etag = docker.parseImageRef(existingImageRef), ctag = docker.parseImageRef(currentImageRef); @@ -165,7 +162,7 @@ async function startAppServices(app) { for (const addon of Object.keys(app.manifest.addons || {})) { if (!(addon in APP_SERVICES)) continue; - const [error] = await safe(APP_SERVICES()[addon].start(instance)); // assume addons name is service name + const [error] = await safe(APP_SERVICES[addon].start(instance)); // assume addons name is service name // error ignored because we don't want "start app" to error. use can fix it from Services if (error) debug(`startAppServices: ${addon}:${instance}. %o`, error); } @@ -178,7 +175,7 @@ async function stopAppServices(app) { for (const addon of Object.keys(app.manifest.addons || {})) { if (!(addon in APP_SERVICES)) continue; - const [error] = await safe(APP_SERVICES()[addon].stop(instance)); // assume addons name is service name + const [error] = await safe(APP_SERVICES[addon].stop(instance)); // assume addons name is service name // error ignored because we don't want "start app" to error. use can fix it from Services if (error) debug(`stopAppServices: ${addon}:${instance}. %o`, error); } @@ -1174,7 +1171,7 @@ async function setupRedis(app, options) { const redisServiceToken = hat(4 * 48); // Compute redis memory limit based on app's memory limit (this is arbitrary) - const memoryLimit = app.servicesConfig['redis']?.memoryLimit || APP_SERVICES()['redis'].defaultMemoryLimit; + const memoryLimit = app.servicesConfig['redis']?.memoryLimit || APP_SERVICES['redis'].defaultMemoryLimit; const recoveryMode = app.servicesConfig['redis']?.recoveryMode || false; const readOnly = !recoveryMode ? '--read-only' : ''; @@ -1412,75 +1409,71 @@ async function restartGraphite() { await docker.restartContainer('graphite'); } -function SERVICES() { - if (_services) return _services; - _services = { - turn: { - name: 'TURN', - status: statusTurn, - restart: docker.restartContainer.bind(null, 'turn'), - defaultMemoryLimit: 256 * 1024 * 1024 - }, - mail: { - name: 'Mail', - status: containerStatus.bind(null, 'mail'), - restart: mailServer.restart, - defaultMemoryLimit: mailServer.DEFAULT_MEMORY_LIMIT - }, - mongodb: { - name: 'MongoDB', - status: statusMongodb, - restart: restartMongodb, - defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 - }, - mysql: { - name: 'MySQL', - status: containerStatus.bind(null, 'mysql'), - restart: docker.restartContainer.bind(null, 'mysql'), - defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 - }, - postgresql: { - name: 'PostgreSQL', - status: containerStatus.bind(null, 'postgresql'), - restart: docker.restartContainer.bind(null, 'postgresql'), - defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 - }, - docker: { - name: 'Docker', - status: statusDocker, - restart: restartDocker, - defaultMemoryLimit: 0 - }, - unbound: { - name: 'Unbound', - status: statusUnbound, - restart: restartUnbound, - defaultMemoryLimit: 0 - }, - sftp: { - name: 'Filemanager', - status: containerStatus.bind(null, 'sftp'), - restart: docker.restartContainer.bind(null, 'sftp'), - defaultMemoryLimit: sftp.DEFAULT_MEMORY_LIMIT - }, - graphite: { - name: 'Metrics', - status: statusGraphite, - restart: restartGraphite, - defaultMemoryLimit: 256 * 1024 * 1024 - }, - nginx: { - name: 'Nginx', - status: statusNginx, - restart: restartNginx, - defaultMemoryLimit: 0 - } - }; - return _services; -} +const SERVICES = { + turn: { + name: 'TURN', + status: statusTurn, + restart: () => docker.restartContainer('turn'), + defaultMemoryLimit: 256 * 1024 * 1024 + }, + mail: { + name: 'Mail', + status: containerStatus.bind(null, 'mail'), + restart: () => mailServer.restart(), + get defaultMemoryLimit() { return mailServer.DEFAULT_MEMORY_LIMIT; } + }, + mongodb: { + name: 'MongoDB', + status: statusMongodb, + restart: restartMongodb, + defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 + }, + mysql: { + name: 'MySQL', + status: containerStatus.bind(null, 'mysql'), + restart: () => docker.restartContainer('mysql'), + defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 + }, + postgresql: { + name: 'PostgreSQL', + status: containerStatus.bind(null, 'postgresql'), + restart: () => docker.restartContainer('postgresql'), + defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 + }, + docker: { + name: 'Docker', + status: statusDocker, + restart: restartDocker, + defaultMemoryLimit: 0 + }, + unbound: { + name: 'Unbound', + status: statusUnbound, + restart: restartUnbound, + defaultMemoryLimit: 0 + }, + sftp: { + name: 'Filemanager', + status: containerStatus.bind(null, 'sftp'), + restart: () => docker.restartContainer('sftp'), + get defaultMemoryLimit() { return sftp.DEFAULT_MEMORY_LIMIT; } + }, + graphite: { + name: 'Metrics', + status: statusGraphite, + restart: restartGraphite, + defaultMemoryLimit: 256 * 1024 * 1024 + }, + nginx: { + name: 'Nginx', + status: statusNginx, + restart: restartNginx, + defaultMemoryLimit: 0 + } +}; async function listServices() { - const serviceIds = Object.keys(SERVICES()).map(k => { return { id: k, name: SERVICES()[k].name }; }); + const serviceIds = Object.keys(SERVICES).map(k => { return { id: k, name: SERVICES[k].name }; }); const result = await apps.list(); for (const app of result) { @@ -1497,11 +1490,11 @@ async function getServiceStatus(id) { let containerStatusFunc, service; if (instance) { - service = APP_SERVICES()[name]; + service = APP_SERVICES[name]; if (!service) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); containerStatusFunc = service.status.bind(null, instance); - } else if (SERVICES()[name]) { - service = SERVICES()[name]; + } else if (SERVICES[name]) { + service = SERVICES[name]; containerStatusFunc = service.status; } else { throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); @@ -1541,8 +1534,8 @@ async function getServiceLogs(id, options) { const [name, instance ] = id.split(':'); if (instance) { - if (!APP_SERVICES()[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); - } else if (!SERVICES()[name]) { + if (!APP_SERVICES[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); + } else if (!SERVICES[name]) { throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); } @@ -1555,7 +1548,7 @@ async function getServiceLogs(id, options) { } else if (name === 'nginx') { cp = logs.tail(['/var/log/nginx/access.log', '/var/log/nginx/error.log'], { lines: options.lines, follow: options.follow }); } else { - const containerName = APP_SERVICES()[name] ? `${name}-${instance}` : name; + const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name; cp = logs.tail([path.join(paths.LOG_DIR, containerName, 'app.log')], { lines: options.lines, follow: options.follow }); } @@ -1574,11 +1567,11 @@ async function restartService(id, auditSource) { const [name, instance ] = id.split(':'); if (instance) { - if (!APP_SERVICES()[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); + if (!APP_SERVICES[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); - await APP_SERVICES()[name].restart(instance); - } else if (SERVICES()[name]) { - await SERVICES()[name].restart(); + await APP_SERVICES[name].restart(instance); + } else if (SERVICES[name]) { + await SERVICES[name].restart(); } else { throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); } @@ -1594,18 +1587,18 @@ async function applyMemoryLimit(id) { const serviceConfig = await getServiceConfig(id); if (instance) { - if (!APP_SERVICES()[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); + if (!APP_SERVICES[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); containerName = `${name}-${instance}`; - memoryLimit = serviceConfig && serviceConfig.memoryLimit ? serviceConfig.memoryLimit : APP_SERVICES()[name].defaultMemoryLimit; - } else if (SERVICES()[name]) { + memoryLimit = serviceConfig && serviceConfig.memoryLimit ? serviceConfig.memoryLimit : APP_SERVICES[name].defaultMemoryLimit; + } else if (SERVICES[name]) { if (name === 'mongodb' && !await hasAVX()) { debug('applyMemoryLimit: skipping mongodb because CPU does not have AVX'); return; } containerName = name; - memoryLimit = serviceConfig && serviceConfig.memoryLimit ? serviceConfig.memoryLimit : SERVICES()[name].defaultMemoryLimit; + memoryLimit = serviceConfig && serviceConfig.memoryLimit ? serviceConfig.memoryLimit : SERVICES[name].defaultMemoryLimit; } else { throw new BoxError(BoxError.NOT_FOUND, 'No such service'); } @@ -1620,7 +1613,7 @@ async function startTurn(existingInfra) { const serviceConfig = await getServiceConfig('turn'); const image = infra.images.turn; - const memoryLimit = serviceConfig.memoryLimit || SERVICES()['turn'].defaultMemoryLimit; + const memoryLimit = serviceConfig.memoryLimit || SERVICES['turn'].defaultMemoryLimit; const { fqdn:realm } = await dashboard.getLocation(); let turnSecret = await blobs.getString(blobs.ADDON_TURN_SECRET); @@ -1716,10 +1709,10 @@ async function configureService(id, data, auditSource) { assert.strictEqual(typeof auditSource, 'object'); const [name, instance ] = id.split(':'); - let needsRebuild = false; + let needsRebuild; if (instance) { - if (!APP_SERVICES()[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); + if (!APP_SERVICES[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found'); const app = await apps.get(instance); if (!app) throw new BoxError(BoxError.NOT_FOUND, 'App not found'); @@ -1729,7 +1722,7 @@ async function configureService(id, data, auditSource) { servicesConfig[name] = data; await apps.update(instance, { servicesConfig }); - } else if (SERVICES()[name]) { + } else if (SERVICES[name]) { const servicesConfig = await getConfig(); needsRebuild = servicesConfig[name]?.recoveryMode != data.recoveryMode; // intentional != since 'recoveryMode' may or may not be there