Compare commits

...

79 Commits

Author SHA1 Message Date
Girish Ramakrishnan 6bd9173a9d this docker registry keeps going down 2015-11-12 16:22:53 -08:00
Girish Ramakrishnan 0cef3e1090 do not trust the health state blindly 2015-11-12 16:16:05 -08:00
Girish Ramakrishnan 6bd68961d1 typo 2015-11-12 16:13:15 -08:00
Girish Ramakrishnan 7f8ad917d9 filter out non-healthy apps 2015-11-12 16:04:33 -08:00
Girish Ramakrishnan 7cd89accaf better pullImage debug output 2015-11-12 15:58:39 -08:00
Girish Ramakrishnan ffee084d2b new format of provisioning info 2015-11-12 14:22:43 -08:00
Girish Ramakrishnan 2bb657a733 rename variable for clarity 2015-11-12 12:40:41 -08:00
Girish Ramakrishnan bc48171626 use fallback cert from backup if it exists 2015-11-12 12:37:43 -08:00
Girish Ramakrishnan 50924b0cd3 use admin.cert and admin.key if present in backup dir 2015-11-12 12:33:52 -08:00
Girish Ramakrishnan 3d86950cc9 fix indentation 2015-11-12 12:28:05 -08:00
Girish Ramakrishnan db9ddf9969 backup fallback cert 2015-11-12 12:27:25 -08:00
Girish Ramakrishnan 1b507370dc Cannot use >= node 4.1.2
https://github.com/nodejs/node/issues/3803
2015-11-12 12:19:13 -08:00
Girish Ramakrishnan 3c5e221c39 change engine requirements 2015-11-11 15:55:59 -08:00
Girish Ramakrishnan 9c37f35d5a new shrinkwrap for 4.2.2 2015-11-11 15:55:24 -08:00
Girish Ramakrishnan 4044070d76 Add -app prefix for all app sources
so that this doesn't conflict with some user.
2015-11-11 13:27:45 -08:00
Girish Ramakrishnan 8f05917d97 delete container on network error 2015-11-10 21:56:17 -08:00
Girish Ramakrishnan 3766d67daa create new container from cloudron exec 2015-11-10 21:36:20 -08:00
Johannes Zellner b1290c073e log lines should be newline separated 2015-11-10 11:31:07 +01:00
Girish Ramakrishnan 36daf86ea2 send mail even if no related app was found (for addons) 2015-11-10 01:39:02 -08:00
Girish Ramakrishnan 4fb07a6ab3 make crashnotifier send mails again
mailer module waits for dns syncing. crashnotifier has no time for all that.
neither does it initialize the database. it simply wants to send mail.
(the crash itself could have happenned because of some db issue)

maybe it should simply use a shell script at some point.
2015-11-10 00:25:47 -08:00
Girish Ramakrishnan 8f2119272b print all missing images 2015-11-09 23:31:04 -08:00
Girish Ramakrishnan ee5bd456e0 set bucket and prefix to make migrate test pass 2015-11-09 22:45:07 -08:00
Girish Ramakrishnan 9c549ed4d8 wait for old apptask to finish before starting new one
kill() is async and takes no callback :/
2015-11-09 22:10:10 -08:00
Girish Ramakrishnan 61fc8b7968 better taskmanager debugs 2015-11-09 21:58:34 -08:00
Girish Ramakrishnan 6b30d65e05 add backupConfig in test 2015-11-09 21:08:23 -08:00
Girish Ramakrishnan 10c876ac75 make migrate test work 2015-11-09 20:34:27 -08:00
Girish Ramakrishnan 0966bd0bb1 use debug instead of console.error 2015-11-09 19:10:33 -08:00
Girish Ramakrishnan 294d1bfca4 remove ununsed require 2015-11-09 18:57:57 -08:00
Girish Ramakrishnan af1d1236ea expose api server origin to determine if the box is dev/staging/prod 2015-11-09 17:50:09 -08:00
Girish Ramakrishnan eaf9febdfd do not save aws and backupKey
it is now part of backupConfig
2015-11-09 08:39:01 -08:00
Johannes Zellner 8748226ef3 The controller already ensures we don't show the views here 2015-11-09 09:56:59 +01:00
Johannes Zellner 73568777c0 Only show dns and cert pages for custom domain cloudrons 2015-11-09 09:56:08 +01:00
Girish Ramakrishnan c64697dde7 cannot read propery provider of null 2015-11-09 00:42:25 -08:00
Girish Ramakrishnan 0701e38a04 do not set dnsConfig for caas on activation 2015-11-09 00:20:16 -08:00
Girish Ramakrishnan 2a27d96e08 pass dnsConfig in update 2015-11-08 23:57:42 -08:00
Girish Ramakrishnan ba42611701 token is not a function 2015-11-08 23:54:47 -08:00
Girish Ramakrishnan 54486138f0 pass dnsConfig to backend api 2015-11-08 23:21:55 -08:00
Girish Ramakrishnan 13d3f506b0 always add dns config in tests 2015-11-08 23:05:55 -08:00
Girish Ramakrishnan 32ca686e1f read dnsConfig from settings to choose api backend 2015-11-08 22:55:31 -08:00
Girish Ramakrishnan a5ef9ff372 Add getAllPaged to storage api 2015-11-08 22:41:13 -08:00
Girish Ramakrishnan 738bfa7601 remove unused variable 2015-11-08 11:04:39 -08:00
Girish Ramakrishnan 40cdd270b1 ensure correct token is used in tests 2015-11-07 22:19:23 -08:00
Girish Ramakrishnan 53a2a8015e set the backupConfig in backups test 2015-11-07 22:18:39 -08:00
Girish Ramakrishnan 15aaa440a2 Add test for settings.backupConfig 2015-11-07 22:17:08 -08:00
Girish Ramakrishnan d8a4014eff remove bad reference to config.aws 2015-11-07 22:12:03 -08:00
Girish Ramakrishnan d25d423ccd remove legacy update params
these are now part of settings
2015-11-07 22:07:25 -08:00
Girish Ramakrishnan 49b0fde18b remove config.aws and config.backupKey 2015-11-07 22:06:56 -08:00
Girish Ramakrishnan 8df7f17186 load backup config from settingsdb 2015-11-07 22:06:09 -08:00
Girish Ramakrishnan adc395f888 save backupConfig in db 2015-11-07 21:45:38 -08:00
Girish Ramakrishnan e770664365 Add backup config to settings 2015-11-07 18:02:45 -08:00
Girish Ramakrishnan 05d4ad3b5d read new format of restore keys 2015-11-07 17:53:54 -08:00
Girish Ramakrishnan cc6f726f71 change backup provider to caas 2015-11-07 09:17:58 -08:00
Girish Ramakrishnan a4923f894c prepare for new backupConfig 2015-11-07 00:26:12 -08:00
Girish Ramakrishnan 12200f2e0d split aws code into storage backends 2015-11-06 18:22:29 -08:00
Girish Ramakrishnan a853afc407 backups: add api call 2015-11-06 18:14:59 -08:00
Girish Ramakrishnan de471b0012 return BackupError and not SubdomainError 2015-11-06 18:08:25 -08:00
Girish Ramakrishnan b6f1ad75b8 merge SubdomainError into subdomains.js like other error classes 2015-11-06 17:58:01 -08:00
Girish Ramakrishnan e6840f352d remove spurious debugs 2015-11-05 11:59:11 -08:00
Johannes Zellner 6456874f97 Avoid ui glitch in setup if no custom domain
This moves the wizard flow logic to next() instead of the views individually
2015-11-05 20:32:30 +01:00
Johannes Zellner 66b4a4b02a Remove unused favicon middleware 2015-11-05 19:29:08 +01:00
Girish Ramakrishnan 7e36b3f8e5 mailer: set dnsReady flag 2015-11-05 10:28:41 -08:00
Girish Ramakrishnan 12061cc707 mailDnsRecordIds is never used 2015-11-05 10:22:25 -08:00
Girish Ramakrishnan afcc62ecf6 mailServer is never used 2015-11-05 10:22:25 -08:00
Johannes Zellner bec6850c98 Specify icon for oauth views 2015-11-05 19:19:28 +01:00
Girish Ramakrishnan d253a06bab Revert "Remove unused very old yellowtent favicon"
This reverts commit c15a200d4a.

This is used by the favicon middleware
2015-11-05 10:14:59 -08:00
Johannes Zellner 857c5c69b1 force reload the webadmin on cert upload 2015-11-05 18:38:32 +01:00
Girish Ramakrishnan 766fc49f39 setup dkim records for custom domain 2015-11-05 09:30:23 -08:00
Johannes Zellner 941e09ca9f Update the cloudron icon 2015-11-05 18:05:19 +01:00
Johannes Zellner 2466a97fb8 Remove more unused image assets 2015-11-05 18:04:54 +01:00
Johannes Zellner 81f92f5182 Remover another unused favicon 2015-11-05 17:49:25 +01:00
Johannes Zellner 91e1d442ff Update the avatar 2015-11-05 17:48:18 +01:00
Johannes Zellner a1d6ae2296 Remove unused very old yellowtent favicon 2015-11-05 17:46:17 +01:00
Girish Ramakrishnan b529fd3bea we expect the server cert to be first like the rfc says 2015-11-04 19:22:37 -08:00
Girish Ramakrishnan bf319cf593 more verbose certificate message 2015-11-04 19:15:23 -08:00
Girish Ramakrishnan 15eedd2a84 pass on certificate errors 2015-11-04 19:13:16 -08:00
Girish Ramakrishnan d0cd3d1c32 Put dns first since it says dns & certs 2015-11-04 18:23:51 -08:00
Girish Ramakrishnan 747786d0c8 fix avatar url for custom domains 2015-11-04 18:21:46 -08:00
Girish Ramakrishnan b232255170 move dns and certs to own view 2015-11-04 18:21:43 -08:00
Girish Ramakrishnan bd2982ea69 move backups after about 2015-11-04 16:41:33 -08:00
56 changed files with 3122 additions and 4041 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

