Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c4e163709 | |||
| d1acc6c466 | |||
| f879d6f529 | |||
| 1ac38d4921 | |||
| 4818e9a8e4 | |||
| c4ed471d1c | |||
| 83c0b2986a | |||
| b8cddf559a | |||
| 4ba9f80d44 | |||
| d1d3309e91 | |||
| 84cffe8888 | |||
| 3929b3ca0a | |||
| d649a470f9 | |||
| db330b23cb | |||
| cda649884e | |||
| 45053205db | |||
| 3f1533896e | |||
| e20dfe1b26 | |||
| 946d9db296 | |||
| 6dc2e1aa14 | |||
| 001749564d | |||
| ffcba4646c | |||
| 01d0c8eb9c | |||
| 0cf40bd207 | |||
| 4a283e9f35 | |||
| 5ab37bcf7e | |||
| 9151965cd6 | |||
| c5cd71f9e3 | |||
| 602b335c0e | |||
| 837c8b85c2 | |||
| 7d16396e72 | |||
| 66d3d07148 | |||
| b5c1161caa | |||
| b0420889ad | |||
| 527819d886 | |||
| 1ad0cff28e | |||
| 783ec03ac9 | |||
| 6cd395d494 | |||
| 681079e01c | |||
| aabbc43769 | |||
| 2692f6ef4e |
@@ -1755,3 +1755,24 @@
|
||||
* Stopped apps cannot be updated or auto-updated
|
||||
* eventlog: track support ticket creation and remote support status
|
||||
|
||||
[4.4.3]
|
||||
* Add restart button in recovery section
|
||||
* Fix issue where memory usage was not computed correctly
|
||||
* cloudflare: support API tokens
|
||||
|
||||
[4.4.4]
|
||||
* Fix bug where restart button in terminal was not working
|
||||
* Add search field in apps view
|
||||
* Make app view tags and domain filter persistent
|
||||
* Add timezone UI
|
||||
|
||||
[4.4.5]
|
||||
* Fix user listing regression in group edit dialog
|
||||
* Do not show error page for 503
|
||||
* Add mail list and mail box update events
|
||||
* Certs of stopped apps are not renewed anymore
|
||||
* Fix broken memory sliders in the services UI
|
||||
* Set CPU Shares
|
||||
* Update nodejs to 12.14.1
|
||||
* Update MySQL addon packet size to 64M
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
Copyright (c) 2020 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
@@ -62,10 +62,10 @@ apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
mkdir -p /usr/local/node-10.18.1
|
||||
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.18.1
|
||||
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
@@ -126,6 +126,10 @@ timedatectl set-timezone UTC
|
||||
echo "==> Adding sshd configuration warning"
|
||||
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://cloudron.io/documentation/security/#securing-ssh-access' -i /etc/ssh/sshd_config
|
||||
|
||||
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
|
||||
echo "==> Disabling motd news"
|
||||
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
systemctl stop bind9 || true
|
||||
systemctl disable bind9 || true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM domains', function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(domains, function (domain, iteratorCallback) {
|
||||
if (domain.provider !== 'cloudflare') return iteratorCallback();
|
||||
|
||||
let config = JSON.parse(domain.configJson);
|
||||
config.tokenType = 'GlobalApiKey';
|
||||
|
||||
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN cpuShares INTEGER DEFAULT 512', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN cpuShares', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -78,6 +78,7 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
cpuShares INTEGER DEFAULT 512,
|
||||
xFrameOptions VARCHAR(512),
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
debugModeJson TEXT, // options for development mode
|
||||
|
||||
Generated
+1265
-1385
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -17,8 +17,8 @@
|
||||
"@google-cloud/dns": "^1.1.0",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||
"async": "^2.6.2",
|
||||
"aws-sdk": "^2.476.0",
|
||||
"async": "^2.6.3",
|
||||
"aws-sdk": "^2.610.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^4.0.0",
|
||||
"connect": "^3.7.0",
|
||||
|
||||
@@ -41,8 +41,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
echo "This script requires node 10.15.1"
|
||||
if [[ "$(node --version)" != "v10.18.1" ]]; then
|
||||
echo "This script requires node 10.18.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -57,12 +57,12 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
$curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-8.11.2 /usr/local/node-8.9.3
|
||||
if [[ "$(node --version)" != "v10.18.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.18.1
|
||||
$curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.18.1
|
||||
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-10.15.1
|
||||
fi
|
||||
|
||||
# this is here (and not in updater.js) because rebuild requires the above node
|
||||
|
||||
@@ -37,4 +37,4 @@
|
||||
# notifyCloudronAdmins: false
|
||||
#
|
||||
# footer:
|
||||
# body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
# body: '© 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
|
||||
@@ -624,7 +624,6 @@ function updateServiceConfig(platformConfig, callback) {
|
||||
|
||||
debug('updateServiceConfig: %j', platformConfig);
|
||||
|
||||
// TODO: this should possibly also rollback memory to default
|
||||
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb', 'graphite' ], function iterator(serviceName, iteratorCallback) {
|
||||
const containerConfig = platformConfig[serviceName];
|
||||
let memory, memorySwap;
|
||||
|
||||
+5
-4
@@ -40,7 +40,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.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||
@@ -242,6 +242,7 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
const accessRestriction = data.accessRestriction || null;
|
||||
const accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
const memoryLimit = data.memoryLimit || 0;
|
||||
const cpuShares = data.cpuShares || 512;
|
||||
const installationState = data.installationState;
|
||||
const runState = data.runState;
|
||||
const sso = 'sso' in data ? data.sso : null;
|
||||
@@ -256,10 +257,10 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
var queries = [];
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, '
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
|
||||
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) '
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit,
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares,
|
||||
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ]
|
||||
});
|
||||
|
||||
|
||||
@@ -186,9 +186,10 @@ function processApp(callback) {
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = result
|
||||
const alive = result
|
||||
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + (a.manifest.id || 'customapp'); }).join(', ');
|
||||
.map(a => a.fqdn)
|
||||
.join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
|
||||
+78
-10
@@ -19,6 +19,7 @@ exports = module.exports = {
|
||||
setIcon: setIcon,
|
||||
setTags: setTags,
|
||||
setMemoryLimit: setMemoryLimit,
|
||||
setCpuShares: setCpuShares,
|
||||
setAutomaticBackup: setAutomaticBackup,
|
||||
setAutomaticUpdate: setAutomaticUpdate,
|
||||
setReverseProxyConfig: setReverseProxyConfig,
|
||||
@@ -43,6 +44,7 @@ exports = module.exports = {
|
||||
|
||||
start: start,
|
||||
stop: stop,
|
||||
restart: restart,
|
||||
|
||||
exec: exec,
|
||||
|
||||
@@ -79,6 +81,7 @@ exports = module.exports = {
|
||||
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app. this is state because it blocks other operations
|
||||
ISTATE_PENDING_START: 'pending_start',
|
||||
ISTATE_PENDING_STOP: 'pending_stop',
|
||||
ISTATE_PENDING_RESTART: 'pending_restart',
|
||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||
ISTATE_INSTALLED: 'installed', // app is installed
|
||||
|
||||
@@ -247,6 +250,14 @@ function validateMemoryLimit(manifest, memoryLimit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCpuShares(cpuShares) {
|
||||
assert.strictEqual(typeof cpuShares, 'number');
|
||||
|
||||
if (cpuShares < 2 || cpuShares > 1024) return new BoxError(BoxError.BAD_FIELD, 'cpuShares has to be between 2 and 1024');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateDebugMode(debugMode) {
|
||||
assert.strictEqual(typeof debugMode, 'object');
|
||||
|
||||
@@ -316,23 +327,25 @@ function validateEnv(env) {
|
||||
function validateDataDir(dataDir) {
|
||||
if (dataDir === null) return null;
|
||||
|
||||
if (path.resolve(dataDir) !== dataDir) return new BoxError(BoxError.BAD_FIELD, 'dataDir must be an absolute path', { field: 'dataDir' });
|
||||
if (!path.isAbsolute(dataDir)) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not an absolute path`, { field: 'dataDir' });
|
||||
if (dataDir.endsWith('/')) return new BoxError(BoxError.BAD_FIELD, `${dataDir} contains trailing slash`, { field: 'dataDir' });
|
||||
if (path.normalize(dataDir) !== dataDir) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not a normalized path`, { field: 'dataDir' });
|
||||
|
||||
// nfs shares will have the directory mounted already
|
||||
let stat = safe.fs.lstatSync(dataDir);
|
||||
if (stat) {
|
||||
if (!stat.isDirectory()) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} is not a directory`, { field: 'dataDir' });
|
||||
if (!stat.isDirectory()) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not a directory`, { field: 'dataDir' });
|
||||
let entries = safe.fs.readdirSync(dataDir);
|
||||
if (!entries) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} could not be listed`, { field: 'dataDir' });
|
||||
if (entries.length !== 0) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} is not empty`, { field: 'dataDir' });
|
||||
if (!entries) return new BoxError(BoxError.BAD_FIELD, `${dataDir} could not be listed`, { field: 'dataDir' });
|
||||
if (entries.length !== 0) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not empty. If this is the root of a mounted volume, provide a subdirectory.`, { field: 'dataDir' });
|
||||
}
|
||||
|
||||
// backup logic relies on paths not overlapping (because it recurses)
|
||||
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`, { field: 'dataDir' });
|
||||
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new BoxError(BoxError.BAD_FIELD, `${dataDir} cannot be inside apps data`, { field: 'dataDir' });
|
||||
|
||||
// if we made it this far, it cannot start with any of these realistically
|
||||
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
|
||||
if (fhs.some((p) => dataDir.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`, { field: 'dataDir' });
|
||||
if (fhs.some((p) => dataDir.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, `${dataDir} cannot be placed inside this location`, { field: 'dataDir' });
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -381,7 +394,7 @@ function removeInternalFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
|
||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
|
||||
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
||||
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir');
|
||||
}
|
||||
@@ -652,7 +665,7 @@ function checkAppState(app, state) {
|
||||
if (app.error.installationState === state) return null;
|
||||
|
||||
// allow uninstall from any state
|
||||
if (state !== exports.ISTATE_PENDING_UNINSTALL) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
|
||||
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -931,6 +944,35 @@ function setMemoryLimit(appId, memoryLimit, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setCpuShares(appId, cpuShares, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof cpuShares, 'number');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
error = checkAppState(app, exports.ISTATE_PENDING_RESIZE);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateCpuShares(cpuShares);
|
||||
if (error) return callback(error);
|
||||
|
||||
const task = {
|
||||
args: {},
|
||||
values: { cpuShares }
|
||||
};
|
||||
addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, cpuShares, taskId: result.taskId });
|
||||
|
||||
callback(null, { taskId: result.taskId });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setEnvironment(appId, env, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof env, 'object');
|
||||
@@ -1362,7 +1404,7 @@ function repair(appId, data, auditSource, callback) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE;
|
||||
let errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE;
|
||||
|
||||
const task = {
|
||||
args: {},
|
||||
@@ -1383,6 +1425,8 @@ function repair(appId, data, auditSource, callback) {
|
||||
task.values.manifest = data.manifest;
|
||||
task.args.oldManifest = app.manifest;
|
||||
}
|
||||
} else {
|
||||
errorState = exports.ISTATE_PENDING_CONFIGURE;
|
||||
}
|
||||
|
||||
addTask(appId, errorState, task, function (error, result) {
|
||||
@@ -1547,7 +1591,7 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
error = validatePortBindings(portBindings, manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
const mailboxName = mailboxNameForLocation(location, manifest);
|
||||
const mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, manifest) : app.mailboxName;
|
||||
const locations = [{subdomain: location, domain}];
|
||||
validateLocations(locations, function (error, domainObjectMap) {
|
||||
if (error) return callback(error);
|
||||
@@ -1678,6 +1722,30 @@ function stop(appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restart(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will restart app with id:%s', appId);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
error = checkAppState(app, exports.ISTATE_PENDING_RESTART);
|
||||
if (error) return callback(error);
|
||||
|
||||
const task = {
|
||||
args: {},
|
||||
values: { runState: exports.RSTATE_RUNNING }
|
||||
};
|
||||
addTask(appId, exports.ISTATE_PENDING_RESTART, task, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { taskId: result.taskId });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkManifestConstraints(manifest) {
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@ function registerWithLoginCredentials(options, callback) {
|
||||
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, provider: settings.provider(), version: constants.VERSION }, callback);
|
||||
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, provider: settings.provider(), version: constants.VERSION, purpose: options.purpose || '' }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -920,6 +920,10 @@ function start(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 20, message: 'Starting container' }),
|
||||
docker.startContainer.bind(null, app.id),
|
||||
|
||||
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
|
||||
progressCallback.bind(null, { percent: 60, message: 'Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
], function seriesDone(error) {
|
||||
@@ -952,6 +956,27 @@ function stop(app, args, progressCallback, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restart(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 20, message: 'Restarting container' }),
|
||||
docker.restartContainer.bind(null, app.id),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error starting app: %s', error);
|
||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
@@ -1029,6 +1054,8 @@ function run(appId, args, progressCallback, callback) {
|
||||
return start(app, args, progressCallback, callback);
|
||||
case apps.ISTATE_PENDING_STOP:
|
||||
return stop(app, args, progressCallback, callback);
|
||||
case apps.ISTATE_PENDING_RESTART:
|
||||
return restart(app, args, progressCallback, callback);
|
||||
default:
|
||||
debugApp(app, 'apptask launched with invalid command');
|
||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
|
||||
|
||||
+2
-2
@@ -98,7 +98,7 @@ function recreateJobs(tz) {
|
||||
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
gJobs.backup = new CronJob({
|
||||
cronTime: '00 00 */6 * * *', // check every 6 hours
|
||||
cronTime: '00 00 1,3,5,23 * * *',
|
||||
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
@@ -149,7 +149,7 @@ function recreateJobs(tz) {
|
||||
|
||||
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
|
||||
gJobs.cleanupBackups = new CronJob({
|
||||
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
|
||||
cronTime: '00 45 1,3,5,23 * * *', // every 6 hours. try not to overlap with ensureBackup job
|
||||
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ const DEFAULT_SPEC = {
|
||||
notifyCloudronAdmins: true
|
||||
},
|
||||
footer: {
|
||||
body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
body: '© 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,4 +63,4 @@ function uiSpec() {
|
||||
|
||||
function spec() {
|
||||
return gSpec;
|
||||
}
|
||||
}
|
||||
|
||||
+30
-22
@@ -48,15 +48,29 @@ function translateRequestError(result, callback) {
|
||||
callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
}
|
||||
|
||||
function createRequest(method, url, dnsConfig) {
|
||||
assert.strictEqual(typeof method, 'string');
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
|
||||
let request = superagent(method, url)
|
||||
.timeout(30 * 1000);
|
||||
|
||||
if (dnsConfig.tokenType === 'GlobalApiKey') {
|
||||
request.set('X-Auth-Key', dnsConfig.token).set('X-Auth-Email', dnsConfig.email);
|
||||
} else {
|
||||
request.set('Authorization', 'Bearer ' + dnsConfig.token);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
createRequest('GET', CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active', dnsConfig)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
@@ -74,11 +88,8 @@ function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
createRequest('GET', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records', dnsConfig)
|
||||
.query({ type: type, name: fqdn })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
@@ -132,11 +143,8 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
if (i >= dnsRecords.length) { // create a new record
|
||||
debug(`upsert: Adding new record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: false`);
|
||||
|
||||
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
createRequest('POST', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records', dnsConfig)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorCallback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
|
||||
@@ -148,11 +156,8 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
debug(`upsert: Updating existing record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: ${data.proxied}`);
|
||||
|
||||
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id)
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
createRequest('PUT', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id, dnsConfig)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
++i; // increment, as we have consumed the record
|
||||
|
||||
@@ -217,10 +222,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
if (tmp.length === 0) return callback(null);
|
||||
|
||||
async.eachSeries(tmp, function (record, callback) {
|
||||
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
createRequest('DELETE', CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id, dnsConfig)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
@@ -277,14 +279,20 @@ function verifyDnsConfig(domainObject, callback) {
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName;
|
||||
|
||||
// token can be api token or global api key
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
|
||||
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
|
||||
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
|
||||
|
||||
if (dnsConfig.tokenType === 'GlobalApiKey') {
|
||||
if ('email' in dnsConfig && typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
|
||||
}
|
||||
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
email: dnsConfig.email
|
||||
tokenType: dnsConfig.tokenType,
|
||||
email: dnsConfig.email || null
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+23
-5
@@ -14,6 +14,7 @@ exports = module.exports = {
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
restartContainer: restartContainer,
|
||||
stopContainer: stopContainer,
|
||||
stopContainerByName: stopContainer,
|
||||
stopContainers: stopContainers,
|
||||
@@ -107,9 +108,10 @@ function ping(callback) {
|
||||
|
||||
connection.ping(function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
if (result !== 'OK') return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
|
||||
if (Buffer.isBuffer(result) && result.toString('utf8') === 'OK') return callback(null); // sometimes it returns buffer
|
||||
if (result === 'OK') return callback(null);
|
||||
|
||||
callback(null);
|
||||
callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,7 +297,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
'Name': isAppContainer ? 'unless-stopped' : 'no',
|
||||
'MaximumRetryCount': 0
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
CpuShares: app.cpuShares,
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
NetworkMode: 'cloudron', // user defined bridge network
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
@@ -344,7 +346,23 @@ function startContainer(containerId, callback) {
|
||||
container.start(function (error) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
|
||||
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); // 304 means already started
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function restartContainer(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = gConnection.getContainer(containerId);
|
||||
debug('Restarting container %s', containerId);
|
||||
|
||||
container.restart(function (error) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
|
||||
if (error && error.statusCode !== 204) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -431,7 +449,7 @@ function stopContainers(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('stopping containers of %s', appId);
|
||||
debug('Stopping containers of %s', appId);
|
||||
|
||||
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
+3
-3
@@ -86,9 +86,9 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
|
||||
|
||||
const domainObject = { config: dnsConfig, domain: domain, zoneName: zoneName };
|
||||
api(provider).verifyDnsConfig(domainObject, function (error, result) {
|
||||
if (error && error.reason === BoxError.ACCESS_DENIED) return callback(new BoxError(BoxError.BAD_FIELD, 'Incorrect configuration. Access denied'));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, 'Configuration error: ' + error.message));
|
||||
if (error && error.reason === BoxError.ACCESS_DENIED) return callback(new BoxError(BoxError.BAD_FIELD, `Access denied: ${error.message}`));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_FIELD, `Zone not found: ${error.message}`));
|
||||
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`));
|
||||
if (error) return callback(error);
|
||||
|
||||
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
|
||||
|
||||
@@ -40,8 +40,10 @@ exports = module.exports = {
|
||||
ACTION_MAIL_DISABLED: 'mail.disabled',
|
||||
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
|
||||
ACTION_MAIL_MAILBOX_REMOVE: 'mail.box.remove',
|
||||
ACTION_MAIL_MAILBOX_UPDATE: 'mail.box.update',
|
||||
ACTION_MAIL_LIST_ADD: 'mail.list.add',
|
||||
ACTION_MAIL_LIST_REMOVE: 'mail.list.remove',
|
||||
ACTION_MAIL_LIST_UPDATE: 'mail.list.update',
|
||||
|
||||
ACTION_PROVISION: 'cloudron.provision',
|
||||
ACTION_RESTORE: 'cloudron.restore', // unused
|
||||
|
||||
@@ -15,7 +15,7 @@ exports = module.exports = {
|
||||
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
|
||||
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.2@sha256:a28320f313785816be60e3f865e09065504170a3d20ed37de675c719b32b01eb' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.1.0@sha256:eee0dfd3829d563f2063084bc0d7c8802c4bdd6e233159c6226a17ff7a9a3503' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
|
||||
|
||||
+2
-1
@@ -143,10 +143,11 @@ function userSearch(req, res, next) {
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['user'],
|
||||
objectclass: ['user', 'inetorgperson', 'person' ],
|
||||
objectcategory: 'person',
|
||||
cn: entry.id,
|
||||
uid: entry.id,
|
||||
entryuuid: entry.id, // to support OpenLDAP clients
|
||||
mail: entry.email,
|
||||
mailAlternateAddress: entry.fallbackEmail,
|
||||
displayname: displayName,
|
||||
|
||||
+24
-10
@@ -1106,18 +1106,25 @@ function addMailbox(name, domain, userId, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateMailboxOwner(name, domain, userId, callback) {
|
||||
function updateMailboxOwner(name, domain, userId, auditSource, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
|
||||
getMailbox(name, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, userId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1196,12 +1203,12 @@ function getLists(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getList(domain, listName, callback) {
|
||||
function getList(name, domain, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof listName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mailboxdb.getList(listName, domain, function (error, result) {
|
||||
mailboxdb.getList(name, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
@@ -1227,16 +1234,17 @@ function addList(name, domain, members, auditSource, callback) {
|
||||
mailboxdb.addList(name, domain, members, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
|
||||
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members });
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function updateList(name, domain, members, callback) {
|
||||
function updateList(name, domain, members, auditSource, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(Array.isArray(members));
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
name = name.toLowerCase();
|
||||
@@ -1248,10 +1256,16 @@ function updateList(name, domain, members, callback) {
|
||||
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid email: ' + members[i]));
|
||||
}
|
||||
|
||||
mailboxdb.updateList(name, domain, members, function (error) {
|
||||
getList(name, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
mailboxdb.updateList(name, domain, members, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -108,7 +108,9 @@ server {
|
||||
<% } -%>
|
||||
|
||||
proxy_http_version 1.1;
|
||||
# intercept errors (>= 400) and use the error_page handler
|
||||
proxy_intercept_errors on;
|
||||
# nginx will return 504 on connect/timeout errors
|
||||
proxy_read_timeout 3500;
|
||||
proxy_connect_timeout 3250;
|
||||
|
||||
@@ -125,7 +127,8 @@ server {
|
||||
|
||||
# only serve up the status page if we get proxy gateway errors
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
error_page 502 503 504 /appstatus.html;
|
||||
# some apps use 503 to indicate updating or maintenance
|
||||
error_page 502 504 /appstatus.html;
|
||||
location /appstatus.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">© 2016-19 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted">© 2016-20 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,6 @@ exports = module.exports = {
|
||||
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
|
||||
LICENSE_FILE: '/etc/cloudron/LICENSE',
|
||||
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
|
||||
|
||||
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
|
||||
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
|
||||
|
||||
+1
-28
@@ -27,7 +27,6 @@ var appstore = require('./appstore.js'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
tld = require('tldjs'),
|
||||
_ = require('underscore');
|
||||
@@ -151,31 +150,6 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setTimeZone(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('setTimeZone ip:%s', ip);
|
||||
|
||||
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
|
||||
if ((error && !error.response) || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location: %s', error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var timezone = safe.query(result.body, 'location.time_zone');
|
||||
|
||||
if (!timezone || typeof timezone !== 'string') {
|
||||
debug('No timezone in geoip response : %j', result.body);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
debug('Setting timezone to ', timezone);
|
||||
|
||||
settings.setTimeZone(timezone, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, displayName, ip, auditSource, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
@@ -187,8 +161,6 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
|
||||
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated'));
|
||||
if (error) return callback(error);
|
||||
@@ -274,6 +246,7 @@ function getStatus(callback) {
|
||||
callback(null, _.extend({
|
||||
version: constants.VERSION,
|
||||
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
|
||||
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
|
||||
provider: settings.provider(),
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
|
||||
|
||||
@@ -582,6 +582,8 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
|
||||
// add app main
|
||||
allApps.forEach(function (app) {
|
||||
if (app.runState === apps.RSTATE_STOPPED) return; // do not renew certs of stopped apps
|
||||
|
||||
appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app, nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf') });
|
||||
|
||||
app.alternateDomains.forEach(function (alternateDomain) {
|
||||
|
||||
@@ -20,6 +20,7 @@ exports = module.exports = {
|
||||
setTags: setTags,
|
||||
setIcon: setIcon,
|
||||
setMemoryLimit: setMemoryLimit,
|
||||
setCpuShares: setCpuShares,
|
||||
setAutomaticBackup: setAutomaticBackup,
|
||||
setAutomaticUpdate: setAutomaticUpdate,
|
||||
setReverseProxyConfig: setReverseProxyConfig,
|
||||
@@ -32,6 +33,7 @@ exports = module.exports = {
|
||||
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
restartApp: restartApp,
|
||||
exec: exec,
|
||||
execWebSocket: execWebSocket,
|
||||
|
||||
@@ -206,6 +208,19 @@ function setMemoryLimit(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function setCpuShares(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number'));
|
||||
|
||||
apps.setCpuShares(req.params.id, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function setAutomaticBackup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
@@ -480,6 +495,18 @@ function stopApp(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function restartApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Restart app id:%s', req.params.id);
|
||||
|
||||
apps.restart(req.params.id, function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function updateApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
@@ -68,6 +68,7 @@ function registerCloudron(req, res, next) {
|
||||
if (typeof req.body.password !== 'string' || !req.body.password) return next(new HttpError(400, 'password must be string'));
|
||||
if ('totpToken' in req.body && typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be string'));
|
||||
if (typeof req.body.signup !== 'boolean') return next(new HttpError(400, 'signup must be a boolean'));
|
||||
if ('purpose' in req.body && typeof req.body.purpose !== 'string') return next(new HttpError(400, 'purpose must be string'));
|
||||
|
||||
appstore.registerWithLoginCredentials(req.body, function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
+3
-3
@@ -239,7 +239,7 @@ function updateMailbox(req, res, next) {
|
||||
|
||||
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
|
||||
|
||||
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, function (error) {
|
||||
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
@@ -316,7 +316,7 @@ function getList(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
assert.strictEqual(typeof req.params.name, 'string');
|
||||
|
||||
mail.getList(req.params.domain, req.params.name, function (error, result) {
|
||||
mail.getList(req.params.name, req.params.domain, function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { list: result }));
|
||||
@@ -353,7 +353,7 @@ function updateList(req, res, next) {
|
||||
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
||||
}
|
||||
|
||||
mail.updateList(req.params.name, req.params.domain, req.body.members, function (error) {
|
||||
mail.updateList(req.params.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
|
||||
@@ -40,10 +40,11 @@ function configure(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
if (typeof req.body.memory !== 'number') return next(new HttpError(400, 'memory must be a number'));
|
||||
if (typeof req.body.memorySwap !== 'number') return next(new HttpError(400, 'memorySwap must be a number'));
|
||||
|
||||
const data = {
|
||||
memory: req.body.memory,
|
||||
memorySwap: req.body.memory * 2
|
||||
memorySwap: req.body.memorySwap
|
||||
};
|
||||
|
||||
addons.configureService(req.params.service, data, function (error) {
|
||||
|
||||
@@ -1570,6 +1570,34 @@ describe('App API', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can restart app', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/restart')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
taskId = res.body.taskId;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('wait for app to restart', function (done) {
|
||||
waitForTask(taskId, function () { setTimeout(done, 12000); }); // give app 12 seconds (to die and start)
|
||||
});
|
||||
|
||||
it('did restart the app', function (done) {
|
||||
apps.get(APP_ID, function (error, app) {
|
||||
if (error) return done(error);
|
||||
|
||||
superagent.get('http://localhost:' + app.httpPort + APP_MANIFEST.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
if (res && res.statusCode === 200) return done();
|
||||
done(new Error('app is not running'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('uninstall', function () {
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('REST API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup twice succeeds', function (done) {
|
||||
xit('dns setup twice succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} }, tlsConfig: { provider: 'fallback' } })
|
||||
.end(function (error, result) {
|
||||
|
||||
+1
-11
@@ -37,17 +37,7 @@ function create(req, res, next) {
|
||||
users.create(username, password, email, displayName, { invitor: req.user, admin: req.body.admin }, auditSource.fromRequest(req), function (error, user) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
var userInfo = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
email: user.email,
|
||||
fallbackEmail: user.fallbackEmail,
|
||||
groupIds: [ ],
|
||||
resetToken: user.resetToken
|
||||
};
|
||||
|
||||
next(new HttpSuccess(201, userInfo));
|
||||
next(new HttpSuccess(201, users.removePrivateFields(user)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ function runTask(appId, taskName, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING || app.health !== apps.HEALTH_HEALTHY) {
|
||||
debug(`runTask: skipped task ${taskName} because app ${app.fqdn} has run state ${app.installationState}`);
|
||||
debug(`runTask: skipped task ${taskName} because app ${app.fqdn} has state ${app.installationState} / ${app.runState}`);
|
||||
return callback();
|
||||
}
|
||||
|
||||
|
||||
@@ -259,6 +259,7 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/apps/:id/configure/tags', appsManageScope, routes.apps.setTags);
|
||||
router.post('/api/v1/apps/:id/configure/icon', appsManageScope, routes.apps.setIcon);
|
||||
router.post('/api/v1/apps/:id/configure/memory_limit', appsManageScope, routes.apps.setMemoryLimit);
|
||||
router.post('/api/v1/apps/:id/configure/cpu_shares', appsManageScope, routes.apps.setCpuShares);
|
||||
router.post('/api/v1/apps/:id/configure/automatic_backup', appsManageScope, routes.apps.setAutomaticBackup);
|
||||
router.post('/api/v1/apps/:id/configure/automatic_update', appsManageScope, routes.apps.setAutomaticUpdate);
|
||||
router.post('/api/v1/apps/:id/configure/reverse_proxy', appsManageScope, routes.apps.setReverseProxyConfig);
|
||||
@@ -277,6 +278,7 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/apps/:id/backups', appsManageScope, routes.apps.listBackups);
|
||||
router.post('/api/v1/apps/:id/stop', appsManageScope, routes.apps.stopApp);
|
||||
router.post('/api/v1/apps/:id/start', appsManageScope, routes.apps.startApp);
|
||||
router.post('/api/v1/apps/:id/restart', appsManageScope, routes.apps.restartApp);
|
||||
router.get ('/api/v1/apps/:id/logstream', appsManageScope, routes.apps.getLogStream);
|
||||
router.get ('/api/v1/apps/:id/logs', appsManageScope, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', appsManageScope, routes.apps.exec);
|
||||
|
||||
+1
-3
@@ -614,15 +614,13 @@ function initCache(callback) {
|
||||
getAll(function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const provider = safe.fs.readFileSync(paths.PROVIDER_FILE, 'utf8');
|
||||
|
||||
gCache = {
|
||||
apiServerOrigin: allSettings[exports.API_SERVER_ORIGIN_KEY],
|
||||
webServerOrigin: allSettings[exports.WEB_SERVER_ORIGIN_KEY],
|
||||
adminDomain: allSettings[exports.ADMIN_DOMAIN_KEY],
|
||||
adminFqdn: allSettings[exports.ADMIN_FQDN_KEY],
|
||||
isDemo: allSettings[exports.DEMO_KEY],
|
||||
provider: provider ? provider.trim() : 'generic'
|
||||
provider: allSettings[exports.SYSINFO_CONFIG_KEY].provider
|
||||
};
|
||||
|
||||
callback();
|
||||
|
||||
@@ -113,6 +113,7 @@ describe('Apps', function () {
|
||||
portBindings: { PORT: 5678 },
|
||||
accessRestriction: null,
|
||||
memoryLimit: 0,
|
||||
cpuShares: 512,
|
||||
reverseProxyConfig: null,
|
||||
sso: false,
|
||||
mailboxDomain: DOMAIN_0.domain,
|
||||
@@ -137,6 +138,7 @@ describe('Apps', function () {
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] },
|
||||
memoryLimit: 0,
|
||||
cpuShares: 512,
|
||||
env: {},
|
||||
dataDir: '',
|
||||
mailboxDomain: DOMAIN_0.domain,
|
||||
@@ -157,6 +159,7 @@ describe('Apps', function () {
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'someuser', USER_0.id ], groups: [ GROUP_1.id ] },
|
||||
memoryLimit: 0,
|
||||
cpuShares: 512,
|
||||
reverseProxyConfig: null,
|
||||
sso: false,
|
||||
env: {},
|
||||
@@ -232,6 +235,7 @@ describe('Apps', function () {
|
||||
expect(app.iconUrl).to.be(null);
|
||||
expect(app.fqdn).to.eql(APP_0.location + '.' + DOMAIN_0.domain);
|
||||
expect(app.memoryLimit).to.eql(0);
|
||||
expect(app.cpuShares).to.eql(512);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -405,6 +405,7 @@ describe('database', function () {
|
||||
accessRestriction: null,
|
||||
lastBackupId: null,
|
||||
memoryLimit: 4294967296,
|
||||
cpuShares: 1024,
|
||||
sso: true,
|
||||
debugMode: null,
|
||||
reverseProxyConfig: {},
|
||||
@@ -983,6 +984,7 @@ describe('database', function () {
|
||||
health: null,
|
||||
accessRestriction: null,
|
||||
memoryLimit: 4294967296,
|
||||
cpuShares: 256,
|
||||
sso: true,
|
||||
debugMode: null,
|
||||
reverseProxyConfig: {},
|
||||
@@ -1015,6 +1017,7 @@ describe('database', function () {
|
||||
health: null,
|
||||
accessRestriction: { users: [ 'foobar' ] },
|
||||
memoryLimit: 0,
|
||||
cpuShares: 512,
|
||||
sso: true,
|
||||
debugMode: null,
|
||||
reverseProxyConfig: {},
|
||||
@@ -1111,6 +1114,7 @@ describe('database', function () {
|
||||
APP_0.accessRestriction = '';
|
||||
APP_0.httpPort = 1337;
|
||||
APP_0.memoryLimit = 1337;
|
||||
APP_0.cpuShares = 1024;
|
||||
|
||||
var data = {
|
||||
installationState: APP_0.installationState,
|
||||
@@ -1119,7 +1123,8 @@ describe('database', function () {
|
||||
manifest: APP_0.manifest,
|
||||
accessRestriction: APP_0.accessRestriction,
|
||||
httpPort: APP_0.httpPort,
|
||||
memoryLimit: APP_0.memoryLimit
|
||||
memoryLimit: APP_0.memoryLimit,
|
||||
cpuShares: APP_0.cpuShares
|
||||
};
|
||||
|
||||
appdb.update(APP_0.id, data, function (error) {
|
||||
|
||||
Reference in New Issue
Block a user