Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bd9173a9d | ||
|
|
0cef3e1090 | ||
|
|
6bd68961d1 | ||
|
|
7f8ad917d9 | ||
|
|
7cd89accaf | ||
|
|
ffee084d2b | ||
|
|
2bb657a733 | ||
|
|
bc48171626 | ||
|
|
50924b0cd3 | ||
|
|
3d86950cc9 | ||
|
|
db9ddf9969 | ||
|
|
1b507370dc | ||
|
|
3c5e221c39 | ||
|
|
9c37f35d5a | ||
|
|
4044070d76 | ||
|
|
8f05917d97 | ||
|
|
3766d67daa | ||
|
|
b1290c073e | ||
|
|
36daf86ea2 | ||
|
|
4fb07a6ab3 | ||
|
|
8f2119272b | ||
|
|
ee5bd456e0 | ||
|
|
9c549ed4d8 | ||
|
|
61fc8b7968 | ||
|
|
6b30d65e05 | ||
|
|
10c876ac75 | ||
|
|
0966bd0bb1 | ||
|
|
294d1bfca4 | ||
|
|
af1d1236ea | ||
|
|
eaf9febdfd | ||
|
|
8748226ef3 | ||
|
|
73568777c0 | ||
|
|
c64697dde7 | ||
|
|
0701e38a04 | ||
|
|
2a27d96e08 | ||
|
|
ba42611701 | ||
|
|
54486138f0 | ||
|
|
13d3f506b0 | ||
|
|
32ca686e1f | ||
|
|
a5ef9ff372 | ||
|
|
738bfa7601 | ||
|
|
40cdd270b1 | ||
|
|
53a2a8015e | ||
|
|
15aaa440a2 | ||
|
|
d8a4014eff | ||
|
|
d25d423ccd | ||
|
|
49b0fde18b | ||
|
|
8df7f17186 | ||
|
|
adc395f888 | ||
|
|
e770664365 | ||
|
|
05d4ad3b5d | ||
|
|
cc6f726f71 | ||
|
|
a4923f894c | ||
|
|
12200f2e0d | ||
|
|
a853afc407 | ||
|
|
de471b0012 | ||
|
|
b6f1ad75b8 | ||
|
|
e6840f352d | ||
|
|
6456874f97 | ||
|
|
66b4a4b02a | ||
|
|
7e36b3f8e5 | ||
|
|
12061cc707 | ||
|
|
afcc62ecf6 | ||
|
|
bec6850c98 | ||
|
|
d253a06bab | ||
|
|
857c5c69b1 | ||
|
|
766fc49f39 | ||
|
|
941e09ca9f | ||
|
|
2466a97fb8 | ||
|
|
81f92f5182 | ||
|
|
91e1d442ff | ||
|
|
a1d6ae2296 | ||
|
|
b529fd3bea | ||
|
|
bf319cf593 | ||
|
|
15eedd2a84 | ||
|
|
d0cd3d1c32 | ||
|
|
747786d0c8 | ||
|
|
b232255170 | ||
|
|
bd2982ea69 | ||
|
|
1c948cc83c | ||
|
|
ccde1e51ad | ||
|
|
03ec940352 | ||
|
|
bd5b15e279 | ||
|
|
b6897a4577 | ||
|
|
f7225523ec | ||
|
|
9d9509525c | ||
|
|
b1dbb3570b | ||
|
|
c075160e5d | ||
|
|
612ceba98a | ||
|
|
7d5e0040bc | ||
|
|
d6e19d2000 | ||
|
|
a01d5db2a0 | ||
|
|
5de3baffd4 | ||
|
|
63c10e8f02 | ||
|
|
a99e7c2783 | ||
|
|
88b1cc553f | ||
|
|
316e8dedd3 | ||
|
|
f106a76cd5 | ||
|
|
95b2bea828 |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -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
@@ -10,7 +10,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"engines": [
|
||||
"node >= 0.12.0"
|
||||
"node >=4.0.0 <=4.1.1"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
|
||||
@@ -16,8 +16,8 @@ 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" -- "$@")
|
||||
eval set -- "${args}"
|
||||
@@ -37,17 +37,17 @@ 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_backup_config=$(echo "$2" | $json backupConfig)
|
||||
[[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
|
||||
|
||||
arg_aws=$(echo "$2" | $json aws)
|
||||
[[ "${arg_aws}" == "null" ]] && arg_aws=""
|
||||
arg_dns_config=$(echo "$2" | $json dnsConfig)
|
||||
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
|
||||
@@ -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,22 @@ 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"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
# Add webadmin oauth client
|
||||
# The domain might have changed, therefor we have to update the record
|
||||
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
|
||||
|
||||
@@ -9,6 +9,7 @@ exports = module.exports = {
|
||||
getEnvironment: getEnvironment,
|
||||
getLinksSync: getLinksSync,
|
||||
getBindsSync: getBindsSync,
|
||||
getContainerNamesSync: getContainerNamesSync,
|
||||
|
||||
// exported for testing
|
||||
_setupOauth: setupOauth,
|
||||
@@ -239,6 +240,27 @@ function getBindsSync(app, addons) {
|
||||
return binds;
|
||||
}
|
||||
|
||||
function getContainerNamesSync(app, addons) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
|
||||
var names = [ ];
|
||||
|
||||
if (!addons) return names;
|
||||
|
||||
for (var addon in addons) {
|
||||
switch (addon) {
|
||||
case 'scheduler':
|
||||
// names here depend on how scheduler.js creates containers
|
||||
names = names.concat(Object.keys(addons.scheduler).map(function (taskName) { return app.id + '-' + taskName; }));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
function setupOauth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -369,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()
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
116
src/apps.js
@@ -23,7 +23,6 @@ exports = module.exports = {
|
||||
backup: backup,
|
||||
backupApp: backupApp,
|
||||
|
||||
getLogStream: getLogStream,
|
||||
getLogs: getLogs,
|
||||
|
||||
start: start,
|
||||
@@ -53,15 +52,17 @@ 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'),
|
||||
settings = require('./settings.js'),
|
||||
semver = require('semver'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
@@ -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');
|
||||
|
||||
@@ -475,58 +478,50 @@ function update(appId, force, manifest, portBindings, icon, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLogStream(appId, fromLine, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof fromLine, 'number'); // behaves like tail -n
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
function appLogFilter(app) {
|
||||
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
var tail = fromLine < 0 ? -fromLine : 'all';
|
||||
|
||||
// note: cannot access docker file directly because it needs root access
|
||||
container.logs({ stdout: true, stderr: true, follow: true, timestamps: true, tail: tail }, function (error, logStream) {
|
||||
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var lineCount = 0;
|
||||
var skipLinesStream = split(function mapper(line) {
|
||||
if (++lineCount < fromLine) return undefined;
|
||||
var timestamp = line.substr(0, line.indexOf(' ')); // sometimes this has square brackets around it
|
||||
return JSON.stringify({ lineNumber: lineCount, timestamp: timestamp.replace(/[[\]]/g,''), log: line.substr(timestamp.length + 1) });
|
||||
});
|
||||
skipLinesStream.close = logStream.req.abort;
|
||||
logStream.pipe(skipLinesStream);
|
||||
return callback(null, skipLinesStream);
|
||||
});
|
||||
});
|
||||
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
|
||||
}
|
||||
|
||||
function getLogs(appId, callback) {
|
||||
function getLogs(appId, lines, follow, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof follow, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
// note: cannot access docker file directly because it needs root access
|
||||
container.logs({ stdout: true, stderr: true, follow: false, timestamps: true, tail: 'all' }, function (error, logStream) {
|
||||
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args = args.concat(appLogFilter(app));
|
||||
|
||||
return callback(null, logStream);
|
||||
var cp = spawn('/bin/journalctl', args);
|
||||
|
||||
var transformStream = split(function mapper(line) {
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
|
||||
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: source || 'main'
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
cp.stdout.pipe(transformStream);
|
||||
|
||||
return callback(null, transformStream);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -651,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
|
||||
119
src/aws.js
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
112
src/cloudron.js
@@ -19,11 +19,12 @@ exports = module.exports = {
|
||||
backup: backup,
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
isActivatedSync: isActivatedSync,
|
||||
isConfiguredSync: isConfiguredSync,
|
||||
|
||||
events: new (require('events').EventEmitter)(),
|
||||
|
||||
EVENT_ACTIVATED: 'activated'
|
||||
EVENT_ACTIVATED: 'activated',
|
||||
EVENT_CONFIGURED: 'configured'
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
@@ -64,7 +65,7 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
var gUpdatingDns = false, // flag for dns update reentrancy
|
||||
gCloudronDetails = null, // cached cloudron details like region,size...
|
||||
gIsActivated = null; // cached activation state so that return value is synchronous. null means we are not initialized yet
|
||||
gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
@@ -115,27 +116,59 @@ CloudronError.NOT_FOUND = 'Not found';
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.events.on(settings.DNS_CONFIG_KEY, function() { addDnsRecords(); });
|
||||
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||
|
||||
userdb.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
gIsActivated = count !== 0;
|
||||
|
||||
if (gIsActivated) addDnsRecords(); // reboot/restore/upgrade
|
||||
|
||||
callback(null);
|
||||
});
|
||||
syncConfigState(callback);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events.removeListener(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function isActivatedSync() {
|
||||
return gIsActivated === true;
|
||||
function isConfiguredSync() {
|
||||
return gIsConfigured === true;
|
||||
}
|
||||
|
||||
function isConfigured(callback) {
|
||||
// set of rules to see if we have the configs required for cloudron to function
|
||||
// note this checks for missing configs and not invalid configs
|
||||
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!dnsConfig) return callback(null, false);
|
||||
|
||||
var isConfigured = (config.isCustomDomain() && dnsConfig.provider === 'route53') ||
|
||||
(!config.isCustomDomain() && dnsConfig.provider === 'caas');
|
||||
|
||||
callback(null, isConfigured);
|
||||
});
|
||||
}
|
||||
|
||||
function syncConfigState(callback) {
|
||||
assert(!gIsConfigured);
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
isConfigured(function (error, configured) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('syncConfigState: configured = %s', configured);
|
||||
|
||||
if (configured) {
|
||||
exports.events.emit(exports.EVENT_CONFIGURED);
|
||||
} else {
|
||||
settings.events.once(settings.DNS_CONFIG_KEY, function () { syncConfigState(); }); // check again later
|
||||
}
|
||||
|
||||
gIsConfigured = configured;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function setTimeZone(ip, callback) {
|
||||
@@ -189,8 +222,6 @@ function activate(username, password, email, ip, callback) {
|
||||
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
gIsActivated = true;
|
||||
|
||||
// EE API is sync. do not keep the REST API reponse waiting
|
||||
process.nextTick(function () { exports.events.emit(exports.EVENT_ACTIVATED); });
|
||||
|
||||
@@ -212,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
|
||||
});
|
||||
});
|
||||
@@ -241,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 = {
|
||||
@@ -328,15 +361,15 @@ function txtRecordsWithSpf(callback) {
|
||||
if (i == txtRecords.length) {
|
||||
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ~all"';
|
||||
} else {
|
||||
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + txtRecords[i].slice('"v=spf1"'.length);
|
||||
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
|
||||
}
|
||||
|
||||
return callback(null, txtRecords);
|
||||
});
|
||||
}
|
||||
|
||||
function addDnsRecords(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
function addDnsRecords() {
|
||||
var callback = NOOP_CALLBACK;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
@@ -362,6 +395,7 @@ function addDnsRecords(callback) {
|
||||
var records = [ ];
|
||||
if (config.isCustomDomain()) {
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
} else {
|
||||
records.push(nakedDomainRecord);
|
||||
records.push(webadminRecord);
|
||||
@@ -381,7 +415,11 @@ function addDnsRecords(callback) {
|
||||
|
||||
async.eachSeries(records, function (record, iteratorCallback) {
|
||||
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
|
||||
}, retryCallback);
|
||||
}, function (error) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
gUpdatingDns = false;
|
||||
@@ -526,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')
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
14
src/cron.js
@@ -38,9 +38,6 @@ var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
|
||||
gHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
onTick: cloudron.sendHeartbeat,
|
||||
@@ -48,7 +45,7 @@ function initialize(callback) {
|
||||
});
|
||||
cloudron.sendHeartbeat(); // latest unpublished version of CronJob has runOnInit
|
||||
|
||||
if (cloudron.isActivatedSync()) {
|
||||
if (cloudron.isConfiguredSync()) {
|
||||
recreateJobs(callback);
|
||||
} else {
|
||||
cloudron.events.on(cloudron.EVENT_ACTIVATED, recreateJobs);
|
||||
@@ -110,14 +107,20 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
});
|
||||
|
||||
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
||||
|
||||
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
function autoupdatePatternChanged(pattern) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert(gBoxUpdateCheckerJob);
|
||||
|
||||
debug('Auto update pattern changed to %s', pattern);
|
||||
|
||||
@@ -149,6 +152,9 @@ function uninitialize(callback) {
|
||||
|
||||
cloudron.events.removeListener(cloudron.EVENT_ACTIVATED, recreateJobs);
|
||||
|
||||
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
|
||||
if (gAutoupdaterJob) gAutoupdaterJob.stop();
|
||||
gAutoupdaterJob = null;
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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);
|
||||
@@ -82,8 +75,6 @@ function add(zoneName, subdomain, type, values, callback) {
|
||||
Type: type,
|
||||
Name: fqdn,
|
||||
ResourceRecords: records,
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
}
|
||||
}]
|
||||
@@ -91,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 = {
|
||||
@@ -142,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);
|
||||
@@ -178,8 +160,6 @@ function del(zoneName, subdomain, type, values, callback) {
|
||||
Name: fqdn,
|
||||
Type: type,
|
||||
ResourceRecords: records,
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
};
|
||||
|
||||
@@ -193,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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,9 +117,11 @@ function downloadImage(manifest, callback) {
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function createSubcontainer(app, 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,
|
||||
@@ -157,6 +159,7 @@ function createSubcontainer(app, cmd, callback) {
|
||||
if (error) return callback(new Error('Error getting addon environment : ' + error));
|
||||
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
// do _not_ set hostname to app fqdn. doing so sets up the dns name to look up the internal docker ip. this makes curl from within container fail
|
||||
Hostname: semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location),
|
||||
Tty: isAppContainer,
|
||||
@@ -190,18 +193,19 @@ function createSubcontainer(app, 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, null, callback);
|
||||
createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */, callback);
|
||||
}
|
||||
|
||||
function startContainer(containerId, callback) {
|
||||
|
||||
@@ -48,10 +48,10 @@ var gMailQueue = [ ],
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (cloudron.isActivatedSync()) {
|
||||
if (cloudron.isConfiguredSync()) {
|
||||
checkDns();
|
||||
} else {
|
||||
cloudron.events.on(cloudron.EVENT_ACTIVATED, checkDns);
|
||||
cloudron.events.on(cloudron.EVENT_CONFIGURED, checkDns);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -60,7 +60,7 @@ function initialize(callback) {
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
cloudron.events.removeListener(cloudron.EVENT_ACTIVATED, checkDns);
|
||||
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, checkDns);
|
||||
|
||||
// TODO: interrupt processQueue as well
|
||||
clearTimeout(gCheckDnsTimerId);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
|
||||
@@ -286,14 +286,14 @@ function getLogStream(req, res, next) {
|
||||
|
||||
debug('Getting logstream of app id:%s', req.params.id);
|
||||
|
||||
var fromLine = req.query.fromLine ? parseInt(req.query.fromLine, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(fromLine)) return next(new HttpError(400, 'fromLine must be a valid number'));
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
|
||||
apps.getLogStream(req.params.id, fromLine, function (error, logStream) {
|
||||
apps.getLogs(req.params.id, lines, true /* follow */, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -309,7 +309,7 @@ function getLogStream(req, res, next) {
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.lineNumber, JSON.stringify(obj)));
|
||||
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
@@ -319,9 +319,12 @@ function getLogStream(req, res, next) {
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
debug('Getting logs of app id:%s', req.params.id);
|
||||
|
||||
apps.getLogs(req.params.id, function (error, logStream) {
|
||||
apps.getLogs(req.params.id, lines, false /* follow */, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -365,6 +368,8 @@ function exec(req, res, next) {
|
||||
|
||||
duplexStream.pipe(res.socket);
|
||||
res.socket.pipe(duplexStream);
|
||||
|
||||
res.on('close', duplexStream.close);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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, {}));
|
||||
|
||||
@@ -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')
|
||||
@@ -838,7 +841,7 @@ describe('App installation', function () {
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('logs - stdout and stderr', function (done) {
|
||||
xit('logs - stdout and stderr', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
@@ -852,7 +855,7 @@ describe('App installation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('logStream - requires event-stream accept header', function (done) {
|
||||
xit('logStream - requires event-stream accept header', function (done) {
|
||||
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logstream')
|
||||
.query({ access_token: token, fromLine: 0 })
|
||||
.end(function (err, res) {
|
||||
@@ -862,7 +865,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
|
||||
it('logStream - stream logs', function (done) {
|
||||
xit('logStream - stream logs', function (done) {
|
||||
var options = {
|
||||
port: config.get('port'), host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token,
|
||||
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
|
||||
|
||||
@@ -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 () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -180,7 +180,8 @@ function doTask(appId, taskName, callback) {
|
||||
|
||||
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
|
||||
|
||||
docker.createSubcontainer(app, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) {
|
||||
// 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 ], { } /* options */, function (error, container) {
|
||||
appState.containerIds[taskName] = container.id;
|
||||
|
||||
saveState(gState);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ 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'),
|
||||
locker = require('./locker.js'),
|
||||
_ = require('underscore');
|
||||
@@ -18,12 +20,38 @@ var gActiveTasks = { };
|
||||
var gPendingTasks = [ ];
|
||||
|
||||
var TASK_CONCURRENCY = 5;
|
||||
var NOOP_CALLBACK = function (error) { console.error(error); };
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// resume app installs and uninstalls
|
||||
locker.on('unlocked', startNextTask);
|
||||
|
||||
if (cloudron.isConfiguredSync()) {
|
||||
resumeTasks();
|
||||
} else {
|
||||
cloudron.events.on(cloudron.EVENT_CONFIGURED, resumeTasks);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
|
||||
|
||||
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
|
||||
locker.removeListener('unlocked', startNextTask);
|
||||
|
||||
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
|
||||
}
|
||||
|
||||
|
||||
// resume app installs and uninstalls
|
||||
function resumeTasks(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -36,21 +64,6 @@ function initialize(callback) {
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
locker.on('unlocked', startNextTask);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
|
||||
for (var appId in gActiveTasks) {
|
||||
stopAppTask(appId);
|
||||
}
|
||||
|
||||
locker.removeListener('unlocked', startNextTask);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function startNextTask() {
|
||||
@@ -80,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);
|
||||
}
|
||||
@@ -90,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);
|
||||
}
|
||||
|
||||
@@ -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/')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1021 B |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -120,8 +120,8 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand navbar-brand-icon" href="index.html"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="index.html">Cloudron</a>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="#/">Cloudron</a>
|
||||
</div>
|
||||
<!-- /.navbar-header -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
125
webadmin/src/views/certs.html
Normal 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> <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
@@ -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;
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -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> <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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||