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

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

View File

@@ -36,12 +36,7 @@ function main() {
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
mailer.initialize(function (error) {
if (error) return console.error(error);
sendCrashNotification(processName);
});
sendCrashNotification(processName);
}
main();

5296
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"type": "git"
},
"engines": [
"node >= 0.12.0"
"node >=4.0.0 <=4.1.1"
],
"dependencies": {
"async": "^1.2.1",

View File

@@ -16,8 +16,7 @@ arg_tls_key=""
arg_token=""
arg_version=""
arg_web_server_origin=""
arg_backup_key=""
arg_aws=""
arg_backup_config=""
arg_dns_config=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
@@ -38,17 +37,14 @@ EOF
arg_tls_cert=$(echo "$2" | $json tlsCert)
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_key=$(echo "$2" | $json restoreKey)
arg_restore_key=$(echo "$2" | $json restore.key)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_backup_key=$(echo "$2" | $json backupKey)
[[ "${arg_backup_key}" == "null" ]] && arg_backup_key=""
arg_aws=$(echo "$2" | $json aws)
[[ "${arg_aws}" == "null" ]] && arg_aws=""
arg_backup_config=$(echo "$2" | $json backupConfig)
[[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""

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"
# 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" \
-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"
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert
echo "${arg_tls_key}" > ${DATA_DIR}/nginx/cert/host.key
if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
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"
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
set -eu
echo "Creating cloudron.conf"
# note that arg_aws is a javascript object and intentionally unquoted below
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
@@ -141,9 +151,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"password": "${mysql_root_password}",
"port": 3306,
"name": "box"
},
"backupKey": "${arg_backup_key}",
"aws": ${arg_aws}
}
}
CONF_END
@@ -155,6 +163,14 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
CONF_END
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
if [[ ! -z "${arg_dns_config}" ]]; then
echo "Add DNS Config"

View File

@@ -391,7 +391,7 @@ function setupSendMail(app, options, callback) {
var env = [
'MAIL_SMTP_SERVER=mail',
'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()
];

View File

@@ -110,7 +110,11 @@ function processApps(callback) {
async.each(apps, checkAppHealth, function (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);
});
@@ -151,7 +155,7 @@ function processDockerEvents() {
debug('OOM Context: %s', context);
// 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
});
});

View File

@@ -52,9 +52,10 @@ var addons = require('./addons.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js').connection,
docker = require('./docker.js'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
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'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -511,7 +514,7 @@ function getLogs(appId, lines, follow, callback) {
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: source || 'main'
});
}) + '\n';
});
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) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var container = docker.getContainer(app.containerId);
var execOptions = {
var createOptions = {
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: cmd
OpenStdin: true,
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));
var startOptions = {
Detach: false,
Tty: true,
stdin: true // this is a dockerode option that enabled openStdin in the modem
};
exec.start(startOptions, function(error, stream) {
container.attach({ stream: true, stdin: true, stdout: true, stderr: true }, function (error, stream) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}
docker.startContainer(container.id, function (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);
});
});
});
});

View File

@@ -51,7 +51,7 @@ var addons = require('./addons.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
SubdomainError = require('./subdomainerror.js'),
SubdomainError = require('./subdomains.js').SubdomainError,
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),

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);
});
}

View File

