Compare commits
243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edc2c25bda | |||
| 2fa44879e9 | |||
| 7e27413b29 | |||
| 77508d180e | |||
| 111f5a7c99 | |||
| d2182559e8 | |||
| 9fe91cf9cb | |||
| de000648dc | |||
| 0958a57c45 | |||
| 9aae0d9d4c | |||
| ccfd385beb | |||
| ee6cca5cdf | |||
| 0093e840c6 | |||
| 7c6e5ac32b | |||
| 15039bf293 | |||
| 89b6cd9d1f | |||
| 60992405d5 | |||
| d96b1cc864 | |||
| 5165cd8f40 | |||
| 9f8b47daa9 | |||
| 9372afad9a | |||
| eef6056174 | |||
| a1dfc758c6 | |||
| 8caf5cc741 | |||
| 7739f8f174 | |||
| 0618431be7 | |||
| 609c4388f0 | |||
| 28243956db | |||
| ff3a4f65dd | |||
| 44da148fd1 | |||
| 0b37479838 | |||
| c09aa2a498 | |||
| 00d032616f | |||
| 041285b187 | |||
| fa9aa50fdf | |||
| e0b1ebba92 | |||
| 581bbafa06 | |||
| ce93518c0a | |||
| 0ba0b009c7 | |||
| eed8f109bc | |||
| 63946509b3 | |||
| 668ff99450 | |||
| 03984a811f | |||
| 7c733ae150 | |||
| 9fe02d3818 | |||
| f10b80d90d | |||
| caf1d18250 | |||
| c700635656 | |||
| d6d2ee7d19 | |||
| f5a5da6731 | |||
| 8f2ce5f4f8 | |||
| 62619d21c0 | |||
| bf7707b70b | |||
| 698f3c833b | |||
| 5996a107ed | |||
| 0307dc5145 | |||
| f9aa09f717 | |||
| 2688a57d46 | |||
| 7ad069fd94 | |||
| 06d043dac4 | |||
| 94be6a9e3c | |||
| 97567b7d2a | |||
| 95d691154d | |||
| 9ac9b49522 | |||
| 6a887c2bba | |||
| 0250508a89 | |||
| f97973626c | |||
| 5cdf9d1c6f | |||
| 009e888686 | |||
| e3478c9d13 | |||
| 2e3ddba7e5 | |||
| 81ac44b7da | |||
| ffe50ff977 | |||
| 73faba3c28 | |||
| c1db52927e | |||
| e7120bd086 | |||
| 91ad94f978 | |||
| ee517da4f4 | |||
| 0d04213199 | |||
| 114f6c596d | |||
| 5dadd083be | |||
| 28d61a4d70 | |||
| a49969f2be | |||
| 65eaf0792e | |||
| 6153f45fd9 | |||
| d5ffb8b118 | |||
| bc283f1485 | |||
| 2d427a86f0 | |||
| 2a6edd53b6 | |||
| cf8bb3da9e | |||
| 7c1325cb34 | |||
| f4ad912cf3 | |||
| 78936a35c5 | |||
| 4ab2f8c153 | |||
| 858f03e02d | |||
| 045cfeeb0d | |||
| bbc121399e | |||
| 03d513a3b1 | |||
| 539447409e | |||
| 4525c6f39e | |||
| a6618c5813 | |||
| 5c1a0c1305 | |||
| 62c9fc90f9 | |||
| 2c60614d4b | |||
| 816fa94555 | |||
| bbdafc6a2f | |||
| 5333db5239 | |||
| 6254fe196a | |||
| 79d3713a4b | |||
| f1da537c80 | |||
| b19fc23cb2 | |||
| 5366524dc0 | |||
| f86d4f0755 | |||
| 3ad495528f | |||
| 5bfb253869 | |||
| 630fbb373c | |||
| 8d09ec5ca6 | |||
| 23af20ddc9 | |||
| 86441bfeb6 | |||
| be51daabf0 | |||
| 86e8db435a | |||
| d8401f9ef9 | |||
| 64f98aca5a | |||
| f660947594 | |||
| 576e22e1a0 | |||
| e004a00073 | |||
| c0fdac5b34 | |||
| a2a035235e | |||
| 85ac2bbe52 | |||
| c26e9bbef7 | |||
| 3e85029ea1 | |||
| 8dd3c55ecf | |||
| 1ee902a541 | |||
| 55cbe46c7c | |||
| 5a8a4e7907 | |||
| 3b5be641f0 | |||
| a34fe120fb | |||
| b9918cb6fb | |||
| 21a86175b4 | |||
| 84150f53e7 | |||
| 42c1f8bb04 | |||
| a504759b95 | |||
| af763eadd4 | |||
| 818735e2c8 | |||
| aefa8ed0d6 | |||
| 78d3aafd7a | |||
| 1fed7335cf | |||
| 34626abdcf | |||
| 477f8a3ca1 | |||
| bd7bd2adae | |||
| d47fd34f66 | |||
| f6ceee7f50 | |||
| a08f05fb44 | |||
| 6214ba7b31 | |||
| e69004548b | |||
| c8216d84ac | |||
| 5cfc3b22fa | |||
| a755aecfc5 | |||
| 64e34e13be | |||
| e455ea987a | |||
| bf87d3fc8b | |||
| cfdb939bff | |||
| 16fab63442 | |||
| fe14bcf155 | |||
| 9732b899b0 | |||
| 23b9854c57 | |||
| 0b3f65c70e | |||
| 83d56f79c6 | |||
| 8632939c6e | |||
| 93cdba8137 | |||
| 2686a1b9e3 | |||
| eec1fe1272 | |||
| f7f26fdf78 | |||
| d980d44833 | |||
| af4b2b075e | |||
| 3fcf6aa339 | |||
| 1b205ac107 | |||
| bcee0aa2ad | |||
| e2bf52b69d | |||
| 3c6dffbbc7 | |||
| 691b876d61 | |||
| ed14115ff1 | |||
| 6d9c6ffba3 | |||
| b8dd01d502 | |||
| 7e16b96abe | |||
| 705d0ba7f9 | |||
| 306c80dd93 | |||
| c8ed1d950b | |||
| 705bf3db98 | |||
| e2b388f721 | |||
| 3f34734933 | |||
| 391ee00db8 | |||
| 64a7b80395 | |||
| 46a00c839b | |||
| 9f6621434f | |||
| 3963eb687f | |||
| 56cd97147a | |||
| 614b3ed5d1 | |||
| 9f622c5e65 | |||
| 4f6a467181 | |||
| a46e208c63 | |||
| a0fd60408b | |||
| ffbbba938a | |||
| a222b3ed58 | |||
| 6ba574432a | |||
| 96075c7c20 | |||
| 64665542bc | |||
| 54d2a4f17b | |||
| 1d829c4af2 | |||
| dc4dc05628 | |||
| b2469de9b0 | |||
| 812d3576a9 | |||
| 6012eb7898 | |||
| a591b5910e | |||
| 1e084e98d1 | |||
| b4224a7f8d | |||
| ab8a010b94 | |||
| 4c164c17cf | |||
| 650f181a07 | |||
| 97ab521038 | |||
| c138c4bb5f | |||
| 1067ff882a | |||
| b6ad6e121b | |||
| a756345138 | |||
| 35f69cfea9 | |||
| 3f0bc6165b | |||
| d0dde04695 | |||
| 2f38a4018c | |||
| 2c76716bc7 | |||
| f38b87c660 | |||
| 9bac2acc37 | |||
| 68536b6d7d | |||
| 1d0a52404a | |||
| 017460b497 | |||
| 8efd496579 | |||
| 229fe0f66f | |||
| efa36ab161 | |||
| 191c85d01f | |||
| 88330ab415 | |||
| 76f5b22c07 | |||
| 0639ca1594 | |||
| e620a26c04 | |||
| 867c3595b2 |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,3 @@ installer/src/certs/server.key
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"multistr": true,
|
||||
"globalstrict": true,
|
||||
"predef": [ "angular", "$" ],
|
||||
"esnext": true
|
||||
}
|
||||
@@ -1376,14 +1376,18 @@
|
||||
[3.1.3]
|
||||
* Prevent dashboard domain from being deleted
|
||||
* Add alternateDomains to app install route
|
||||
|
||||
[3.1.4]
|
||||
* Fix issue where support tab was redirecting
|
||||
* cloudflare: Fix crash when access denied
|
||||
|
||||
[3.1.4]
|
||||
* Fix issue where support tab was redirecting
|
||||
|
||||
[3.2.0]
|
||||
* Add DO Spaces SFO2 region
|
||||
* Wildcard DNS now validates the config
|
||||
* Add ACMEv2 support
|
||||
* Add Wildcard Let's Encrypt provider
|
||||
|
||||
[3.2.1]
|
||||
* Add acme2 support. This provides DNS based validation removing inbound port 80 requirement
|
||||
* Add support for wildcard certificates
|
||||
* Allow mailbox name to be reset to the buit-in '.app' name
|
||||
@@ -1393,3 +1397,31 @@
|
||||
* Add SFO2 region for DigitalOcean Spaces
|
||||
* Show the title in port bindings instead of the long description
|
||||
|
||||
[3.2.2]
|
||||
* Update Haraka to 2.8.20
|
||||
* (mail) Fix issue where LDAP connections where not cleaned up
|
||||
|
||||
[3.3.0]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
|
||||
[3.3.1]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password password pas
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
|
||||
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||
apt-get -y install \
|
||||
acl \
|
||||
awscli \
|
||||
@@ -34,12 +35,14 @@ apt-get -y install \
|
||||
curl \
|
||||
dmsetup \
|
||||
iptables \
|
||||
libpython2.7 \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
rcconf \
|
||||
resolvconf \
|
||||
sudo \
|
||||
swaks \
|
||||
unattended-upgrades \
|
||||
unbound \
|
||||
@@ -87,11 +90,12 @@ if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}"
|
||||
docker pull "${image%@sha256:*}" # this will tag the image for readability
|
||||
done
|
||||
|
||||
echo "==> Install collectd"
|
||||
@@ -101,6 +105,11 @@ if ! apt-get install -y collectd collectd-utils; then
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# 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
|
||||
@@ -113,3 +122,7 @@ systemctl disable dnsmasq || true
|
||||
systemctl stop postfix || true
|
||||
systemctl disable postfix || true
|
||||
|
||||
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
|
||||
systemctl stop systemd-resolved || true
|
||||
systemctl disable systemd-resolved || true
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
let async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
@@ -36,8 +35,7 @@ console.log();
|
||||
async.series([
|
||||
server.start,
|
||||
ldap.start,
|
||||
dockerProxy.start,
|
||||
appHealthMonitor.start,
|
||||
dockerProxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
@@ -49,6 +47,8 @@ async.series([
|
||||
var NOOP_CALLBACK = function () { };
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
console.log('Received SIGINT. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
@@ -56,6 +56,8 @@ process.on('SIGINT', function () {
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
console.log('Received SIGTERM. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE IF NOT EXISTS appEnvVars(' +
|
||||
'appId VARCHAR(128) NOT NULL,' +
|
||||
'name TEXT NOT NULL,' +
|
||||
'value TEXT NOT NULL,' +
|
||||
'FOREIGN KEY(appId) REFERENCES apps(id)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE appEnvVars', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -118,6 +118,12 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
|
||||
value VARCHAR(512) NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appEnvVars(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP,
|
||||
|
||||
Generated
+73
-23
@@ -1632,9 +1632,9 @@
|
||||
}
|
||||
},
|
||||
"cloudron-manifestformat": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.13.1.tgz",
|
||||
"integrity": "sha512-KvWaUw0q2U+EL+y7LJ+Q8YZfERYgyGRwj48ZRfsREaIjckS8mG03Oa2UIf2x/uGLfw0frWvTNdQXe3ZpC94N9g==",
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.14.2.tgz",
|
||||
"integrity": "sha512-+VQwlP/2NY0VIjPTkANhlg8DrS62IxkAVS7B7KG6DrzRp+hRCejbDMQjUB8GvyPSVQGerqUoEu5R/tc4/UBiAA==",
|
||||
"requires": {
|
||||
"cron": "1.3.0",
|
||||
"java-packagename-regex": "1.0.0",
|
||||
@@ -3244,10 +3244,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fstream": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
|
||||
@@ -3988,18 +3984,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"once": "1.4.0",
|
||||
"path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
|
||||
}
|
||||
},
|
||||
"globule": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
|
||||
@@ -4592,7 +4576,7 @@
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
|
||||
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -5449,7 +5433,7 @@
|
||||
},
|
||||
"ncp": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
|
||||
"resolved": "http://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
|
||||
"integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -7369,9 +7353,75 @@
|
||||
"rimraf": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||
"integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
|
||||
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
|
||||
"requires": {
|
||||
"glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz"
|
||||
"glob": "7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "1.0.0",
|
||||
"inflight": "1.0.6",
|
||||
"inherits": "2.0.3",
|
||||
"minimatch": "3.0.4",
|
||||
"once": "1.4.0",
|
||||
"path-is-absolute": "1.0.1"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "1.4.0",
|
||||
"wrappy": "1.0.2"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.11"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
},
|
||||
"s3-block-read-stream": {
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
"async": "^2.6.1",
|
||||
"aws-sdk": "^2.253.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.13.1",
|
||||
"cloudron-manifestformat": "^2.14.2",
|
||||
"connect": "^3.6.6",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
|
||||
echo "${currentValue}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function wait_for_status() {
|
||||
key="$1"
|
||||
expectedValue="$2"
|
||||
|
||||
echo "wait_for_status: $key to be $expectedValue"
|
||||
while true; do
|
||||
if currentValue=$(get_status "${key}"); then
|
||||
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
|
||||
if [[ "${currentValue}" == $expectedValue ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
domain=""
|
||||
domainProvider=""
|
||||
domainConfigJson="{}"
|
||||
domainTlsProvider="letsencrypt-prod"
|
||||
adminUsername="superadmin"
|
||||
adminPassword="Secret123#"
|
||||
adminEmail="admin@server.local"
|
||||
appstoreUserId=""
|
||||
appstoreToken=""
|
||||
backupDir="/var/backups"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--domain-provider) domainProvider="$2"; shift 2;;
|
||||
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
|
||||
--admin-username) adminUsername="$2"; shift 2;;
|
||||
--admin-password) adminPassword="$2"; shift 2;;
|
||||
--admin-email) adminEmail="$2"; shift 2;;
|
||||
--appstore-user) appstoreUser="$2"; shift 2;;
|
||||
--appstore-token) appstoreToken="$2"; shift 2;;
|
||||
--backup-dir) backupDir="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
|
||||
echo "=> Domain setup"
|
||||
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
|
||||
|
||||
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
|
||||
echo "DNS Setup Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
else
|
||||
echo "=> Skipping Domain setup"
|
||||
fi
|
||||
|
||||
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
|
||||
if [[ $(get_status "activated") == "false" ]]; then
|
||||
echo "=> Activating"
|
||||
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
|
||||
echo "Failed to activate with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "activated" "true"
|
||||
else
|
||||
echo "=> Skipping Activation"
|
||||
fi
|
||||
|
||||
echo "=> Getting token"
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
|
||||
echo "Failed to login with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
|
||||
|
||||
echo "=> Setting up App Store account with accessToken ${accessToken}"
|
||||
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
|
||||
|
||||
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
|
||||
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
|
||||
|
||||
chown -R yellowtent:yellowtent "${backupDir}"
|
||||
|
||||
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Done!"
|
||||
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
ip=""
|
||||
zone=""
|
||||
subdomain=""
|
||||
cloudflare_token=""
|
||||
cloudflare_email=""
|
||||
tls_cert_file=""
|
||||
tls_key_file=""
|
||||
appstore_id=""
|
||||
appstore_token=""
|
||||
|
||||
args=$(getopt -o "" -l "subdomain:,zone:,ip:,cloudflare-token:,cloudflare-email:,tls-cert:,tls-key:,appstore-id:,appstore-token:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--ip) ip="$2"; shift 2;;
|
||||
--subdomain) subdomain="$2"; shift 2;;
|
||||
--zone) zone="$2"; shift 2;;
|
||||
--cloudflare-token) cloudflare_token="$2"; shift 2;;
|
||||
--cloudflare-email) cloudflare_email="$2"; shift 2;;
|
||||
--tls-cert) tls_cert_file="$2"; shift 2;;
|
||||
--tls-key) tls_key_file="$2"; shift 2;;
|
||||
--appstore-id) appstore_id="$2"; shift 2;;
|
||||
--appstore-token) appstore_token="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
|
||||
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
|
||||
echo "${currentValue}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function wait_for_status() {
|
||||
key="$1"
|
||||
expectedValue="$2"
|
||||
|
||||
echo "wait_for_status: $key to be $expectedValue"
|
||||
while true; do
|
||||
if currentValue=$(get_status "${key}"); then
|
||||
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
|
||||
if [[ "${currentValue}" == $expectedValue ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
echo "Provisioning Cloudron ${subdomain}.${zone}"
|
||||
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
|
||||
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
|
||||
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
|
||||
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
|
||||
else
|
||||
fallback_cert=null
|
||||
fi
|
||||
|
||||
setupData=$(printf '{ "dnsConfig": { "domain": "%s", "provider": "cloudflare", "config": { "token": "%s", "email": "%s", "hyphenatedSubdomains": true }, "tlsConfig": { "provider": "fallback" }, "fallbackCertificate": %s }, "autoconf": { "appstoreConfig": { "userId": "%s", "token": "%s" } } }' "${subdomain}.${zone}" "${cloudflare_token}" "${cloudflare_email}" "${fallback_cert}" "${appstore_id}" "${appstore_token}")
|
||||
|
||||
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
|
||||
echo "Failed to setup with ${setupData}: ${setupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
|
||||
echo "Cloudron is ready at https://my-${subdomain}.${zone}"
|
||||
|
||||
+44
-30
@@ -4,7 +4,6 @@ set -eu -o pipefail
|
||||
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly LOG_FILE="/var/log/cloudron-setup.log"
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
|
||||
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
|
||||
|
||||
@@ -48,14 +47,11 @@ edition=""
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
prerelease="false"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
baseDataDir=""
|
||||
|
||||
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,edition:,skip-reboot" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
@@ -68,17 +64,13 @@ while true; do
|
||||
if [[ "$2" == "dev" ]]; then
|
||||
apiServerOrigin="https://api.dev.cloudron.io"
|
||||
webServerOrigin="https://dev.cloudron.io"
|
||||
prerelease="true"
|
||||
elif [[ "$2" == "staging" ]]; then
|
||||
apiServerOrigin="https://api.staging.cloudron.io"
|
||||
webServerOrigin="https://staging.cloudron.io"
|
||||
prerelease="true"
|
||||
fi
|
||||
shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--prerelease) prerelease="true"; shift;;
|
||||
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
@@ -91,11 +83,15 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
fi
|
||||
|
||||
# Only --help works with mismatched ubuntu
|
||||
if [[ $(lsb_release -rs) != "16.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Can only write after we have confirmed script has root access
|
||||
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
@@ -154,7 +150,7 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
||||
fi
|
||||
|
||||
echo "=> Checking version"
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
|
||||
echo "Failed to get release information"
|
||||
exit 1
|
||||
fi
|
||||
@@ -170,19 +166,6 @@ if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build data
|
||||
# from 1.9, we use autoprovision.json
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "=> Downloading version ${version} ..."
|
||||
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||
|
||||
@@ -200,13 +183,44 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# NOTE: this install script only supports 3.x and above
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "${DATA_FILE}"
|
||||
else
|
||||
mkdir -p /etc/cloudron
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
rm "${DATA_FILE}"
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
|
||||
PASTEBIN="https://paste.cloudron.io"
|
||||
OUT="/tmp/cloudron-support.log"
|
||||
LINE="\n========================================================\n"
|
||||
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "Generating Cloudron Support stats..."
|
||||
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
if [[ $SUDO_USER === "" ]]; then
|
||||
ssh_user="root"
|
||||
ssh_folder="/root/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
else
|
||||
ssh_user="$SUDO_USER"
|
||||
ssh_folder="/home/$SUDO_USER/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
|
||||
echo -e $LINE"cloudron.conf"$LINE >> $OUT
|
||||
cat /etc/cloudron/cloudron.conf &>> $OUT
|
||||
|
||||
echo -e $LINE"Docker container"$LINE >> $OUT
|
||||
docker ps -a &>> $OUT
|
||||
|
||||
echo -e $LINE"Filesystem stats"$LINE >> $OUT
|
||||
df -h &>> $OUT
|
||||
|
||||
echo -e $LINE"System daemon status"$LINE >> $OUT
|
||||
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
iptables -L &>> $OUT
|
||||
|
||||
echo "Done"
|
||||
|
||||
echo -n "Uploading information..."
|
||||
# for some reason not using $(cat $OUT) will not contain newlines!?
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p "${ssh_folder}"
|
||||
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
|
||||
chown -R ${ssh_user} "${ssh_folder}"
|
||||
chmod 600 "${authorized_key_file}"
|
||||
echo "Done"
|
||||
|
||||
echo ""
|
||||
echo "Please send the following link to support@cloudron.io"
|
||||
echo ""
|
||||
echo "${PASTEBIN}/${paste_key}"
|
||||
@@ -7,21 +7,28 @@ set -eu
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
|
||||
args=$(${GNU_GETOPT} -o "" -l "output:,version:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
bundle_file=""
|
||||
version=""
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--output) bundle_file="$2"; shift 2;;
|
||||
--version) version="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${version}" ]]; then
|
||||
echo "--version is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
|
||||
|
||||
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
@@ -41,19 +48,16 @@ fi
|
||||
|
||||
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [[ "${branch}" == "master" ]]; then
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
|
||||
else
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
|
||||
fi
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
|
||||
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
|
||||
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
|
||||
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}-${version}.tar.gz"
|
||||
|
||||
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
|
||||
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
|
||||
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
|
||||
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
|
||||
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
|
||||
echo "${version}" > "${bundle_dir}/VERSION"
|
||||
|
||||
echo "==> Installing modules for dashboard asset generation"
|
||||
(cd "${bundle_dir}/dashboard.build" && npm install --production)
|
||||
|
||||
+15
-28
@@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is run before the box code is switched. This means that we can
|
||||
# put network related/curl downloads here. If the script fails, the old code
|
||||
# will continue to run
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
@@ -10,29 +14,12 @@ fi
|
||||
readonly USER=yellowtent
|
||||
readonly BOX_SRC_DIR=/home/${USER}/box
|
||||
readonly BASE_DATA_DIR=/home/${USER}
|
||||
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
|
||||
|
||||
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
|
||||
|
||||
arg_data=""
|
||||
arg_data_dir=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,data-file:,data-dir:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--data) arg_data="$2"; shift 2;;
|
||||
--data-file) arg_data=$(cat $2); shift 2;;
|
||||
--data-dir) arg_data_dir="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
@@ -94,6 +81,15 @@ if [[ ${try} -eq 10 ]]; then
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo "==> installer: downloading new addon images"
|
||||
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}" # this pulls the image using the sha256
|
||||
docker pull "${image%@sha256:*}" # this will tag the image for readability
|
||||
done
|
||||
|
||||
echo "==> installer: update cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
|
||||
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
|
||||
@@ -115,15 +111,6 @@ if [[ "${is_update}" == "yes" ]]; then
|
||||
${BOX_SRC_DIR}/setup/stop.sh
|
||||
fi
|
||||
|
||||
# setup links to data directory
|
||||
if [[ -n "${arg_data_dir}" ]]; then
|
||||
echo "==> installer: setting up links to data directory"
|
||||
mkdir "${arg_data_dir}/appsdata"
|
||||
ln -s "${arg_data_dir}/appsdata" "${BASE_DATA_DIR}/appsdata"
|
||||
mkdir "${arg_data_dir}/platformdata"
|
||||
ln -s "${arg_data_dir}/platformdata" "${BASE_DATA_DIR}/platformdata"
|
||||
fi
|
||||
|
||||
# ensure we are not inside the source directory, which we will remove now
|
||||
cd /root
|
||||
|
||||
@@ -133,4 +120,4 @@ mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
|
||||
|
||||
echo "==> installer: calling box setup script"
|
||||
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
|
||||
"${BOX_SRC_DIR}/setup/start.sh"
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
json="${source_dir}/../node_modules/.bin/json"
|
||||
|
||||
arg_api_server_origin=""
|
||||
arg_fqdn="" # remove after 1.10
|
||||
arg_admin_domain=""
|
||||
arg_admin_location=""
|
||||
arg_admin_fqdn=""
|
||||
arg_retire_reason=""
|
||||
arg_retire_info=""
|
||||
arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_provider=""
|
||||
arg_is_demo="false"
|
||||
arg_edition=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--retire-reason)
|
||||
arg_retire_reason="$2"
|
||||
shift 2
|
||||
;;
|
||||
--retire-info)
|
||||
arg_retire_info="$2"
|
||||
shift 2
|
||||
;;
|
||||
--data)
|
||||
# these params must be valid in all cases
|
||||
arg_fqdn=$(echo "$2" | $json fqdn)
|
||||
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
|
||||
|
||||
arg_admin_location=$(echo "$2" | $json adminLocation)
|
||||
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
|
||||
|
||||
arg_admin_domain=$(echo "$2" | $json adminDomain)
|
||||
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
|
||||
|
||||
# only update/restore have this valid (but not migrate)
|
||||
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
|
||||
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
|
||||
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
|
||||
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
|
||||
|
||||
# TODO check if and where this is used
|
||||
arg_version=$(echo "$2" | $json version)
|
||||
|
||||
# read possibly empty parameters here
|
||||
arg_is_demo=$(echo "$2" | $json isDemo)
|
||||
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
|
||||
|
||||
arg_provider=$(echo "$2" | $json provider)
|
||||
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
|
||||
|
||||
arg_edition=$(echo "$2" | $json edition)
|
||||
[[ "${arg_edition}" == "" ]] && arg_edition=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Parsed arguments:"
|
||||
echo "api server: ${arg_api_server_origin}"
|
||||
echo "admin fqdn: ${arg_admin_fqdn}"
|
||||
echo "fqdn: ${arg_fqdn}"
|
||||
echo "version: ${arg_version}"
|
||||
echo "web server: ${arg_web_server_origin}"
|
||||
echo "provider: ${arg_provider}"
|
||||
echo "edition: ${arg_edition}"
|
||||
+15
-51
@@ -2,6 +2,9 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script is run after the box code is switched. This means that this script
|
||||
# should pretty much always succeed. No network logic/download code here.
|
||||
|
||||
echo "==> Cloudron Start"
|
||||
|
||||
readonly USER="yellowtent"
|
||||
@@ -12,17 +15,8 @@ readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
|
||||
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
readonly CONFIG_DIR="${HOME_DIR}/configs"
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
hostnamectl set-hostname "${arg_admin_fqdn}"
|
||||
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
@@ -48,19 +42,6 @@ if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /e
|
||||
fi
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
# caas has ssh on port 202 and we disable password login
|
||||
if [[ "${arg_provider}" == "caas" ]]; then
|
||||
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
|
||||
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
|
||||
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
|
||||
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
|
||||
-e 's/^#\?Port .*/Port 202/g' \
|
||||
-i /etc/ssh/sshd_config
|
||||
|
||||
# required so we can connect to this machine since port 22 is blocked by iptables by now
|
||||
systemctl reload sshd
|
||||
fi
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
|
||||
@@ -71,6 +52,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/graphite"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/redis"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
@@ -109,6 +91,14 @@ setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
echo "==> Creating config directory"
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
|
||||
# migration for cloudron.conf file. Can be removed after 3.3
|
||||
if [[ ! -d /etc/cloudron ]]; then
|
||||
echo "==> Migrating existing cloudron.conf to new location"
|
||||
mkdir -p /etc/cloudron
|
||||
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
|
||||
fi
|
||||
chown -R "${USER}" /etc/cloudron
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
||||
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
|
||||
@@ -171,14 +161,8 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
|
||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
# remove this migration after 1.10
|
||||
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
|
||||
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
|
||||
systemctl start nginx
|
||||
|
||||
# bookkeep the version as part of data
|
||||
echo "{ \"version\": \"${arg_version}\", \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${BOX_DATA_DIR}/version"
|
||||
|
||||
# restart mysql to make sure it has latest config
|
||||
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
|
||||
# wait for all running mysql jobs
|
||||
@@ -208,28 +192,6 @@ cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
|
||||
echo "==> Creating cloudron.conf"
|
||||
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"version": "${arg_version}",
|
||||
"apiServerOrigin": "${arg_api_server_origin}",
|
||||
"webServerOrigin": "${arg_web_server_origin}",
|
||||
"adminDomain": "${arg_admin_domain}",
|
||||
"adminFqdn": "${arg_admin_fqdn}",
|
||||
"adminLocation": "${arg_admin_location}",
|
||||
"provider": "${arg_provider}",
|
||||
"isDemo": ${arg_is_demo},
|
||||
"edition": "${arg_edition}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
echo "==> Creating config.json for dashboard"
|
||||
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
|
||||
{
|
||||
"webServerOrigin": "${arg_web_server_origin}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
|
||||
@@ -240,9 +202,11 @@ fi
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
# sudo logging breaks journalctl output with very long urls (systemd bug)
|
||||
Defaults !syslog
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmaddon.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
|
||||
|
||||
+741
-196
File diff suppressed because it is too large
Load Diff
+47
-9
@@ -106,7 +106,7 @@ function postProcess(result) {
|
||||
delete result.environmentVariables;
|
||||
delete result.portTypes;
|
||||
|
||||
for (var i = 0; i < environmentVariables.length; i++) {
|
||||
for (let i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
|
||||
}
|
||||
|
||||
@@ -130,6 +130,14 @@ function postProcess(result) {
|
||||
delete d.appId;
|
||||
delete d.type;
|
||||
});
|
||||
|
||||
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
|
||||
delete result.envNames;
|
||||
delete result.envValues;
|
||||
result.env = {};
|
||||
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
|
||||
if (envNames[i]) result.env[envNames[i]] = envValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -137,9 +145,11 @@ function get(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -153,7 +163,7 @@ function get(id, callback) {
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,9 +172,11 @@ function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -186,9 +198,11 @@ function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -209,9 +223,11 @@ function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -259,6 +275,7 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
var sso = 'sso' in data ? data.sso : null;
|
||||
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
var env = data.env || {};
|
||||
|
||||
var queries = [];
|
||||
|
||||
@@ -280,6 +297,13 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(env).forEach(function (name) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
|
||||
args: [ id, name, env[name] ]
|
||||
});
|
||||
});
|
||||
|
||||
// only allocate a mailbox if mailboxName is set
|
||||
if (data.mailboxName) {
|
||||
queries.push({
|
||||
@@ -354,12 +378,13 @@ function del(id, callback) {
|
||||
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -372,6 +397,7 @@ function clear(callback) {
|
||||
database.query.bind(null, 'DELETE FROM subdomains'),
|
||||
database.query.bind(null, 'DELETE FROM appPortBindings'),
|
||||
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
|
||||
database.query.bind(null, 'DELETE FROM appEnvVars'),
|
||||
database.query.bind(null, 'DELETE FROM apps')
|
||||
], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -391,6 +417,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
|
||||
assert(!('env' in app) || typeof app.env === 'object');
|
||||
|
||||
var queries = [ ];
|
||||
|
||||
@@ -404,6 +431,17 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
if ('env' in app) {
|
||||
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
|
||||
|
||||
Object.keys(app.env).forEach(function (name) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
|
||||
args: [ id, name, app.env[name] ]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ('location' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
@@ -424,7 +462,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains') {
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
}
|
||||
@@ -617,7 +655,7 @@ function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error, results) {
|
||||
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
|
||||
+50
-59
@@ -12,15 +12,14 @@ var appdb = require('./appdb.js'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
run: run
|
||||
};
|
||||
|
||||
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
|
||||
var gHealthInfo = { }; // { time, emailSent }
|
||||
var gRunTimeout = null;
|
||||
var gDockerEventStream = null;
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
@@ -88,6 +87,9 @@ function checkAppHealth(app, callback) {
|
||||
return setHealth(app, appdb.HEALTH_DEAD, callback);
|
||||
}
|
||||
|
||||
// non-appstore apps may not have healthCheckPath
|
||||
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
|
||||
// poll through docker network instead of nginx to bypass any potential oauth proxy
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
superagent
|
||||
@@ -110,48 +112,23 @@ function checkAppHealth(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function processApps(callback) {
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function run() {
|
||||
processApps(function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
OOM can be tested using stress tool like so:
|
||||
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
|
||||
apt-get update && apt-get install stress
|
||||
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||
*/
|
||||
function processDockerEvents() {
|
||||
// note that for some reason, the callback is called only on the first event
|
||||
debug('Listening for docker events');
|
||||
function processDockerEvents(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
|
||||
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
|
||||
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
|
||||
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
|
||||
|
||||
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
gDockerEventStream = stream;
|
||||
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
@@ -173,34 +150,48 @@ function processDockerEvents() {
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
console.error('Error reading docker events', error);
|
||||
gDockerEventStream = null; // TODO: reconnect?
|
||||
debug('Error reading docker events', error);
|
||||
callback();
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
console.error('Docker event stream ended');
|
||||
gDockerEventStream = null; // TODO: reconnect?
|
||||
stream.on('end', callback);
|
||||
|
||||
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
|
||||
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
|
||||
});
|
||||
}
|
||||
|
||||
function processApp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
function run(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
|
||||
debug('Starting apphealthmonitor');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
processDockerEvents();
|
||||
async.series([
|
||||
processDockerEvents.bind(null, interval),
|
||||
processApp
|
||||
], function (error) {
|
||||
if (error) debug(error);
|
||||
|
||||
run();
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clearTimeout(gRunTimeout);
|
||||
if (gDockerEventStream) gDockerEventStream.end();
|
||||
|
||||
callback();
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
+44
-25
@@ -291,6 +291,16 @@ function validateBackupFormat(format) {
|
||||
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
|
||||
}
|
||||
|
||||
function validateEnv(env) {
|
||||
for (let key in env) {
|
||||
if (key.length > 512) return new AppsError(AppsError.BAD_FIELD, 'Max env var key length is 512');
|
||||
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new AppsError(AppsError.BAD_FIELD, `"${key}" is not a valid environment variable`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
@@ -325,7 +335,8 @@ function getAppConfig(app) {
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
|
||||
robotsTxt: app.robotsTxt,
|
||||
sso: app.sso,
|
||||
alternateDomains: app.alternateDomains || []
|
||||
alternateDomains: app.alternateDomains || [],
|
||||
env: app.env
|
||||
};
|
||||
}
|
||||
|
||||
@@ -335,7 +346,7 @@ function removeInternalFields(app) {
|
||||
'location', 'domain', 'fqdn', 'mailboxName',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
|
||||
'alternateDomains', 'ownerId');
|
||||
'alternateDomains', 'ownerId', 'env');
|
||||
}
|
||||
|
||||
function removeRestrictedFields(app) {
|
||||
@@ -535,7 +546,8 @@ function install(data, user, auditSource, callback) {
|
||||
backupId = data.backupId || null,
|
||||
backupFormat = data.backupFormat || 'tgz',
|
||||
ownerId = data.ownerId,
|
||||
alternateDomains = data.alternateDomains || [];
|
||||
alternateDomains = data.alternateDomains || [],
|
||||
env = data.env || {};
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -573,6 +585,9 @@ function install(data, user, auditSource, callback) {
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
|
||||
error = validateEnv(env);
|
||||
if (error) return callback(error);
|
||||
|
||||
var appId = uuid.v4();
|
||||
|
||||
if (icon) {
|
||||
@@ -594,8 +609,7 @@ function install(data, user, auditSource, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
if (cert && key) {
|
||||
let fqdn = domains.fqdn(location, domain, domainObject);
|
||||
error = reverseProxy.validateCertificate(fqdn, cert, key);
|
||||
error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
}
|
||||
|
||||
@@ -611,7 +625,8 @@ function install(data, user, auditSource, callback) {
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt,
|
||||
alternateDomains: alternateDomains
|
||||
alternateDomains: alternateDomains,
|
||||
env: env
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
@@ -619,11 +634,11 @@ function install(data, user, auditSource, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appstore.purchase(appId, appStoreId, function (appstoreError) {
|
||||
appstore.purchase(appId, { appstoreId: appStoreId, manifestId: manifest.id }, function (appstoreError) {
|
||||
// if purchase failed, rollback the appdb record
|
||||
if (appstoreError) {
|
||||
appdb.del(appId, function (error) {
|
||||
if (error) console.error('Failed to rollback app installation.', error);
|
||||
if (error) debug('install: Failed to rollback app installation.', error);
|
||||
|
||||
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
|
||||
@@ -637,9 +652,8 @@ function install(data, user, auditSource, callback) {
|
||||
|
||||
// save cert to boxdata/certs
|
||||
if (cert && key) {
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
let error = reverseProxy.setAppCertificateSync(location, domainObject, { cert, key });
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
|
||||
}
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
@@ -732,6 +746,12 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
values.alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
}
|
||||
|
||||
if ('env' in data) {
|
||||
values.env = data.env;
|
||||
error = validateEnv(data.env);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
@@ -743,18 +763,13 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
|
||||
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('cert' in data && 'key' in data) {
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
if (data.cert && data.key) {
|
||||
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
|
||||
error = reverseProxy.validateCertificate(location, domainObject, { cert: data.cert, key: data.key });
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
} else { // remove existing cert/key
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
|
||||
}
|
||||
|
||||
error = reverseProxy.setAppCertificateSync(location, domainObject, { cert: data.cert, key: data.key });
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
|
||||
}
|
||||
|
||||
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
|
||||
@@ -869,7 +884,7 @@ function getLogs(appId, options, callback) {
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
|
||||
get(appId, function (error /*, app */) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
@@ -883,6 +898,7 @@ function getLogs(appId, options, callback) {
|
||||
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, appId, 'apptask.log'));
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
|
||||
if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
|
||||
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
@@ -1015,18 +1031,19 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
enableBackup: app.enableBackup,
|
||||
robotsTxt: app.robotsTxt
|
||||
robotsTxt: app.robotsTxt,
|
||||
env: app.env
|
||||
};
|
||||
|
||||
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appstore.purchase(newAppId, app.appStoreId, function (appstoreError) {
|
||||
appstore.purchase(newAppId, { appstoreId: app.appStoreId, manifestId: manifest.id }, function (appstoreError) {
|
||||
// if purchase failed, rollback the appdb record
|
||||
if (appstoreError) {
|
||||
appdb.del(newAppId, function (error) {
|
||||
if (error) console.error('Failed to rollback app installation.', error);
|
||||
if (error) debug('install: Failed to rollback app installation.', error);
|
||||
|
||||
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
|
||||
@@ -1065,7 +1082,7 @@ function uninstall(appId, auditSource, callback) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appstore.unpurchase(appId, app.appStoreId, function (error) {
|
||||
appstore.unpurchase(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
@@ -1163,6 +1180,8 @@ function exec(appId, options, callback) {
|
||||
};
|
||||
|
||||
container.exec(execOptions, function (error, exec) {
|
||||
if (error && error.statusCode === 409) return callback(new AppsError(AppsError.BAD_STATE, error.message)); // container restarting/not running
|
||||
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
var startOptions = {
|
||||
Detach: false,
|
||||
|
||||
+8
-12
@@ -19,8 +19,7 @@ exports = module.exports = {
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
@@ -96,18 +95,16 @@ function isFreePlan(subscription) {
|
||||
}
|
||||
|
||||
// See app.js install it will create a db record first but remove it again if appstore purchase fails
|
||||
function purchase(appId, appstoreId, callback) {
|
||||
function purchase(appId, data, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (appstoreId === '') return callback(null);
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
@@ -121,13 +118,12 @@ function purchase(appId, appstoreId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function unpurchase(appId, appstoreId, callback) {
|
||||
function unpurchase(appId, data, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (appstoreId === '') return callback(null);
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -139,7 +135,7 @@ function unpurchase(appId, appstoreId, callback) {
|
||||
if (result.statusCode === 404) return callback(null); // was never purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
+61
-22
@@ -10,8 +10,8 @@ exports = module.exports = {
|
||||
_reserveHttpPort: reserveHttpPort,
|
||||
_configureReverseProxy: configureReverseProxy,
|
||||
_unconfigureReverseProxy: unconfigureReverseProxy,
|
||||
_createVolume: createVolume,
|
||||
_deleteVolume: deleteVolume,
|
||||
_createAppDir: createAppDir,
|
||||
_deleteAppDir: deleteAppDir,
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
@@ -36,6 +36,7 @@ var addons = require('./addons.js'),
|
||||
ejs = require('ejs'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
mkdirp = require('mkdirp'),
|
||||
net = require('net'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
@@ -52,9 +53,7 @@ var addons = require('./addons.js'),
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
|
||||
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'),
|
||||
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
|
||||
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
|
||||
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -161,19 +160,47 @@ function deleteContainers(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function createVolume(app, callback) {
|
||||
function createAppDir(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
|
||||
mkdirp(path.join(paths.APPS_DATA_DIR, app.id), callback);
|
||||
}
|
||||
|
||||
function deleteVolume(app, options, callback) {
|
||||
function deleteAppDir(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, app.id);
|
||||
|
||||
// resolve any symlinked data dir
|
||||
const stat = safe.fs.lstatSync(appDataDir);
|
||||
if (!stat) return callback(null);
|
||||
|
||||
const resolvedAppDataDir = stat.isSymbolicLink() ? safe.fs.readlinkSync(appDataDir) : appDataDir;
|
||||
|
||||
if (safe.fs.existsSync(resolvedAppDataDir)) {
|
||||
const entries = safe.fs.readdirSync(resolvedAppDataDir);
|
||||
if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`);
|
||||
|
||||
// remove only files. directories inside app dir are currently volumes managed by the addons
|
||||
entries.forEach(function (entry) {
|
||||
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
|
||||
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
|
||||
});
|
||||
}
|
||||
|
||||
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
|
||||
if (options.removeDirectory) {
|
||||
if (stat.isSymbolicLink()) {
|
||||
if (!safe.fs.unlinkSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
|
||||
} else {
|
||||
if (!safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function addCollectdProfile(app, callback) {
|
||||
@@ -283,7 +310,10 @@ function registerSubdomain(app, overwrite, callback) {
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
|
||||
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
|
||||
debug('Upsert error. Will retry.', error.message);
|
||||
return retryCallback(error); // try again
|
||||
}
|
||||
|
||||
retryCallback(null, error);
|
||||
});
|
||||
@@ -340,8 +370,10 @@ function registerAlternateDomains(app, overwrite, callback) {
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
|
||||
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
|
||||
debug('Upsert error. Will retry.', error.message);
|
||||
return retryCallback(error); // try again
|
||||
}
|
||||
retryCallback(null, error);
|
||||
});
|
||||
});
|
||||
@@ -358,9 +390,14 @@ function unregisterAlternateDomains(app, all, callback) {
|
||||
assert.strictEqual(typeof all, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var obsoleteDomains;
|
||||
if (all) obsoleteDomains = app.alternateDomains;
|
||||
else obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) { return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; }); });
|
||||
let obsoleteDomains = [];
|
||||
if (all) {
|
||||
obsoleteDomains = app.alternateDomains;
|
||||
} else if (app.oldConfig) { // oldConfig can be null during an infra update
|
||||
obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) {
|
||||
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
|
||||
});
|
||||
}
|
||||
|
||||
if (obsoleteDomains.length === 0) return callback();
|
||||
|
||||
@@ -399,8 +436,10 @@ function cleanupLogs(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// note that redis container logs are cleaned up by the addon
|
||||
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
|
||||
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -459,13 +498,11 @@ function install(app, callback) {
|
||||
deleteMainContainer.bind(null, app),
|
||||
function teardownAddons(next) {
|
||||
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
|
||||
var addonsToRemove = !isRestoring
|
||||
? app.manifest.addons
|
||||
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
|
||||
addons.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
|
||||
// for restore case
|
||||
function deleteImageIfChanged(done) {
|
||||
@@ -489,7 +526,7 @@ function install(app, callback) {
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
|
||||
createVolume.bind(null, app),
|
||||
createAppDir.bind(null, app),
|
||||
|
||||
function restoreFromBackup(next) {
|
||||
if (!restoreConfig) {
|
||||
@@ -500,6 +537,8 @@ function install(app, callback) {
|
||||
} else {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
addons.clearAddons.bind(null, app, app.manifest.addons),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
|
||||
], next);
|
||||
}
|
||||
@@ -596,7 +635,7 @@ function configure(app, callback) {
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
|
||||
createVolume.bind(null, app),
|
||||
createAppDir.bind(null, app),
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
|
||||
@@ -765,7 +804,7 @@ function uninstall(app, callback) {
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
|
||||
deleteVolume.bind(null, app, { removeDirectory: true }),
|
||||
deleteAppDir.bind(null, app, { removeDirectory: true }),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
|
||||
docker.deleteImage.bind(null, app.manifest),
|
||||
|
||||
+7
-3
@@ -21,7 +21,8 @@ exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
// testing
|
||||
_name: 'acme'
|
||||
_name: 'acme',
|
||||
_getChallengeSubdomain: getChallengeSubdomain
|
||||
};
|
||||
|
||||
function Acme2Error(reason, errorOrMessage) {
|
||||
@@ -435,11 +436,14 @@ function getChallengeSubdomain(hostname, domain) {
|
||||
if (hostname === domain) {
|
||||
challengeSubdomain = '_acme-challenge';
|
||||
} else if (hostname.includes('*')) { // wildcard
|
||||
challengeSubdomain = hostname.replace('*', '_acme-challenge').slice(0, -domain.length - 1);
|
||||
let subdomain = hostname.slice(0, -domain.length - 1);
|
||||
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
|
||||
} else {
|
||||
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
|
||||
}
|
||||
|
||||
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
|
||||
|
||||
return challengeSubdomain;
|
||||
}
|
||||
|
||||
@@ -466,7 +470,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
|
||||
domains.waitForDnsRecord(`${challengeSubdomain}`, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback(null, challenge);
|
||||
|
||||
+10
-31
@@ -22,7 +22,6 @@ exports = module.exports = {
|
||||
setAdminFqdn: setAdminFqdn,
|
||||
setAdminLocation: setAdminLocation,
|
||||
version: version,
|
||||
setVersion: setVersion,
|
||||
database: database,
|
||||
edition: edition,
|
||||
|
||||
@@ -37,13 +36,11 @@ exports = module.exports = {
|
||||
hasIPv6: hasIPv6,
|
||||
dkimSelector: dkimSelector,
|
||||
|
||||
isManaged: isManaged,
|
||||
isDemo: isDemo,
|
||||
|
||||
// feature flags based on editions (these have a separate license from standard edition)
|
||||
isSpacesEnabled: isSpacesEnabled,
|
||||
allowHyphenatedSubdomains: allowHyphenatedSubdomains,
|
||||
allowOperatorActions: allowOperatorActions,
|
||||
isAdminDomainLocked: isAdminDomainLocked,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: _reset
|
||||
@@ -59,24 +56,20 @@ var assert = require('assert'),
|
||||
// assert on unknown environment can't proceed
|
||||
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
|
||||
|
||||
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
|
||||
var data = { };
|
||||
|
||||
function baseDir() {
|
||||
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
if (exports.CLOUDRON) return homeDir;
|
||||
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
|
||||
// cannot reach
|
||||
}
|
||||
|
||||
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
|
||||
|
||||
// only tests can run without a config file on disk, they use the defaults with runtime overrides
|
||||
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
|
||||
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
|
||||
|
||||
function saveSync() {
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
version: data.version,
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
adminDomain: data.adminDomain,
|
||||
@@ -104,7 +97,6 @@ function initConfig() {
|
||||
data.adminDomain = '';
|
||||
data.adminLocation = 'my';
|
||||
data.port = 3000;
|
||||
data.version = null;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
data.provider = 'generic';
|
||||
@@ -125,7 +117,6 @@ function initConfig() {
|
||||
|
||||
// overrides for local testings
|
||||
if (exports.TEST) {
|
||||
data.version = '1.1.1-test';
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
@@ -214,11 +205,8 @@ function sysadminOrigin() {
|
||||
}
|
||||
|
||||
function version() {
|
||||
return get('version');
|
||||
}
|
||||
|
||||
function setVersion(version) {
|
||||
set('version', version);
|
||||
if (exports.TEST) return '3.0.0-test';
|
||||
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
|
||||
}
|
||||
|
||||
function database() {
|
||||
@@ -233,23 +221,14 @@ function isSpacesEnabled() {
|
||||
return get('edition') === 'education';
|
||||
}
|
||||
|
||||
function allowHyphenatedSubdomains() {
|
||||
// we should move caas also to hostingprovider edition at some point
|
||||
return get('edition') === 'hostingprovider' || get('provider') === 'caas';
|
||||
}
|
||||
|
||||
function allowOperatorActions() {
|
||||
return get('edition') !== 'hostingprovider';
|
||||
}
|
||||
|
||||
function isAdminDomainLocked() {
|
||||
return get('edition') === 'hostingprovider';
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return get('provider');
|
||||
}
|
||||
|
||||
function isManaged() {
|
||||
return edition() === 'hostingprovider';
|
||||
}
|
||||
|
||||
function hasIPv6() {
|
||||
const IPV6_PROC_FILE = '/proc/net/if_inet6';
|
||||
return fs.existsSync(IPV6_PROC_FILE);
|
||||
|
||||
+12
-2
@@ -7,7 +7,8 @@ exports = module.exports = {
|
||||
stopJobs: stopJobs
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||
apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
@@ -43,7 +44,8 @@ var gJobs = {
|
||||
digestEmail: null,
|
||||
dockerVolumeCleaner: null,
|
||||
dynamicDNS: null,
|
||||
schedulerSync: null
|
||||
schedulerSync: null,
|
||||
appHealthMonitor: null
|
||||
};
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
@@ -196,6 +198,14 @@ function recreateJobs(tz) {
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
|
||||
gJobs.appHealthMonitor = new CronJob({
|
||||
cronTime: '*/10 * * * * *', // every 10 seconds
|
||||
onTick: appHealthMonitor.run.bind(null, 10),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function boxAutoupdatePatternChanged(pattern) {
|
||||
|
||||
@@ -29,13 +29,6 @@ function translateRequestError(result, callback) {
|
||||
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
|
||||
let error = result.body.errors[0];
|
||||
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
if (error.code === 6003) {
|
||||
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
|
||||
else message = 'Invalid credentials';
|
||||
} else if (error.error_chain[0] && error.error_chain[0].message) {
|
||||
message = message + ' ' + error.error_chain[0].message;
|
||||
}
|
||||
|
||||
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
|
||||
}
|
||||
|
||||
|
||||
+120
-4
@@ -2,6 +2,8 @@
|
||||
|
||||
exports = module.exports = {
|
||||
connection: connectionInstance(),
|
||||
setRegistryConfig: setRegistryConfig,
|
||||
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
@@ -16,7 +18,10 @@ exports = module.exports = {
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
execContainer: execContainer
|
||||
execContainer: execContainer,
|
||||
createVolume: createVolume,
|
||||
removeVolume: removeVolume,
|
||||
clearVolume: clearVolume
|
||||
};
|
||||
|
||||
function connectionInstance() {
|
||||
@@ -43,19 +48,62 @@ var addons = require('./addons.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:docker.js'),
|
||||
mkdirp = require('mkdirp'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh');
|
||||
|
||||
function DockerError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(DockerError, Error);
|
||||
DockerError.INTERNAL_ERROR = 'Internal Error';
|
||||
DockerError.BAD_FIELD = 'Bad field';
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function setRegistryConfig(auth, callback) {
|
||||
assert.strictEqual(typeof auth, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const isLogin = !!auth.password;
|
||||
|
||||
// currently, auth info is not stashed in the db but maybe it should for restore to work?
|
||||
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
|
||||
|
||||
child_process.exec(cmd, { }, function (error, stdout, stderr) {
|
||||
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function pullImage(manifest, callback) {
|
||||
var docker = exports.connection;
|
||||
|
||||
@@ -140,6 +188,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
let appEnv = [];
|
||||
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
|
||||
|
||||
// first check db record, then manifest
|
||||
var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0;
|
||||
|
||||
@@ -167,9 +218,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
Tty: isAppContainer,
|
||||
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv),
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
ExposedPorts: isAppContainer ? exposedPorts : { },
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
'/tmp': {},
|
||||
@@ -181,7 +233,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
'isSubcontainer': String(!isAppContainer)
|
||||
},
|
||||
HostConfig: {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
||||
LogConfig: {
|
||||
Type: 'syslog',
|
||||
Config: {
|
||||
@@ -352,7 +404,8 @@ function deleteImage(manifest, callback) {
|
||||
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
|
||||
// not created anymore after https://github.com/docker/docker/pull/10571
|
||||
docker.getImage(dockerImage).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image
|
||||
if (error && error.statusCode === 404) return callback(null); // not found
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
|
||||
if (error) debug('Error removing image %s : %j', dockerImage, error);
|
||||
@@ -426,3 +479,66 @@ function execContainer(containerId, cmd, options, callback) {
|
||||
|
||||
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
|
||||
}
|
||||
|
||||
function createVolume(app, name, subdir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
const volumeDataDir = path.join(paths.APPS_DATA_DIR, app.id, subdir);
|
||||
|
||||
const volumeOptions = {
|
||||
Name: name,
|
||||
Driver: 'local',
|
||||
DriverOpts: { // https://github.com/moby/moby/issues/19990#issuecomment-248955005
|
||||
type: 'none',
|
||||
device: volumeDataDir,
|
||||
o: 'bind'
|
||||
},
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id
|
||||
},
|
||||
};
|
||||
|
||||
mkdirp(volumeDataDir, function (error) {
|
||||
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
|
||||
|
||||
docker.createVolume(volumeOptions, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clearVolume(app, name, subdir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
}
|
||||
|
||||
function removeVolume(app, name, subdir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
let volume = docker.getVolume(name);
|
||||
volume.remove(function (error) {
|
||||
if (error && error.statusCode !== 404) {
|
||||
debug(`removeVolume: Error removing volume of ${app.id} ${error}`);
|
||||
callback(error);
|
||||
}
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
});
|
||||
}
|
||||
|
||||
+48
-18
@@ -8,6 +8,8 @@ module.exports = exports = {
|
||||
del: del,
|
||||
isLocked: isLocked,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
fqdn: fqdn,
|
||||
setAdmin: setAdmin,
|
||||
|
||||
@@ -24,7 +26,12 @@ module.exports = exports = {
|
||||
|
||||
makeWildcard: makeWildcard,
|
||||
|
||||
DomainsError: DomainsError
|
||||
parentDomain: parentDomain,
|
||||
|
||||
DomainsError: DomainsError,
|
||||
|
||||
// exported for testing
|
||||
_getName: getName
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -97,6 +104,11 @@ function api(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
function parentDomain(domain) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
|
||||
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
@@ -207,15 +219,16 @@ function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConf
|
||||
}
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
let error = reverseProxy.validateCertificate('test', { domain: domain, config: dnsConfig }, fallbackCertificate);
|
||||
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
} else {
|
||||
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain: domain, config: dnsConfig });
|
||||
if (fallbackCertificate.error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, fallbackCertificate.error));
|
||||
}
|
||||
|
||||
let error = validateTlsConfig(tlsConfig, provider);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
@@ -237,7 +250,7 @@ function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConf
|
||||
}
|
||||
|
||||
function isLocked(domain) {
|
||||
return domain === config.adminDomain() && config.isAdminDomainLocked();
|
||||
return domain === config.adminDomain() && config.edition() === 'hostingprovider';
|
||||
}
|
||||
|
||||
function get(domain, callback) {
|
||||
@@ -287,26 +300,24 @@ function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsC
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.get(domain, function (error, result) {
|
||||
domaindb.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = result.zoneName;
|
||||
zoneName = domainObject.zoneName;
|
||||
}
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate);
|
||||
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
}
|
||||
|
||||
error = validateTlsConfig(tlsConfig, provider);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
@@ -347,19 +358,25 @@ function del(domain, callback) {
|
||||
|
||||
// returns the 'name' that needs to be inserted into zone
|
||||
function getName(domain, subdomain, type) {
|
||||
// support special caas domains
|
||||
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
|
||||
if (domain.provider === 'caas') return subdomain;
|
||||
|
||||
if (domain.domain === domain.zoneName) return subdomain;
|
||||
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
|
||||
|
||||
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
|
||||
if (subdomain === '') return part;
|
||||
|
||||
if (subdomain === '') {
|
||||
return part;
|
||||
} else if (type === 'TXT') {
|
||||
return `${subdomain}.${part}`;
|
||||
if (!domain.config.hyphenatedSubdomains) return part ? `${subdomain}.${part}` : subdomain;
|
||||
|
||||
// hyphenatedSubdomains
|
||||
if (type !== 'TXT') return `${subdomain}-${part}`;
|
||||
|
||||
if (subdomain.startsWith('_acme-challenge.')) {
|
||||
return `${subdomain}-${part}`;
|
||||
} else if (subdomain === '_acme-challenge') {
|
||||
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
|
||||
return up ? `${subdomain}.${up}` : subdomain;
|
||||
} else {
|
||||
return subdomain + (domain.config.hyphenatedSubdomains ? '-' : '.') + part;
|
||||
return `${subdomain}.${part}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,3 +504,16 @@ function makeWildcard(hostname) {
|
||||
parts[0] = '*';
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function renewCerts(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// trigger renewal in the background
|
||||
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
|
||||
debug('renewCerts', error);
|
||||
});
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
startGraphite: startGraphite
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
infra = require('./infra_version.js'),
|
||||
paths = require('./paths.js'),
|
||||
shell = require('./shell.js');
|
||||
|
||||
function startGraphite(existingInfra, callback) {
|
||||
assert.strictEqual(typeof existingInfra, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const tag = infra.images.graphite.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
|
||||
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=graphite \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v "${dataDir}/graphite:/var/lib/graphite" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startGraphite', cmd);
|
||||
|
||||
callback();
|
||||
}
|
||||
+13
-12
@@ -5,20 +5,21 @@
|
||||
// Do not require anything here!
|
||||
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.11.0',
|
||||
// a version change recreates all containers with latest docker config
|
||||
'version': '48.12.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
||||
],
|
||||
|
||||
// Note that if any of the databases include an upgrade, bump the infra version above
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
// 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:1.1.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.4.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.1@sha256:5a13360da4a2085c7d474bea6b1090c5eb24732d4f73459942af7612d4993d7f' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.1@sha256:0f320ba40080943840fadb3e66b98066fc4f3dc98b96638e3067a8a5ab84bcee' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
}
|
||||
};
|
||||
|
||||
+4
-4
@@ -743,24 +743,24 @@ function setDnsRecords(domain, callback) {
|
||||
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] });
|
||||
}
|
||||
|
||||
debug('addDnsRecords: %j', records);
|
||||
debug('setDnsRecords: %j', records);
|
||||
|
||||
txtRecordsWithSpf(domain, function (error, txtRecords) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
|
||||
|
||||
debug('addDnsRecords: will update %j', records);
|
||||
debug('setDnsRecords: will update %j', records);
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) {
|
||||
debug(`addDnsRecords: failed to update: ${error}`);
|
||||
debug(`setDnsRecords: failed to update: ${error}`);
|
||||
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
debug('setDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
@@ -33,8 +33,6 @@ exports = module.exports = {
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
|
||||
|
||||
AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json'),
|
||||
|
||||
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
|
||||
// this pattern is for the cloudron logs API route to work
|
||||
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
|
||||
+43
-210
@@ -4,32 +4,31 @@ exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop,
|
||||
|
||||
handleCertChanged: handleCertChanged
|
||||
handleCertChanged: handleCertChanged,
|
||||
|
||||
// exported for testing
|
||||
_isReady: false
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
hat = require('./hat.js'),
|
||||
graphs = require('./graphs.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
locker = require('./locker.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gPlatformReadyTimer = null;
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function start(callback) {
|
||||
@@ -45,13 +44,11 @@ function start(callback) {
|
||||
if (!existingInfra) existingInfra = { version: 'corrupt' };
|
||||
}
|
||||
|
||||
settings.events.on(settings.PLATFORM_CONFIG_KEY, updateAddons);
|
||||
|
||||
// short-circuit for the restart case
|
||||
if (_.isEqual(infra, existingInfra)) {
|
||||
debug('platform is uptodate at version %s', infra.version);
|
||||
|
||||
emitPlatformReady();
|
||||
onPlatformReady();
|
||||
|
||||
return callback();
|
||||
}
|
||||
@@ -63,8 +60,9 @@ function start(callback) {
|
||||
|
||||
async.series([
|
||||
stopContainers.bind(null, existingInfra),
|
||||
startAddons.bind(null, existingInfra),
|
||||
removeOldImages,
|
||||
graphs.startGraphite.bind(null, existingInfra),
|
||||
addons.startAddons.bind(null, existingInfra),
|
||||
pruneInfraImages,
|
||||
startApps.bind(null, existingInfra),
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
|
||||
], function (error) {
|
||||
@@ -72,251 +70,86 @@ function start(callback) {
|
||||
|
||||
locker.unlock(locker.OP_PLATFORM_START);
|
||||
|
||||
emitPlatformReady();
|
||||
onPlatformReady();
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
clearTimeout(gPlatformReadyTimer);
|
||||
gPlatformReadyTimer = null;
|
||||
exports.events = null;
|
||||
taskmanager.pauseTasks(callback);
|
||||
}
|
||||
|
||||
function updateAddons(platformConfig, callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
// TODO: this should possibly also rollback memory to default
|
||||
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(containerName, iteratorCallback) {
|
||||
const containerConfig = platformConfig[containerName];
|
||||
if (!containerConfig) return iteratorCallback();
|
||||
|
||||
if (!containerConfig.memory || !containerConfig.memorySwap) return iteratorCallback();
|
||||
|
||||
const args = `update --memory ${containerConfig.memory} --memory-swap ${containerConfig.memorySwap} ${containerName}`.split(' ');
|
||||
shell.exec(`update${containerName}`, '/usr/bin/docker', args, { }, iteratorCallback);
|
||||
}, callback);
|
||||
function onPlatformReady() {
|
||||
debug('onPlatformReady: resuming task manager');
|
||||
exports._isReady = true;
|
||||
taskmanager.resumeTasks();
|
||||
}
|
||||
|
||||
function emitPlatformReady() {
|
||||
// give some time for the platform to "settle". For example, mysql might still be initing the
|
||||
// database dir and we cannot call service scripts until that's done.
|
||||
// TODO: make this smarter to not wait for 15secs for the crash-restart case
|
||||
gPlatformReadyTimer = setTimeout(function () {
|
||||
debug('emitting platform ready');
|
||||
gPlatformReadyTimer = null;
|
||||
taskmanager.resumeTasks();
|
||||
}, 15000);
|
||||
}
|
||||
function pruneInfraImages(callback) {
|
||||
debug('pruneInfraImages: checking existing images');
|
||||
|
||||
function removeOldImages(callback) {
|
||||
debug('removing old addon images');
|
||||
// cannot blindly remove all unused images since redis image may not be used
|
||||
let images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
|
||||
|
||||
for (var imageName in infra.images) {
|
||||
if (imageName === 'redis') continue; // see #223
|
||||
var image = infra.images[imageName];
|
||||
debug('cleaning up images of %j', image);
|
||||
var cmd = 'docker images "%s" | tail -n +2 | awk \'{ print $1 ":" $2 }\' | grep -v "%s" | xargs --no-run-if-empty docker rmi';
|
||||
shell.execSync('removeOldImagesSync', util.format(cmd, image.repo, image.tag));
|
||||
for (let image of images) {
|
||||
let output = execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
|
||||
let lines = output.trim().split('\n');
|
||||
for (let line of lines) {
|
||||
if (!line) continue;
|
||||
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
if (image.tag === parts[1]) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
|
||||
shell.execSync('pruneInfraImages', `docker rmi ${parts[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function stopContainers(existingInfra, callback) {
|
||||
// TODO: be nice and stop addons cleanly (example, shutdown commands)
|
||||
|
||||
// always stop addons to restart them on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
debug('stopping all containers for infra upgrade');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
|
||||
} else {
|
||||
assert(typeof infra.images, 'object');
|
||||
var changedAddons = [ ];
|
||||
for (var imageName in infra.images) {
|
||||
if (imageName === 'redis') continue; // see #223
|
||||
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
|
||||
}
|
||||
|
||||
debug('stopping addons for incremental infra update: %j', changedAddons);
|
||||
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
|
||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
||||
shell.execSync('stopContainers', 'docker rm -f ' + changedAddons.join(' ') + ' || true');
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`);
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startGraphite(callback) {
|
||||
const tag = infra.images.graphite.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=graphite \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v "${dataDir}/graphite:/app/data" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startGraphite', cmd);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startMysql(callback) {
|
||||
const tag = infra.images.mysql.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mysql_vars.sh',
|
||||
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
|
||||
return callback(new Error('Could not create mysql var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mysql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v "${dataDir}/mysql:/var/lib/mysql" \
|
||||
-v "${dataDir}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startMysql', cmd);
|
||||
|
||||
setTimeout(callback, 5000);
|
||||
}
|
||||
|
||||
function startPostgresql(callback) {
|
||||
const tag = infra.images.postgresql.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/postgresql_vars.sh', 'POSTGRESQL_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
return callback(new Error('Could not create postgresql var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=postgresql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v "${dataDir}/postgresql:/var/lib/postgresql" \
|
||||
-v "${dataDir}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startPostgresql', cmd);
|
||||
|
||||
setTimeout(callback, 5000);
|
||||
}
|
||||
|
||||
function startMongodb(callback) {
|
||||
const tag = infra.images.mongodb.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mongodb_vars.sh', 'MONGODB_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
return callback(new Error('Could not create mongodb var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mongodb \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v "${dataDir}/mongodb:/var/lib/mongodb" \
|
||||
-v "${dataDir}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startMongodb', cmd);
|
||||
|
||||
setTimeout(callback, 5000);
|
||||
}
|
||||
|
||||
function startAddons(existingInfra, callback) {
|
||||
var startFuncs = [ ];
|
||||
|
||||
// always start addons on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
debug('startAddons: no existing infra or infra upgrade. starting all addons');
|
||||
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, mail.startMail);
|
||||
} else {
|
||||
assert.strictEqual(typeof existingInfra.images, 'object');
|
||||
|
||||
if (infra.images.graphite.tag !== existingInfra.images.graphite.tag) startFuncs.push(startGraphite);
|
||||
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql);
|
||||
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql);
|
||||
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb);
|
||||
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
|
||||
|
||||
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
|
||||
}
|
||||
|
||||
async.series(startFuncs, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
updateAddons(platformConfig, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startApps(existingInfra, callback) {
|
||||
// Infra version change strategy:
|
||||
// * no existing version - restore apps
|
||||
// * major versions - restore apps
|
||||
// * minor versions - reconfigure apps
|
||||
|
||||
if (existingInfra.version === infra.version) {
|
||||
debug('startApp: apps are already uptodate');
|
||||
callback();
|
||||
} else if (existingInfra.version === 'none' || !semver.valid(existingInfra.version) || semver.major(existingInfra.version) !== semver.major(infra.version)) {
|
||||
if (existingInfra.version === 'none') {
|
||||
debug('startApps: restoring installed apps');
|
||||
apps.restoreInstalledApps(callback);
|
||||
} else {
|
||||
} else if (existingInfra.version !== infra.version) {
|
||||
debug('startApps: reconfiguring installed apps');
|
||||
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
|
||||
apps.configureInstalledApps(callback);
|
||||
} else {
|
||||
debug('startApps: apps are already uptodate');
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCertChanged(cn) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
|
||||
debug('handleCertChanged', cn);
|
||||
|
||||
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
|
||||
mail.startMail(NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
+125
-38
@@ -6,11 +6,15 @@ exports = module.exports = {
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
getFallbackCertificate: getFallbackCertificate,
|
||||
|
||||
generateFallbackCertificateSync: generateFallbackCertificateSync,
|
||||
setAppCertificateSync: setAppCertificateSync,
|
||||
|
||||
validateCertificate: validateCertificate,
|
||||
|
||||
getCertificate: getCertificate,
|
||||
|
||||
renewAll: renewAll,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
configureDefaultServer: configureDefaultServer,
|
||||
|
||||
@@ -33,7 +37,7 @@ var acme2 = require('./cert/acme2.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:certificates'),
|
||||
debug = require('debug')('box:reverseproxy'),
|
||||
domains = require('./domains.js'),
|
||||
ejs = require('ejs'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -84,11 +88,11 @@ function getCertApi(domain, callback) {
|
||||
domains.get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
|
||||
if (result.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
|
||||
|
||||
var api = result.tlsConfig.provider === 'caas' ? caas : acme2;
|
||||
|
||||
var options = { };
|
||||
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||
if (result.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
|
||||
options.prod = result.tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
options.performHttpAuthorization = result.provider.match(/noop|manual|wildcard/) !== null;
|
||||
@@ -120,22 +124,51 @@ function isExpiringSync(certFilePath, hours) {
|
||||
return result.status === 1; // 1 - expired 0 - not expired
|
||||
}
|
||||
|
||||
function providerMatchesSync(certFilePath, apiOptions) {
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof apiOptions, 'object');
|
||||
|
||||
if (!fs.existsSync(certFilePath)) return false; // not found
|
||||
|
||||
if (apiOptions.fallback) {
|
||||
return certFilePath.includes('.host.cert');
|
||||
}
|
||||
|
||||
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
|
||||
|
||||
const isWildcardCert = subjectAndIssuer.match(/^subject=(.*)$/m)[1].includes('*');
|
||||
const isLetsEncryptProd = subjectAndIssuer.match(/^issuer=(.*)$/m)[1].includes('Let\'s Encrypt Authority');
|
||||
|
||||
const mismatch = ((apiOptions.wildcard && !isWildcardCert)
|
||||
|| (!apiOptions.wildcard && isWildcardCert)
|
||||
|| (apiOptions.prod && !isLetsEncryptProd)
|
||||
|| (!apiOptions.prod && isLetsEncryptProd));
|
||||
|
||||
debug(`providerMatchesSync: ${certFilePath} ${subjectAndIssuer.trim().replace('\n', ' ')} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
|
||||
|
||||
return !mismatch;
|
||||
}
|
||||
|
||||
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
||||
// servers certificate appears first (and not the intermediate cert)
|
||||
function validateCertificate(domain, cert, key) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
function validateCertificate(location, domainObject, certificate) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert(certificate && typeof certificate, 'object');
|
||||
|
||||
const cert = certificate.cert, key = certificate.key;
|
||||
|
||||
// check for empty cert and key strings
|
||||
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
|
||||
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
|
||||
|
||||
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
|
||||
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
|
||||
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
|
||||
|
||||
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
|
||||
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
@@ -155,27 +188,52 @@ function reload(callback) {
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
|
||||
}
|
||||
|
||||
function generateFallbackCertificateSync(domainObject) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
|
||||
const domain = domainObject.domain;
|
||||
const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`);
|
||||
const keyFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.key`);
|
||||
|
||||
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
|
||||
let opensslConfWithSan;
|
||||
let cn = domainObject.config.hyphenatedSubdomains ? domains.parentDomain(domain) : domain;
|
||||
|
||||
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn} hyphenated=${domainObject.config.hyphenatedSubdomains}`);
|
||||
|
||||
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
|
||||
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
|
||||
if (!safe.child_process.execSync(certCommand)) return { error: new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message) };
|
||||
safe.fs.unlinkSync(configFile);
|
||||
|
||||
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
|
||||
if (!cert) return { error: safe.error };
|
||||
safe.fs.unlinkSync(certFilePath);
|
||||
|
||||
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
|
||||
if (!key) return { error: safe.error };
|
||||
safe.fs.unlinkSync(keyFilePath);
|
||||
|
||||
return { cert: cert, key: key, error: null };
|
||||
}
|
||||
|
||||
function setFallbackCertificate(domain, fallback, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(fallback && typeof fallback === 'object');
|
||||
assert.strictEqual(typeof fallback, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
|
||||
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
|
||||
|
||||
if (fallback) {
|
||||
// backup the cert
|
||||
if (fallback.restricted) { // restricted certs are not backed up
|
||||
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
} else {
|
||||
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
|
||||
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
|
||||
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
|
||||
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`);
|
||||
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
safe.fs.unlinkSync(configFile);
|
||||
}
|
||||
|
||||
platform.handleCertChanged('*.' + domain);
|
||||
@@ -204,6 +262,23 @@ function getFallbackCertificate(domain, callback) {
|
||||
callback(null, { certFilePath, keyFilePath, type: 'fallback' });
|
||||
}
|
||||
|
||||
function setAppCertificateSync(location, domainObject, certificate) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof certificate, 'object');
|
||||
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
if (certificate.cert && certificate.key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), certificate.cert)) return safe.error;
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), certificate.key)) return safe.error;
|
||||
} else { // remove existing cert/key
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCertificateByHostname(hostname, domain, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
@@ -251,19 +326,19 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCertificateByHostname(vhost, domain, function (error, result) {
|
||||
if (result) {
|
||||
debug(`ensureCertificate: ${vhost}. certificate already exists at ${result.keyFilePath}`);
|
||||
getCertApi(domain, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.certFilePath.endsWith('.user.cert')) return callback(null, result); // user certs cannot be renewed
|
||||
if (!isExpiringSync(result.certFilePath, 24 * 30)) return callback(null, result);
|
||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||
} else {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
}
|
||||
getCertificateByHostname(vhost, domain, function (error, result) {
|
||||
if (result) {
|
||||
debug(`ensureCertificate: ${vhost} certificate already exists at ${result.keyFilePath}`);
|
||||
|
||||
getCertApi(domain, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
if (result.certFilePath.endsWith('.user.cert')) return callback(null, result); // user certs cannot be renewed
|
||||
if (!isExpiringSync(result.certFilePath, 24 * 30) && providerMatchesSync(result.certFilePath, apiOptions)) return callback(null, result);
|
||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||
} else {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
}
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||
|
||||
@@ -420,19 +495,18 @@ function unconfigureApp(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function renewAll(auditSource, callback) {
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('renewAll: Checking certificates for renewal');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var appDomains = [];
|
||||
|
||||
// add webadmin domain
|
||||
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: constants.NGINX_ADMIN_CONFIG_FILE_NAME });
|
||||
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_ADMIN_CONFIG_FILE_NAME) });
|
||||
|
||||
// add app main
|
||||
allApps.forEach(function (app) {
|
||||
@@ -444,6 +518,8 @@ function renewAll(auditSource, callback) {
|
||||
});
|
||||
});
|
||||
|
||||
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
|
||||
|
||||
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
|
||||
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
@@ -452,6 +528,8 @@ function renewAll(auditSource, callback) {
|
||||
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
|
||||
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
|
||||
|
||||
debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`);
|
||||
|
||||
// reconfigure since the cert changed
|
||||
var configureFunc;
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
|
||||
@@ -471,6 +549,15 @@ function renewAll(auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function renewAll(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('renewAll: Checking certificates for renewal');
|
||||
|
||||
renewCerts({}, auditSource, callback);
|
||||
}
|
||||
|
||||
function removeAppConfigs() {
|
||||
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
||||
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
|
||||
|
||||
@@ -4,6 +4,8 @@ exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
isUnmanaged: isUnmanaged,
|
||||
|
||||
scope: scope,
|
||||
websocketAuth: websocketAuth
|
||||
};
|
||||
@@ -15,6 +17,7 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
clients = require('../clients.js'),
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
ClientsError = clients.ClientsError,
|
||||
config = require('../config.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
passport = require('passport'),
|
||||
@@ -138,3 +141,9 @@ function websocketAuth(requiredScopes, req, res, next) {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function isUnmanaged(req, res, next) {
|
||||
if (!config.isManaged()) return next();
|
||||
|
||||
next(new HttpError(401, 'Managed instance does not permit this operation'));
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ function installApp(req, res, next) {
|
||||
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
||||
}
|
||||
|
||||
if ('env' in data) {
|
||||
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
||||
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
}
|
||||
|
||||
debug('Installing app :%j', data);
|
||||
|
||||
apps.install(data, req.user, auditSource(req), function (error, app) {
|
||||
@@ -194,6 +199,11 @@ function configureApp(req, res, next) {
|
||||
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
||||
}
|
||||
|
||||
if ('env' in data) {
|
||||
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
||||
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
}
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
|
||||
|
||||
+33
-4
@@ -7,6 +7,8 @@ exports = module.exports = {
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
verifyDomainLock: verifyDomainLock
|
||||
};
|
||||
|
||||
@@ -16,6 +18,12 @@ var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
// this code exists for the hosting provider edition
|
||||
function verifyDomainLock(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
@@ -36,8 +44,12 @@ function add(req, res, next) {
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
|
||||
if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if (req.body.fallbackCertificate) {
|
||||
let fallbackCertificate = req.body.fallbackCertificate;
|
||||
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
|
||||
}
|
||||
|
||||
if ('tlsConfig' in req.body) {
|
||||
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
|
||||
@@ -90,8 +102,12 @@ function update(req, res, next) {
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if (req.body.fallbackCertificate) {
|
||||
let fallbackCertificate = req.body.fallbackCertificate;
|
||||
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
|
||||
}
|
||||
|
||||
if ('tlsConfig' in req.body) {
|
||||
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
|
||||
@@ -122,3 +138,16 @@ function del(req, res, next) {
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
domains.renewCerts(req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
+45
-1
@@ -23,10 +23,17 @@ exports = module.exports = {
|
||||
setAppstoreConfig: setAppstoreConfig,
|
||||
|
||||
getPlatformConfig: getPlatformConfig,
|
||||
setPlatformConfig: setPlatformConfig
|
||||
setPlatformConfig: setPlatformConfig,
|
||||
|
||||
getDynamicDnsConfig: getDynamicDnsConfig,
|
||||
setDynamicDnsConfig: setDynamicDnsConfig,
|
||||
|
||||
setRegistryConfig: setRegistryConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
docker = require('../docker.js'),
|
||||
DockerError = docker.DockerError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
@@ -204,6 +211,28 @@ function setPlatformConfig(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getDynamicDnsConfig(req, res, next) {
|
||||
settings.getDynamicDnsConfig(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { enabled: enabled }));
|
||||
});
|
||||
}
|
||||
|
||||
function setDynamicDnsConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
|
||||
|
||||
settings.setDynamicDnsConfig(req.body.enabled, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getAppstoreConfig(req, res, next) {
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -235,3 +264,18 @@ function setAppstoreConfig(req, res, next) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setRegistryConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.serveraddress !== 'string') return next(new HttpError(400, 'serveraddress is required'));
|
||||
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
|
||||
|
||||
docker.setRegistryConfig(req.body, function (error) {
|
||||
if (error && error.reason === DockerError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
+25
-12
@@ -3,7 +3,7 @@
|
||||
exports = module.exports = {
|
||||
providerTokenAuth: providerTokenAuth,
|
||||
setupTokenAuth: setupTokenAuth,
|
||||
dnsSetup: dnsSetup,
|
||||
provision: provision,
|
||||
activate: activate,
|
||||
restore: restore,
|
||||
getStatus: getStatus,
|
||||
@@ -18,7 +18,8 @@ var assert = require('assert'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
setup = require('../setup.js'),
|
||||
SetupError = require('../setup.js').SetupError,
|
||||
superagent = require('superagent');
|
||||
superagent = require('superagent'),
|
||||
_ = require('underscore');
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
@@ -62,20 +63,29 @@ function setupTokenAuth(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function dnsSetup(req, res, next) {
|
||||
function provision(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
|
||||
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
|
||||
if (typeof req.body.adminFqdn !== 'string' || !req.body.adminFqdn) return next(new HttpError(400, 'adminFqdn is required'));
|
||||
if (!req.body.dnsConfig || typeof req.body.dnsConfig !== 'object') return next(new HttpError(400, 'dnsConfig is required'));
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
const dnsConfig = req.body.dnsConfig;
|
||||
|
||||
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
|
||||
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
if (typeof dnsConfig.provider !== 'string' || !dnsConfig.provider) return next(new HttpError(400, 'provider is required'));
|
||||
if (typeof dnsConfig.domain !== 'string' || !dnsConfig.domain) return next(new HttpError(400, 'domain is required'));
|
||||
|
||||
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
if ('zoneName' in dnsConfig && typeof dnsConfig.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if (!dnsConfig.config || typeof dnsConfig.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
|
||||
if ('tlsConfig' in dnsConfig && typeof dnsConfig.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
|
||||
if (dnsConfig.tlsConfig && (!dnsConfig.tlsConfig.provider || typeof dnsConfig.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
|
||||
// it can take sometime to setup DNS, register cloudron
|
||||
req.clearTimeout();
|
||||
|
||||
setup.provision(dnsConfig, req.body.autoconf || {}, function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
@@ -143,7 +153,10 @@ function restore(req, res, next) {
|
||||
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
|
||||
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
|
||||
|
||||
setup.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
|
||||
setup.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
|
||||
@@ -15,7 +15,6 @@ var accesscontrol = require('../../accesscontrol.js'),
|
||||
clients = require('../../clients.js'),
|
||||
config = require('../../config.js'),
|
||||
constants = require('../../constants.js'),
|
||||
apphealthmonitor = require('../../apphealthmonitor.js'),
|
||||
database = require('../../database.js'),
|
||||
docker = require('../../docker.js').connection,
|
||||
expect = require('expect.js'),
|
||||
@@ -28,6 +27,7 @@ var accesscontrol = require('../../accesscontrol.js'),
|
||||
nock = require('nock'),
|
||||
path = require('path'),
|
||||
paths = require('../../paths.js'),
|
||||
platform = require('../../platform.js'),
|
||||
safe = require('safetydance'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
@@ -234,13 +234,16 @@ function startBox(done) {
|
||||
}
|
||||
return false;
|
||||
}, callback);
|
||||
}
|
||||
], function (error) {
|
||||
if (error) return done(error);
|
||||
},
|
||||
|
||||
console.log('This test can take ~40 seconds to start as it waits for infra to be ready');
|
||||
setTimeout(done, 40000);
|
||||
});
|
||||
function (callback) {
|
||||
async.retry({ times: 50, interval: 10000 }, function (retryCallback) {
|
||||
if (platform._isReady) return retryCallback();
|
||||
console.log('Platform not ready yet, retry in 10 secs');
|
||||
retryCallback('Platform not ready yet');
|
||||
}, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function stopBox(done) {
|
||||
@@ -251,7 +254,6 @@ function stopBox(done) {
|
||||
|
||||
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
|
||||
async.series([
|
||||
apphealthmonitor.stop,
|
||||
taskmanager.stopPendingTasks,
|
||||
taskmanager.waitForPendingTasks,
|
||||
appdb._clear,
|
||||
@@ -447,7 +449,7 @@ describe('App API', function () {
|
||||
it('app install succeeds with purchase', function (done) {
|
||||
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
|
||||
var fake2 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
|
||||
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
|
||||
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID, 'manifestId': APP_MANIFEST.id }).reply(201, { });
|
||||
|
||||
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
|
||||
if (error) return done(error);
|
||||
@@ -577,7 +579,7 @@ describe('App API', function () {
|
||||
|
||||
it('app install succeeds again', function (done) {
|
||||
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
|
||||
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
|
||||
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID, 'manifestId': APP_MANIFEST.id }).reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
@@ -592,7 +594,7 @@ describe('App API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('app install succeeds without password but developer token', function (done) {
|
||||
it('app install fails with developer token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/developer/login')
|
||||
.send({ username: USERNAME, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
@@ -608,22 +610,11 @@ describe('App API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(res.body.id).to.be.a('string');
|
||||
APP_ID = res.body.id;
|
||||
expect(res.statusCode).to.equal(424); // appstore purchase external error
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can uninstall app without password but developer token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('App installation', function () {
|
||||
@@ -643,7 +634,6 @@ describe('App installation', function () {
|
||||
|
||||
async.series([
|
||||
startBox,
|
||||
apphealthmonitor.start,
|
||||
|
||||
function (callback) {
|
||||
apiHockInstance
|
||||
@@ -673,7 +663,7 @@ describe('App installation', function () {
|
||||
|
||||
it('can install test app', function (done) {
|
||||
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
|
||||
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
|
||||
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID, 'manifestId': APP_MANIFEST.id }).reply(201, { });
|
||||
|
||||
var count = 0;
|
||||
function checkInstallStatus() {
|
||||
@@ -745,7 +735,13 @@ describe('App installation', function () {
|
||||
|
||||
it('installation - volume created', function (done) {
|
||||
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP_ID));
|
||||
done();
|
||||
let volume = docker.getVolume(APP_ID + '-localstorage');
|
||||
volume.inspect(function (error, volume) {
|
||||
expect(error).to.be(null);
|
||||
expect(volume.Labels.appId).to.eql(APP_ID);
|
||||
expect(volume.Options.device).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('installation - http is up and running', function (done) {
|
||||
@@ -781,13 +777,7 @@ describe('App installation', function () {
|
||||
it('installation - running container has volume mounted', function (done) {
|
||||
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// support newer docker versions
|
||||
if (data.Volumes) {
|
||||
expect(data.Volumes['/app/data']).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
|
||||
} else {
|
||||
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
|
||||
}
|
||||
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Type).to.eql('volume');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -50,7 +50,6 @@ function setup(done) {
|
||||
nock.cleanAll();
|
||||
config._reset();
|
||||
config.set('provider', 'caas');
|
||||
config.setVersion('1.2.3');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
@@ -10,7 +10,6 @@ var async = require('async'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
mail = require('../../mail.js'),
|
||||
domains = require('../../domains.js'),
|
||||
maildb = require('../../maildb.js'),
|
||||
server = require('../../server.js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -48,8 +47,8 @@ function setup(done) {
|
||||
database._clear.bind(null),
|
||||
|
||||
function dnsSetup(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, adminFqdn: 'my.' + ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config })
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(200);
|
||||
|
||||
+128
-151
@@ -20,7 +20,6 @@ var token = null;
|
||||
|
||||
function setup(done) {
|
||||
config._reset();
|
||||
config.setVersion('1.2.3');
|
||||
|
||||
async.series([
|
||||
server.start,
|
||||
@@ -40,234 +39,212 @@ describe('REST API', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('dns setup fails without provider', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { domain: DOMAIN, config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with invalid provider', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'foobar', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'foobar', domain: DOMAIN, config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with missing domain', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', adminFqdn: 'my.' + DOMAIN, config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with invalid domain', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: '.foo', adminFqdn: 'my.' + DOMAIN, config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: '.foo', config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with missing adminFqdn', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with invalid adminFqdn', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my', config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with invalid config', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: 'not an object' })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: 'not an object' } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with invalid zoneName', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, zoneName: 1337 })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: {}, zoneName: 1337 } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with invalid tlsConfig', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: 'foobar' })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: {}, tlsConfig: 'foobar' } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup fails with invalid tlsConfig provider', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: { provider: 1337 } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, config: {}, tlsConfig: { provider: 1337 } } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(200);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(200);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup twice fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
|
||||
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(409);
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(409);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activation fails without username', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activation fails with invalid username', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '?this.is-not!valid', password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '?this.is-not!valid', password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activation fails without email', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activation fails with invalid email', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: 'notanemail' })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: 'notanemail' })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activation fails without password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activation fails with invalid password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: 'short', email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: 'short', email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(400);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activation succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('activating twice fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(409);
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(409);
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not crash with invalid JSON', function (done) {
|
||||
|
||||
@@ -32,7 +32,6 @@ const DOMAIN_0 = {
|
||||
function setup(done) {
|
||||
config._reset();
|
||||
config.setFqdn(DOMAIN_0.domain);
|
||||
config.setVersion('1.2.3');
|
||||
|
||||
async.series([
|
||||
server.start,
|
||||
@@ -89,4 +88,4 @@ describe('Internal API', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-9
@@ -11,12 +11,10 @@ exports = module.exports = {
|
||||
createInvite: createInvite,
|
||||
sendInvite: sendInvite,
|
||||
setGroups: setGroups,
|
||||
transferOwnership: transferOwnership,
|
||||
verifyOperator: verifyOperator
|
||||
transferOwnership: transferOwnership
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
users = require('../users.js'),
|
||||
@@ -27,12 +25,6 @@ function auditSource(req) {
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function verifyOperator(req, res, next) {
|
||||
if (config.allowOperatorActions()) return next();
|
||||
|
||||
next(new HttpError(401, 'Not allowed in this edition'));
|
||||
}
|
||||
|
||||
function create(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
|
||||
@@ -32,7 +32,13 @@ if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
if [[ "${cmd}" == "remove" ]]; then
|
||||
echo "Removing collectd stats of ${appid}"
|
||||
|
||||
rm -rf ${HOME}/platformdata/graphite/whisper/collectd/localhost/*${appid}*
|
||||
for i in {1..10}; do
|
||||
if rm -rf ${HOME}/platformdata/graphite/whisper/collectd/localhost/*${appid}*; then
|
||||
break
|
||||
fi
|
||||
echo "Failed to remove collectd directory. collectd possibly generated data in the middle of removal"
|
||||
sleep 3
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly app_data_dir="${HOME}/appsdata/$1"
|
||||
|
||||
mkdir -p "${app_data_dir}/data"
|
||||
# only the top level ownership is changed because containers own the subdirectores
|
||||
# and will chown them as necessary
|
||||
chown yellowtent:yellowtent "${app_data_dir}"
|
||||
chown yellowtent:yellowtent "${app_data_dir}/data"
|
||||
else
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/$1"
|
||||
mkdir -p "${app_data_dir}/data"
|
||||
chown ${SUDO_USER}:${SUDO_USER} "${app_data_dir}"
|
||||
fi
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
addon="$1"
|
||||
appid="${2:-}" # only valid for redis
|
||||
if [[ "${addon}" != "postgresql" && "${addon}" != "mysql" && "${addon}" != "mongodb" && "${addon}" != "redis" ]]; then
|
||||
echo "${addon} must be postgresql/mysql/mongodb/redis"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly addon_dir="${HOME}/platformdata/${addon}/${appid}"
|
||||
else
|
||||
readonly addon_dir="${HOME}/.cloudron_test/platformdata/${addon}/${appid}"
|
||||
fi
|
||||
|
||||
rm -rf "${addon_dir}"
|
||||
if [[ "${addon}" != "redis" ]]; then
|
||||
mkdir "${addon_dir}"
|
||||
elif [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
rm -rf "${HOME}/appsdata/${appid}/redis" # legacy directory
|
||||
fi
|
||||
|
||||
@@ -20,18 +20,13 @@ fi
|
||||
# this script is called from redis addon as well!
|
||||
|
||||
appid="$1"
|
||||
rmdir="$2"
|
||||
subdir="$2"
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly app_data_dir="${HOME}/appsdata/${appid}"
|
||||
readonly volume_dir="${HOME}/appsdata/${appid}/${subdir}"
|
||||
else
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/${appid}"
|
||||
readonly volume_dir="${HOME}/.cloudron_test/appsdata/${appid}/${subdir}"
|
||||
fi
|
||||
|
||||
# the approach below ensures symlinked contents are also deleted
|
||||
rm -rf "${volume_dir}"
|
||||
|
||||
find -H "${app_data_dir}" -mindepth 1 -delete || true # -H means resolve symlink in args
|
||||
|
||||
if [[ "${rmdir}" == "true" ]]; then
|
||||
rm -rf "${app_data_dir}"
|
||||
fi
|
||||
@@ -8,20 +8,18 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
fi
|
||||
|
||||
readonly UPDATER_SERVICE="cloudron-updater"
|
||||
readonly DATA_FILE="/root/cloudron-update-data.json"
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $# != 2 ]]; then
|
||||
echo "sourceDir and data arguments required"
|
||||
if [[ $# != 1 ]]; then
|
||||
echo "sourceDir argument required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly source_dir="${1}"
|
||||
readonly data="${2}"
|
||||
|
||||
echo "Updating Cloudron with ${source_dir}"
|
||||
|
||||
@@ -32,11 +30,8 @@ if systemctl reset-failed "${UPDATER_SERVICE}"; then
|
||||
echo "=> service has failed earlier"
|
||||
fi
|
||||
|
||||
# Save user data in file, to avoid argument length limit with systemd-run
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
|
||||
echo "=> Run installer.sh as cloudron-updater.service"
|
||||
if ! systemd-run --unit "${UPDATER_SERVICE}" ${installer_path} --data-file "${DATA_FILE}"; then
|
||||
if ! systemd-run --unit "${UPDATER_SERVICE}" ${installer_path}; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+18
-13
@@ -102,14 +102,14 @@ function initializeExpressSync() {
|
||||
var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE);
|
||||
var appstoreScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPSTORE);
|
||||
|
||||
const verifyOperator = routes.users.verifyOperator;
|
||||
const isUnmanaged = routes.accesscontrol.isUnmanaged;
|
||||
const verifyDomainLock = routes.domains.verifyDomainLock;
|
||||
|
||||
// csrf protection
|
||||
var csrf = routes.oauth2.csrf();
|
||||
|
||||
// public routes
|
||||
router.post('/api/v1/cloudron/dns_setup', routes.setup.providerTokenAuth, routes.setup.dnsSetup); // only available until no-domain
|
||||
router.post('/api/v1/cloudron/setup', routes.setup.providerTokenAuth, routes.setup.provision); // only available until no-domain
|
||||
router.post('/api/v1/cloudron/restore', routes.setup.restore); // only available until activated
|
||||
router.post('/api/v1/cloudron/activate', routes.setup.setupTokenAuth, routes.setup.activate);
|
||||
router.get ('/api/v1/cloudron/status', routes.setup.getStatus);
|
||||
@@ -129,10 +129,10 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
|
||||
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
|
||||
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, verifyOperator, routes.ssh.getAuthorizedKeys);
|
||||
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, verifyOperator, routes.ssh.addAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, verifyOperator, routes.ssh.getAuthorizedKey);
|
||||
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, verifyOperator, routes.ssh.delAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKeys);
|
||||
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.addAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKey);
|
||||
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
|
||||
|
||||
// config route (for dashboard)
|
||||
@@ -227,15 +227,19 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
|
||||
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
|
||||
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, verifyOperator, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, verifyOperator, routes.settings.setBackupConfig);
|
||||
router.get ('/api/v1/settings/platform_config', settingsScope, verifyOperator, routes.settings.getPlatformConfig);
|
||||
router.post('/api/v1/settings/platform_config', settingsScope, verifyOperator, routes.settings.setPlatformConfig);
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, isUnmanaged, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, isUnmanaged, routes.settings.setBackupConfig);
|
||||
router.get ('/api/v1/settings/platform_config', settingsScope, isUnmanaged, routes.settings.getPlatformConfig);
|
||||
router.post('/api/v1/settings/platform_config', settingsScope, isUnmanaged, routes.settings.setPlatformConfig);
|
||||
router.get ('/api/v1/settings/dynamic_dns', settingsScope, isUnmanaged, routes.settings.getDynamicDnsConfig);
|
||||
router.post('/api/v1/settings/dynamic_dns', settingsScope, isUnmanaged, routes.settings.setDynamicDnsConfig);
|
||||
|
||||
router.get ('/api/v1/settings/time_zone', settingsScope, routes.settings.getTimeZone);
|
||||
router.post('/api/v1/settings/time_zone', settingsScope, routes.settings.setTimeZone);
|
||||
router.get ('/api/v1/settings/appstore_config', appstoreScope, verifyOperator, routes.settings.getAppstoreConfig);
|
||||
router.post('/api/v1/settings/appstore_config', appstoreScope, verifyOperator, routes.settings.setAppstoreConfig);
|
||||
router.get ('/api/v1/settings/appstore_config', appstoreScope, isUnmanaged, routes.settings.getAppstoreConfig);
|
||||
router.post('/api/v1/settings/appstore_config', appstoreScope, isUnmanaged, routes.settings.setAppstoreConfig);
|
||||
|
||||
router.post('/api/v1/settings/registry_config', appstoreScope, routes.settings.setRegistryConfig);
|
||||
|
||||
// email routes
|
||||
router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain);
|
||||
@@ -264,7 +268,7 @@ function initializeExpressSync() {
|
||||
router.del ('/api/v1/mail/:domain/lists/:name', mailScope, routes.mail.removeList);
|
||||
|
||||
// feedback
|
||||
router.post('/api/v1/feedback', cloudronScope, verifyOperator, routes.cloudron.feedback);
|
||||
router.post('/api/v1/feedback', cloudronScope, isUnmanaged, routes.cloudron.feedback);
|
||||
|
||||
// backup routes
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
@@ -275,6 +279,7 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
|
||||
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
|
||||
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
|
||||
router.post ('/api/v1/domains/:domain/renew_certs', domainsManageScope, verifyDomainLock, routes.domains.renewCerts);
|
||||
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
|
||||
|
||||
// caas routes
|
||||
|
||||
+7
-5
@@ -60,13 +60,15 @@ exports = module.exports = {
|
||||
events: null
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var addons = require('./addons.js'),
|
||||
assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = backups.BackupsError,
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:settings'),
|
||||
moment = require('moment-timezone'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -89,7 +91,7 @@ var gDefaults = (function () {
|
||||
retentionSecs: 2 * 24 * 60 * 60, // 2 days
|
||||
intervalSecs: 24 * 60 * 60 // ~1 day
|
||||
};
|
||||
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
|
||||
result[exports.UPDATE_CONFIG_KEY] = {};
|
||||
result[exports.APPSTORE_CONFIG_KEY] = {};
|
||||
result[exports.CAAS_CONFIG_KEY] = {};
|
||||
result[exports.EMAIL_DIGEST] = true;
|
||||
@@ -397,9 +399,7 @@ function setPlatformConfig(platformConfig, callback) {
|
||||
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.PLATFORM_CONFIG_KEY, platformConfig);
|
||||
|
||||
callback(null);
|
||||
addons.updateAddonConfig(platformConfig, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -441,6 +441,8 @@ function setAppstoreConfig(appstoreConfig, callback) {
|
||||
|
||||
cloudronId = result.body.cloudron.id;
|
||||
|
||||
debug(`setAppstoreConfig: Cloudron registered with id ${cloudronId}`);
|
||||
|
||||
setNewConfig();
|
||||
});
|
||||
}
|
||||
|
||||
+48
-54
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
dnsSetup: dnsSetup,
|
||||
provision: provision,
|
||||
restore: restore,
|
||||
getStatus: getStatus,
|
||||
activate: activate,
|
||||
@@ -23,10 +23,8 @@ var assert = require('assert'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = domains.DomainsError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
@@ -80,35 +78,36 @@ SetupError.INTERNAL_ERROR = 'Internal Error';
|
||||
SetupError.EXTERNAL_ERROR = 'External Error';
|
||||
SetupError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
|
||||
function autoprovision(callback) {
|
||||
function autoprovision(autoconf, callback) {
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const confJson = safe.fs.readFileSync(paths.AUTO_PROVISION_FILE, 'utf8');
|
||||
if (!confJson) return callback();
|
||||
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
|
||||
debug(`autoprovision: ${key}`);
|
||||
|
||||
const conf = safe.JSON.parse(confJson);
|
||||
if (!conf) return callback();
|
||||
|
||||
async.eachSeries(Object.keys(conf), function (key, iteratorDone) {
|
||||
var name;
|
||||
switch (key) {
|
||||
case 'appstoreConfig': name = settings.APPSTORE_CONFIG_KEY; break;
|
||||
case 'caasConfig': name = settings.CAAS_CONFIG_KEY; break;
|
||||
case 'backupConfig': name = settings.BACKUP_CONFIG_KEY; break;
|
||||
case 'tlsCert':
|
||||
debug(`autoprovision: ${key}`);
|
||||
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, config.adminDomain() + '.host.cert'), conf[key], iteratorDone);
|
||||
case 'tlsKey':
|
||||
debug(`autoprovision: ${key}`);
|
||||
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, config.adminDomain() + '.host.key'), conf[key], iteratorDone);
|
||||
case 'appstoreConfig':
|
||||
if (config.provider() === 'caas') { // skip registration
|
||||
settingsdb.set(settings.APPSTORE_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
|
||||
} else { // register cloudron
|
||||
settings.setAppstoreConfig(autoconf[key], iteratorDone);
|
||||
}
|
||||
break;
|
||||
case 'caasConfig':
|
||||
settingsdb.set(settings.CAAS_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
|
||||
break;
|
||||
case 'backupConfig':
|
||||
settings.setBackupConfig(autoconf[key], iteratorDone);
|
||||
break;
|
||||
default:
|
||||
debug(`autoprovision: ${key} ignored`);
|
||||
return iteratorDone();
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
debug(`autoprovision: ${name}`);
|
||||
settingsdb.set(name, JSON.stringify(conf[key]), iteratorDone);
|
||||
}, callback);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function configureWebadmin(callback) {
|
||||
@@ -155,42 +154,21 @@ function configureWebadmin(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, callback) {
|
||||
assert.strictEqual(typeof adminFqdn, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
function provision(dnsConfig, autoconf, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.adminDomain()) return callback(new SetupError(SetupError.ALREADY_SETUP));
|
||||
|
||||
if (gWebadminStatus.configuring || gWebadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
|
||||
|
||||
if (!tld.isValid(adminFqdn) || !adminFqdn.endsWith(domain)) return callback(new SetupError(SetupError.BAD_FIELD, 'adminFqdn must be a subdomain of domain'));
|
||||
const domain = dnsConfig.domain.toLowerCase();
|
||||
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
|
||||
|
||||
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
|
||||
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
|
||||
|
||||
debug(`dnsSetup: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
|
||||
|
||||
function done(error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
config.setAdminFqdn(adminFqdn);
|
||||
config.setAdminLocation('my');
|
||||
|
||||
autoprovision(function (error) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
clients.addDefaultClients(config.adminOrigin(), callback);
|
||||
|
||||
configureWebadmin(NOOP_CALLBACK);
|
||||
});
|
||||
}
|
||||
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
@@ -198,9 +176,24 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
|
||||
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
|
||||
|
||||
async.series([
|
||||
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
|
||||
domains.add.bind(null, domain, zoneName, dnsConfig.provider, dnsConfig.config, dnsConfig.fallbackCertificate || null, dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }),
|
||||
mail.addDomain.bind(null, domain)
|
||||
], done);
|
||||
], function (error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
config.setAdminFqdn(adminFqdn);
|
||||
config.setAdminLocation('my');
|
||||
|
||||
clients.addDefaultClients(config.adminOrigin(), callback);
|
||||
|
||||
async.series([
|
||||
autoprovision.bind(null, autoconf),
|
||||
configureWebadmin
|
||||
], NOOP_CALLBACK);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,10 +256,11 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
});
|
||||
}
|
||||
|
||||
function restore(backupConfig, backupId, version, callback) {
|
||||
function restore(backupConfig, backupId, version, autoconf, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!semver.valid(version)) return callback(new SetupError(SetupError.BAD_STATE, 'version is not a valid semver'));
|
||||
@@ -292,7 +286,7 @@ function restore(backupConfig, backupId, version, callback) {
|
||||
|
||||
async.series([
|
||||
backups.restore.bind(null, backupConfig, backupId),
|
||||
autoprovision,
|
||||
autoprovision.bind(null, autoconf),
|
||||
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
|
||||
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
|
||||
// Once we have a 100% IP based restore, we can skip this
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../config.js'),
|
||||
database = require('../database.js'),
|
||||
acme2 = require('../cert/acme2.js'),
|
||||
expect = require('expect.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
describe('Acme2', function () {
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
async.series([
|
||||
database._clear,
|
||||
database.uninitialize
|
||||
], done);
|
||||
});
|
||||
|
||||
describe('getChallengeSubdomain', function () {
|
||||
it('non-wildcard', function () {
|
||||
expect(acme2._getChallengeSubdomain('example.com', 'example.com')).to.be('_acme-challenge');
|
||||
expect(acme2._getChallengeSubdomain('git.example.com', 'example.com')).to.be('_acme-challenge.git');
|
||||
});
|
||||
|
||||
it('wildcard', function () {
|
||||
expect(acme2._getChallengeSubdomain('*.example.com', 'example.com')).to.be('_acme-challenge');
|
||||
expect(acme2._getChallengeSubdomain('*.git.example.com', 'example.com')).to.be('_acme-challenge.git');
|
||||
expect(acme2._getChallengeSubdomain('*.example.com', 'customer.example.com')).to.be('_acme-challenge'); // for hyphenatedSubdomains
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -110,7 +110,10 @@ describe('Apps', function () {
|
||||
memoryLimit: 0,
|
||||
robotsTxt: null,
|
||||
sso: false,
|
||||
ownerId: USER_0.id
|
||||
ownerId: USER_0.id,
|
||||
env: {
|
||||
'CUSTOM_KEY': 'CUSTOM_VALUE'
|
||||
}
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -125,7 +128,8 @@ describe('Apps', function () {
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] },
|
||||
memoryLimit: 0,
|
||||
ownerId: USER_0.id
|
||||
ownerId: USER_0.id,
|
||||
env: {}
|
||||
};
|
||||
|
||||
var APP_2 = {
|
||||
@@ -142,7 +146,8 @@ describe('Apps', function () {
|
||||
memoryLimit: 0,
|
||||
robotsTxt: null,
|
||||
sso: false,
|
||||
ownerId: USER_0.id
|
||||
ownerId: USER_0.id,
|
||||
env: {}
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('Appstore', function () {
|
||||
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(201, {});
|
||||
|
||||
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
|
||||
appstore.purchase(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('Appstore', function () {
|
||||
.delete(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(204, {});
|
||||
|
||||
appstore.unpurchase(APP_ID, APPSTORE_APP_ID, function (error) {
|
||||
appstore.unpurchase(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.not.be.ok();
|
||||
@@ -164,7 +164,7 @@ describe('Appstore', function () {
|
||||
.delete(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(204, {});
|
||||
|
||||
appstore.unpurchase(APP_ID, APPSTORE_APP_ID, function (error) {
|
||||
appstore.unpurchase(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
@@ -164,16 +164,16 @@ describe('apptask', function () {
|
||||
});
|
||||
|
||||
it('create volume', function (done) {
|
||||
apptask._createVolume(APP, function (error) {
|
||||
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
apptask._createAppDir(APP, function (error) {
|
||||
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id)).to.be(true);
|
||||
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id + '/data')).to.be(false);
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('delete volume - removeDirectory (false) ', function (done) {
|
||||
apptask._deleteVolume(APP, { removeDirectory: false }, function (error) {
|
||||
expect(!fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
apptask._deleteAppDir(APP, { removeDirectory: false }, function (error) {
|
||||
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id)).to.be(true);
|
||||
expect(fs.readdirSync(paths.APPS_DATA_DIR + '/' + APP.id).length).to.be(0); // empty
|
||||
expect(error).to.be(null);
|
||||
@@ -182,7 +182,7 @@ describe('apptask', function () {
|
||||
});
|
||||
|
||||
it('delete volume - removeDirectory (true) ', function (done) {
|
||||
apptask._deleteVolume(APP, { removeDirectory: true }, function (error) {
|
||||
apptask._deleteAppDir(APP, { removeDirectory: true }, function (error) {
|
||||
expect(!fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id)).to.be(true);
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
|
||||
@@ -9,8 +9,8 @@ readonly TEST_IMAGE="cloudron/test:25.2.0"
|
||||
sudo -k || sudo --reset-timestamp
|
||||
|
||||
# checks if all scripts are sudo access
|
||||
scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/createappdir.sh" \
|
||||
scripts=("${SOURCE_DIR}/src/scripts/rmvolume.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/rmaddon.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reboot.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/update.sh" \
|
||||
@@ -40,6 +40,7 @@ images=$(node -e "var i = require('${SOURCE_DIR}/src/infra_version.js'); console
|
||||
for image in ${images}; do
|
||||
if ! docker inspect "${image}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${image}"
|
||||
echo "docker pull ${image%@sha256:*}"
|
||||
image_missing="true"
|
||||
fi
|
||||
done
|
||||
|
||||
+4
-10
@@ -24,9 +24,9 @@ describe('config', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can get and set version', function (done) {
|
||||
config.setVersion('1.2.3');
|
||||
expect(config.version()).to.be('1.2.3');
|
||||
it('can get version', function (done) {
|
||||
expect(config.version()).to.be.ok(); // this gets a dummy text string
|
||||
expect(config.version().includes('\n')).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -36,15 +36,9 @@ describe('config', function () {
|
||||
expect(config.adminLocation()).to.equal('my');
|
||||
});
|
||||
|
||||
it('set saves value in file', function (done) {
|
||||
config.set('version', '1.3.0');
|
||||
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).version).to.eql('1.3.0');
|
||||
done();
|
||||
});
|
||||
|
||||
it('set does not save custom values in file', function (done) {
|
||||
config.set('foobar', 'somevalue');
|
||||
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).foobar).to.not.be.ok();
|
||||
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'cloudron.conf'))).foobar).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
@@ -233,7 +233,8 @@ describe('database', function () {
|
||||
debugMode: null,
|
||||
robotsTxt: null,
|
||||
enableBackup: true,
|
||||
ownerId: USER_0.id
|
||||
ownerId: USER_0.id,
|
||||
env: {}
|
||||
};
|
||||
|
||||
it('cannot delete referenced domain', function (done) {
|
||||
@@ -751,7 +752,10 @@ describe('database', function () {
|
||||
robotsTxt: null,
|
||||
enableBackup: true,
|
||||
ownerId: USER_0.id,
|
||||
alternateDomains: []
|
||||
alternateDomains: [],
|
||||
env: {
|
||||
'CUSTOM_KEY': 'CUSTOM_VALUE'
|
||||
}
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -778,7 +782,8 @@ describe('database', function () {
|
||||
robotsTxt: null,
|
||||
enableBackup: true,
|
||||
ownerId: USER_0.id,
|
||||
alternateDomains: []
|
||||
alternateDomains: [],
|
||||
env: {}
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('Dockerproxy', function () {
|
||||
dockerProxy.start(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
exec(`${DOCKER} run -d ubuntu "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) {
|
||||
exec(`${DOCKER} run -d cloudron/base:1.0.0 "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stderr).to.be.empty();
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('Dockerproxy', function () {
|
||||
});
|
||||
|
||||
it('can create container', function (done) {
|
||||
var cmd = DOCKER + ` run ubuntu "/bin/bash" "-c" "echo 'hello'"`;
|
||||
var cmd = `${DOCKER} run cloudron/base:1.0.0 "/bin/bash" "-c" "echo 'hello'"`;
|
||||
exec(cmd, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stdout).to.contain('hello');
|
||||
@@ -65,7 +65,7 @@ describe('Dockerproxy', function () {
|
||||
});
|
||||
|
||||
it('proxy overwrites the container network option', function (done) {
|
||||
var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail ubuntu "/bin/bash" "-c" "echo 'hello'"`;
|
||||
var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail cloudron/base:1.0.0 "/bin/bash" "-c" "echo 'hello'"`;
|
||||
exec(cmd, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stdout).to.contain('hello');
|
||||
@@ -85,7 +85,7 @@ describe('Dockerproxy', function () {
|
||||
});
|
||||
|
||||
it('can use PUT to upload archive into a container', function (done) {
|
||||
exec(`${DOCKER} cp -a ${__dirname}/proxytestarchive.tar ${containerId}:/tmp/`, function (error, stdout, stderr) {
|
||||
exec(`${DOCKER} cp ${__dirname}/proxytestarchive.tar ${containerId}:/tmp/`, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stderr).to.be.empty();
|
||||
expect(stdout).to.be.empty();
|
||||
|
||||
+111
-6
@@ -29,13 +29,13 @@ describe('Domains', function () {
|
||||
], done);
|
||||
});
|
||||
|
||||
const domain = {
|
||||
domain: 'example.com',
|
||||
zoneName: 'example.com',
|
||||
config: {}
|
||||
};
|
||||
|
||||
describe('validateHostname', function () {
|
||||
const domain = {
|
||||
domain: 'example.com',
|
||||
zoneName: 'example.com',
|
||||
config: {}
|
||||
};
|
||||
|
||||
it('does not allow admin subdomain', function () {
|
||||
config.setFqdn('example.com');
|
||||
config.setAdminFqdn('my.example.com');
|
||||
@@ -85,4 +85,109 @@ describe('Domains', function () {
|
||||
expect(domains.validateHostname('a0.x', domain)).to.be.an(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getName', function () {
|
||||
it('works with zoneName==domain (not hyphenated)', function () {
|
||||
const domain = {
|
||||
domain: 'example.com',
|
||||
zoneName: 'example.com',
|
||||
config: {}
|
||||
};
|
||||
|
||||
expect(domains._getName(domain, '', 'A')).to.be('');
|
||||
expect(domains._getName(domain, 'www', 'A')).to.be('www');
|
||||
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev');
|
||||
|
||||
expect(domains._getName(domain, '', 'MX')).to.be('');
|
||||
|
||||
expect(domains._getName(domain, '', 'TXT')).to.be('');
|
||||
expect(domains._getName(domain, 'www', 'TXT')).to.be('www');
|
||||
expect(domains._getName(domain, 'www.dev', 'TXT')).to.be('www.dev');
|
||||
});
|
||||
|
||||
it('works when zoneName!=domain (not hyphenated)', function () {
|
||||
const domain = {
|
||||
domain: 'dev.example.com',
|
||||
zoneName: 'example.com',
|
||||
config: {}
|
||||
};
|
||||
|
||||
expect(domains._getName(domain, '', 'A')).to.be('dev');
|
||||
expect(domains._getName(domain, 'www', 'A')).to.be('www.dev');
|
||||
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev.dev');
|
||||
|
||||
expect(domains._getName(domain, '', 'MX')).to.be('dev');
|
||||
|
||||
expect(domains._getName(domain, '', 'TXT')).to.be('dev');
|
||||
expect(domains._getName(domain, 'www', 'TXT')).to.be('www.dev');
|
||||
expect(domains._getName(domain, 'www.dev', 'TXT')).to.be('www.dev.dev');
|
||||
});
|
||||
|
||||
it('works when hyphenated - level1', function () {
|
||||
const domain = {
|
||||
domain: 'customer.example.com',
|
||||
zoneName: 'example.com',
|
||||
config: {
|
||||
hyphenatedSubdomains: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(domains._getName(domain, '', 'A')).to.be('customer');
|
||||
expect(domains._getName(domain, 'www', 'A')).to.be('www-customer');
|
||||
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev-customer');
|
||||
|
||||
expect(domains._getName(domain, '', 'MX')).to.be('customer');
|
||||
|
||||
expect(domains._getName(domain, '', 'TXT')).to.be('customer');
|
||||
expect(domains._getName(domain, '_dmarc', 'TXT')).to.be('_dmarc.customer');
|
||||
expect(domains._getName(domain, 'cloudron._domainkey', 'TXT')).to.be('cloudron._domainkey.customer');
|
||||
expect(domains._getName(domain, '_acme-challenge.my', 'TXT')).to.be('_acme-challenge.my-customer');
|
||||
expect(domains._getName(domain, '_acme-challenge', 'TXT')).to.be('_acme-challenge');
|
||||
});
|
||||
|
||||
it('works when hyphenated - level2', function () {
|
||||
const domain = {
|
||||
domain: 'customer.dev.example.com',
|
||||
zoneName: 'example.com',
|
||||
config: {
|
||||
hyphenatedSubdomains: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(domains._getName(domain, '', 'A')).to.be('customer.dev');
|
||||
expect(domains._getName(domain, 'www', 'A')).to.be('www-customer.dev');
|
||||
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev-customer.dev');
|
||||
|
||||
expect(domains._getName(domain, '', 'MX')).to.be('customer.dev');
|
||||
|
||||
expect(domains._getName(domain, '', 'TXT')).to.be('customer.dev');
|
||||
expect(domains._getName(domain, '_dmarc', 'TXT')).to.be('_dmarc.customer.dev');
|
||||
expect(domains._getName(domain, 'cloudron._domainkey', 'TXT')).to.be('cloudron._domainkey.customer.dev');
|
||||
expect(domains._getName(domain, '_acme-challenge.my', 'TXT')).to.be('_acme-challenge.my-customer.dev');
|
||||
expect(domains._getName(domain, '_acme-challenge', 'TXT')).to.be('_acme-challenge.dev');
|
||||
});
|
||||
|
||||
it('works with caas', function () {
|
||||
const domain = {
|
||||
domain: 'customer.example.com',
|
||||
provider: 'caas',
|
||||
zoneName: 'example.com',
|
||||
config: {
|
||||
hyphenatedSubdomains: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(domains._getName(domain, '', 'A')).to.be('');
|
||||
expect(domains._getName(domain, 'www', 'A')).to.be('www');
|
||||
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev');
|
||||
|
||||
expect(domains._getName(domain, '', 'MX')).to.be('');
|
||||
|
||||
expect(domains._getName(domain, '', 'TXT')).to.be('');
|
||||
expect(domains._getName(domain, '_dmarc', 'TXT')).to.be('_dmarc');
|
||||
expect(domains._getName(domain, 'cloudron._domainkey', 'TXT')).to.be('cloudron._domainkey');
|
||||
expect(domains._getName(domain, '_acme-challenge.my', 'TXT')).to.be('_acme-challenge.my');
|
||||
expect(domains._getName(domain, '_acme-challenge', 'TXT')).to.be('_acme-challenge');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,15 @@ function cleanup(done) {
|
||||
|
||||
describe('Certificates', function () {
|
||||
describe('validateCertificate', function () {
|
||||
let foobarDomain = {
|
||||
domain: 'foobar.com',
|
||||
config: { hypenatedSubdomains: false }
|
||||
};
|
||||
|
||||
let amazingDomain = {
|
||||
domain: 'amazing.com',
|
||||
config: {}
|
||||
};
|
||||
/*
|
||||
Generate these with:
|
||||
openssl genrsa -out server.key 512
|
||||
@@ -65,53 +74,94 @@ describe('Certificates', function () {
|
||||
var validKey3 = '-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+nx2TXe7i+YB8\np9E047z6tVDyr6/JHyxxz0S6PSlOh0sF+UzYrpmTSKyKtHMIv+N3ifUDpCs94n6v\nYnjG0eTaP6lq+3xJrYajYzOkB7c5O4ipgl9S3sibd/YtvruK1M+ukDZNHcD90MWV\nrOO754F3e3fj3oOjxoE+lcB05pt8+yu/ecTH3u+uvi3N4reQb27sZi6h3mDKLAXa\n3nBJ4xZsWTaiqQ4K0dFcTvskMpXEgaJ6mdROeRb1vuMiSIFDXHTEcbiyh7BXbKYY\ndLZetQdnjkUUwbvJVinK0pP0jP23L/PlVwtno3OBT+kp9Boh7wymgOlV/lQ14HYM\n4AASgkorAgMBAAECggEAdVSVLMcNqlGuv4vAHtDq2lpOaAKxrZbtkWPlxsisqzRl\nfljT7y+RQfHimkG16LXL+iFFWadsIlxOY/+1nZNGTPwQeNQwzVzs2ZbPC3DgW28E\nkGm56NVOHzu4oLGc2DhjWOxVMCRXTSN66sUPK/K0YunxgqXM2zrtBKvCWXI0VLlo\nN/UWAwHf4i0GWRl8u8PvxgMXlSW9p9l6gSsivWRMag9ADwRQ/NSKrRYkiOoRe3vz\nLxXARBvzeZXvOPVLGVRX4SIR7OmS8cC6Ol/rp1/ZFFID7aN+wdzphPSL1UNUriw4\nDv1mxz73SNakgeYSFBoWRS5BsJI01JoCoILsnhVCiQKBgQDyW+k5+j4K17fzwsmi\nyxZ0Nz/ncpkqxVrWYZM3pn7OVkb2NDArimEk53kmJ0hrT84kKJUYDx55R2TpnzpV\nMLmjxgs9TUrzZzsL/DP2ppkfE3OrPS+06OGa5GbURxD6KPvqDtOmU3oFyJ3f4YJR\nVK7RW+zO4sXEpHIxwdBXbYov1QKBgQDJWbt+W5M0sA2D5LrUBNMTvMdNnKH0syc2\nZlcIOdj6HuUIveYpBRq64Jn9VJpXMxQanwE+IUjCpPTa8wF0OA6MZPy6cfovqb8a\ni1/M/lvCoYVS3KHLcTOvTGD3xej0EUj13xWGNu8y3i7Z9/Bl21hEyjd0q0I5OqJx\no9Qa5TGR/wKBgBPfkYpdiMTe14i3ik09FgRFm4nhDcpCEKbPrYC8uF03Ge6KbQDF\nAh5ClN6aDggurRqt8Tvd0YPkZNP7aI8fxbk2PimystiuuFrNPX2WP6warjt2cvkE\nt6s522zAvxWkUrPor1ZONg1PXBLFrSf6J7OnNA3q7oina23FFM52fwRZAoGAZ7l7\nFffU2IKNI9HT0N7/YZ6RSVEUOXuFCsgjs5AhT5BUynERPTZs87I6gb9wltUwWRpq\nSHhbBDJ4FMa0jAtIq1hmvSF0EdOvJ9x+qJqr6JLOnMYd7zDMwFRna5yfigPRgx+9\n9dsc1CaTGiRYyg/5484MTWTgA51KC6Kq5IQHSj8CgYBr9rWgqM8hVCKSt1cMguQV\nTPaV97+u3kV2jFd/aVgDtCDIVvp5TPuqfskE1v3MsSjJ8hfHdYvyxZB8h8T4LlTD\n2HdxwCjVh2qirAvkar2b1mfA6R8msmVaIxBu4MqDcIPqR823klF7A8jSD3MGzYcU\nbnnxMdwgWQkmx0/6/90ZCg==\n-----END PRIVATE KEY-----\n';
|
||||
|
||||
it('does not allow empty string for cert', function () {
|
||||
expect(reverseProxy.validateCertificate('foobar.com', '', 'key')).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: '', key: 'key' })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow empty string for key', function () {
|
||||
expect(reverseProxy.validateCertificate('foobar.com', 'cert', '')).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: 'cert', key: '' })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert', function () {
|
||||
expect(reverseProxy.validateCertificate('foobar.com', 'someinvalidcert', validKey0)).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: 'someinvalidcert', key: validKey0 })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid key', function () {
|
||||
expect(reverseProxy.validateCertificate('foobar.com', validCert0, 'invalidkey')).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: 'invalidkey' })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain', function () {
|
||||
expect(reverseProxy.validateCertificate('cloudron.io', validCert0, validKey0)).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('', { domain: 'cloudron.io' }, { cert: validCert0, key: validKey0 })).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('cloudron.io', foobarDomain, { cert: validCert0, key: validKey0 })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain', function () {
|
||||
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey0)).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey0 })).to.be(null);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (wildcard)', function () {
|
||||
expect(reverseProxy.validateCertificate('abc.foobar.com', validCert1, validKey1)).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('abc', foobarDomain, { cert: validCert1, key: validKey1 })).to.be(null);
|
||||
});
|
||||
|
||||
it('does now allow cert without matching domain (wildcard)', function () {
|
||||
expect(reverseProxy.validateCertificate('foobar.com', validCert1, validKey1)).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('bar.abc.foobar.com', validCert1, validKey1)).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert1, key: validKey1 })).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('bar.abc', foobarDomain, { cert: validCert1, key: validKey1 })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid cert with matching domain (subdomain)', function () {
|
||||
expect(reverseProxy.validateCertificate('baz.foobar.com', validCert2, validKey2)).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('baz', foobarDomain, { cert: validCert2, key: validKey2 })).to.be(null);
|
||||
});
|
||||
|
||||
it('does not allow cert without matching domain (subdomain)', function () {
|
||||
expect(reverseProxy.validateCertificate('baz.foobar.com', validCert0, validKey0)).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('baz', foobarDomain, { cert: validCert0, key: validKey0 })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow invalid cert/key tuple', function () {
|
||||
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey1)).to.be.an(Error);
|
||||
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey1 })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('picks certificate in SAN', function () {
|
||||
expect(reverseProxy.validateCertificate('amazing.com', validCert3, validKey3)).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('subdomain.amazing.com', validCert3, validKey3)).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('', amazingDomain, { cert: validCert3, key: validKey3 })).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('subdomain', amazingDomain, { cert: validCert3, key: validKey3 })).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateFallbackCertificiate - non-hyphenated', function () {
|
||||
let domainObject = {
|
||||
domain: 'cool.com',
|
||||
config: {}
|
||||
};
|
||||
let result;
|
||||
|
||||
it('can generate fallback certs', function () {
|
||||
result = reverseProxy.generateFallbackCertificateSync(domainObject);
|
||||
expect(result).to.be.ok();
|
||||
expect(result.error).to.be(null);
|
||||
});
|
||||
|
||||
it('can validate the certs', function () {
|
||||
expect(reverseProxy.validateCertificate('foo', domainObject, result)).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('', domainObject, result)).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateFallbackCertificiate - hyphenated', function () {
|
||||
let domainObject = {
|
||||
domain: 'customer.cool.com',
|
||||
config: { hyphenatedSubdomains: true }
|
||||
};
|
||||
let result;
|
||||
|
||||
it('can generate fallback certs', function () {
|
||||
result = reverseProxy.generateFallbackCertificateSync(domainObject);
|
||||
expect(result).to.be.ok();
|
||||
expect(result.error).to.be(null);
|
||||
});
|
||||
|
||||
it('can validate the certs', function () {
|
||||
expect(reverseProxy.validateCertificate('foo', domainObject, result)).to.be(null);
|
||||
expect(reverseProxy.validateCertificate('', domainObject, result)).to.be(null);
|
||||
|
||||
expect(reverseProxy.validateCertificate('foo', { domain: 'customer.cool.com', config: {} }, result)).to.be.an(Error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,7 +181,7 @@ describe('Certificates', function () {
|
||||
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
|
||||
expect(error).to.be(null);
|
||||
expect(api._name).to.be('caas');
|
||||
expect(options).to.eql({ email: 'support@cloudron.io' });
|
||||
expect(options).to.eql({ email: 'support@cloudron.io', "performHttpAuthorization": false, "prod": false, "wildcard": false });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('Server', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.version).to.equal('1.1.1-test');
|
||||
expect(res.body.version).to.contain('-test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ var appdb = require('../appdb.js'),
|
||||
nock = require('nock'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('../settings.js'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
updatechecker = require('../updatechecker.js'),
|
||||
@@ -44,6 +45,8 @@ var AUDIT_SOURCE = {
|
||||
ip: '1.2.3.4'
|
||||
};
|
||||
|
||||
const UPDATE_VERSION = semver.inc(config.version(), 'major');
|
||||
|
||||
function checkMails(number, done) {
|
||||
// mails are enqueued async
|
||||
setTimeout(function () {
|
||||
@@ -110,7 +113,7 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0', changelog: [''], sourceTarballUrl: '2.0.0.tar.gz' } );
|
||||
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
@@ -119,31 +122,8 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('2.0.0.tar.gz');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('offers prerelease', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: '2.0.0-pre.0.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'pro' } } } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0-pre.0');
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
|
||||
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
@@ -157,7 +137,7 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(404, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: '2.0.0-pre.0.tar.gz' } );
|
||||
.reply(404, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: 'box-pre.tar.gz' } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(error).to.be.ok();
|
||||
@@ -194,7 +174,7 @@ describe('updatechecker - box - automatic (no email)', function () {
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0', sourceTarballUrl: '2.0.0.tar.gz' } );
|
||||
.reply(200, { version: UPDATE_VERSION, sourceTarballUrl: 'box.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
@@ -203,7 +183,7 @@ describe('updatechecker - box - automatic (no email)', function () {
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
@@ -238,7 +218,7 @@ describe('updatechecker - box - automatic free (email)', function () {
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0', changelog: [''], sourceTarballUrl: '2.0.0.tar.gz' } );
|
||||
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
@@ -247,7 +227,7 @@ describe('updatechecker - box - automatic free (email)', function () {
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
|
||||
+2
-28
@@ -22,7 +22,6 @@ var assert = require('assert'),
|
||||
paths = require('./paths.js'),
|
||||
progress = require('./progress.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
shell = require('./shell.js'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
util = require('util'),
|
||||
@@ -123,9 +122,6 @@ function verifyUpdateInfo(versionsFile, updateInfo, callback) {
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// skip verification for prereleases because we remove it from release.json
|
||||
if (semver.prerelease(config.version()) !== null) return callback();
|
||||
|
||||
var releases = safe.JSON.parse(safe.fs.readFileSync(versionsFile, 'utf8')) || { };
|
||||
if (!releases[config.version()] || !releases[config.version()].next) return callback(new UpdaterError(UpdaterError.EXTERNAL_ERROR, 'No version info'));
|
||||
var nextVersion = releases[config.version()].next;
|
||||
@@ -176,33 +172,11 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// NOTE: this data is opaque and will be passed through the installer.sh
|
||||
var data= {
|
||||
provider: config.provider(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
|
||||
appstore: {
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
|
||||
version: boxUpdateInfo.version
|
||||
};
|
||||
|
||||
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
|
||||
debug('updating box %s', boxUpdateInfo.sourceTarballUrl);
|
||||
|
||||
progress.set(progress.UPDATE, 70, 'Installing update');
|
||||
|
||||
shell.sudo('update', [ UPDATE_CMD, packageInfo.file, JSON.stringify(data) ], function (error) {
|
||||
shell.sudo('update', [ UPDATE_CMD, packageInfo.file ], function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
|
||||
+4
-4
@@ -75,11 +75,11 @@ function getOwner(callback) {
|
||||
|
||||
// the first created user it the 'owner'
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE admin=1 ORDER BY createdAt LIMIT 1', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, postProcess(result[0]));
|
||||
});
|
||||
callback(null, postProcess(result[0]));
|
||||
});
|
||||
}
|
||||
|
||||
function getByResetToken(email, resetToken, callback) {
|
||||
|
||||
+2
-2
@@ -401,7 +401,7 @@ function updateUser(userId, data, auditSource, callback) {
|
||||
callback(null);
|
||||
|
||||
get(userId, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId, user: removePrivateFields(result) });
|
||||
|
||||
@@ -575,7 +575,7 @@ function setTwoFactorAuthenticationSecret(userId, callback) {
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
qrcode.toDataURL(secret.otpauth_url, function (error, dataUrl) {
|
||||
if (error) console.error(error);
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { secret: secret.base32, qrcode: dataUrl });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user