+1 -6
View File
@@ -36,12 +36,7 @@ function main() {
var processName = process.argv[2]; var processName = process.argv[2];
console.log('Started crash notifier for', processName); console.log('Started crash notifier for', processName);
mailer.initialize(function (error) { sendCrashNotification(processName);
if (error) return console.error(error);
sendCrashNotification(processName);
});
} }
main(); main();
+2025 -3271
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -10,7 +10,7 @@
"type": "git" "type": "git"
}, },
"engines": [ "engines": [
"node >= 0.12.0" "node >=4.0.0 <=4.1.1"
], ],
"dependencies": { "dependencies": {
"async": "^1.2.1", "async": "^1.2.1",
+5 -9
View File
@@ -16,8 +16,7 @@ arg_tls_key=""
arg_token="" arg_token=""
arg_version="" arg_version=""
arg_web_server_origin="" arg_web_server_origin=""
arg_backup_key="" arg_backup_config=""
arg_aws=""
arg_dns_config="" arg_dns_config=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@") args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
@@ -38,17 +37,14 @@ EOF
arg_tls_cert=$(echo "$2" | $json tlsCert) arg_tls_cert=$(echo "$2" | $json tlsCert)
arg_tls_key=$(echo "$2" | $json tlsKey) arg_tls_key=$(echo "$2" | $json tlsKey)
arg_restore_url=$(echo "$2" | $json restoreUrl) arg_restore_url=$(echo "$2" | $json restore.url)
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url="" [[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
arg_restore_key=$(echo "$2" | $json restoreKey) arg_restore_key=$(echo "$2" | $json restore.key)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key="" [[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_backup_key=$(echo "$2" | $json backupKey) arg_backup_config=$(echo "$2" | $json backupConfig)
[[ "${arg_backup_key}" == "null" ]] && arg_backup_key="" [[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
arg_aws=$(echo "$2" | $json aws)
[[ "${arg_aws}" == "null" ]] && arg_aws=""
arg_dns_config=$(echo "$2" | $json dnsConfig) arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config="" [[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
+23 -7
View File
@@ -106,12 +106,23 @@ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs
-O "{ \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/nginx.conf" -O "{ \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/nginx.conf"
# generate these for update code paths as well to overwrite splash # generate these for update code paths as well to overwrite splash
admin_cert_file="cert/host.cert"
admin_key_file="cert/host.key"
if [[ -f "${DATA_DIR}/box/certs/admin.cert" && -f "${DATA_DIR}/box/certs/admin.key" ]]; then
admin_cert_file="${DATA_DIR}/box/certs/admin.cert"
admin_key_file="${DATA_DIR}/box/certs/admin.key"
fi
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf" -O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert" mkdir -p "${DATA_DIR}/nginx/cert"
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
echo "${arg_tls_key}" > ${DATA_DIR}/nginx/cert/host.key cp "${DATA_DIR}/box/certs/host.cert" "${DATA_DIR}/nginx/cert/host.cert"
cp "${DATA_DIR}/box/certs/host.key" "${DATA_DIR}/nginx/cert/host.key"
else
echo "${arg_tls_cert}" > "${DATA_DIR}/nginx/cert/host.cert"
echo "${arg_tls_key}" > "${DATA_DIR}/nginx/cert/host.key"
fi
set_progress "33" "Changing ownership" set_progress "33" "Changing ownership"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons"
@@ -124,7 +135,6 @@ 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}",
@@ -141,9 +151,7 @@ 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
@@ -155,6 +163,14 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
CONF_END CONF_END
EOF EOF
# Add Backup Configuration
if [[ ! -z "${arg_backup_config}" ]]; then
echo "Add Backup Config"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
fi
# Add DNS Configuration # Add DNS Configuration
if [[ ! -z "${arg_dns_config}" ]]; then if [[ ! -z "${arg_dns_config}" ]]; then
echo "Add DNS Config" echo "Add DNS Config"
+1 -1
View File
@@ -391,7 +391,7 @@ function setupSendMail(app, options, callback) {
var env = [ var env = [
'MAIL_SMTP_SERVER=mail', 'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container 'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + (app.location || app.id), // use app.id for bare domains 'MAIL_SMTP_USERNAME=' + (app.location || app.id) + '-app', // use app.id for bare domains
'MAIL_DOMAIN=' + config.fqdn() 'MAIL_DOMAIN=' + config.fqdn()
]; ];
+6 -2
View File
@@ -110,7 +110,11 @@ function processApps(callback) {
async.each(apps, checkAppHealth, function (error) { async.each(apps, checkAppHealth, function (error) {
if (error) console.error(error); if (error) console.error(error);
debug('apps alive: [%s]', apps.map(function (a) { return a.location; }).join(', ')); var alive =apps
.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; }).join(', ');
debug('apps alive: [%s]', alive);
callback(null); callback(null);
}); });
@@ -151,7 +155,7 @@ function processDockerEvents() {
debug('OOM Context: %s', context); debug('OOM Context: %s', context);
// do not send mails for dev apps // do not send mails for dev apps
if (app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash if (error || app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
}); });
}); });
+32 -18
View File
@@ -52,9 +52,10 @@ var addons = require('./addons.js'),
constants = require('./constants.js'), constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'), DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'), debug = require('debug')('box:apps'),
docker = require('./docker.js').connection, docker = require('./docker.js'),
fs = require('fs'), fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'), manifestFormat = require('cloudron-manifestformat'),
once = require('once'),
path = require('path'), path = require('path'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
@@ -72,6 +73,8 @@ var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'), RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'); BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function debugApp(app, args) { function debugApp(app, args) {
assert(!app || typeof app === 'object'); assert(!app || typeof app === 'object');
@@ -511,7 +514,7 @@ function getLogs(appId, lines, follow, callback) {
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP, monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE, message: obj.MESSAGE,
source: source || 'main' source: source || 'main'
}); }) + '\n';
}); });
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
@@ -643,31 +646,42 @@ function exec(appId, options, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app')); if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var container = docker.getContainer(app.containerId); var createOptions = {
var execOptions = {
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
Tty: true, OpenStdin: true,
Cmd: cmd StdinOnce: false,
Tty: true
}; };
container.exec(execOptions, function (error, exec) { docker.createSubcontainer(app, app.id + '-exec-' + Date.now(), cmd, createOptions, function (error, container) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var startOptions = {
Detach: false, container.attach({ stream: true, stdin: true, stdout: true, stderr: true }, function (error, stream) {
Tty: true,
stdin: true // this is a dockerode option that enabled openStdin in the modem
};
exec.start(startOptions, function(error, stream) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) { docker.startContainer(container.id, function (error) {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); }); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
}
return callback(null, stream); if (options.rows && options.columns) {
container.resize({ h: options.rows, w: options.columns }, NOOP_CALLBACK);
}
var deleteContainer = once(docker.deleteContainer.bind(null, container.id, NOOP_CALLBACK));
container.wait(function (error) {
if (error) debug('Error waiting on container', error);
debug('exec: container finished', container.id);
deleteContainer();
});
stream.close = deleteContainer;
callback(null, stream);
});
}); });
}); });
}); });
+1 -1
View File
@@ -51,7 +51,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'),
SubdomainError = require('./subdomainerror.js'), SubdomainError = require('./subdomains.js').SubdomainError,
subdomains = require('./subdomains.js'), subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
-119
View File
@@ -1,119 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
copyObject: copyObject
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('./config.js'),
debug = require('debug')('box:aws'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent');
function getBackupCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
// CaaS
if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.post(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'));
var credentials = {
accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey,
sessionToken: result.body.credentials.SessionToken,
region: 'us-east-1'
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
callback(null, credentials);
});
} else {
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new SubdomainError(SubdomainError.MISSING_CREDENTIALS));
var credentials = {
accessKeyId: config.aws().accessKeyId,
secretAccessKey: config.aws().secretAccessKey,
region: 'us-east-1'
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
callback(null, credentials);
}
}
function getSignedUploadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedUploadUrl: %s', filename);
getBackupCredentials(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: %s', filename);
getBackupCredentials(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 copyObject(from, to, callback) {
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: config.aws().backupBucket, // target bucket
Key: config.aws().backupPrefix + '/' + to, // target file
CopySource: config.aws().backupBucket + '/' + config.aws().backupPrefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
+55 -32
View File
@@ -12,10 +12,11 @@ exports = module.exports = {
}; };
var assert = require('assert'), var assert = require('assert'),
aws = require('./aws.js'), caas = require('./storage/caas.js'),
config = require('./config.js'), config = require('./config.js'),
debug = require('debug')('box:backups'), debug = require('debug')('box:backups'),
superagent = require('superagent'), s3 = require('./storage/s3.js'),
settings = require('./settings.js'),
util = require('util'); util = require('util');
function BackupsError(reason, errorOrMessage) { function BackupsError(reason, errorOrMessage) {
@@ -39,21 +40,30 @@ function BackupsError(reason, errorOrMessage) {
util.inherits(BackupsError, Error); util.inherits(BackupsError, Error);
BackupsError.EXTERNAL_ERROR = 'external error'; BackupsError.EXTERNAL_ERROR = 'external error';
BackupsError.INTERNAL_ERROR = 'internal error'; BackupsError.INTERNAL_ERROR = 'internal error';
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
// choose which storage backend we use for test purpose we use s3
function api(provider) {
switch (provider) {
case 'caas': return caas;
case 's3': return s3;
default: return null;
}
}
function getAllPaged(page, perPage, callback) { function getAllPaged(page, perPage, callback) {
assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number'); assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups'; settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
superagent.get(url).query({ token: config.token() }).end(function (error, result) { api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
if (!result.body || !util.isArray(result.body.backups)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first) return callback(null, backups); // [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first
return callback(null, result.body.backups); });
}); });
} }
@@ -68,19 +78,23 @@ function getBackupUrl(app, callback) {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version()); filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
} }
aws.getSignedUploadUrl(filename, function (error, result) { settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error); if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var obj = { api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
id: filename, if (error) return callback(error);
url: result.url,
sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey); var obj = {
id: filename,
url: result.url,
sessionToken: result.sessionToken,
backupKey: backupConfig.key
};
callback(null, obj); debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
});
}); });
} }
@@ -89,19 +103,23 @@ function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
aws.getSignedDownloadUrl(backupId, function (error, result) { settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error); if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var obj = { api(backupConfig.provider).getSignedDownloadUrl(backupConfig, backupId, function (error, result) {
id: backupId, if (error) return callback(error);
url: result.url,
sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey); var obj = {
id: backupId,
url: result.url,
sessionToken: result.sessionToken,
backupKey: backupConfig.key
};
callback(null, obj); debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
});
}); });
} }
@@ -111,9 +129,14 @@ function copyLastBackup(app, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version); var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
aws.copyObject(app.lastBackupId, toFilename, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename); settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
});
}); });
} }
+25 -10
View File
@@ -243,6 +243,8 @@ function getStatus(callback) {
callback(null, { callback(null, {
activated: count !== 0, activated: count !== 0,
version: config.version(), version: config.version(),
boxVersionsUrl: config.get('boxVersionsUrl'),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
cloudronName: cloudronName cloudronName: cloudronName
}); });
}); });
@@ -272,7 +274,7 @@ function getConfig(callback) {
getCloudronDetails(function (error, result) { getCloudronDetails(function (error, result) {
if (error) { if (error) {
console.error('Failed to fetch cloudron details.', error); debug('Failed to fetch cloudron details.', error);
// set fallback values to avoid dependency on appstore // set fallback values to avoid dependency on appstore
result = { result = {
@@ -393,6 +395,7 @@ function addDnsRecords() {
var records = [ ]; var records = [ ];
if (config.isCustomDomain()) { if (config.isCustomDomain()) {
records.push(webadminRecord); records.push(webadminRecord);
records.push(dkimRecord);
} else { } else {
records.push(nakedDomainRecord); records.push(nakedDomainRecord);
records.push(webadminRecord); records.push(webadminRecord);
@@ -561,19 +564,31 @@ function doUpdate(boxUpdateInfo, callback) {
// this data is opaque to the installer // this data is opaque to the installer
data: { data: {
apiServerOrigin: config.apiServerOrigin(),
aws: config.aws(),
backupKey: config.backupKey(),
boxVersionsUrl: config.get('boxVersionsUrl'),
fqdn: config.fqdn(),
isCustomDomain: config.isCustomDomain(),
restoreUrl: null,
restoreKey: null,
token: config.token(), token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'), tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'), tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
tlsConfig: {
provider: 'caas',
cert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
key: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
},
version: boxUpdateInfo.version, version: boxUpdateInfo.version,
webServerOrigin: config.webServerOrigin() boxVersionsUrl: config.get('boxVersionsUrl')
} }
}; };
-23
View File
@@ -33,9 +33,6 @@ 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
}; };
@@ -75,9 +72,7 @@ function initConfig() {
data.fqdn = 'localhost'; data.fqdn = 'localhost';
data.token = null; data.token = null;
data.mailServer = null;
data.adminEmail = null; data.adminEmail = null;
data.mailDnsRecordIds = [ ];
data.boxVersionsUrl = null; data.boxVersionsUrl = null;
data.version = null; data.version = null;
data.isCustomDomain = false; data.isCustomDomain = false;
@@ -86,13 +81,6 @@ function initConfig() {
data.ldapPort = 3002; data.ldapPort = 3002;
data.oauthProxyPort = 3003; data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004; data.simpleAuthPort = 3004;
data.backupKey = 'backupKey';
data.aws = {
backupBucket: null,
backupPrefix: null,
accessKeyId: null, // selfhosting only
secretAccessKey: null // selfhosting only
};
if (exports.CLOUDRON) { if (exports.CLOUDRON) {
data.port = 3000; data.port = 3000;
@@ -109,9 +97,6 @@ function initConfig() {
name: 'boxtest' name: 'boxtest'
}; };
data.token = 'APPSTORE_TOKEN'; data.token = 'APPSTORE_TOKEN';
data.aws.backupBucket = 'testbucket';
data.aws.backupPrefix = 'testprefix';
data.aws.endpoint = 'http://localhost:5353';
} else { } else {
assert(false, 'Unknown environment. This should not happen!'); assert(false, 'Unknown environment. This should not happen!');
} }
@@ -204,11 +189,3 @@ function database() {
function isDev() { function isDev() {
return /dev/i.test(get('boxVersionsUrl')); return /dev/i.test(get('boxVersionsUrl'));
} }
function backupKey() {
return get('backupKey');
}
function aws() {
return get('aws');
}
+17 -12
View File
@@ -13,12 +13,13 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
config = require('../config.js'), config = require('../config.js'),
debug = require('debug')('box:dns/caas'), debug = require('debug')('box:dns/caas'),
SubdomainError = require('../subdomainerror.js'), SubdomainError = require('../subdomains.js').SubdomainError,
superagent = require('superagent'), superagent = require('superagent'),
util = require('util'), util = require('util'),
_ = require('underscore'); _ = require('underscore');
function add(zoneName, subdomain, type, values, callback) { function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
@@ -36,7 +37,7 @@ function add(zoneName, subdomain, type, values, callback) {
superagent superagent
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn) .post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token() }) .query({ token: dnsConfig.token })
.send(data) .send(data)
.end(function (error, result) { .end(function (error, result) {
if (error) return callback(error); if (error) return callback(error);
@@ -47,7 +48,8 @@ function add(zoneName, subdomain, type, values, callback) {
}); });
} }
function get(zoneName, subdomain, type, callback) { function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
@@ -59,7 +61,7 @@ function get(zoneName, subdomain, type, callback) {
superagent superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn) .get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token(), type: type }) .query({ token: dnsConfig.token, type: type })
.end(function (error, result) { .end(function (error, result) {
if (error) return callback(error); if (error) return callback(error);
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body))); if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
@@ -68,23 +70,25 @@ function get(zoneName, subdomain, type, callback) {
}); });
} }
function update(zoneName, subdomain, type, values, callback) { function update(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
assert(util.isArray(values)); assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
get(zoneName, subdomain, type, function (error, result) { get(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error); if (error) return callback(error);
if (_.isEqual(values, result)) return callback(); if (_.isEqual(values, result)) return callback();
add(zoneName, subdomain, type, values, callback); add(dnsConfig, zoneName, subdomain, type, values, callback);
}); });
} }
function del(zoneName, subdomain, type, values, callback) { function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
@@ -100,7 +104,7 @@ function del(zoneName, subdomain, type, values, callback) {
superagent superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain)) .del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
.query({ token: config.token() }) .query({ token: dnsConfig.token })
.send(data) .send(data)
.end(function (error, result) { .end(function (error, result) {
if (error) return callback(error); if (error) return callback(error);
@@ -112,7 +116,8 @@ function del(zoneName, subdomain, type, values, callback) {
}); });
} }
function getChangeStatus(changeId, callback) { function getChangeStatus(dnsConfig, changeId, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof changeId, 'string'); assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -120,7 +125,7 @@ function getChangeStatus(changeId, callback) {
superagent superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId) .get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
.query({ token: config.token() }) .query({ token: dnsConfig.token })
.end(function (error, result) { .end(function (error, result) {
if (error) return callback(error); if (error) return callback(error);
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body))); if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
+73 -95
View File
@@ -14,52 +14,45 @@ var assert = require('assert'),
AWS = require('aws-sdk'), AWS = require('aws-sdk'),
config = require('../config.js'), config = require('../config.js'),
debug = require('debug')('box:dns/route53'), debug = require('debug')('box:dns/route53'),
settings = require('../settings.js'), SubdomainError = require('../subdomains.js').SubdomainError,
SubdomainError = require('../subdomainerror.js'),
util = require('util'), util = require('util'),
_ = require('underscore'); _ = require('underscore');
function getDnsCredentials(callback) { function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof dnsConfig, 'object');
settings.getDnsConfig(function (error, dnsConfig) { var credentials = {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error)); accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region
};
var credentials = { if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint);
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region
};
if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint); return credentials;
callback(null, credentials);
});
} }
function getZoneByName(zoneName, callback) { function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
getDnsCredentials(function (error, credentials) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
if (error) return callback(error); route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
var route53 = new AWS.Route53(credentials); var zone = result.HostedZones.filter(function (zone) {
route53.listHostedZones({}, function (error, result) { return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error))); })[0];
var zone = result.HostedZones.filter(function (zone) { if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such 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')); callback(null, zone);
callback(null, zone);
});
}); });
} }
function add(zoneName, subdomain, type, values, callback) { function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
@@ -68,7 +61,7 @@ function add(zoneName, subdomain, type, values, callback) {
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getZoneByName(zoneName, function (error, zone) { getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error); if (error) return callback(error);
var fqdn = config.appFqdn(subdomain); var fqdn = config.appFqdn(subdomain);
@@ -89,48 +82,44 @@ function add(zoneName, subdomain, type, values, callback) {
HostedZoneId: zone.Id HostedZoneId: zone.Id
}; };
getDnsCredentials(function (error, credentials) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
if (error) return callback(error); route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
var route53 = new AWS.Route53(credentials); callback(null, result.ChangeInfo.Id);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
debug('addSubdomain: success. changeInfoId:%j', result);
callback(null, result.ChangeInfo.Id);
});
}); });
}); });
} }
function update(zoneName, subdomain, type, values, callback) { function update(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
assert(util.isArray(values)); assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
get(zoneName, subdomain, type, function (error, result) { get(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error); if (error) return callback(error);
if (_.isEqual(values, result)) return callback(); if (_.isEqual(values, result)) return callback();
add(zoneName, subdomain, type, values, callback); add(dnsConfig, zoneName, subdomain, type, values, callback);
}); });
} }
function get(zoneName, subdomain, type, callback) { function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
getZoneByName(zoneName, function (error, zone) { getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error); if (error) return callback(error);
var params = { var params = {
@@ -140,33 +129,28 @@ function get(zoneName, subdomain, type, callback) {
StartRecordType: type StartRecordType: type
}; };
getDnsCredentials(function (error, credentials) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
if (error) return callback(error); route53.listResourceRecordSets(params, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
if (result.ResourceRecordSets[0].Name !== params.StartRecordName && result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
var route53 = new AWS.Route53(credentials); var values = result.ResourceRecordSets[0].ResourceRecords.map(function (record) { return record.Value; });
route53.listResourceRecordSets(params, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
if (result.ResourceRecordSets[0].Name !== params.StartRecordName && result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
var values = result.ResourceRecordSets[0].ResourceRecords.map(function (record) { return record.Value; }); callback(null, values);
callback(null, values);
});
}); });
}); });
} }
function del(zoneName, subdomain, type, values, callback) { function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string'); assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
assert(util.isArray(values)); assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); getZoneByName(dnsConfig, zoneName, function (error, zone) {
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error); if (error) return callback(error);
var fqdn = config.appFqdn(subdomain); var fqdn = config.appFqdn(subdomain);
@@ -189,48 +173,42 @@ function del(zoneName, subdomain, type, values, callback) {
HostedZoneId: zone.Id HostedZoneId: zone.Id
}; };
getDnsCredentials(function (error, credentials) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
if (error) return callback(error); 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)));
}
var route53 = new AWS.Route53(credentials); callback(null);
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)));
}
callback(null);
});
}); });
}); });
} }
function getChangeStatus(changeId, callback) { function getChangeStatus(dnsConfig, changeId, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof changeId, 'string'); assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC'); if (changeId === '') return callback(null, 'INSYNC');
getDnsCredentials(function (error, credentials) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.getChange({ Id: changeId }, function (error, result) {
if (error) return callback(error); if (error) return callback(error);
var route53 = new AWS.Route53(credentials); callback(null, result.ChangeInfo.Status);
route53.getChange({ Id: changeId }, function (error, result) {
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);
});
}); });
} }
+15 -13
View File
@@ -8,7 +8,8 @@ var addons = require('./addons.js'),
Docker = require('dockerode'), Docker = require('dockerode'),
safe = require('safetydance'), safe = require('safetydance'),
semver = require('semver'), semver = require('semver'),
util = require('util'); util = require('util'),
_ = require('underscore');
exports = module.exports = { exports = module.exports = {
connection: connectionInstance(), connection: connectionInstance(),
@@ -64,18 +65,17 @@ function pullImage(manifest, callback) {
// is emitted as a chunk // is emitted as a chunk
stream.on('data', function (chunk) { stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { }; var data = safe.JSON.parse(chunk) || { };
debug('pullImage data: %j', data); debug('pullImage %s: %j', manifest.id, data);
// The information here is useless because this is per layer as opposed to per image // The information here is useless because this is per layer as opposed to per image
if (data.status) { if (data.status) {
// debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
} else if (data.error) { } else if (data.error) {
debug('pullImage error detail: %s', data.errorDetail.message); debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
} }
}); });
stream.on('end', function () { stream.on('end', function () {
debug('downloaded image %s successfully', manifest.dockerImage); debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
var image = docker.getImage(manifest.dockerImage); var image = docker.getImage(manifest.dockerImage);
@@ -84,14 +84,14 @@ function pullImage(manifest, callback) {
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4))); if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed')); if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
debug('This image exposes ports: %j', data.Config.ExposedPorts); debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
callback(null); callback(null);
}); });
}); });
stream.on('error', function (error) { stream.on('error', function (error) {
debug('error pulling image %s : %j', manifest.dockerImage, error); debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
callback(error); callback(error);
}); });
@@ -102,12 +102,12 @@ function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object'); assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('downloadImage %s', manifest.dockerImage); debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
var attempt = 1; var attempt = 1;
async.retry({ times: 5, interval: 15000 }, function (retryCallback) { async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
debug('Downloading image. attempt: %s', attempt++); debug('Downloading image %s %s. attempt: %s', manifest.id, manifest.dockerImage, attempt++);
pullImage(manifest, function (error) { pullImage(manifest, function (error) {
if (error) console.error(error); if (error) console.error(error);
@@ -117,10 +117,11 @@ function downloadImage(manifest, callback) {
}, callback); }, callback);
} }
function createSubcontainer(app, name, cmd, callback) { function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert(!cmd || util.isArray(cmd)); assert(!cmd || util.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var docker = exports.connection, var docker = exports.connection,
@@ -192,18 +193,19 @@ function createSubcontainer(app, name, cmd, callback) {
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
} }
}; };
containerOptions = _.extend(containerOptions, options);
// older versions wanted a writable /var/log // older versions wanted a writable /var/log
if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {}; if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {};
debugApp(app, 'Creating container for %s', app.manifest.dockerImage); debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
docker.createContainer(containerOptions, callback); docker.createContainer(containerOptions, callback);
}); });
} }
function createContainer(app, callback) { function createContainer(app, callback) {
createSubcontainer(app, app.id /* name */, null /* cmd */, callback); createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */, callback);
} }
function startContainer(containerId, callback) { function startContainer(containerId, callback) {
+17 -6
View File
@@ -125,11 +125,23 @@ function checkDns() {
} }
debug('checkDns: SPF check passed. commencing mail processing'); debug('checkDns: SPF check passed. commencing mail processing');
gDnsReady = true;
processQueue(); processQueue();
}); });
} }
function processQueue() { function processQueue() {
assert(gDnsReady);
sendMails(gMailQueue);
gMailQueue = [ ];
}
// note : this function should NOT access the database. it is called by the crashnotifier
// which does not initialize mailer or the databse
function sendMails(queue) {
assert(util.isArray(queue));
docker.getContainer('mail').inspect(function (error, data) { docker.getContainer('mail').inspect(function (error, data) {
if (error) return console.error(error); if (error) return console.error(error);
@@ -141,12 +153,9 @@ function processQueue() {
port: 2500 // this value comes from mail container port: 2500 // this value comes from mail container
})); }));
var mailQueueCopy = gMailQueue; debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
gMailQueue = [ ];
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp); async.mapSeries(queue, function iterator(mailOptions, callback) {
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) { transport.sendMail(mailOptions, function (error) {
if (error) return console.error(error); // TODO: requeue? if (error) return console.error(error); // TODO: requeue?
debug('Email sent to ' + mailOptions.to); debug('Email sent to ' + mailOptions.to);
@@ -320,6 +329,8 @@ function appUpdateAvailable(app, updateInfo) {
}); });
} }
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// crashnotifier should be able to send mail when there is no db
function sendCrashNotification(program, context) { function sendCrashNotification(program, context) {
assert.strictEqual(typeof program, 'string'); assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string'); assert.strictEqual(typeof context, 'string');
@@ -331,7 +342,7 @@ function sendCrashNotification(program, context) {
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' }) text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
}; };
enqueue(mailOptions); sendMails([ mailOptions ]);
} }
function sendFeedback(user, type, subject, description) { function sendFeedback(user, type, subject, description) {
+2
View File
@@ -6,6 +6,8 @@
<title> Cloudron Login </title> <title> Cloudron Login </title>
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- 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="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
-2
View File
@@ -28,7 +28,5 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'), CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'), CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json') UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
}; };
+2
View File
@@ -368,6 +368,8 @@ function exec(req, res, next) {
duplexStream.pipe(res.socket); duplexStream.pipe(res.socket);
res.socket.pipe(duplexStream); res.socket.pipe(duplexStream);
res.on('close', duplexStream.close);
}); });
} }
+26 -2
View File
@@ -15,6 +15,9 @@ exports = module.exports = {
getDnsConfig: getDnsConfig, getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig, setDnsConfig: setDnsConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
setCertificate: setCertificate, setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate setAdminCertificate: setAdminCertificate
}; };
@@ -111,6 +114,27 @@ function setDnsConfig(req, res, next) {
}); });
} }
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, config));
});
}
function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
settings.setBackupConfig(req.body, 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));
});
}
// default fallback cert // default fallback cert
function setCertificate(req, res, next) { function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.body, 'object');
@@ -119,7 +143,7 @@ function setCertificate(req, res, next) {
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string')); if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
settings.setCertificate(req.body.cert, req.body.key, function (error) { settings.setCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, 'cert not applicable')); if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error)); if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {})); next(new HttpSuccess(202, {}));
@@ -134,7 +158,7 @@ function setAdminCertificate(req, res, next) {
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string')); if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
settings.setAdminCertificate(req.body.cert, req.body.key, function (error) { settings.setAdminCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, 'cert not applicable')); if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error)); if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {})); next(new HttpSuccess(202, {}));
+7 -4
View File
@@ -146,12 +146,17 @@ function setup(done) {
callback(null); callback(null);
}); });
}, function (callback) { },
function (callback) {
token_1 = tokendb.generateToken(); token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 100000, '*', callback); tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 100000, '*', callback);
} },
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], done); ], done);
} }
@@ -590,8 +595,6 @@ describe('App installation', function () {
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback); apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
}, },
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
function (callback) { function (callback) {
awsHockInstance awsHockInstance
.get('/2013-04-01/hostedzone') .get('/2013-04-01/hostedzone')
+8 -4
View File
@@ -13,6 +13,7 @@ var appdb = require('../../appdb.js'),
expect = require('expect.js'), expect = require('expect.js'),
request = require('superagent'), request = require('superagent'),
server = require('../../server.js'), server = require('../../server.js'),
settings = require('../../settings.js'),
nock = require('nock'), nock = require('nock'),
userdb = require('../../userdb.js'); userdb = require('../../userdb.js');
@@ -52,6 +53,10 @@ function setup(done) {
function addApp(callback) { function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } }; var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback); appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
} }
], done); ], done);
} }
@@ -70,7 +75,7 @@ describe('Backups API', function () {
describe('get', function () { describe('get', function () {
it('cannot get backups with appstore request failing', function (done) { it('cannot get backups with appstore request failing', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(401, {}); var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(401, {});
request.get(SERVER_URL + '/api/v1/backups') request.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token }) .query({ access_token: token })
@@ -82,7 +87,7 @@ describe('Backups API', function () {
}); });
it('can get backups', function (done) { it('can get backups', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(200, { backups: ['foo', 'bar']}); var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(200, { backups: ['foo', 'bar']});
request.get(SERVER_URL + '/api/v1/backups') request.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token }) .query({ access_token: token })
@@ -119,7 +124,7 @@ describe('Backups API', function () {
it('succeeds', function (done) { it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin()) var scope = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN') .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
request.post(SERVER_URL + '/api/v1/backups') request.post(SERVER_URL + '/api/v1/backups')
@@ -141,4 +146,3 @@ describe('Backups API', function () {
}); });
}); });
}); });
+15 -4
View File
@@ -282,6 +282,19 @@ describe('Cloudron', function () {
callback(); callback();
}); });
}, },
function setupBackupConfig(callback) {
request.post(SERVER_URL + '/api/v1/settings/backup_config')
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
callback();
});
}
], done); ], done);
}); });
@@ -341,7 +354,6 @@ describe('Cloudron', function () {
}); });
}); });
it('fails with wrong region type', function (done) { it('fails with wrong region type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/migrate') request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD }) .send({ size: 'small', region: 4, password: PASSWORD })
@@ -355,7 +367,7 @@ describe('Cloudron', function () {
it('fails when in wrong state', function (done) { it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin()) var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN') .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin()) var scope3 = nock(config.apiServerOrigin())
@@ -391,7 +403,6 @@ describe('Cloudron', function () {
}); });
}); });
it('succeeds', function (done) { it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) { var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey; return body.size && body.region && body.restoreKey;
@@ -404,7 +415,7 @@ describe('Cloudron', function () {
.reply(200, { id: 'someid' }); .reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin()) var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN') .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock(); injectShellMock();
+2 -2
View File
@@ -215,7 +215,7 @@ describe('Settings API', function () {
it('set succeeds', function (done) { it('set succeeds', function (done) {
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar') request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token }) .query({ access_token: token })
.attach('avatar', paths.FAVICON_FILE) .attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
.end(function (err, res) { .end(function (err, res) {
expect(res.statusCode).to.equal(202); expect(res.statusCode).to.equal(202);
done(); done();
@@ -227,7 +227,7 @@ describe('Settings API', function () {
.query({ access_token: token }) .query({ access_token: token })
.end(function (err, res) { .end(function (err, res) {
expect(res.statusCode).to.equal(200); expect(res.statusCode).to.equal(200);
expect(res.body.toString()).to.eql(fs.readFileSync(paths.FAVICON_FILE, 'utf-8')); expect(res.body.toString()).to.eql(fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE, 'utf-8'));
done(err); done(err);
}); });
}); });
-2
View File
@@ -66,8 +66,6 @@ start_mongodb() {
} }
start_mail() { start_mail() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
docker rm -f mail 2>/dev/null 1>&2 || true docker rm -f mail 2>/dev/null 1>&2 || true
docker run -dP --name=mail -e DOMAIN_NAME="localhost" \ docker run -dP --name=mail -e DOMAIN_NAME="localhost" \
+1 -1
View File
@@ -181,7 +181,7 @@ function doTask(appId, taskName, callback) {
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command); debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
// NOTE: if you change container name here, fix addons.js to return correct container names // NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) { docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
appState.containerIds[taskName] = container.id; appState.containerIds[taskName] = container.id;
saveState(gState); saveState(gState);
+2 -2
View File
@@ -20,7 +20,6 @@ var assert = require('assert'),
middleware = require('./middleware'), middleware = require('./middleware'),
passport = require('passport'), passport = require('passport'),
path = require('path'), path = require('path'),
paths = require('./paths.js'),
routes = require('./routes/index.js'), routes = require('./routes/index.js'),
taskmanager = require('./taskmanager.js'); taskmanager = require('./taskmanager.js');
@@ -53,7 +52,6 @@ function initializeExpressSync() {
.use(json) .use(json)
.use(urlencoded) .use(urlencoded)
.use(middleware.cookieParser()) .use(middleware.cookieParser())
.use(middleware.favicon(paths.FAVICON_FILE)) // used when serving oauth login page
.use(middleware.cors({ origins: [ '*' ], allowCredentials: true })) .use(middleware.cors({ origins: [ '*' ], allowCredentials: true }))
.use(middleware.session({ secret: 'yellow is blue', resave: true, saveUninitialized: true, cookie: { path: '/', httpOnly: true, secure: false, maxAge: 600000 } })) .use(middleware.session({ secret: 'yellow is blue', resave: true, saveUninitialized: true, cookie: { path: '/', httpOnly: true, secure: false, maxAge: 600000 } }))
.use(passport.initialize()) .use(passport.initialize())
@@ -162,6 +160,8 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar); router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig); router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig);
router.post('/api/v1/settings/dns_config', settingsScope, routes.settings.setDnsConfig); router.post('/api/v1/settings/dns_config', settingsScope, routes.settings.setDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
router.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate); router.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate); router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate);
+44 -8
View File
@@ -23,6 +23,9 @@ exports = module.exports = {
getDnsConfig: getDnsConfig, getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig, setDnsConfig: setDnsConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getDefaultSync: getDefaultSync, getDefaultSync: getDefaultSync,
getAll: getAll, getAll: getAll,
@@ -35,6 +38,7 @@ exports = module.exports = {
CLOUDRON_NAME_KEY: 'cloudron_name', CLOUDRON_NAME_KEY: 'cloudron_name',
DEVELOPER_MODE_KEY: 'developer_mode', DEVELOPER_MODE_KEY: 'developer_mode',
DNS_CONFIG_KEY: 'dns_config', DNS_CONFIG_KEY: 'dns_config',
BACKUP_CONFIG_KEY: 'backup_config',
events: new (require('events').EventEmitter)() events: new (require('events').EventEmitter)()
}; };
@@ -62,6 +66,7 @@ var gDefaults = (function () {
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron'; result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false; result[exports.DEVELOPER_MODE_KEY] = false;
result[exports.DNS_CONFIG_KEY] = { }; result[exports.DNS_CONFIG_KEY] = { };
result[exports.BACKUP_CONFIG_KEY] = { };
return result; return result;
})(); })();
@@ -271,6 +276,34 @@ function setDnsConfig(dnsConfig, callback) {
}); });
} }
function getBackupConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.BACKUP_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.BACKUP_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value)); // provider, token, key, region, prefix, bucket
});
}
function setBackupConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (backupConfig.provider !== 'caas') {
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be caas'));
}
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.BACKUP_CONFIG_KEY, backupConfig);
callback(null);
});
}
function getDefaultSync(name) { function getDefaultSync(name) {
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
@@ -290,6 +323,8 @@ function getAll(callback) {
}); });
} }
// 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(cert, key, fqdn) { function validateCertificate(cert, key, fqdn) {
assert(cert === null || typeof cert === 'string'); assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string'); assert(key === null || typeof key === 'string');
@@ -303,7 +338,7 @@ function validateCertificate(cert, key, fqdn) {
try { try {
content = x509.parseCert(cert); content = x509.parseCert(cert);
} catch (e) { } catch (e) {
return new Error('invalid cert'); return new Error('invalid cert: ' + e.message);
} }
// check expiration // check expiration
@@ -318,7 +353,7 @@ function validateCertificate(cert, key, fqdn) {
// check domain // check domain
var domains = content.altNames.concat(content.subject.commonName); var domains = content.altNames.concat(content.subject.commonName);
if (!domains.some(matchesDomain)) return new Error('cert is not valid for this domain'); if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify // 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 }); var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
@@ -336,13 +371,13 @@ function setCertificate(cert, key, callback) {
var error = validateCertificate(cert, key, '*.' + config.fqdn()); var error = validateCertificate(cert, key, '*.' + config.fqdn());
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message)); if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) { // backup the cert
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message)); if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
} if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) { // copy over fallback cert
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message)); if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
} if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) { shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
@@ -365,6 +400,7 @@ function setAdminCertificate(cert, key, callback) {
var error = validateCertificate(cert, key, vhost); var error = validateCertificate(cert, key, vhost);
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message)); if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message)); if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message)); if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
+129
View File
@@ -0,0 +1,129 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
copyObject: copyObject,
getAllPaged: getAllPaged
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
superagent = require('superagent'),
util = require('util');
function getBackupCredentials(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
assert(backupConfig.token);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.post(url).query({ token: backupConfig.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'));
var credentials = {
accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey,
sessionToken: result.body.credentials.SessionToken,
region: 'us-east-1'
};
if (backupConfig.endpoint) credentials.endpoint = new AWS.Endpoint(backupConfig.endpoint);
callback(null, credentials);
});
}
function getAllPaged(backupConfig, page, perPage, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups';
superagent.get(url).query({ token: backupConfig.token }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new Error(result.text));
if (!result.body || !util.isArray(result.body.backups)) return callback(new Error('Unexpected response'));
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
return callback(null, result.body.backups);
});
}
function getSignedUploadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function copyObject(backupConfig, from, to, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: backupConfig.bucket, // target bucket
Key: backupConfig.prefix + '/' + to, // target file
CopySource: backupConfig.bucket + '/' + backupConfig.prefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
+104
View File
@@ -0,0 +1,104 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
copyObject: copyObject,
getAllPaged: getAllPaged
};
var assert = require('assert'),
AWS = require('aws-sdk');
function getBackupCredentials(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
assert(backupConfig.accessKeyId && backupConfig.secretAccessKey);
var credentials = {
accessKeyId: backupConfig.accessKeyId,
secretAccessKey: backupConfig.secretAccessKey,
region: 'us-east-1'
};
if (backupConfig.endpoint) credentials.endpoint = new AWS.Endpoint(backupConfig.endpoint);
callback(null, credentials);
}
function getAllPaged(backupConfig, page, perPage, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
return callback(new Error('Not implemented yet'));
}
function getSignedUploadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function copyObject(backupConfig, from, to, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: backupConfig.bucket, // target bucket
Key: backupConfig.prefix + '/' + to, // target file
CopySource: backupConfig.bucket + '/' + backupConfig.prefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
-33
View File
@@ -1,33 +0,0 @@
/* 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.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
+79 -25
View File
@@ -2,24 +2,58 @@
'use strict'; 'use strict';
var assert = require('assert'),
caas = require('./dns/caas.js'),
config = require('./config.js'),
route53 = require('./dns/route53.js'),
SubdomainError = require('./subdomainerror.js'),
util = require('util');
module.exports = exports = { module.exports = exports = {
add: add, add: add,
remove: remove, remove: remove,
status: status, status: status,
update: update, // unlike add, this fetches latest value, compares and adds if necessary. atomicity depends on backend update: update, // unlike add, this fetches latest value, compares and adds if necessary. atomicity depends on backend
get: get get: get,
SubdomainError: SubdomainError
}; };
var assert = require('assert'),
caas = require('./dns/caas.js'),
config = require('./config.js'),
route53 = require('./dns/route53.js'),
settings = require('./settings.js'),
util = require('util');
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.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
SubdomainError.INTERNAL_ERROR = 'Missing credentials';
// choose which subdomain backend we use for test purpose we use route53 // choose which subdomain backend we use for test purpose we use route53
function api() { function api(provider) {
return config.isCustomDomain() || config.TEST ? route53 : caas; assert.strictEqual(typeof provider, 'string');
switch (provider) {
case 'caas': return caas;
case 'route53': return route53;
default: return null;
}
} }
function add(subdomain, type, values, callback) { function add(subdomain, type, values, callback) {
@@ -28,9 +62,13 @@ function add(subdomain, type, values, callback) {
assert(util.isArray(values)); assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
api().add(config.zoneName(), subdomain, type, values, function (error, changeId) { settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(error); if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
callback(null, changeId);
api(dnsConfig.provider).add(dnsConfig, config.zoneName(), subdomain, type, values, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
});
}); });
} }
@@ -39,10 +77,14 @@ function get(subdomain, type, callback) {
assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
api().get(config.zoneName(), subdomain, type, function (error, values) { settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(error); if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
callback(null, values); api(dnsConfig.provider).get(dnsConfig, config.zoneName(), subdomain, type, function (error, values) {
if (error) return callback(error);
callback(null, values);
});
}); });
} }
@@ -52,10 +94,14 @@ function update(subdomain, type, values, callback) {
assert(util.isArray(values)); assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
api().update(config.zoneName(), subdomain, type, values, function (error) { settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(error); if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
callback(null); api(dnsConfig.provider).update(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
if (error) return callback(error);
callback(null);
});
}); });
} }
@@ -65,10 +111,14 @@ function remove(subdomain, type, values, callback) {
assert(util.isArray(values)); assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
api().del(config.zoneName(), subdomain, type, values, function (error) { settings.getDnsConfig(function (error, dnsConfig) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error); if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
callback(null); api(dnsConfig.provider).del(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
callback(null);
});
}); });
} }
@@ -76,8 +126,12 @@ function status(changeId, callback) {
assert.strictEqual(typeof changeId, 'string'); assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
api().getChangeStatus(changeId, function (error, status) { settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error)); if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending');
api(dnsConfig.provider).getChangeStatus(dnsConfig, changeId, function (error, status) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending');
});
}); });
} }
+28 -15
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
var appdb = require('./appdb.js'), var appdb = require('./appdb.js'),
assert = require('assert'), assert = require('assert'),
async = require('async'),
child_process = require('child_process'), child_process = require('child_process'),
cloudron = require('./cloudron.js'), cloudron = require('./cloudron.js'),
debug = require('debug')('box:taskmanager'), debug = require('debug')('box:taskmanager'),
@@ -39,14 +40,11 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
for (var appId in gActiveTasks) {
stopAppTask(appId);
}
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks); cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
locker.removeListener('unlocked', startNextTask); locker.removeListener('unlocked', startNextTask);
callback(null); async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
} }
@@ -95,8 +93,12 @@ function startAppTask(appId) {
} }
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]); gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]);
var pid = gActiveTasks[appId].pid;
debug('Started task of %s pid: %s', appId, pid);
gActiveTasks[appId].once('exit', function (code) { gActiveTasks[appId].once('exit', function (code) {
debug('Task for %s completed with status %s', appId, code); debug('Task for %s pid %s completed with status %s', appId, pid, code);
if (code && code !== 50) { // apptask crashed if (code && code !== 50) { // apptask crashed
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code }, NOOP_CALLBACK); appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code }, NOOP_CALLBACK);
} }
@@ -105,21 +107,32 @@ function startAppTask(appId) {
}); });
} }
function stopAppTask(appId) { function stopAppTask(appId, callback) {
assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
if (gActiveTasks[appId]) { if (gActiveTasks[appId]) {
debug('stopAppTask : Killing existing task of %s with pid %s: ', appId, gActiveTasks[appId].pid); debug('stopAppTask : Killing existing task of %s with pid %s', appId, gActiveTasks[appId].pid);
gActiveTasks[appId].once('exit', function () { callback(); });
gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler
delete gActiveTasks[appId]; return;
} else if (gPendingTasks.indexOf(appId) !== -1) {
debug('stopAppTask: Removing existing pending task : %s', appId);
gPendingTasks = _.without(gPendingTasks, appId);
} }
if (gPendingTasks.indexOf(appId) !== -1) {
debug('stopAppTask: Removing pending task : %s', appId);
gPendingTasks = _.without(gPendingTasks, appId);
} else {
debug('stopAppTask: no task for %s to be stopped', appId);
}
callback();
} }
function restartAppTask(appId) { function restartAppTask(appId, callback) {
stopAppTask(appId); callback = callback || NOOP_CALLBACK;
startAppTask(appId);
}
async.series([
stopAppTask.bind(null, appId),
startAppTask.bind(null, appId)
], callback);
}
+1 -1
View File
@@ -220,7 +220,7 @@ describe('apptask', function () {
it('unregisters subdomain', function (done) { it('unregisters subdomain', function (done) {
nock.cleanAll(); nock.cleanAll();
var awsScope = nock(config.aws().endpoint) var awsScope = nock('http://localhost:5353')
.get('/2013-04-01/hostedzone') .get('/2013-04-01/hostedzone')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} })) .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/') .post('/2013-04-01/hostedzone/ZONEID/rrset/')
+21 -9
View File
@@ -35,28 +35,40 @@ for script in "${scripts[@]}"; do
fi fi
done done
image_missing=""
if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull "${TEST_IMAGE}" for tests to run" echo "docker pull ${TEST_IMAGE}"
exit 1 image_missing="true"
fi fi
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${REDIS_IMAGE} for tests to run" echo "docker pull ${REDIS_IMAGE}"
exit 1 image_missing="true"
fi fi
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MYSQL_IMAGE} for tests to run" echo "docker pull ${MYSQL_IMAGE}"
exit 1 image_missing="true"
fi fi
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${POSTGRESQL_IMAGE} for tests to run" echo "docker pull ${POSTGRESQL_IMAGE}"
exit 1 image_missing="true"
fi fi
if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MONGODB_IMAGE} for tests to run" echo "docker pull ${MONGODB_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MAIL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MAIL_IMAGE}"
image_missing="true"
fi
if [[ "${image_missing}" == "true" ]]; then
echo "Pull above images before running tests"
exit 1 exit 1
fi fi
+16
View File
@@ -100,6 +100,22 @@ describe('Settings', function () {
}); });
}); });
it('can set backup config', function (done) {
settings.setBackupConfig({ provider: 'caas', token: 'TOKEN' }, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get backup config', function (done) {
settings.getBackupConfig(function (error, dnsConfig) {
expect(error).to.be(null);
expect(dnsConfig.provider).to.be('caas');
expect(dnsConfig.token).to.be('TOKEN');
done();
});
});
it('can get all values', function (done) { it('can get all values', function (done) {
settings.getAll(function (error, allSettings) { settings.getAll(function (error, allSettings) {
expect(error).to.be(null); expect(error).to.be(null);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

+1
View File
@@ -147,6 +147,7 @@
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li> <li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li> <li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li> <li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin && config.isCustomDomain"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> DNS & Certs</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li> <li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li> <li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
+3
View File
@@ -31,6 +31,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/graphs', { }).when('/graphs', {
controller: 'GraphsController', controller: 'GraphsController',
templateUrl: 'views/graphs.html' templateUrl: 'views/graphs.html'
}).when('/certs', {
controller: 'CertsController',
templateUrl: 'views/certs.html'
}).when('/settings', { }).when('/settings', {
controller: 'SettingsController', controller: 'SettingsController',
templateUrl: 'views/settings.html' templateUrl: 'views/settings.html'
+24 -9
View File
@@ -150,8 +150,24 @@ app.service('Wizard', [ function () {
app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) { app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) {
$scope.wizard = Wizard; $scope.wizard = Wizard;
$scope.next = function (page, bad) { $scope.next = function (bad) {
if (!bad) $location.path(page); if (bad) return;
var current = $location.path();
var next = '';
if (current === '/step1') {
next = '/step2';
} else if (current === '/step2') {
if (Wizard.dnsConfig === null) next = '/step4';
else next = '/step3';
} else if (current === '/step3') {
next = '/step4';
} else {
next = '/step1';
}
$location.path(next);
}; };
$scope.focusNext = function (elemId, bad) { $scope.focusNext = function (elemId, bad) {
@@ -194,7 +210,7 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
image = null; image = null;
}; };
image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url; image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url;
} else if ($route.current.templateUrl === 'views/setup/step3.html' && Wizard.dnsConfig.provider === 'caas') { } else if ($route.current.templateUrl === 'views/setup/step3.html' && Wizard.dnsConfig === null) {
$location.path('/step4'); // not using custom domain $location.path('/step4'); // not using custom domain
} }
@@ -213,6 +229,11 @@ app.controller('FinishController', ['$scope', '$location', 'Wizard', 'Client', f
Client.changeCloudronAvatar($scope.wizard.avatarBlob, function (error) { Client.changeCloudronAvatar($scope.wizard.avatarBlob, function (error) {
if (error) return console.error('Unable to set avatar.', error); if (error) return console.error('Unable to set avatar.', error);
if ($scope.wizard.dnsConfig === null) {
window.location.href = '/';
return;
}
Client.setDnsConfig($scope.wizard.dnsConfig, function (error) { Client.setDnsConfig($scope.wizard.dnsConfig, function (error) {
if (error) return console.error('Unable to set dns config.', error); if (error) return console.error('Unable to set dns config.', error);
@@ -240,12 +261,6 @@ app.controller('SetupController', ['$scope', '$location', 'Client', 'Wizard', fu
accessKeyId: null, accessKeyId: null,
secretAccessKey: null secretAccessKey: null
}; };
} else {
Wizard.dnsConfig = {
provider: 'caas',
accessKeyId: '',
secretAccessKey: ''
};
} }
Client.isServerFirstTime(function (error, isFirstTime) { Client.isServerFirstTime(function (error, isFirstTime) {
+125
View File
@@ -0,0 +1,125 @@
<div style="max-width: 600px; margin: 0 auto;">
<div class="text-left">
<h1>DNS & Certs</h1>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;">
<div class="text-left">
<h3>DNS Credentials</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
<p>Currently only Amazon <a href="https://aws.amazon.com/route53/">Route53</a> is supported. Let us know if you require a different DNS provider <a href="#/support">here</a>.</p>
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Access Key Id</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Secret Access Key</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;"><i>hidden</i></td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;"></td>
<td class="text-right" style="vertical-align: top;"><span class="text-success" ng-show="dnsCredentials.success"><b>Done</b></span> &nbsp; &nbsp; <button class="btn btn-outline btn-xs btn-primary" ng-show="!dnsCredentials.formVisible" ng-click="showDnsCredentialsForm()">Change</button></td>
</tr>
</table>
<div class="collapse" id="collapseDnsCredentialsForm" data-toggle="false">
<p>The security credentials have to be valid for full Route53 access.</p>
<form name="dnsCredentialsForm" ng-submit="setDnsCredentials()">
<fieldset>
<div class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</div>
<div class="form-group" ng-class="{ 'has-error': false }">
<label class="control-label" for="dnsCredentialsAccessKeyId">Access Key Id</label>
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" id="dnsCredentialsAccessKeyId" name="accessKeyId" ng-disabled="dnsCredentials.busy" ng-minlength="16" ng-maxlength="32" required>
</div>
<div class="form-group" ng-class="{ 'has-error': false }">
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret Access Key</label>
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" required>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="dnsCredentialsForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="dnsCredentials.busy"></i> Save</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;">
<div class="text-left">
<h3>SSL Certificates</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
<form name="defaultCertForm" ng-submit="setDefaultCert()">
<fieldset>
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
<p>This certificate has to be wildcard certificates and will be used for all apps, which were not configured to use a specific certificate.</p>
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
<div class="input-group">
<input type="file" id="defaultCertFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
<div class="input-group">
<input type="file" id="defaultKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
</span>
</div>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="defaultCert.busy"></i> Upload</button>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form name="adminCertForm" ng-submit="setAdminCert()">
<fieldset>
<label class="control-label" for="adminCertInput">Settings Certificate</label>
<p>This certificate will be used for this Settings application.</p>
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
<div class="input-group">
<input type="file" id="adminCertFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
<div class="input-group">
<input type="file" id="adminKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
</span>
</div>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="adminCert.busy"></i> Upload</button>
</fieldset>
</form>
</div>
</div>
</div>
+151
View File
@@ -0,0 +1,151 @@
'use strict';
angular.module('Application').controller('CertsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin || !Client.getConfig().isCustomDomain) $location.path('/'); });
$scope.defaultCert = {
error: null,
success: false,
busy: false,
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.adminCert = {
error: null,
success: false,
busy: false,
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.dnsCredentials = {
error: null,
success: false,
busy: false,
formVisible: false,
accessKeyId: '',
secretAccessKey: '',
provider: 'route53'
};
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
obj[file] = null;
obj[fileName] = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
obj[file] = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
}
document.getElementById('defaultCertFileInput').onchange = readFileLocally($scope.defaultCert, 'certificateFile', 'certificateFileName');
document.getElementById('defaultKeyFileInput').onchange = readFileLocally($scope.defaultCert, 'keyFile', 'keyFileName');
document.getElementById('adminCertFileInput').onchange = readFileLocally($scope.adminCert, 'certificateFile', 'certificateFileName');
document.getElementById('adminKeyFileInput').onchange = readFileLocally($scope.adminCert, 'keyFile', 'keyFileName');
$scope.setDefaultCert = function () {
$scope.defaultCert.busy = true;
$scope.defaultCert.error = null;
$scope.defaultCert.success = false;
Client.setCertificate($scope.defaultCert.certificateFile, $scope.defaultCert.keyFile, function (error) {
if (error) {
$scope.defaultCert.error = error.message;
} else {
$scope.defaultCert.success = true;
$scope.defaultCert.certificateFileName = '';
$scope.defaultCert.keyFileName = '';
}
$scope.defaultCert.busy = false;
});
};
$scope.setAdminCert = function () {
$scope.adminCert.busy = true;
$scope.adminCert.error = null;
$scope.adminCert.success = false;
Client.setAdminCertificate($scope.adminCert.certificateFile, $scope.adminCert.keyFile, function (error) {
if (error) {
$scope.adminCert.error = error.message;
} else {
$scope.adminCert.success = true;
$scope.adminCert.certificateFileName = '';
$scope.adminCert.keyFileName = '';
}
$scope.adminCert.busy = false;
// attempt to reload to make the browser get the new certs
window.location.reload(true);
});
};
$scope.setDnsCredentials = function () {
$scope.dnsCredentials.busy = true;
$scope.dnsCredentials.error = null;
$scope.dnsCredentials.success = false;
var data = {
provider: $scope.dnsCredentials.provider,
accessKeyId: $scope.dnsCredentials.accessKeyId,
secretAccessKey: $scope.dnsCredentials.secretAccessKey
};
Client.setDnsConfig(data, function (error) {
if (error) {
$scope.dnsCredentials.error = error.message;
} else {
$scope.dnsCredentials.success = true;
$scope.dnsConfig.accessKeyId = $scope.dnsCredentials.accessKeyId;
$scope.dnsConfig.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
$scope.dnsCredentials.accessKeyId = '';
$scope.dnsCredentials.secretAccessKey = '';
$('#collapseDnsCredentialsForm').collapse('hide');
$scope.dnsCredentials.formVisible = false;
// attempt to reload to make the browser get the new certs
window.location.reload(true);
}
$scope.dnsCredentials.busy = false;
});
};
$scope.showDnsCredentialsForm = function () {
$scope.dnsCredentials.busy = false;
$scope.dnsCredentials.success = false;
$scope.dnsCredentials.error = null;
$scope.dnsCredentials.accessKeyId = '';
$scope.dnsCredentials.secretAccessKey = '';
$scope.dnsCredentialsForm.$setPristine();
$scope.dnsCredentialsForm.$setUntouched();
$scope.dnsCredentials.formVisible = true;
$('#collapseDnsCredentialsForm').collapse('show');
$('#dnsCredentialsAccessKeyId').focus();
};
Client.onReady(function () {
Client.getDnsConfig(function (error, result) {
if (error) return console.error(error);
$scope.dnsConfig = result;
});
});
}]);
+28 -148
View File
@@ -85,6 +85,34 @@
</div> </div>
</div> </div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left">
<h3>About</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
<div class="row">
<div class="col-xs-4" style="min-width: 150px;">
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
<div class="overlay"></div>
</div>
</div>
<div class="col-xs-8">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Version</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
</tr>
</table>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin"> <div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left"> <div class="text-left">
<h3>Backups</h3> <h3>Backups</h3>
@@ -115,154 +143,6 @@
</div> </div>
</div> </div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left">
<h3>About</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
<div class="row">
<div class="col-xs-4" style="min-width: 150px;">
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
<div class="overlay"></div>
</div>
</div>
<div class="col-xs-8">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Version</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
</tr>
</table>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin && config.isCustomDomain">
<div class="text-left">
<h3>SSL Certificates</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin && config.isCustomDomain">
<div class="row">
<div class="col-md-12">
<form name="defaultCertForm" ng-submit="setDefaultCert()">
<fieldset>
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
<p>This certificate has to be wildcard certificates and will be used for all apps, which were not configured to use a specific certificate.</p>
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
<div class="input-group">
<input type="file" id="defaultCertFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
<div class="input-group">
<input type="file" id="defaultKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
</span>
</div>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="defaultCert.busy"></i> Upload</button>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form name="adminCertForm" ng-submit="setAdminCert()">
<fieldset>
<label class="control-label" for="adminCertInput">Settings Certificate</label>
<p>This certificate will be used for this Settings application.</p>
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
<div class="input-group">
<input type="file" id="adminCertFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
<div class="input-group">
<input type="file" id="adminKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
</span>
</div>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="adminCert.busy"></i> Upload</button>
</fieldset>
</form>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin && config.isCustomDomain">
<div class="text-left">
<h3>DNS Credentials</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin && config.isCustomDomain">
<div class="row">
<div class="col-md-12">
<p>Currently only Amazon <a href="https://aws.amazon.com/route53/">Route53</a> is supported. Let us know if you require a different DNS provider <a href="#/support">here</a>.</p>
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Access Key Id</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Secret Access Key</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;"><i>hidden</i></td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;"></td>
<td class="text-right" style="vertical-align: top;"><span class="text-success" ng-show="dnsCredentials.success"><b>Done</b></span> &nbsp; &nbsp; <button class="btn btn-outline btn-xs btn-primary" ng-show="!dnsCredentials.formVisible" ng-click="showDnsCredentialsForm()">Change</button></td>
</tr>
</table>
<div class="collapse" id="collapseDnsCredentialsForm" data-toggle="false">
<p>The security credentials have to be valid for full Route53 access.</p>
<form name="dnsCredentialsForm" ng-submit="setDnsCredentials()">
<fieldset>
<div class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</div>
<div class="form-group" ng-class="{ 'has-error': false }">
<label class="control-label" for="dnsCredentialsAccessKeyId">Access Key Id</label>
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" id="dnsCredentialsAccessKeyId" name="accessKeyId" ng-disabled="dnsCredentials.busy" ng-minlength="16" ng-maxlength="32" required>
</div>
<div class="form-group" ng-class="{ 'has-error': false }">
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret Access Key</label>
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" required>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="dnsCredentialsForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="dnsCredentials.busy"></i> Save</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin"> <div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left"> <div class="text-left">
<h3>Developer Mode</h3> <h3>Developer Mode</h3>
+1 -139
View File
@@ -84,124 +84,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
}] }]
}; };
$scope.defaultCert = {
error: null,
success: false,
busy: false,
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.adminCert = {
error: null,
success: false,
busy: false,
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.dnsCredentials = {
error: null,
success: false,
busy: false,
formVisible: false,
accessKeyId: '',
secretAccessKey: '',
provider: 'route53'
};
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
obj[file] = null;
obj[fileName] = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
obj[file] = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
}
document.getElementById('defaultCertFileInput').onchange = readFileLocally($scope.defaultCert, 'certificateFile', 'certificateFileName');
document.getElementById('defaultKeyFileInput').onchange = readFileLocally($scope.defaultCert, 'keyFile', 'keyFileName');
document.getElementById('adminCertFileInput').onchange = readFileLocally($scope.adminCert, 'certificateFile', 'certificateFileName');
document.getElementById('adminKeyFileInput').onchange = readFileLocally($scope.adminCert, 'keyFile', 'keyFileName');
$scope.setDefaultCert = function () {
$scope.defaultCert.busy = true;
$scope.defaultCert.error = null;
$scope.defaultCert.success = false;
Client.setCertificate($scope.defaultCert.certificateFile, $scope.defaultCert.keyFile, function (error) {
if (error) {
$scope.defaultCert.error = error.message;
} else {
$scope.defaultCert.success = true;
$scope.defaultCert.certificateFileName = '';
$scope.defaultCert.keyFileName = '';
}
$scope.defaultCert.busy = false;
});
};
$scope.setAdminCert = function () {
$scope.adminCert.busy = true;
$scope.adminCert.error = null;
$scope.adminCert.success = false;
Client.setAdminCertificate($scope.adminCert.certificateFile, $scope.adminCert.keyFile, function (error) {
if (error) {
$scope.adminCert.error = error.message;
} else {
$scope.adminCert.success = true;
$scope.adminCert.certificateFileName = '';
$scope.adminCert.keyFileName = '';
}
$scope.adminCert.busy = false;
});
};
$scope.setDnsCredentials = function () {
$scope.dnsCredentials.busy = true;
$scope.dnsCredentials.error = null;
$scope.dnsCredentials.success = false;
var data = {
provider: $scope.dnsCredentials.provider,
accessKeyId: $scope.dnsCredentials.accessKeyId,
secretAccessKey: $scope.dnsCredentials.secretAccessKey
};
Client.setDnsConfig(data, function (error) {
if (error) {
$scope.dnsCredentials.error = error.message;
} else {
$scope.dnsCredentials.success = true;
$scope.dnsConfig.accessKeyId = $scope.dnsCredentials.accessKeyId;
$scope.dnsConfig.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
$scope.dnsCredentials.accessKeyId = '';
$scope.dnsCredentials.secretAccessKey = '';
$('#collapseDnsCredentialsForm').collapse('hide');
$scope.dnsCredentials.formVisible = false;
}
$scope.dnsCredentials.busy = false;
});
};
$scope.setPreviewAvatar = function (avatar) { $scope.setPreviewAvatar = function (avatar) {
$scope.avatarChange.avatar = avatar; $scope.avatarChange.avatar = avatar;
}; };
@@ -347,20 +229,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
}); });
}; };
$scope.showDnsCredentialsForm = function () {
$scope.dnsCredentials.busy = false;
$scope.dnsCredentials.success = false;
$scope.dnsCredentials.error = null;
$scope.dnsCredentials.accessKeyId = '';
$scope.dnsCredentials.secretAccessKey = '';
$scope.dnsCredentialsForm.$setPristine();
$scope.dnsCredentialsForm.$setUntouched();
$scope.dnsCredentials.formVisible = true;
$('#collapseDnsCredentialsForm').collapse('show');
$('#dnsCredentialsAccessKeyId').focus();
};
$scope.showChangeDeveloperMode = function () { $scope.showChangeDeveloperMode = function () {
developerModeChangeReset(); developerModeChangeReset();
$('#developerModeChangeModal').modal('show'); $('#developerModeChangeModal').modal('show');
@@ -395,13 +263,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
Client.onReady(function () { Client.onReady(function () {
fetchBackups(); fetchBackups();
$scope.avatar.url = '//my-' + $scope.config.fqdn + '/api/v1/cloudron/avatar'; $scope.avatar.url = ($scope.config.isCustomDomain ? '//my.' : '//my-') + $scope.config.fqdn + '/api/v1/cloudron/avatar';
Client.getDnsConfig(function (error, result) {
if (error) return console.error(error);
$scope.dnsConfig = result;
});
}); });
// setup all the dialog focus handling // setup all the dialog focus handling
+1 -1
View File
@@ -38,6 +38,6 @@
<div class="row"> <div class="row">
<div class="col-md-12 text-center"> <div class="col-md-12 text-center">
<a class="btn btn-primary" href="#/step2">Next</a> <button class="btn btn-primary" ng-click="next()">Next</button>
</div> </div>
</div> </div>
+2 -2
View File
@@ -16,12 +16,12 @@
</div> </div>
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }"> <div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
<!-- <label class="control-label" for="inputPassword">Password</label> --> <!-- <label class="control-label" for="inputPassword">Password</label> -->
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next('/step3', setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off"> <input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next(setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12 text-center"> <div class="col-md-12 text-center">
<button class="btn btn-primary" ng-click="next('/step3', setup_form.username.$invalid || setup_form.password.$invalid)" ng-disabled="setup_form.username.$invalid || setup_form.password.$invalid">Done</button> <button class="btn btn-primary" ng-click="next(setup_form.username.$invalid || setup_form.password.$invalid)" ng-disabled="setup_form.username.$invalid || setup_form.password.$invalid">Done</button>
</div> </div>
</div> </div>
+2 -2
View File
@@ -16,12 +16,12 @@
</div> </div>
<div class="form-group" ng-class="{ 'has-error': setup_form.secretAccessKey.$dirty && setup_form.secretAccessKey.$invalid }"> <div class="form-group" ng-class="{ 'has-error': setup_form.secretAccessKey.$dirty && setup_form.secretAccessKey.$invalid }">
<!-- <label class="control-label" for="inputPassword">Password</label> --> <!-- <label class="control-label" for="inputPassword">Password</label> -->
<input type="text" class="form-control" ng-model="wizard.dnsConfig.secretAccessKey" id="inputSecretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-enter="next('/step4', setup_form.secretAccessKey.$invalid)" ng-maxlength="512" ng-minlength="3" required autocomplete="off"> <input type="text" class="form-control" ng-model="wizard.dnsConfig.secretAccessKey" id="inputSecretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-enter="next(setup_form.secretAccessKey.$invalid)" ng-maxlength="512" ng-minlength="3" required autocomplete="off">
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12 text-center"> <div class="col-md-12 text-center">
<button class="btn btn-primary" ng-click="next('/step4', setup_form.accessKeyId.$invalid || setup_form.secretAccessKey.$invalid)" ng-disabled="setup_form.accessKeyId.$invalid || setup_form.secretAccessKey.$invalid">Done</button> <button class="btn btn-primary" ng-click="next(setup_form.accessKeyId.$invalid || setup_form.secretAccessKey.$invalid)" ng-disabled="setup_form.accessKeyId.$invalid || setup_form.secretAccessKey.$invalid">Done</button>
</div> </div>
</div> </div>