@@ -12,10 +12,11 @@ exports = module.exports = {
};
var assert = require('assert'),
aws = require('./aws.js'),
caas = require('./storage/caas.js'),
config = require('./config.js'),
debug = require('debug')('box:backups'),
superagent = require('superagent'),
s3 = require('./storage/s3.js'),
settings = require('./settings.js'),
util = require('util');
function BackupsError(reason, errorOrMessage) {
@@ -39,21 +40,30 @@ function BackupsError(reason, errorOrMessage) {
util.inherits(BackupsError, Error);
BackupsError.EXTERNAL_ERROR = 'external 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) {
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';
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) {
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'));
api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
return callback(null, result.body.backups);
return callback(null, backups); // [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first
});
});
}
@@ -68,19 +78,23 @@ function getBackupUrl(app, callback) {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
}
aws.getSignedUploadUrl(filename, function (error, result) {
if (error) return callback(error);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var obj = {
id: filename,
url: result.url,
sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
if (error) return callback(error);
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 callback, 'function');
aws.getSignedDownloadUrl(backupId, function (error, result) {
if (error) return callback(error);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var obj = {
id: backupId,
url: result.url,
sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
api(backupConfig.provider).getSignedDownloadUrl(backupConfig, backupId, function (error, result) {
if (error) return callback(error);
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');
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);
});
});
}

View File

@@ -243,6 +243,8 @@ function getStatus(callback) {
callback(null, {
activated: count !== 0,
version: config.version(),
boxVersionsUrl: config.get('boxVersionsUrl'),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
cloudronName: cloudronName
});
});
@@ -272,7 +274,7 @@ function getConfig(callback) {
getCloudronDetails(function (error, result) {
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
result = {
@@ -393,6 +395,7 @@ function addDnsRecords() {
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
records.push(nakedDomainRecord);
records.push(webadminRecord);
@@ -561,19 +564,31 @@ function doUpdate(boxUpdateInfo, callback) {
// this data is opaque to the installer
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(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
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,
webServerOrigin: config.webServerOrigin()
boxVersionsUrl: config.get('boxVersionsUrl')
}
};

View File

@@ -33,9 +33,6 @@ exports = module.exports = {
isDev: isDev,
backupKey: backupKey,
aws: aws,
// for testing resets to defaults
_reset: initConfig
};
@@ -75,9 +72,7 @@ function initConfig() {
data.fqdn = 'localhost';
data.token = null;
data.mailServer = null;
data.adminEmail = null;
data.mailDnsRecordIds = [ ];
data.boxVersionsUrl = null;
data.version = null;
data.isCustomDomain = false;
@@ -86,13 +81,6 @@ function initConfig() {
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004;
data.backupKey = 'backupKey';
data.aws = {
backupBucket: null,
backupPrefix: null,
accessKeyId: null, // selfhosting only
secretAccessKey: null // selfhosting only
};
if (exports.CLOUDRON) {
data.port = 3000;
@@ -109,9 +97,6 @@ function initConfig() {
name: 'boxtest'
};
data.token = 'APPSTORE_TOKEN';
data.aws.backupBucket = 'testbucket';
data.aws.backupPrefix = 'testprefix';
data.aws.endpoint = 'http://localhost:5353';
} else {
assert(false, 'Unknown environment. This should not happen!');
}
@@ -204,11 +189,3 @@ function database() {
function isDev() {
return /dev/i.test(get('boxVersionsUrl'));
}
function backupKey() {
return get('backupKey');
}
function aws() {
return get('aws');
}

View File

@@ -13,12 +13,13 @@ exports = module.exports = {
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
SubdomainError = require('../subdomainerror.js'),
SubdomainError = require('../subdomains.js').SubdomainError,
superagent = require('superagent'),
util = require('util'),
_ = 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 subdomain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -36,7 +37,7 @@ function add(zoneName, subdomain, type, values, callback) {
superagent
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token() })
.query({ token: dnsConfig.token })
.send(data)
.end(function (error, result) {
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 subdomain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -59,7 +61,7 @@ function get(zoneName, subdomain, type, callback) {
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token(), type: type })
.query({ token: dnsConfig.token, type: type })
.end(function (error, result) {
if (error) return callback(error);
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 subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
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 (_.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 subdomain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -100,7 +104,7 @@ function del(zoneName, subdomain, type, values, callback) {
superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
.query({ token: config.token() })
.query({ token: dnsConfig.token })
.send(data)
.end(function (error, result) {
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 callback, 'function');
@@ -120,7 +125,7 @@ function getChangeStatus(changeId, callback) {
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
.query({ token: config.token() })
.query({ token: dnsConfig.token })
.end(function (error, result) {
if (error) return callback(error);
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));

View File

@@ -14,52 +14,45 @@ var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
settings = require('../settings.js'),
SubdomainError = require('../subdomainerror.js'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util'),
_ = require('underscore');
function getDnsCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
var credentials = {
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region
};
var credentials = {
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region
};
if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint);
if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint);
callback(null, credentials);
});
return credentials;
}
function getZoneByName(zoneName, callback) {
function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
var route53 = new AWS.Route53(credentials);
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
})[0];
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
})[0];
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
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 subdomain, '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);
getZoneByName(zoneName, function (error, zone) {
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
@@ -89,48 +82,44 @@ function add(zoneName, subdomain, type, values, callback) {
HostedZoneId: zone.Id
};
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
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);
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);
});
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 subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
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 (_.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 subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(zoneName, function (error, zone) {
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
@@ -140,33 +129,28 @@ function get(zoneName, subdomain, type, callback) {
StartRecordType: type
};
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
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);
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; });
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 subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
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);
var fqdn = config.appFqdn(subdomain);
@@ -189,48 +173,42 @@ function del(zoneName, subdomain, type, values, callback) {
HostedZoneId: zone.Id
};
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
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);
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);
});
callback(null);
});
});
}
function getChangeStatus(changeId, callback) {
function getChangeStatus(dnsConfig, changeId, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
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);
var route53 = new AWS.Route53(credentials);
route53.getChange({ Id: changeId }, function (error, result) {
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);
});
callback(null, result.ChangeInfo.Status);
});
}

