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