Compare commits

...

96 Commits

Author SHA1 Message Date
Johannes Zellner 212d0bd55a Revert "Add hack for broken app backup tarballs"
This reverts commit 9723951bfc.
2015-08-31 21:44:24 -07:00
Girish Ramakrishnan 712ada940e Add hack for broken app backup tarballs 2015-08-31 18:58:38 -07:00
Johannes Zellner ba690c6346 Add missing records argument 2015-08-30 23:00:01 -07:00
Johannes Zellner e910e19f57 Fix debug tag 2015-08-30 22:54:52 -07:00
Johannes Zellner 0c2532b0b5 Give default value to config.dnsInSync 2015-08-30 22:35:44 -07:00
Johannes Zellner 9c9b17a5f0 Remove cloudron.config prior to every test run 2015-08-30 22:35:44 -07:00
Johannes Zellner 816dea91ec Assert for dns record values 2015-08-30 22:35:44 -07:00
Johannes Zellner c228f8d4d5 Merge admin dns and mail dns setup
This now also checks if the mail records are in sync
2015-08-30 22:35:43 -07:00
Johannes Zellner 05bb99fad4 give dns record changeIds as a result for addMany() 2015-08-30 22:35:43 -07:00
Johannes Zellner 51b2457b3d Setup webadmin domain on the box side 2015-08-30 22:35:43 -07:00
Girish Ramakrishnan ed71fca23e Fix css 2015-08-30 22:25:18 -07:00
Girish Ramakrishnan 20e8e72ac2 reserved blocks are used 2015-08-30 22:24:57 -07:00
Girish Ramakrishnan 13fe0eb882 Only display one donut for memory usage 2015-08-30 22:13:01 -07:00
Girish Ramakrishnan e0476c9030 Reboot is a post route 2015-08-30 21:38:54 -07:00
Girish Ramakrishnan fca82fd775 Display upto 600mb for apps 2015-08-30 17:21:44 -07:00
Johannes Zellner 37c8ba8ddd Reduce logging for aws credentials 2015-08-30 17:03:10 -07:00
Johannes Zellner f87011b5c2 Also always check for dns propagation 2015-08-30 17:00:23 -07:00
Johannes Zellner 7f149700f8 Remove wrong optimization for subdomain records 2015-08-30 16:54:33 -07:00
Johannes Zellner 78ba9070fc use config.appFqdn() to handle custom domains 2015-08-30 16:29:09 -07:00
Johannes Zellner e31e5e1f69 Reuse dnsRecordId for record status id 2015-08-30 15:58:54 -07:00
Johannes Zellner 31d9027677 Query dns status with aws statusId 2015-08-30 15:51:33 -07:00
Johannes Zellner debcd6f353 aws provides uppercase properties 2015-08-30 15:47:08 -07:00
Johannes Zellner 5cb1681922 Fixup the zonename comparison 2015-08-30 15:37:18 -07:00
Johannes Zellner 9074bccea0 Move subdomain management from appstore to box 2015-08-30 15:29:14 -07:00
Girish Ramakrishnan 291798f574 Pass along aws config for updates 2015-08-27 22:45:04 -07:00
Girish Ramakrishnan b104843ae1 Add missing quotes to cloudron.conf 2015-08-27 20:15:04 -07:00
Girish Ramakrishnan dd062c656f Fix failing test 2015-08-27 11:43:36 -07:00
Girish Ramakrishnan ae2eb718c6 check if response has credentials object 2015-08-27 11:43:02 -07:00
Girish Ramakrishnan 7ac26bb653 Fix backup response 2015-08-27 11:19:40 -07:00
Girish Ramakrishnan 41a726e8a7 Fix backup test 2015-08-27 11:17:36 -07:00
Girish Ramakrishnan 4b69216548 bash: quote the array expansion 2015-08-27 10:13:05 -07:00
Girish Ramakrishnan 99395ddf5a bash: quoting array expansion because thats how it is 2015-08-27 09:49:44 -07:00
Girish Ramakrishnan 5f9fa5c352 bash: empty array expansion barfs with set -u 2015-08-27 09:33:40 -07:00
Girish Ramakrishnan 9013331917 Fix coding style 2015-08-27 09:30:32 -07:00
Girish Ramakrishnan 3a8f80477b getSignedDownloadUrl must return an object with url and sessionToken 2015-08-27 09:26:19 -07:00
Johannes Zellner 813c680ed0 pass full box data to the update 2015-08-26 10:59:17 -07:00
Johannes Zellner a0eccd615f Send new version to update to to the installer 2015-08-26 09:42:48 -07:00
Johannes Zellner 59be539ecd make restoreapp.sh support aws session tokens 2015-08-26 09:14:15 -07:00
Johannes Zellner a04740114c Generate app restore urls locally 2015-08-26 09:11:28 -07:00
Johannes Zellner 60b5d71c74 appBackupIds are not needed for backup url generation 2015-08-26 09:06:45 -07:00
Johannes Zellner 0a8b4b0c43 Load our style sheet as early as possible 2015-08-25 21:59:01 -07:00
Johannes Zellner ec21105c47 use backupKey from userData 2015-08-25 18:44:52 -07:00
Girish Ramakrishnan 444258e7ee backupKey is a function 2015-08-25 18:37:51 -07:00
Johannes Zellner e6fd05c2bd Support optional aws related userData 2015-08-25 17:52:01 -07:00
Johannes Zellner 9fdcd452d0 Use locally generate signed urls for app backup 2015-08-25 17:52:01 -07:00
Johannes Zellner f39b9d5618 Support session tokens in backupapp.sh 2015-08-25 17:52:00 -07:00
Johannes Zellner 76e4c4919d Only federated tokens need session token 2015-08-25 17:52:00 -07:00
Johannes Zellner d1f159cdb4 Also send the restoreKey for the backup done webhook 2015-08-25 17:52:00 -07:00
Johannes Zellner c63065e460 Also send the sessionToken when using the pre-signed url 2015-08-25 17:52:00 -07:00
Johannes Zellner 124c1d94a4 Translate the federated credentials 2015-08-25 17:52:00 -07:00
Johannes Zellner e9161b726a AWS credential creation returns 201 2015-08-25 17:52:00 -07:00
Johannes Zellner fd0d27b192 AWS credentials are now dealt with a level down 2015-08-25 17:52:00 -07:00
Johannes Zellner 50064a40fe Use dev bucket for now as a default 2015-08-25 17:52:00 -07:00
Johannes Zellner c9bc5fc38e Use signed urls for upload on the box side 2015-08-25 17:52:00 -07:00
Johannes Zellner 58f533fe50 Add config.aws().backupPrefix 2015-08-25 17:52:00 -07:00
Johannes Zellner efcdffd8ff Add getSignedUploadUrl() to aws.js 2015-08-25 17:52:00 -07:00
Johannes Zellner 22793c3886 move aws-sdk from dev to normal dependencies 2015-08-25 17:52:00 -07:00
Johannes Zellner 797ddbacc0 Return aws credentials from config.js 2015-08-25 17:52:00 -07:00
Johannes Zellner e011962469 refactor backupBoxWithAppBackupIds() 2015-08-25 17:52:00 -07:00
Johannes Zellner b376ad9815 Add webhooks.js 2015-08-25 17:51:59 -07:00
Johannes Zellner 77248fe65c Construct backupUrl locally 2015-08-25 17:51:59 -07:00
Johannes Zellner 1dad115203 Add initial aws object to config.js 2015-08-25 17:51:59 -07:00
Johannes Zellner 8812d58031 Add backupKey to config 2015-08-25 17:51:59 -07:00
Johannes Zellner fff7568f7e Add aws.js 2015-08-25 17:51:59 -07:00
Johannes Zellner ff6662579d Fix typo in backupapp.sh help output 2015-08-25 17:51:59 -07:00
Girish Ramakrishnan 0cf9fbd909 Merge data into args 2015-08-25 15:55:52 -07:00
Girish Ramakrishnan 848b745fcb Fix boolean logic 2015-08-25 12:24:02 -07:00
Girish Ramakrishnan 9a35c40b24 Add force argument
This fixes crash when auto-updating apps
2015-08-25 10:01:20 -07:00
Girish Ramakrishnan 1f1e6124cd oldConfig can be null during a restore/upgrade 2015-08-25 09:59:44 -07:00
Girish Ramakrishnan 033df970ad Update manifestformat@1.7.0 2015-08-24 22:56:02 -07:00
Girish Ramakrishnan dd80a795a0 Read memoryLimit from manifest 2015-08-24 22:44:35 -07:00
Girish Ramakrishnan 1eec6a39c6 Show upto 200mb 2015-08-24 22:39:06 -07:00
Girish Ramakrishnan dd6b8face9 Set app memory limit to 200MB (includes 100 MB swap) 2015-08-24 21:58:19 -07:00
Girish Ramakrishnan 288de7e03a Add RSTATE_ERROR 2015-08-24 21:58:19 -07:00
Girish Ramakrishnan a760ef4d22 Rebase addons to use base image 0.3.3 2015-08-24 10:19:18 -07:00
Johannes Zellner 0dd745bce4 Fix form submit with enter for update form 2015-08-22 17:21:25 -07:00
Johannes Zellner d4d5d371ac Use POST heartbeat route instead of GET 2015-08-22 16:51:56 -07:00
Johannes Zellner 205bf4ddbd Offset the footer in apps view 2015-08-20 23:50:52 -07:00
Girish Ramakrishnan 4ab84d42c6 Delete image only if it changed
This optimization won't work if we have two dockerImage with same
image id....
2015-08-19 14:24:32 -07:00
Girish Ramakrishnan ee74badf3a Check for dockerImage in manifest in install/update/restore routes 2015-08-19 11:08:45 -07:00
Girish Ramakrishnan aa173ff74c restore without a backup is the same as re-install 2015-08-19 11:00:00 -07:00
Girish Ramakrishnan b584fc33f5 CN of admin group is admins 2015-08-18 16:35:52 -07:00
Girish Ramakrishnan 15c9d8682e Base image is now 0.3.3 2015-08-18 15:43:50 -07:00
Girish Ramakrishnan 361be8c26b containerId can be null 2015-08-18 15:43:50 -07:00
Girish Ramakrishnan 4db9a5edd6 Clean up the old image and not the current one 2015-08-18 10:01:15 -07:00
Johannes Zellner bcc878da43 Hide update input fields and update button if it is blocked by apps 2015-08-18 16:59:36 +02:00
Johannes Zellner 79f179fed4 Add note, why sendError() is required 2015-08-18 16:53:29 +02:00
Johannes Zellner a924a9a627 Revert "remove obsolete sendError() function"
This reverts commit 5d9b122dd5.
2015-08-18 16:49:53 +02:00
Girish Ramakrishnan 45d444df0e leave a note about force_update 2015-08-17 21:30:56 -07:00
Girish Ramakrishnan 92461a3366 Remove ununsed require 2015-08-17 21:23:32 -07:00
Girish Ramakrishnan 032a430c51 Fix debug message 2015-08-17 21:23:27 -07:00
Girish Ramakrishnan a6a3855e79 Do not remove icon for non-appstore installs
Fixes #466
2015-08-17 19:37:51 -07:00
Girish Ramakrishnan 2386545814 Add a note why oldConfig can be null 2015-08-17 10:05:07 -07:00
Johannes Zellner 2059152dd3 remove obsolete sendError() function 2015-08-17 14:55:56 +02:00
Johannes Zellner 32d2c260ab Move appstore badges out of the way for the app titles 2015-08-17 11:50:31 +02:00
Johannes Zellner 384c7873aa Correctly mark apps pending for approval
Fixes #339
2015-08-17 11:50:08 +02:00
36 changed files with 803 additions and 272 deletions
+34 -10
View File
@@ -7,6 +7,28 @@
"from": "https://registry.npmjs.org/async/-/async-1.2.1.tgz", "from": "https://registry.npmjs.org/async/-/async-1.2.1.tgz",
"resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz" "resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz"
}, },
"aws-sdk": {
"version": "2.1.46",
"from": "aws-sdk@*",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.46.tgz",
"dependencies": {
"sax": {
"version": "0.5.3",
"from": "sax@0.5.3",
"resolved": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz"
},
"xml2js": {
"version": "0.2.8",
"from": "xml2js@0.2.8",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz"
},
"xmlbuilder": {
"version": "0.4.2",
"from": "xmlbuilder@0.4.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz"
}
}
},
"body-parser": { "body-parser": {
"version": "1.13.1", "version": "1.13.1",
"from": "https://registry.npmjs.org/body-parser/-/body-parser-1.13.1.tgz", "from": "https://registry.npmjs.org/body-parser/-/body-parser-1.13.1.tgz",
@@ -105,23 +127,24 @@
} }
}, },
"cloudron-manifestformat": { "cloudron-manifestformat": {
"version": "1.6.0", "version": "1.7.0",
"from": "cloudron-manifestformat@1.6.0", "from": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-1.7.0.tgz",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-1.7.0.tgz",
"dependencies": { "dependencies": {
"java-packagename-regex": { "java-packagename-regex": {
"version": "1.0.0", "version": "1.0.0",
"from": "java-packagename-regex@>=1.0.0 <2.0.0", "from": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz" "resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz"
}, },
"safetydance": { "safetydance": {
"version": "0.0.15", "version": "0.0.15",
"from": "safetydance@0.0.15", "from": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz",
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz" "resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz"
}, },
"tv4": { "tv4": {
"version": "1.1.12", "version": "1.2.3",
"from": "tv4@>=1.1.9 <2.0.0", "from": "https://registry.npmjs.org/tv4/-/tv4-1.2.3.tgz",
"resolved": "http://registry.npmjs.org/tv4/-/tv4-1.1.12.tgz" "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.3.tgz"
} }
} }
}, },
@@ -133,15 +156,16 @@
"connect-lastmile": { "connect-lastmile": {
"version": "0.0.13", "version": "0.0.13",
"from": "connect-lastmile@0.0.13", "from": "connect-lastmile@0.0.13",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "2.1.3", "version": "2.1.3",
"from": "debug@>=2.1.0 <2.2.0", "from": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
"dependencies": { "dependencies": {
"ms": { "ms": {
"version": "0.7.0", "version": "0.7.0",
"from": "ms@0.7.0", "from": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz" "resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
} }
} }
@@ -2244,7 +2268,7 @@
}, },
"safetydance": { "safetydance": {
"version": "0.0.19", "version": "0.0.19",
"from": "safetydance@0.0.19", "from": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz" "resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
}, },
"semver": { "semver": {
+2 -2
View File
@@ -17,8 +17,9 @@
}, },
"dependencies": { "dependencies": {
"async": "^1.2.1", "async": "^1.2.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1", "body-parser": "^1.13.1",
"cloudron-manifestformat": "^1.6.0", "cloudron-manifestformat": "^1.7.0",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13", "connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0", "connect-timeout": "^1.5.0",
@@ -68,7 +69,6 @@
}, },
"devDependencies": { "devDependencies": {
"apidoc": "*", "apidoc": "*",
"aws-sdk": "^2.1.10",
"bootstrap-sass": "^3.3.3", "bootstrap-sass": "^3.3.3",
"del": "^1.1.1", "del": "^1.1.1",
"expect.js": "*", "expect.js": "*",
+7 -7
View File
@@ -7,11 +7,11 @@ INFRA_VERSION=8
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING # WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well # These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.3.1 BASE_IMAGE=cloudron/base:0.3.3
MYSQL_IMAGE=cloudron/mysql:0.3.2 MYSQL_IMAGE=cloudron/mysql:0.3.3
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.1 POSTGRESQL_IMAGE=cloudron/postgresql:0.3.2
MONGODB_IMAGE=cloudron/mongodb:0.3.1 MONGODB_IMAGE=cloudron/mongodb:0.3.2
REDIS_IMAGE=cloudron/redis:0.3.1 # if you change this, fix src/addons.js as well REDIS_IMAGE=cloudron/redis:0.3.2 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.3.1 MAIL_IMAGE=cloudron/mail:0.3.2
GRAPHITE_IMAGE=cloudron/graphite:0.3.3 GRAPHITE_IMAGE=cloudron/graphite:0.3.4
+8
View File
@@ -16,6 +16,8 @@ arg_tls_key=""
arg_token="" arg_token=""
arg_version="" arg_version=""
arg_web_server_origin="" arg_web_server_origin=""
arg_backup_key=""
arg_aws=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@") args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}" eval set -- "${args}"
@@ -41,6 +43,12 @@ EOF
arg_restore_key=$(echo "$2" | $json restoreKey) arg_restore_key=$(echo "$2" | $json restoreKey)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key="" [[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_backup_key=$(echo "$2" | $json backupKey)
[[ "${arg_backup_key}" == "null" ]] && arg_backup_key=""
arg_aws=$(echo "$2" | $json aws)
[[ "${arg_aws}" == "null" ]] && arg_aws=""
shift 2 shift 2
;; ;;
--) break;; --) break;;
+4 -1
View File
@@ -122,6 +122,7 @@ set_progress "65" "Creating cloudron.conf"
sudo -u yellowtent -H bash <<EOF sudo -u yellowtent -H bash <<EOF
set -eu set -eu
echo "Creating cloudron.conf" echo "Creating cloudron.conf"
# note that arg_aws is a javascript object and intentionally unquoted below
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{ {
"version": "${arg_version}", "version": "${arg_version}",
@@ -138,7 +139,9 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"password": "${mysql_root_password}", "password": "${mysql_root_password}",
"port": 3306, "port": 3306,
"name": "box" "name": "box"
} },
"backupKey": "${arg_backup_key}",
"aws": ${arg_aws}
} }
CONF_END CONF_END
+2 -3
View File
@@ -38,8 +38,7 @@ var appdb = require('./appdb.js'),
tokendb = require('./tokendb.js'), tokendb = require('./tokendb.js'),
util = require('util'), util = require('util'),
uuid = require('node-uuid'), uuid = require('node-uuid'),
vbox = require('./vbox.js'), vbox = require('./vbox.js');
_ = require('underscore');
var NOOP = function (app, callback) { return callback(); }; var NOOP = function (app, callback) { return callback(); };
@@ -665,7 +664,7 @@ function setupRedis(app, callback) {
name: 'redis-' + app.id, name: 'redis-' + app.id,
Hostname: config.appFqdn(app.location), Hostname: config.appFqdn(app.location),
Tty: true, Tty: true,
Image: 'cloudron/redis:0.3.1', Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null, Cmd: null,
Volumes: {}, Volumes: {},
VolumesFrom: [] VolumesFrom: []
+3 -1
View File
@@ -35,12 +35,13 @@ exports = module.exports = {
ISTATE_ERROR: 'error', // error executing last pending_* command ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed ISTATE_INSTALLED: 'installed', // app is installed
// run codes (keep in sync in UI)
RSTATE_RUNNING: 'running', RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start', RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop', RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by use RSTATE_STOPPED: 'stopped', // app stopped by use
RSTATE_ERROR: 'error',
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy', HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy', HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error', HEALTH_ERROR: 'error',
@@ -335,6 +336,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
// Rules are: // Rules are:
// uninstall is allowed in any state // uninstall is allowed in any state
// force update is allowed in any state including pending_uninstall! (for better or worse)
// restore is allowed from installed or error state // restore is allowed from installed or error state
// update and configure are allowed only in installed state // update and configure are allowed only in installed state
+27 -23
View File
@@ -490,28 +490,30 @@ function restore(appId, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND)); if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var restoreConfig = app.lastBackupConfig; // restore without a backup is the same as re-install
if (!restoreConfig) return callback(new AppsError(AppsError.BAD_STATE, 'No restore point')); var restoreConfig = app.lastBackupConfig, values = { };
if (restoreConfig) {
// re-validate because this new box version may not accept old configs.
// if we restore location, it should be validated here as well
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
// re-validate because this new box version may not accept old configs. if we restore location, it should be validated here as well error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
error = checkManifestConstraints(restoreConfig.manifest); if (error) return callback(error);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now // ## should probably query new location, access restriction from user
if (error) return callback(error); values = {
manifest: restoreConfig.manifest,
portBindings: restoreConfig.portBindings,
// ## should probably query new location, access restriction from user oldConfig: {
var values = { location: app.location,
manifest: restoreConfig.manifest, accessRestriction: app.accessRestriction,
portBindings: restoreConfig.portBindings, portBindings: app.portBindings,
manifest: app.manifest
oldConfig: { }
location: app.location, };
accessRestriction: app.accessRestriction, }
portBindings: app.portBindings,
manifest: app.manifest
}
};
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) { appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
@@ -573,6 +575,8 @@ function stop(appId, callback) {
} }
function checkManifestConstraints(manifest) { function checkManifestConstraints(manifest) {
if (!manifest.dockerImage) return new Error('Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) { if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
return new Error('Box version exceeds Apps maxBoxVersion'); return new Error('Box version exceeds Apps maxBoxVersion');
} }
@@ -664,7 +668,7 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
return iteratorDone(); return iteratorDone();
} }
update(appId, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) { update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
if (error) debug('Error initiating autoupdate of %s', appId); if (error) debug('Error initiating autoupdate of %s', appId);
iteratorDone(null); iteratorDone(null);
@@ -700,7 +704,7 @@ function backupApp(app, addonsToBackup, callback) {
return callback(safe.error); return callback(safe.error);
} }
backups.getBackupUrl(app, null, function (error, result) { backups.getBackupUrl(app, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message)); if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -709,7 +713,7 @@ function backupApp(app, addonsToBackup, callback) {
async.series([ async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
addons.backupAddons.bind(null, app, addonsToBackup), addons.backupAddons.bind(null, app, addonsToBackup),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey ]), shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) { ], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -756,7 +760,7 @@ function restoreApp(app, addonsToRestore, callback) {
debugApp(app, 'restoreApp: restoreUrl:%s', result.url); debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey ], function (error) { shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
addons.restoreAddons(app, addonsToRestore, callback); addons.restoreAddons(app, addonsToRestore, callback);
+50 -67
View File
@@ -46,6 +46,7 @@ var addons = require('./addons.js'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
shell = require('./shell.js'), shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
util = require('util'), util = require('util'),
@@ -248,7 +249,7 @@ function deleteImage(app, manifest, callback) {
noprune: false noprune: false
}; };
// delete image by id because docker pull pulls down all the tags and this is the only way to delete all tags // delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) { docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null); if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image if (error && error.statusCode === 409) return callback(null); // another container using the image
@@ -331,8 +332,12 @@ function startContainer(app, callback) {
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort); vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
} }
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
var startOptions = { var startOptions = {
Binds: addons.getBindsSync(app, app.manifest.addons), Binds: addons.getBindsSync(app, app.manifest.addons),
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: dockerPortBindings, PortBindings: dockerPortBindings,
PublishAllPorts: false, PublishAllPorts: false,
Links: addons.getLinksSync(app, app.manifest.addons), Links: addons.getLinksSync(app, app.manifest.addons),
@@ -355,6 +360,11 @@ function startContainer(app, callback) {
} }
function stopContainer(app, callback) { function stopContainer(app, callback) {
if (!app.containerId) {
debugApp(app, 'No previous container to stop');
return callback();
}
var container = docker.getContainer(app.containerId); var container = docker.getContainer(app.containerId);
debugApp(app, 'Stopping container %s', container.id); debugApp(app, 'Stopping container %s', container.id);
@@ -420,43 +430,27 @@ function registerSubdomain(app, callback) {
// need to register it so that we have a dnsRecordId to wait for it to complete // need to register it so that we have a dnsRecordId to wait for it to complete
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() }; var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
superagent subdomains.add(record, function (error, changeId) {
.post(config.apiServerOrigin() + '/api/v1/subdomains') if (error) return callback(error);
.set('Accept', 'application/json')
.query({ token: config.token() })
.send({ records: [ record ] })
.end(function (error, res) {
if (error) return callback(error);
debugApp(app, 'Registered subdomain status: %s', res.status); debugApp(app, 'Registered subdomain.');
if (res.status === 409) return callback(null); // already registered updateApp(app, { dnsRecordId: changeId }, callback);
if (res.status !== 201) return callback(new Error(util.format('Subdomain Registration failed. %s %j', res.status, res.body))); });
updateApp(app, { dnsRecordId: res.body.ids[0] }, callback);
});
} }
function unregisterSubdomain(app, callback) { function unregisterSubdomain(app, location, callback) {
debugApp(app, 'Unregistering subdomain: dnsRecordId=%s', app.dnsRecordId); debugApp(app, 'Unregistering subdomain: %s', location);
if (!app.dnsRecordId) return callback(null);
// do not unregister bare domain because we show a error/cloudron info page there // do not unregister bare domain because we show a error/cloudron info page there
if (app.location === '') return updateApp(app, { dnsRecordId: null }, callback); if (location === '') return callback(null);
superagent var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
.del(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId) subdomains.remove(record, function (error) {
.query({ token: config.token() }) if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
.end(function (error, res) {
if (error) {
debugApp(app, 'Error making request: %s', error);
} else if (res.status !== 204) {
debugApp(app, 'Error unregistering subdomain:', res.status, res.body);
}
updateApp(app, { dnsRecordId: null }, callback); updateApp(app, { dnsRecordId: null }, callback);
}); });
} }
function removeIcon(app, callback) { function removeIcon(app, callback) {
@@ -477,21 +471,15 @@ function waitForDnsPropagation(app, callback) {
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000); setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
} }
superagent subdomains.status(app.dnsRecordId, function (error, result) {
.get(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId + '/status') if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
.set('Accept', 'application/json')
.query({ token: config.token() })
.end(function (error, res) {
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, res.status); debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
if (res.status !== 200) return retry(new Error(util.format('Error getting record status: %s %j', res.status, res.body))); if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
if (res.body.status !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, res.body.status))); callback(null);
});
callback(null);
});
} }
// updates the app object and the database // updates the app object and the database
@@ -530,9 +518,9 @@ function install(app, callback) {
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
addons.teardownAddons.bind(null, app, app.manifest.addons), addons.teardownAddons.bind(null, app, app.manifest.addons),
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
unregisterSubdomain.bind(null, app), unregisterSubdomain.bind(null, app, app.location),
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
removeIcon.bind(null, app), // removeIcon.bind(null, app), // do not remove icon for non-appstore installs
unconfigureNginx.bind(null, app), unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }), updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
@@ -617,7 +605,11 @@ function restore(app, callback) {
// oldConfig can be null during upgrades // oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null), addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null),
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
deleteImage.bind(null, app, app.manifest), function deleteImageIfChanged(done) {
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
deleteImage(app, app.oldConfig.manifest, done);
},
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
removeIcon.bind(null, app), removeIcon.bind(null, app),
unconfigureNginx.bind(null, app), unconfigureNginx.bind(null, app),
@@ -671,16 +663,15 @@ function restore(app, callback) {
// note that configure is called after an infra update as well // note that configure is called after an infra update as well
function configure(app, callback) { function configure(app, callback) {
var locationChanged = app.oldConfig ? app.oldConfig.location !== app.location : true;
async.series([ async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app), removeCollectdProfile.bind(null, app),
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
function (next) { function (next) {
if (!locationChanged) return next(); // oldConfig can be null during an infra update
unregisterSubdomain(app, next); if (!app.oldConfig || app.oldConfig.location === app.location) return next();
unregisterSubdomain(app, app.oldConfig.location, next);
}, },
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
unconfigureNginx.bind(null, app), unconfigureNginx.bind(null, app),
@@ -691,14 +682,8 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }), updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
allocateOAuthProxyCredentials.bind(null, app), allocateOAuthProxyCredentials.bind(null, app),
function (next) { updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
if (!locationChanged) return next(); registerSubdomain.bind(null, app),
async.series([
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
registerSubdomain.bind(null, app)
], next);
},
// re-setup addons since they rely on the app's fqdn (e.g oauth) // re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }), updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
@@ -712,14 +697,8 @@ function configure(app, callback) {
runApp.bind(null, app), runApp.bind(null, app),
function (next) { updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
if (!locationChanged) return next(); exports._waitForDnsPropagation.bind(null, app),
async.series([
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app)
], next);
},
// done! // done!
function (callback) { function (callback) {
@@ -753,7 +732,11 @@ function update(app, callback) {
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
addons.teardownAddons.bind(null, app, unusedAddons), addons.teardownAddons.bind(null, app, unusedAddons),
deleteImage.bind(null, app, app.manifest), // delete image even if did not change (see df158b111f) function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
deleteImage(app, app.oldConfig.manifest, done);
},
// removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time... // removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time...
function (next) { function (next) {
@@ -819,7 +802,7 @@ function uninstall(app, callback) {
deleteImage.bind(null, app, app.manifest), deleteImage.bind(null, app, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }), updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
unregisterSubdomain.bind(null, app), unregisterSubdomain.bind(null, app, app.location),
updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }), updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }),
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
+282
View File
@@ -0,0 +1,282 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
AWSError: AWSError,
getAWSCredentials: getAWSCredentials,
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('./config.js'),
debug = require('debug')('box:aws'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AWSError(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(AWSError, Error);
AWSError.INTERNAL_ERROR = 'Internal Error';
AWSError.MISSING_CREDENTIALS = 'Missing AWS credentials';
function getAWSCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
// CaaS
if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.get(url).query({ token: config.token() }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 201) return callback(new Error(result.text));
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
return callback(null, {
accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey,
sessionToken: result.body.credentials.SessionToken,
region: 'us-east-1'
});
});
} else {
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new AWSError(AWSError.MISSING_CREDENTIALS));
callback(null, {
accessKeyId: config.aws().accessKeyId,
secretAccessKey: config.aws().secretAccessKey,
region: 'us-east-1'
});
}
}
function getSignedUploadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedUploadUrl()');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedDownloadUrl()');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function getZoneByName(zoneName, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getZoneByName: %s', zoneName);
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
})[0];
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
debug('getZoneByName: found zone', zone);
callback(null, zone);
});
});
}
function addSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var params = {
ChangeBatch: {
Changes: [{
Action: 'UPSERT',
ResourceRecordSet: {
Type: type,
Name: fqdn,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
TTL: 1
}
}]
},
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
debug('addSubdomain: success. changeInfoId:%j', result);
callback(null, result.ChangeInfo.Id);
});
});
});
}
function delSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var resourceRecordSet = {
Name: fqdn,
Type: type,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
TTL: 1
};
var params = {
ChangeBatch: {
Changes: [{
Action: 'DELETE',
ResourceRecordSet: resourceRecordSet
}]
},
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
debug('delSubdomain: resource record set not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('delSubdomain: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('delSubdomain: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('delSubdomain: invalid change batch. No such record to be deleted.');
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error) {
debug('delSubdomain: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
debug('delSubdomain: success');
callback(null);
});
});
});
}
function getChangeStatus(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.getChange({ Id: changeId }, function (error, result) {
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);
});
});
}
+31 -22
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
}; };
var assert = require('assert'), var assert = require('assert'),
aws = require('./aws.js'),
config = require('./config.js'), config = require('./config.js'),
debug = require('debug')('box:backups'), debug = require('debug')('box:backups'),
superagent = require('superagent'), superagent = require('superagent'),
@@ -54,42 +55,50 @@ function getAllPaged(page, perPage, callback) {
}); });
} }
function getBackupUrl(app, appBackupIds, callback) { function getBackupUrl(app, callback) {
assert(!app || typeof app === 'object'); assert(!app || typeof app === 'object');
assert(!appBackupIds || util.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupurl'; var filename = '';
if (app) {
filename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
} else {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
}
var data = { aws.getSignedUploadUrl(filename, function (error, result) {
boxVersion: config.version(), if (error) return callback(error);
appId: app ? app.id : null,
appVersion: app ? app.manifest.version : null,
appBackupIds: appBackupIds
};
superagent.put(url).query({ token: config.token() }).send(data).end(function (error, result) { var obj = {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); id: filename,
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text)); url: result.url,
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response')); sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
return callback(null, result.body); debug('getBackupUrl: ', obj);
callback(null, obj);
}); });
} }
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreUrl(backupId, callback) { function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/restoreurl'; aws.getSignedDownloadUrl(backupId, function (error, result) {
if (error) return callback(error);
superagent.put(url).query({ token: config.token(), backupId: backupId }).end(function (error, result) { var obj = {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); id: backupId,
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text)); url: result.url,
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response')); sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
return callback(null, result.body); debug('getRestoreUrl: ', obj);
callback(null, obj);
}); });
} }
+75 -54
View File
@@ -39,6 +39,7 @@ var apps = require('./apps.js'),
settings = require('./settings.js'), settings = require('./settings.js'),
SettingsError = settings.SettingsError, SettingsError = settings.SettingsError,
shell = require('./shell.js'), shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
tokendb = require('./tokendb.js'), tokendb = require('./tokendb.js'),
@@ -46,7 +47,8 @@ var apps = require('./apps.js'),
user = require('./user.js'), user = require('./user.js'),
UserError = user.UserError, UserError = user.UserError,
userdb = require('./userdb.js'), userdb = require('./userdb.js'),
util = require('util'); util = require('util'),
webhooks = require('./webhooks.js');
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'), var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'), REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
@@ -54,7 +56,7 @@ var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'), BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update'; INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
var gAddMailDnsRecordsTimerId = null, var gAddDnsRecordsTimerId = null,
gCloudronDetails = null; // cached cloudron details like region,size... gCloudronDetails = null; // cached cloudron details like region,size...
function debugApp(app, args) { function debugApp(app, args) {
@@ -108,20 +110,17 @@ function initialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV !== 'test') { if (process.env.BOX_ENV !== 'test') {
addMailDnsRecords(); addDnsRecords();
} }
// Send heartbeat once we are up and running, this speeds up the Cloudron creation, as otherwise we are bound to the cron.js settings
sendHeartbeat();
callback(null); callback(null);
} }
function uninitialize(callback) { function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
clearTimeout(gAddMailDnsRecordsTimerId); clearTimeout(gAddDnsRecordsTimerId);
gAddMailDnsRecordsTimerId = null; gAddDnsRecordsTimerId = null;
callback(null); callback(null);
} }
@@ -269,18 +268,20 @@ function getConfig(callback) {
} }
function sendHeartbeat() { function sendHeartbeat() {
// Only send heartbeats after the admin dns record is synced to give appstore a chance to know that fact
if (!config.get('dnsInSync')) return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat'; var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
// TODO: this must be a POST superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
superagent.get(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
if (error) debug('Error sending heartbeat.', error); if (error) debug('Error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text); else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url); else debug('Heartbeat sent to %s', url);
}); });
} }
function sendMailDnsRecordsRequest(callback) { function addDnsRecords() {
assert.strictEqual(typeof callback, 'function'); if (config.get('dnsInSync')) return sendHeartbeat(); // already registered send heartbeat
var DKIM_SELECTOR = 'mail'; var DKIM_SELECTOR = 'mail';
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io'; var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
@@ -288,13 +289,20 @@ function sendMailDnsRecordsRequest(callback) {
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public'); var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8'); var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) return callback(new Error('Error reading dkim public key')); if (publicKey === null) {
console.error('Error reading dkim public key. Stop DNS setup.');
return;
}
// remove header, footer and new lines // remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join(''); publicKey = publicKey.split('\n').slice(1, -2).join('');
// note that dmarc requires special DNS records for external RUF and RUA // note that dmarc requires special DNS records for external RUF and RUA
var records = [ var records = [
// naked domain
{ subdomain: '', type: 'A', value: sysinfo.getIp() },
// webadmin domain
{ subdomain: 'my', type: 'A', value: sysinfo.getIp() },
// softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future // softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future
{ subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' }, { subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
// t=s limits the domainkey to this domain and not it's subdomains // t=s limits the domainkey to this domain and not it's subdomains
@@ -303,38 +311,47 @@ function sendMailDnsRecordsRequest(callback) {
{ subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' } { subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' }
]; ];
debug('sendMailDnsRecords request:%s', JSON.stringify(records)); debug('addDnsRecords:', records);
superagent subdomains.addMany(records, function (error, changeIds) {
.post(config.apiServerOrigin() + '/api/v1/subdomains')
.set('Accept', 'application/json')
.query({ token: config.token() })
.send({ records: records })
.end(function (error, res) {
if (error) return callback(error);
debug('sendMailDnsRecords status: %s', res.status);
if (res.status === 409) return callback(null); // already registered
if (res.status !== 201) return callback(new Error(util.format('Failed to add Mail DNS records: %s %j', res.status, res.body)));
return callback(null, res.body.ids);
});
}
function addMailDnsRecords() {
if (config.get('mailDnsRecordIds').length !== 0) return; // already registered
sendMailDnsRecordsRequest(function (error, ids) {
if (error) { if (error) {
console.error('Mail DNS record addition failed', error); console.error('Admin DNS record addition failed', error);
gAddMailDnsRecordsTimerId = setTimeout(addMailDnsRecords, 30000); gAddDnsRecordsTimerId = setTimeout(addDnsRecords, 10000);
return; return;
} }
debug('Added Mail DNS records successfully'); function checkIfInSync() {
config.set('mailDnsRecordIds', ids); debug('addDnsRecords: Check if admin DNS record is in sync.');
var allDone = true;
async.each(changeIds, function (changeId, callback) {
subdomains.status(changeId, function (error, result) {
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
if (result !== 'done') allDone = false;
callback(null);
});
}, function (error) {
if (error) console.error(error);
// retry if needed
if (error || !allDone) {
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
return;
}
config.set('dnsInSync', true);
// send heartbeat after the dns records are done
sendHeartbeat();
debug('addDnsRecords: done');
});
}
checkIfInSync();
}); });
} }
@@ -492,19 +509,20 @@ function doUpdate(boxUpdateInfo, callback) {
var args = { var args = {
sourceTarballUrl: result.body.url, sourceTarballUrl: result.body.url,
// IMPORTANT: if you change this, fix up argparser.sh as well. keep these sorted for readability // this data is opaque to the installer
data: { data: {
apiServerOrigin: config.apiServerOrigin(),
boxVersionsUrl: config.get('boxVersionsUrl'), boxVersionsUrl: config.get('boxVersionsUrl'),
fqdn: config.fqdn(),
isCustomDomain: config.isCustomDomain(),
restoreKey: null,
restoreUrl: null,
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
token: config.token(),
version: boxUpdateInfo.version, version: boxUpdateInfo.version,
webServerOrigin: config.webServerOrigin() apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
token: config.token(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
restoreUrl: null,
restoreKey: null,
aws: config.aws()
} }
}; };
@@ -561,7 +579,7 @@ function ensureBackup(callback) {
function backupBoxWithAppBackupIds(appBackupIds, callback) { function backupBoxWithAppBackupIds(appBackupIds, callback) {
assert(util.isArray(appBackupIds)); assert(util.isArray(appBackupIds));
backups.getBackupUrl(null /* app */, appBackupIds, function (error, result) { backups.getBackupUrl(null /* app */, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message)); if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
@@ -569,14 +587,17 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
async.series([ async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey ]), shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) { ], function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('backup: successful'); debug('backup: successful');
callback(null, result.id); webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, result.id);
});
}); });
}); });
} }
@@ -609,7 +630,7 @@ function backupBoxAndApps(callback) {
++processed; ++processed;
apps.backupApp(app, app.manifest.addons, function (error, backupId) { apps.backupApp(app, app.manifest.addons, function (error, backupId) {
progress.set(progress.BACKUP, step * processed, 'Backing up app at ' + app.location); progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
if (error && error.reason === AppsError.BAD_STATE) { if (error && error.reason === AppsError.BAD_STATE) {
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId); debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
+22
View File
@@ -30,6 +30,9 @@ exports = module.exports = {
isDev: isDev, isDev: isDev,
backupKey: backupKey,
aws: aws,
// for testing resets to defaults // for testing resets to defaults
_reset: initConfig _reset: initConfig
}; };
@@ -70,6 +73,14 @@ function initConfig() {
data.webServerOrigin = null; data.webServerOrigin = null;
data.internalPort = 3001; data.internalPort = 3001;
data.ldapPort = 3002; data.ldapPort = 3002;
data.backupKey = 'backupKey';
data.aws = {
backupBucket: null,
backupPrefix: null,
accessKeyId: null, // selfhosting only
secretAccessKey: null // selfhosting only
};
data.dnsInSync = false;
if (exports.CLOUDRON) { if (exports.CLOUDRON) {
data.port = 3000; data.port = 3000;
@@ -86,6 +97,7 @@ function initConfig() {
name: 'boxtest' name: 'boxtest'
}; };
data.token = 'APPSTORE_TOKEN'; data.token = 'APPSTORE_TOKEN';
data.aws.backupBucket = 'testbucket';
} else { } else {
assert(false, 'Unknown environment. This should not happen!'); assert(false, 'Unknown environment. This should not happen!');
} }
@@ -99,6 +111,9 @@ function initConfig() {
saveSync(); saveSync();
} }
// cleanup any old config file we have for tests
if (exports.TEST) safe.fs.unlinkSync(cloudronConfigFileName);
initConfig(); initConfig();
// set(obj) or set(key, value) // set(obj) or set(key, value)
@@ -172,3 +187,10 @@ function isDev() {
return /dev/i.test(get('boxVersionsUrl')); return /dev/i.test(get('boxVersionsUrl'));
} }
function backupKey() {
return get('backupKey');
}
function aws() {
return get('aws');
}
+1 -1
View File
@@ -25,7 +25,7 @@ var gLogger = {
}; };
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron'; var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admin,ou=groups,dc=cloudron'; var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
function start(callback) { function start(callback) {
assert(typeof callback === 'function'); assert(typeof callback === 'function');
+2
View File
@@ -171,6 +171,8 @@ function sendErrorPageOrRedirect(req, res, message) {
} }
} }
// use this instead of sendErrorPageOrRedirect(), in case we have a returnTo provided in the query, to avoid login loops
// This usually happens when the OAuth client ID is wrong
function sendError(req, res, message) { function sendError(req, res, message) {
assert.strictEqual(typeof req, 'object'); assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof res, 'object'); assert.strictEqual(typeof res, 'object');
+2 -2
View File
@@ -119,8 +119,8 @@ describe('Backups API', function () {
it('succeeds', function (done) { it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin()) var scope = nock(config.apiServerOrigin())
.put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }) .get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' }); .reply(201, { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' });
request.post(SERVER_URL + '/api/v1/backups') request.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token }) .query({ access_token: token })
+13 -3
View File
@@ -450,8 +450,18 @@ describe('Cloudron', function () {
}); });
it('fails when in wrong state', function (done) { it('fails when in wrong state', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { size: 'small', region: 'sfo', restoreKey: 'someid' }).reply(409, {}); var scope2 = nock(config.apiServerOrigin())
var scope2 = nock(config.apiServerOrigin()).put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }).reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' }); .get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.filteringRequestBody(function () { return false; })
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN')
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.filteringRequestBody(function () { return false; })
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { }).reply(409, {});
injectShellMock(); injectShellMock();
@@ -463,7 +473,7 @@ describe('Cloudron', function () {
expect(result.statusCode).to.equal(202); expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() { function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone()) { if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock(); restoreShellMock();
return done(); return done();
} }
+11 -2
View File
@@ -13,7 +13,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi fi
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
echo "Usage: backup.sh <appid> <url> <key>" echo "Usage: backupapp.sh <appid> <url> <key> [aws session token]"
exit 1 exit 1
fi fi
@@ -22,6 +22,7 @@ readonly DATA_DIR="${HOME}/data"
app_id="$1" app_id="$1"
backup_url="$2" backup_url="$2"
backup_key="$3" backup_key="$3"
session_token="$4"
readonly now=$(date "+%Y-%m-%dT%H:%M:%S") readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
readonly app_data_dir="${DATA_DIR}/${app_id}" readonly app_data_dir="${DATA_DIR}/${app_id}"
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}" readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
@@ -31,9 +32,17 @@ btrfs subvolume snapshot -r "${app_data_dir}" "${app_data_snapshot}"
for try in `seq 1 5`; do for try in `seq 1 5`; do
echo "Uploading backup to ${backup_url} (try ${try})" echo "Uploading backup to ${backup_url} (try ${try})"
error_log=$(mktemp) error_log=$(mktemp)
headers=("-H" "Content-Type:")
# federated tokens in CaaS case need session token
if [ ! -z "$session_token" ]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if tar -cvzf - -C "${app_data_snapshot}" . \ if tar -cvzf - -C "${app_data_snapshot}" . \
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \ | openssl aes-256-cbc -e -pass "pass:${backup_key}" \
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then | curl --fail -X PUT "${headers[@]}" --data-binary @- "${backup_url}" 2>"${error_log}"; then
break break
fi fi
cat "${error_log}" && rm "${error_log}" cat "${error_log}" && rm "${error_log}"
+11 -2
View File
@@ -13,12 +13,13 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi fi
if [ $# -lt 2 ]; then if [ $# -lt 2 ]; then
echo "Usage: backupbox.sh <url> <key>" echo "Usage: backupbox.sh <url> <key> [aws session token]"
exit 1 exit 1
fi fi
backup_url="$1" backup_url="$1"
backup_key="$2" backup_key="$2"
session_token="$3"
now=$(date "+%Y-%m-%dT%H:%M:%S") now=$(date "+%Y-%m-%dT%H:%M:%S")
BOX_DATA_DIR="${HOME}/data/box" BOX_DATA_DIR="${HOME}/data/box"
box_snapshot_dir="${HOME}/data/snapshots/box-${now}" box_snapshot_dir="${HOME}/data/snapshots/box-${now}"
@@ -32,9 +33,17 @@ btrfs subvolume snapshot -r "${BOX_DATA_DIR}" "${box_snapshot_dir}"
for try in `seq 1 5`; do for try in `seq 1 5`; do
echo "Uploading backup to ${backup_url} (try ${try})" echo "Uploading backup to ${backup_url} (try ${try})"
error_log=$(mktemp) error_log=$(mktemp)
headers=("-H" "Content-Type:")
# federated tokens in CaaS case need session token
if [ ! -z "$session_token" ]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if tar -cvzf - -C "${box_snapshot_dir}" . \ if tar -cvzf - -C "${box_snapshot_dir}" . \
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \ | openssl aes-256-cbc -e -pass "pass:${backup_key}" \
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then | curl --fail -X PUT ${headers[@]} --data-binary @- "${backup_url}" 2>"${error_log}"; then
break break
fi fi
cat "${error_log}" && rm "${error_log}" cat "${error_log}" && rm "${error_log}"
+10 -2
View File
@@ -13,7 +13,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi fi
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
echo "Usage: restoreapp.sh <appid> <url> <key>" echo "Usage: restoreapp.sh <appid> <url> <key> [aws session token]"
exit 1 exit 1
fi fi
@@ -23,6 +23,7 @@ readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max
app_id="$1" app_id="$1"
restore_url="$2" restore_url="$2"
restore_key="$3" restore_key="$3"
session_token="$4"
echo "Downloading backup: ${restore_url} and key: ${restore_key}" echo "Downloading backup: ${restore_url} and key: ${restore_key}"
@@ -30,7 +31,14 @@ for try in `seq 1 5`; do
echo "Download backup from ${restore_url} (try ${try})" echo "Download backup from ${restore_url} (try ${try})"
error_log=$(mktemp) error_log=$(mktemp)
if $curl -L "${restore_url}" \ headers=("") # empty element required (http://stackoverflow.com/questions/7577052/bash-empty-array-expansion-with-set-u)
# federated tokens in CaaS case need session token
if [[ ! -z "${session_token}" ]]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if $curl -L "${headers[@]}" "${restore_url}" \
| openssl aes-256-cbc -d -pass "pass:${restore_key}" \ | openssl aes-256-cbc -d -pass "pass:${restore_key}" \
| tar -zxf - -C "${DATA_DIR}/${app_id}" 2>"${error_log}"; then | tar -zxf - -C "${DATA_DIR}/${app_id}" 2>"${error_log}"; then
chown -R yellowtent:yellowtent "${DATA_DIR}/${app_id}" chown -R yellowtent:yellowtent "${DATA_DIR}/${app_id}"
+1 -1
View File
@@ -97,7 +97,7 @@ function initializeExpressSync() {
// private routes // private routes
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig); router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update); router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.get ('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot); router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate); router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate); router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs); router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
+39
View File
@@ -0,0 +1,39 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
util = require('util');
exports = module.exports = SubdomainError;
function SubdomainError(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(SubdomainError, Error);
SubdomainError.NOT_FOUND = 'No such domain';
SubdomainError.INTERNAL_ERROR = 'Internal error';
SubdomainError.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.FAILED_TOO_OFTEN = 'Failed too often';
SubdomainError.ALREADY_EXISTS = 'Domain already exists';
SubdomainError.BAD_FIELD = 'Bad Field';
SubdomainError.BAD_STATE = 'Bad State';
SubdomainError.INVALID_ZONE_NAME = 'Invalid domain name';
SubdomainError.INVALID_TASK = 'Invalid task';
+82
View File
@@ -0,0 +1,82 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
async = require('async'),
aws = require('./aws.js'),
config = require('./config.js'),
debug = require('debug')('box:subdomains'),
util = require('util'),
SubdomainError = require('./subdomainerror.js');
module.exports = exports = {
add: add,
addMany: addMany,
remove: remove,
status: status
};
function add(record, callback) {
assert.strictEqual(typeof record, 'object');
assert.strictEqual(typeof record.subdomain, 'string');
assert.strictEqual(typeof record.type, 'string');
assert.strictEqual(typeof record.value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('add: ', record);
aws.addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
});
}
function addMany(records, callback) {
assert(util.isArray(records));
assert.strictEqual(typeof callback, 'function');
debug('addMany: ', records);
var changeIds = [];
async.eachSeries(records, function (record, callback) {
add(record, function (error, changeId) {
if (error) return callback(error);
changeIds.push(changeId);
callback(null);
});
}, function (error) {
if (error) return callback(error);
callback(null, changeIds);
});
}
function remove(record, callback) {
assert.strictEqual(typeof record, 'object');
assert.strictEqual(typeof callback, 'function');
debug('remove: ', record);
aws.delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
debug('deleteSubdomain: successfully deleted subdomain from aws.');
callback(null);
});
}
function status(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('status: ', changeId);
aws.getChangeStatus(changeId, function (error, status) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending');
});
}
+47
View File
@@ -0,0 +1,47 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
backupDone: backupDone
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:webhooks'),
superagent = require('superagent'),
util = require('util');
function backupDone(filename, app, appBackupIds, callback) {
assert.strictEqual(typeof filename, 'string');
assert(!app || typeof app === 'object');
assert(!appBackupIds || util.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
debug('backupDone():', filename);
// CaaS
if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupDone';
var data = {
boxVersion: config.version(),
restoreKey: filename,
appId: app ? app.id : null,
appVersion: app ? app.manifest.version : null,
appBackupIds: appBackupIds
};
superagent.post(url).send(data).query({ token: config.token() }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new Error(result.text));
if (!result.body) return callback(new Error('Unexpected response'));
debug('backupDone()', filename);
return callback(null);
});
} else {
// TODO call custom webhook
callback(null);
}
}
+3 -3
View File
@@ -6,13 +6,13 @@
<title> Cloudron App Error </title> <title> Cloudron App Error </title>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- external fonts and CSS --> <!-- external fonts and CSS -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- jQuery--> <!-- jQuery-->
<script src="3rdparty/js/jquery.min.js"></script> <script src="3rdparty/js/jquery.min.js"></script>
+3 -3
View File
@@ -6,13 +6,13 @@
<title> Cloudron Error </title> <title> Cloudron Error </title>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- external fonts and CSS --> <!-- external fonts and CSS -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- jQuery--> <!-- jQuery-->
<script src="3rdparty/js/jquery.min.js"></script> <script src="3rdparty/js/jquery.min.js"></script>
+5 -5
View File
@@ -8,6 +8,9 @@
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png"> <link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
<!-- Custom Fonts --> <!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
@@ -48,9 +51,6 @@
<!-- Main Application --> <!-- Main Application -->
<script src="js/index.js"></script> <script src="js/index.js"></script>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
</head> </head>
<body> <body>
@@ -81,7 +81,7 @@
<li ng-repeat="change in config.update.box.changelog">{{change}}</li> <li ng-repeat="change in config.update.box.changelog">{{change}}</li>
</ul> </ul>
<br/> <br/>
<fieldset> <fieldset ng-show="installedApps | readyToUpdate">
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off"> <form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }"> <div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label> <label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
@@ -99,7 +99,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button> <button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy" ng-show="installedApps | readyToUpdate"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -425,7 +425,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}; };
Client.prototype.reboot = function (callback) { Client.prototype.reboot = function (callback) {
$http.get(client.apiOrigin + '/api/v1/cloudron/reboot').success(function(data, status) { $http.post(client.apiOrigin + '/api/v1/cloudron/reboot', { }).success(function(data, status) {
if (status !== 202 || typeof data !== 'object') return callback(new ClientError(status, data)); if (status !== 202 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data); callback(null, data);
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
+3 -3
View File
@@ -6,13 +6,13 @@
<title> Cloudron </title> <title> Cloudron </title>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- external fonts and CSS --> <!-- external fonts and CSS -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- jQuery--> <!-- jQuery-->
<script src="3rdparty/js/jquery.min.js"></script> <script src="3rdparty/js/jquery.min.js"></script>
+3 -3
View File
@@ -6,6 +6,9 @@
<title> Cloudron Setup </title> <title> Cloudron Setup </title>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
<!-- Custom Fonts --> <!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
@@ -31,9 +34,6 @@
<!-- Setup Application --> <!-- Setup Application -->
<script src="js/setup.js"></script> <script src="js/setup.js"></script>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
</head> </head>
<body class="setup" ng-app="Application" ng-controller="SetupController"> <body class="setup" ng-app="Application" ng-controller="SetupController">
+6 -2
View File
@@ -195,8 +195,8 @@ html {
.appstore-item-badge-testing { .appstore-item-badge-testing {
position: absolute; position: absolute;
right: 15px; right: 0;
top: 15px; top: 2px;
} }
.appstore-item-content-testing { .appstore-item-content-testing {
@@ -330,6 +330,10 @@ html {
background-color: #5CB85C; background-color: #5CB85C;
} }
.badge-warning {
background-color: #EFBD48;
}
.badge-danger { .badge-danger {
background-color: $brand-danger; background-color: $brand-danger;
} }
+3 -3
View File
@@ -6,6 +6,9 @@
<title> Cloudron Update </title> <title> Cloudron Update </title>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
<!-- Custom Fonts --> <!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
@@ -23,9 +26,6 @@
<!-- Update Application --> <!-- Update Application -->
<script src="js/update.js"></script> <script src="js/update.js"></script>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
</head> </head>
<body ng-app="Application" ng-controller="Controller" style="background-color: #7F7F7F"> <body ng-app="Application" ng-controller="Controller" style="background-color: #7F7F7F">
+3
View File
@@ -269,3 +269,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Offset the footer -->
<br/><br/>
+2 -1
View File
@@ -147,8 +147,9 @@
<div class="col-md-10" ng-show="ready && apps.length"> <div class="col-md-10" ng-show="ready && apps.length">
<div class="row-no-margin"> <div class="row-no-margin">
<div class="col-sm-1 appstore-item" ng-repeat="app in apps"> <div class="col-sm-1 appstore-item" ng-repeat="app in apps">
<div class="appstore-item-content highlight" ng-click="showInstall(app)" ng-class="{ 'appstore-item-content-testing': app.publishState === 'testing' }"> <div class="appstore-item-content highlight" ng-click="showInstall(app)" ng-class="{ 'appstore-item-content-testing': (app.publishState === 'testing' || app.publishState === 'pending_approval') }">
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.publishState === 'testing'">Testing</span> <span class="badge badge-danger appstore-item-badge-testing" ng-show="app.publishState === 'testing'">Testing</span>
<span class="badge badge-warning appstore-item-badge-testing" ng-show="app.publishState === 'pending_approval'">Pending Approval</span>
<div class="appstore-item-content-icon col-same-height"> <div class="appstore-item-content-icon col-same-height">
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/> <img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
</div> </div>
+1 -25
View File
@@ -14,39 +14,15 @@
<div class="row shadow memory-app-container"> <div class="row shadow memory-app-container">
<h2>Disk Usage</h2> <h2>Disk Usage</h2>
<br/> <br/>
<div class="col-md-4"> <div class="col-md-offset-4 col-md-4">
<h4>Applications <span class="badge">{{ diskUsage['docker'].sum }} GB</span></h4>
<canvas id="dockerDiskUsageChart" width="200" height="200"></canvas>
<p>
<span class="text-success">Free {{ diskUsage['docker'].free }} GB</span>
&nbsp;
<span class="text-warning">Reserved {{ diskUsage['docker'].reserved }} GB</span>
&nbsp;
<span class="text-primary">Used {{ diskUsage['docker'].used }} GB</span>
</p>
</div>
<div class="col-md-4">
<h4>Data <span class="badge">{{ diskUsage['box'].sum }} GB</span></h4> <h4>Data <span class="badge">{{ diskUsage['box'].sum }} GB</span></h4>
<canvas id="boxDiskUsageChart" width="200" height="200"></canvas> <canvas id="boxDiskUsageChart" width="200" height="200"></canvas>
<p> <p>
<span class="text-success">Free {{ diskUsage['box'].free }} GB</span> <span class="text-success">Free {{ diskUsage['box'].free }} GB</span>
&nbsp; &nbsp;
<span class="text-warning">Reserved {{ diskUsage['box'].reserved }} GB</span>
&nbsp;
<span class="text-primary">Used {{ diskUsage['box'].used }} GB</span> <span class="text-primary">Used {{ diskUsage['box'].used }} GB</span>
</p> </p>
</div> </div>
<div class="col-md-4">
<h4>System (all) <span class="badge">{{ diskUsage['cloudron'].sum }} GB</span></h4>
<canvas id="cloudronDiskUsageChart" width="200" height="200"></canvas>
<p>
<span class="text-success">Free {{ diskUsage['cloudron'].free }} GB</span>
&nbsp;
<span class="text-warning">Reserved {{ diskUsage['cloudron'].reserved }} GB</span>
&nbsp;
<span class="text-primary">Used {{ diskUsage['cloudron'].used }} GB</span>
</p>
</div>
</div> </div>
<br/> <br/>
+4 -20
View File
@@ -33,8 +33,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
function renderDisk(type, free, reserved, used) { function renderDisk(type, free, reserved, used) {
$scope.diskUsage[type] = { $scope.diskUsage[type] = {
used: bytesToGigaBytes(used.datapoints[0][0]), used: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0]),
reserved: bytesToGigaBytes(reserved.datapoints[0][0]),
free: bytesToGigaBytes(free.datapoints[0][0]), free: bytesToGigaBytes(free.datapoints[0][0]),
sum: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0] + free.datapoints[0][0]) sum: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0] + free.datapoints[0][0])
}; };
@@ -44,11 +43,6 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
color: "#2196F3", color: "#2196F3",
highlight: "#82C4F8", highlight: "#82C4F8",
label: "Used" label: "Used"
}, {
value: $scope.diskUsage[type].reserved,
color: "#f0ad4e",
highlight: "#F8D9AC",
label: "Reserved"
}, { }, {
value: $scope.diskUsage[type].free, value: $scope.diskUsage[type].free,
color:"#27CE65", color:"#27CE65",
@@ -98,7 +92,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
var options = { var options = {
scaleOverride: true, scaleOverride: true,
scaleSteps: 10, scaleSteps: 10,
scaleStepWidth: $scope.activeApp === 'system' ? 100 : 10, scaleStepWidth: $scope.activeApp === 'system' ? 200 : 60,
scaleStartValue: 0 scaleStartValue: 0
}; };
@@ -111,21 +105,11 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
Client.graphs([ Client.graphs([
'averageSeries(collectd.localhost.df-loop0.df_complex-free)', 'averageSeries(collectd.localhost.df-loop0.df_complex-free)',
'averageSeries(collectd.localhost.df-loop0.df_complex-reserved)', 'averageSeries(collectd.localhost.df-loop0.df_complex-reserved)',
'averageSeries(collectd.localhost.df-loop0.df_complex-used)', 'averageSeries(collectd.localhost.df-loop0.df_complex-used)'
'averageSeries(collectd.localhost.df-loop1.df_complex-free)',
'averageSeries(collectd.localhost.df-loop1.df_complex-reserved)',
'averageSeries(collectd.localhost.df-loop1.df_complex-used)',
'averageSeries(collectd.localhost.df-vda1.df_complex-free)',
'averageSeries(collectd.localhost.df-vda1.df_complex-reserved)',
'averageSeries(collectd.localhost.df-vda1.df_complex-used)',
], '-1min', function (error, data) { ], '-1min', function (error, data) {
if (error) return console.log(error); if (error) return console.log(error);
renderDisk('docker', data[0], data[1], data[2]); renderDisk('box', data[0], data[1], data[2]);
renderDisk('box', data[3], data[4], data[5]);
renderDisk('cloudron', data[6], data[7], data[8]);
}); });
}; };