View File

@@ -8,7 +8,8 @@ var addons = require('./addons.js'),
Docker = require('dockerode'),
safe = require('safetydance'),
semver = require('semver'),
util = require('util');
util = require('util'),
_ = require('underscore');
exports = module.exports = {
connection: connectionInstance(),
@@ -64,18 +65,17 @@ function pullImage(manifest, callback) {
// is emitted as a chunk
stream.on('data', function (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
if (data.status) {
// debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
} 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 () {
debug('downloaded image %s successfully', manifest.dockerImage);
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
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.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);
});
});
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);
});
@@ -102,12 +102,12 @@ function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
debug('downloadImage %s', manifest.dockerImage);
debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
var attempt = 1;
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
debug('Downloading image. attempt: %s', attempt++);
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
debug('Downloading image %s %s. attempt: %s', manifest.id, manifest.dockerImage, attempt++);
pullImage(manifest, function (error) {
if (error) console.error(error);
@@ -117,10 +117,11 @@ function downloadImage(manifest, callback) {
}, callback);
}
function createSubcontainer(app, name, cmd, callback) {
function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert(!cmd || util.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
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
}
};
containerOptions = _.extend(containerOptions, options);
// older versions wanted a writable /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);
});
}
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) {

View File

@@ -125,11 +125,23 @@ function checkDns() {
}
debug('checkDns: SPF check passed. commencing mail processing');
gDnsReady = true;
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) {
if (error) return console.error(error);
@@ -141,12 +153,9 @@ function processQueue() {
port: 2500 // this value comes from mail container
}));
var mailQueueCopy = gMailQueue;
gMailQueue = [ ];
debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp);
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
async.mapSeries(queue, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) {
if (error) return console.error(error); // TODO: requeue?
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) {
assert.strictEqual(typeof program, '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' })
};
enqueue(mailOptions);
sendMails([ mailOptions ]);
}
function sendFeedback(user, type, subject, description) {

View File

@@ -6,6 +6,8 @@
<title> Cloudron Login </title>
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Custom Fonts -->
<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">

View File

@@ -28,7 +28,5 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/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')
};

View File

@@ -368,6 +368,8 @@ function exec(req, res, next) {
duplexStream.pipe(res.socket);
res.socket.pipe(duplexStream);
res.on('close', duplexStream.close);
});
}

View File

@@ -15,6 +15,9 @@ exports = module.exports = {
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
setCertificate: setCertificate,
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
function setCertificate(req, res, next) {
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'));
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));
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'));
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));
next(new HttpSuccess(202, {}));

View File

