From dcaccc2d7a09ab894c1735ce6a14e2cd9e5b204e Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Sun, 3 May 2020 17:38:15 -0700 Subject: [PATCH] add redis status part of #671 --- CHANGES | 1 + ...0427044800-apps-add-servicesConfigJson.js} | 4 +- src/addons.js | 197 +++++++++++++----- src/appdb.js | 10 +- src/test/database-test.js | 6 +- 5 files changed, 156 insertions(+), 62 deletions(-) rename migrations/{20200427044800-apps-add-serviceConfigJson.js => 20200427044800-apps-add-servicesConfigJson.js} (59%) diff --git a/CHANGES b/CHANGES index 0bbc72a34..f87e4a540 100644 --- a/CHANGES +++ b/CHANGES @@ -1924,4 +1924,5 @@ * Ensure stopped apps are getting backed up * Add OVH Object Storage backend * Add bind mounts (aka volumes) +* Add per-app redis status and configuration to Services diff --git a/migrations/20200427044800-apps-add-serviceConfigJson.js b/migrations/20200427044800-apps-add-servicesConfigJson.js similarity index 59% rename from migrations/20200427044800-apps-add-serviceConfigJson.js rename to migrations/20200427044800-apps-add-servicesConfigJson.js index 324ef924f..bd33c27ac 100644 --- a/migrations/20200427044800-apps-add-serviceConfigJson.js +++ b/migrations/20200427044800-apps-add-servicesConfigJson.js @@ -1,14 +1,14 @@ 'use strict'; exports.up = function(db, callback) { - db.runSql('ALTER TABLE apps ADD COLUMN serviceConfigJson TEXT', function (error) { + db.runSql('ALTER TABLE apps ADD COLUMN servicesConfigJson TEXT', function (error) { if (error) console.error(error); callback(error); }); }; exports.down = function(db, callback) { - db.runSql('ALTER TABLE apps DROP COLUMN serviceConfigJson', function (error) { + db.runSql('ALTER TABLE apps DROP COLUMN servicesConfigJson', function (error) { if (error) console.error(error); callback(error); }); diff --git a/src/addons.js b/src/addons.js index d5a521432..2f877a090 100644 --- a/src/addons.js +++ b/src/addons.js @@ -210,6 +210,14 @@ const SERVICES = { } }; +const APP_SERVICES = { + redis: { + status: (instance, done) => containerStatus(`redis-${instance}`, 'CLOUDRON_REDIS_TOKEN', done), + restart: (instance, done) => restartContainer(`redis-${instance}`, done), + defaultMemoryLimit: 150 * 1024 * 1024 + } +}; + function debugApp(app /*, args */) { assert(typeof app === 'object'); @@ -331,17 +339,57 @@ function getServices(callback) { let services = Object.keys(SERVICES); - callback(null, services); + appdb.getAll(function (error, apps) { + if (error) return callback(error); + + for (let app of apps) { + if (app.manifest.addons && app.manifest.addons['redis']) services.push(`redis:${app.id}`); + } + + callback(null, services); + }); } -function getService(serviceName, callback) { - assert.strictEqual(typeof serviceName, 'string'); +function getServicesConfig(id, callback) { + assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); - if (!SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND)); + const [name, instance ] = id.split(':'); + if (!instance) { + settings.getPlatformConfig(function (error, platformConfig) { + if (error) return callback(error); + + callback(null, SERVICES[name], platformConfig); + }); + + return; + } + + appdb.get(instance, function (error, app) { + if (error) return callback(error); + + callback(null, APP_SERVICES[name], app.servicesConfig); + }); +} + +function getService(id, callback) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof callback, 'function'); + + const [name, instance ] = id.split(':'); + let containerStatusFunc; + + if (instance) { + if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); + containerStatusFunc = APP_SERVICES[name].status.bind(null, instance); + } else if (SERVICES[name]) { + containerStatusFunc = SERVICES[name].status; + } else { + return callback(new BoxError(BoxError.NOT_FOUND)); + } var tmp = { - name: serviceName, + name: name, status: null, memoryUsed: 0, memoryPercent: 0, @@ -353,60 +401,76 @@ function getService(serviceName, callback) { } }; - settings.getPlatformConfig(function (error, platformConfig) { + containerStatusFunc(function (error, result) { if (error) return callback(error); - if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) { - tmp.config.memory = platformConfig[serviceName].memory; - tmp.config.memorySwap = platformConfig[serviceName].memorySwap; - } else if (SERVICES[serviceName].defaultMemoryLimit) { - tmp.config.memory = SERVICES[serviceName].defaultMemoryLimit; - tmp.config.memorySwap = tmp.config.memory * 2; - } + tmp.status = result.status; + tmp.memoryUsed = result.memoryUsed; + tmp.memoryPercent = result.memoryPercent; + tmp.error = result.error || null; - SERVICES[serviceName].status(function (error, result) { + getServicesConfig(id, function (error, service, servicesConfig) { if (error) return callback(error); - tmp.status = result.status; - tmp.memoryUsed = result.memoryUsed; - tmp.memoryPercent = result.memoryPercent; - tmp.error = result.error || null; + const serviceConfig = servicesConfig[name]; + + if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) { + tmp.config.memory = serviceConfig.memory; + tmp.config.memorySwap = serviceConfig.memorySwap; + } else if (service.defaultMemoryLimit) { + tmp.config.memory = service.defaultMemoryLimit; + tmp.config.memorySwap = tmp.config.memory * 2; + } callback(null, tmp); }); }); } -function configureService(serviceName, data, callback) { - assert.strictEqual(typeof serviceName, 'string'); +function configureService(id, data, callback) { + assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof callback, 'function'); - if (!SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND)); + const [name, instance ] = id.split(':'); - settings.getPlatformConfig(function (error, platformConfig) { + if (instance) { + if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); + } else if (!SERVICES[name]) { + return callback(new BoxError(BoxError.NOT_FOUND)); + } + + getServicesConfig(id, function (error, service, servicesConfig) { if (error) return callback(error); - if (!platformConfig[serviceName]) platformConfig[serviceName] = {}; + if (!servicesConfig[name]) servicesConfig[name] = {}; // if not specified we clear the entry and use defaults if (!data.memory || !data.memorySwap) { - delete platformConfig[serviceName]; + delete servicesConfig[name]; } else { - platformConfig[serviceName].memory = data.memory; - platformConfig[serviceName].memorySwap = data.memorySwap; + servicesConfig[name].memory = data.memory; + servicesConfig[name].memorySwap = data.memorySwap; } - settings.setPlatformConfig(platformConfig, function (error) { - if (error) return callback(error); + if (instance) { + appdb.update(instance, { servicesConfig }, function (error) { + if (error) return callback(error); - callback(null); - }); + updateAppServiceConfig(name, instance, servicesConfig, callback); + }); + } else { + settings.setPlatformConfig(servicesConfig, function (error) { + if (error) return callback(error); + + callback(null); + }); + } }); } -function getServiceLogs(serviceName, options, callback) { - assert.strictEqual(typeof serviceName, 'string'); +function getServiceLogs(id, options, callback) { + assert.strictEqual(typeof id, 'string'); assert(options && typeof options === 'object'); assert.strictEqual(typeof callback, 'function'); @@ -414,9 +478,15 @@ function getServiceLogs(serviceName, options, callback) { assert.strictEqual(typeof options.format, 'string'); assert.strictEqual(typeof options.follow, 'boolean'); - if (!SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND)); + const [name, instance ] = id.split(':'); - debug(`Getting logs for ${serviceName}`); + if (instance) { + if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); + } else if (!SERVICES[name]) { + return callback(new BoxError(BoxError.NOT_FOUND)); + } + + debug(`Getting logs for ${name}`); var lines = options.lines, format = options.format || 'json', @@ -425,11 +495,11 @@ function getServiceLogs(serviceName, options, callback) { let cmd, args = []; // docker and unbound use journald - if (serviceName === 'docker' || serviceName === 'unbound') { + if (name === 'docker' || name === 'unbound') { cmd = 'journalctl'; args.push('--lines=' + (lines === -1 ? 'all' : lines)); - args.push(`--unit=${serviceName}`); + args.push(`--unit=${name}`); args.push('--no-pager'); args.push('--output=short-iso'); @@ -439,7 +509,8 @@ function getServiceLogs(serviceName, options, callback) { args.push('--lines=' + (lines === -1 ? '+1' : lines)); if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs - args.push(path.join(paths.LOG_DIR, serviceName, 'app.log')); + const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name; + args.push(path.join(paths.LOG_DIR, containerName, 'app.log')); } var cp = spawn(cmd, args); @@ -458,7 +529,7 @@ function getServiceLogs(serviceName, options, callback) { return JSON.stringify({ realtimeTimestamp: timestamp * 1000, message: message, - source: serviceName + source: name }) + '\n'; }); @@ -469,13 +540,21 @@ function getServiceLogs(serviceName, options, callback) { callback(null, transformStream); } -function restartService(serviceName, callback) { - assert.strictEqual(typeof serviceName, 'string'); +function restartService(id, callback) { + assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); - if (!SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND)); + const [name, instance ] = id.split(':'); - SERVICES[serviceName].restart(callback); + if (instance) { + if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); + + APP_SERVICES[name].restart(instance, callback); + } else if (SERVICES[name]) { + SERVICES[name].restart(callback); + } else { + return callback(new BoxError(BoxError.NOT_FOUND)); + } } function waitForContainer(containerName, tokenEnvName, callback) { @@ -486,7 +565,7 @@ function waitForContainer(containerName, tokenEnvName, callback) { debug(`Waiting for ${containerName}`); getContainerDetails(containerName, tokenEnvName, function (error, result) { - if (error) return callback(error); + if (error) return callback(error); async.retry({ times: 10, interval: 15000 }, function (retryCallback) { request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { @@ -650,6 +729,28 @@ function updateServiceConfig(platformConfig, callback) { }, callback); } +function updateAppServiceConfig(name, instance, servicesConfig, callback) { + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof instance, 'string'); + assert.strictEqual(typeof servicesConfig, 'object'); + assert.strictEqual(typeof callback, 'function'); + + debug(`updateAppServiceConfig: ${name}-${instance} ${JSON.stringify(servicesConfig)}`); + + const serviceConfig = servicesConfig[name]; + let memory, memorySwap; + if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) { + memory = serviceConfig.memory; + memorySwap = serviceConfig.memorySwap; + } else { + memory = APP_SERVICES[name].defaultMemoryLimit; + memorySwap = memory * 2; + } + + const args = `update --memory ${memory} --memory-swap ${memorySwap} ${name}-${instance}`.split(' '); + shell.spawn(`updateAppServiceConfig${name}`, '/usr/bin/docker', args, { }, callback); +} + function startServices(existingInfra, callback) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -1616,15 +1717,7 @@ function setupRedis(app, options, callback) { const redisServiceToken = hat(4 * 48); // Compute redis memory limit based on app's memory limit (this is arbitrary) - var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0; - - if (memoryLimit === -1) { // unrestricted (debug mode) - memoryLimit = 0; - } else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap) - memoryLimit = 150 * 1024 * 1024; // 150m - } else { - memoryLimit = 600 * 1024 * 1024; // 600m - } + const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memoryLimit : APP_SERVICES['redis'].defaultMemoryLimit; const tag = infra.images.redis.tag; const label = app.fqdn; diff --git a/src/appdb.js b/src/appdb.js index a799d191a..1981993d3 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -41,7 +41,7 @@ var assert = require('assert'), var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState', 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares', - 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.serviceConfigJson', 'apps.bindsJson', + 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson', 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate', 'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(','); @@ -94,9 +94,9 @@ function postProcess(result) { result.debugMode = safe.JSON.parse(result.debugModeJson); delete result.debugModeJson; - assert(result.serviceConfigJson === null || typeof result.serviceConfigJson === 'string'); - result.serviceConfig = safe.JSON.parse(result.serviceConfigJson) || {}; - delete result.serviceConfigJson; + assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string'); + result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {}; + delete result.servicesConfigJson; assert(result.bindsJson === null || typeof result.bindsJson === 'string'); result.binds = safe.JSON.parse(result.bindsJson) || {}; @@ -435,7 +435,7 @@ function updateWithConstraints(id, app, constraints, callback) { var fields = [ ], values = [ ]; for (var p in app) { - if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'serviceConfig' || p === 'binds') { + if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') { fields.push(`${p}Json = ?`); values.push(JSON.stringify(app[p])); } else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') { diff --git a/src/test/database-test.js b/src/test/database-test.js index 7ff6bf20c..d4ff05e04 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -420,7 +420,7 @@ describe('database', function () { label: null, taskId: null, binds: {}, - serviceConfig: {} + servicesConfig: {} }; it('cannot delete referenced domain', function (done) { @@ -893,7 +893,7 @@ describe('database', function () { label: null, taskId: null, binds: {}, - serviceConfig: {} + servicesConfig: {} }; var APP_1 = { @@ -926,7 +926,7 @@ describe('database', function () { label: null, taskId: null, binds: {}, - serviceConfig: {} + servicesConfig: {} }; before(function (done) {