@@ -146,12 +146,17 @@ function setup(done) {
callback(null);
});
}, function (callback) {
},
function (callback) {
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...)
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);
}
@@ -590,8 +595,6 @@ describe('App installation', function () {
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
function (callback) {
awsHockInstance
.get('/2013-04-01/hostedzone')

View File

@@ -13,6 +13,7 @@ var appdb = require('../../appdb.js'),
expect = require('expect.js'),
request = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
nock = require('nock'),
userdb = require('../../userdb.js');
@@ -52,6 +53,10 @@ function setup(done) {
function addApp(callback) {
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);
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
}
], done);
}
@@ -70,7 +75,7 @@ describe('Backups API', function () {
describe('get', function () {
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')
.query({ access_token: token })
@@ -82,7 +87,7 @@ describe('Backups API', function () {
});
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')
.query({ access_token: token })
@@ -119,7 +124,7 @@ describe('Backups API', function () {
it('succeeds', function (done) {
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' } });
request.post(SERVER_URL + '/api/v1/backups')
@@ -141,4 +146,3 @@ describe('Backups API', function () {
});
});
});

View File

@@ -282,6 +282,19 @@ describe('Cloudron', function () {
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);
});
@@ -341,7 +354,6 @@ describe('Cloudron', function () {
});
});
it('fails with wrong region type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
@@ -355,7 +367,7 @@ describe('Cloudron', function () {
it('fails when in wrong state', function (done) {
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' } });
var scope3 = nock(config.apiServerOrigin())
@@ -391,7 +403,6 @@ describe('Cloudron', function () {
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
@@ -404,7 +415,7 @@ describe('Cloudron', function () {
.reply(200, { id: 'someid' });
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' } });
injectShellMock();

View File

@@ -215,7 +215,7 @@ describe('Settings API', function () {
it('set succeeds', function (done) {
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token })
.attach('avatar', paths.FAVICON_FILE)
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
@@ -227,7 +227,7 @@ describe('Settings API', function () {
.query({ access_token: token })
.end(function (err, res) {
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);
});
});

View File

@@ -66,8 +66,6 @@ start_mongodb() {
}
start_mail() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
docker rm -f mail 2>/dev/null 1>&2 || true
docker run -dP --name=mail -e DOMAIN_NAME="localhost" \

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);
// 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;
saveState(gState);

View File

@@ -20,7 +20,6 @@ var assert = require('assert'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
paths = require('./paths.js'),
routes = require('./routes/index.js'),
taskmanager = require('./taskmanager.js');
@@ -53,7 +52,6 @@ function initializeExpressSync() {
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
.use(middleware.favicon(paths.FAVICON_FILE)) // used when serving oauth login page
.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(passport.initialize())
@@ -162,6 +160,8 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig);
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/admin_certificate', settingsScope, routes.settings.setAdminCertificate);

View File

@@ -23,6 +23,9 @@ exports = module.exports = {
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getDefaultSync: getDefaultSync,
getAll: getAll,
@@ -35,6 +38,7 @@ exports = module.exports = {
CLOUDRON_NAME_KEY: 'cloudron_name',
DEVELOPER_MODE_KEY: 'developer_mode',
DNS_CONFIG_KEY: 'dns_config',
BACKUP_CONFIG_KEY: 'backup_config',
events: new (require('events').EventEmitter)()
};
@@ -62,6 +66,7 @@ var gDefaults = (function () {
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false;
result[exports.DNS_CONFIG_KEY] = { };
result[exports.BACKUP_CONFIG_KEY] = { };
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) {
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) {
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
@@ -303,7 +338,7 @@ function validateCertificate(cert, key, fqdn) {
try {
content = x509.parseCert(cert);
} catch (e) {
return new Error('invalid cert');
return new Error('invalid cert: ' + e.message);
}
// check expiration
@@ -318,7 +353,7 @@ function validateCertificate(cert, key, fqdn) {
// check domain
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
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());
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, 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));
}
// backup the cert
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)) {
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
}
// copy over fallback cert
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) {
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);
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(keyFilePath, key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));

129
src/storage/caas.js Normal file
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
src/storage/s3.js Normal file
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);
});
}

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';

View File

@@ -2,24 +2,58 @@
'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 = {
add: add,
remove: remove,
status: status,
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
function api() {
return config.isCustomDomain() || config.TEST ? route53 : caas;
function api(provider) {
assert.strictEqual(typeof provider, 'string');
switch (provider) {
case 'caas': return caas;
case 'route53': return route53;
default: return null;
}
}
function add(subdomain, type, values, callback) {
@@ -28,9 +62,13 @@ function add(subdomain, type, values, callback) {
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
api().add(config.zoneName(), subdomain, type, values, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
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 callback, 'function');
api().get(config.zoneName(), subdomain, type, function (error, values) {
if (error) return callback(error);
settings.getDnsConfig(function (error, dnsConfig) {
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.strictEqual(typeof callback, 'function');
api().update(config.zoneName(), subdomain, type, values, function (error) {
if (error) return callback(error);
settings.getDnsConfig(function (error, dnsConfig) {
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.strictEqual(typeof callback, 'function');
api().del(config.zoneName(), subdomain, type, values, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
settings.getDnsConfig(function (error, dnsConfig) {
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 callback, 'function');
api().getChangeStatus(changeId, function (error, status) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending');
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
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');
});
});
}

View File

@@ -9,6 +9,7 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:taskmanager'),
@@ -39,14 +40,11 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
for (var appId in gActiveTasks) {
stopAppTask(appId);
}
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
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 ]);
var pid = gActiveTasks[appId].pid;
debug('Started task of %s pid: %s', appId, pid);
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
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 callback, 'function');
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
delete gActiveTasks[appId];
} else if (gPendingTasks.indexOf(appId) !== -1) {
debug('stopAppTask: Removing existing pending task : %s', appId);
gPendingTasks = _.without(gPendingTasks, appId);
return;
}
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) {
stopAppTask(appId);
startAppTask(appId);
}
function restartAppTask(appId, callback) {
callback = callback || NOOP_CALLBACK;
async.series([
stopAppTask.bind(null, appId),
startAppTask.bind(null, appId)
], callback);
}

View File

@@ -220,7 +220,7 @@ describe('apptask', function () {
it('unregisters subdomain', function (done) {
nock.cleanAll();
var awsScope = nock(config.aws().endpoint)
var awsScope = nock('http://localhost:5353')
.get('/2013-04-01/hostedzone')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/')

View File

@@ -35,28 +35,40 @@ for script in "${scripts[@]}"; do
fi
done
image_missing=""
if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull "${TEST_IMAGE}" for tests to run"
exit 1
echo "docker pull ${TEST_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${REDIS_IMAGE} for tests to run"
exit 1
echo "docker pull ${REDIS_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MYSQL_IMAGE} for tests to run"
exit 1
echo "docker pull ${MYSQL_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${POSTGRESQL_IMAGE} for tests to run"
exit 1
echo "docker pull ${POSTGRESQL_IMAGE}"
image_missing="true"
fi
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
fi

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) {
settings.getAll(function (error, allSettings) {
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

View File

@@ -147,6 +147,7 @@
<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><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 class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>

View File

@@ -31,6 +31,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/graphs', {
controller: 'GraphsController',
templateUrl: 'views/graphs.html'
}).when('/certs', {
controller: 'CertsController',
templateUrl: 'views/certs.html'
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html'

View File

@@ -150,8 +150,24 @@ app.service('Wizard', [ function () {
app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) {
$scope.wizard = Wizard;
$scope.next = function (page, bad) {
if (!bad) $location.path(page);
$scope.next = function (bad) {
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) {
@@ -194,7 +210,7 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
image = null;
};
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
}
@@ -213,6 +229,11 @@ app.controller('FinishController', ['$scope', '$location', 'Wizard', 'Client', f
Client.changeCloudronAvatar($scope.wizard.avatarBlob, function (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) {
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,
secretAccessKey: null
};
} else {
Wizard.dnsConfig = {
provider: 'caas',
accessKeyId: '',
secretAccessKey: ''
};
}
Client.isServerFirstTime(function (error, isFirstTime) {

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
webadmin/src/views/certs.js Normal file
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;
});
});
}]);

View File

@@ -85,6 +85,34 @@
</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 class="text-left">
<h3>Backups</h3>
@@ -115,154 +143,6 @@
</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 class="text-left">
<h3>Developer Mode</h3>

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.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 () {
developerModeChangeReset();
$('#developerModeChangeModal').modal('show');
@@ -395,13 +263,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
Client.onReady(function () {
fetchBackups();
$scope.avatar.url = '//my-' + $scope.config.fqdn + '/api/v1/cloudron/avatar';
Client.getDnsConfig(function (error, result) {
if (error) return console.error(error);
$scope.dnsConfig = result;
});
$scope.avatar.url = ($scope.config.isCustomDomain ? '//my.' : '//my-') + $scope.config.fqdn + '/api/v1/cloudron/avatar';
});
// setup all the dialog focus handling

View File

@@ -38,6 +38,6 @@
<div class="row">
<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>

View File

@@ -16,12 +16,12 @@
</div>
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
<!-- <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 class="row">
<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>

View File

@@ -16,12 +16,12 @@
</div>
<div class="form-group" ng-class="{ 'has-error': setup_form.secretAccessKey.$dirty && setup_form.secretAccessKey.$invalid }">
<!-- <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 class="row">
<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>