Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37c8ba8ddd | |||
| f87011b5c2 | |||
| 7f149700f8 | |||
| 78ba9070fc | |||
| e31e5e1f69 | |||
| 31d9027677 | |||
| debcd6f353 | |||
| 5cb1681922 | |||
| 9074bccea0 | |||
| 291798f574 | |||
| b104843ae1 | |||
| dd062c656f | |||
| ae2eb718c6 | |||
| 7ac26bb653 | |||
| 41a726e8a7 | |||
| 4b69216548 | |||
| 99395ddf5a | |||
| 5f9fa5c352 | |||
| 9013331917 | |||
| 3a8f80477b | |||
| 813c680ed0 | |||
| a0eccd615f | |||
| 59be539ecd | |||
| a04740114c | |||
| 60b5d71c74 | |||
| 0a8b4b0c43 | |||
| ec21105c47 | |||
| 444258e7ee | |||
| e6fd05c2bd | |||
| 9fdcd452d0 | |||
| f39b9d5618 | |||
| 76e4c4919d | |||
| d1f159cdb4 | |||
| c63065e460 | |||
| 124c1d94a4 | |||
| e9161b726a | |||
| fd0d27b192 | |||
| 50064a40fe | |||
| c9bc5fc38e | |||
| 58f533fe50 | |||
| efcdffd8ff | |||
| 22793c3886 | |||
| 797ddbacc0 | |||
| e011962469 | |||
| b376ad9815 | |||
| 77248fe65c | |||
| 1dad115203 | |||
| 8812d58031 | |||
| fff7568f7e | |||
| ff6662579d | |||
| 0cf9fbd909 | |||
| 848b745fcb | |||
| 9a35c40b24 | |||
| 1f1e6124cd | |||
| 033df970ad | |||
| dd80a795a0 | |||
| 1eec6a39c6 | |||
| dd6b8face9 | |||
| 288de7e03a | |||
| a760ef4d22 | |||
| 0dd745bce4 | |||
| d4d5d371ac | |||
| 205bf4ddbd | |||
| 4ab84d42c6 | |||
| ee74badf3a | |||
| aa173ff74c | |||
| b584fc33f5 | |||
| 15c9d8682e | |||
| 361be8c26b | |||
| 4db9a5edd6 | |||
| bcc878da43 | |||
| 79f179fed4 | |||
| a924a9a627 | |||
| 45d444df0e | |||
| 92461a3366 | |||
| 032a430c51 | |||
| a6a3855e79 | |||
| 2386545814 | |||
| 2059152dd3 | |||
| 32d2c260ab | |||
| 384c7873aa |
Generated
+34
-10
@@ -7,6 +7,28 @@
|
||||
"from": "https://registry.npmjs.org/async/-/async-1.2.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz"
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.1.46",
|
||||
"from": "aws-sdk@*",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.46.tgz",
|
||||
"dependencies": {
|
||||
"sax": {
|
||||
"version": "0.5.3",
|
||||
"from": "sax@0.5.3",
|
||||
"resolved": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz"
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.2.8",
|
||||
"from": "xml2js@0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz"
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "0.4.2",
|
||||
"from": "xmlbuilder@0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.13.1",
|
||||
"from": "https://registry.npmjs.org/body-parser/-/body-parser-1.13.1.tgz",
|
||||
@@ -105,23 +127,24 @@
|
||||
}
|
||||
},
|
||||
"cloudron-manifestformat": {
|
||||
"version": "1.6.0",
|
||||
"from": "cloudron-manifestformat@1.6.0",
|
||||
"version": "1.7.0",
|
||||
"from": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-1.7.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-1.7.0.tgz",
|
||||
"dependencies": {
|
||||
"java-packagename-regex": {
|
||||
"version": "1.0.0",
|
||||
"from": "java-packagename-regex@>=1.0.0 <2.0.0",
|
||||
"from": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz"
|
||||
},
|
||||
"safetydance": {
|
||||
"version": "0.0.15",
|
||||
"from": "safetydance@0.0.15",
|
||||
"from": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz",
|
||||
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz"
|
||||
},
|
||||
"tv4": {
|
||||
"version": "1.1.12",
|
||||
"from": "tv4@>=1.1.9 <2.0.0",
|
||||
"resolved": "http://registry.npmjs.org/tv4/-/tv4-1.1.12.tgz"
|
||||
"version": "1.2.3",
|
||||
"from": "https://registry.npmjs.org/tv4/-/tv4-1.2.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.3.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -133,15 +156,16 @@
|
||||
"connect-lastmile": {
|
||||
"version": "0.0.13",
|
||||
"from": "connect-lastmile@0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.1.3",
|
||||
"from": "debug@>=2.1.0 <2.2.0",
|
||||
"from": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.7.0",
|
||||
"from": "ms@0.7.0",
|
||||
"from": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
|
||||
}
|
||||
}
|
||||
@@ -2244,7 +2268,7 @@
|
||||
},
|
||||
"safetydance": {
|
||||
"version": "0.0.19",
|
||||
"from": "safetydance@0.0.19",
|
||||
"from": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz",
|
||||
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
|
||||
},
|
||||
"semver": {
|
||||
|
||||
+2
-2
@@ -17,8 +17,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"cloudron-manifestformat": "^1.6.0",
|
||||
"cloudron-manifestformat": "^1.7.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "0.0.13",
|
||||
"connect-timeout": "^1.5.0",
|
||||
@@ -68,7 +69,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "*",
|
||||
"aws-sdk": "^2.1.10",
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
"del": "^1.1.1",
|
||||
"expect.js": "*",
|
||||
|
||||
+7
-7
@@ -7,11 +7,11 @@ INFRA_VERSION=8
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
BASE_IMAGE=cloudron/base:0.3.1
|
||||
MYSQL_IMAGE=cloudron/mysql:0.3.2
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.1
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.3.1
|
||||
REDIS_IMAGE=cloudron/redis:0.3.1 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.3.1
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.3.3
|
||||
BASE_IMAGE=cloudron/base:0.3.3
|
||||
MYSQL_IMAGE=cloudron/mysql:0.3.3
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.2
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.3.2
|
||||
REDIS_IMAGE=cloudron/redis:0.3.2 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.3.2
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.3.4
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ arg_tls_key=""
|
||||
arg_token=""
|
||||
arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_backup_key=""
|
||||
arg_aws=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
@@ -41,6 +43,12 @@ EOF
|
||||
arg_restore_key=$(echo "$2" | $json restoreKey)
|
||||
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
|
||||
|
||||
arg_backup_key=$(echo "$2" | $json backupKey)
|
||||
[[ "${arg_backup_key}" == "null" ]] && arg_backup_key=""
|
||||
|
||||
arg_aws=$(echo "$2" | $json aws)
|
||||
[[ "${arg_aws}" == "null" ]] && arg_aws=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
|
||||
+4
-1
@@ -122,6 +122,7 @@ 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}",
|
||||
@@ -138,7 +139,9 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
"password": "${mysql_root_password}",
|
||||
"port": 3306,
|
||||
"name": "box"
|
||||
}
|
||||
},
|
||||
"backupKey": "${arg_backup_key}",
|
||||
"aws": ${arg_aws}
|
||||
}
|
||||
CONF_END
|
||||
|
||||
|
||||
+2
-3
@@ -38,8 +38,7 @@ var appdb = require('./appdb.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
vbox = require('./vbox.js'),
|
||||
_ = require('underscore');
|
||||
vbox = require('./vbox.js');
|
||||
|
||||
var NOOP = function (app, callback) { return callback(); };
|
||||
|
||||
@@ -665,7 +664,7 @@ function setupRedis(app, callback) {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.3.1',
|
||||
Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Cmd: null,
|
||||
Volumes: {},
|
||||
VolumesFrom: []
|
||||
|
||||
+3
-1
@@ -35,12 +35,13 @@ exports = module.exports = {
|
||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||
ISTATE_INSTALLED: 'installed', // app is installed
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
RSTATE_RUNNING: 'running',
|
||||
RSTATE_PENDING_START: 'pending_start',
|
||||
RSTATE_PENDING_STOP: 'pending_stop',
|
||||
RSTATE_STOPPED: 'stopped', // app stopped by use
|
||||
RSTATE_ERROR: 'error',
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
HEALTH_HEALTHY: 'healthy',
|
||||
HEALTH_UNHEALTHY: 'unhealthy',
|
||||
HEALTH_ERROR: 'error',
|
||||
@@ -335,6 +336,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
||||
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// force update is allowed in any state including pending_uninstall! (for better or worse)
|
||||
// restore is allowed from installed or error state
|
||||
// update and configure are allowed only in installed state
|
||||
|
||||
|
||||
+27
-23
@@ -490,28 +490,30 @@ function restore(appId, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var restoreConfig = app.lastBackupConfig;
|
||||
if (!restoreConfig) return callback(new AppsError(AppsError.BAD_STATE, 'No restore point'));
|
||||
// restore without a backup is the same as re-install
|
||||
var restoreConfig = app.lastBackupConfig, values = { };
|
||||
if (restoreConfig) {
|
||||
// re-validate because this new box version may not accept old configs.
|
||||
// if we restore location, it should be validated here as well
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
||||
|
||||
// re-validate because this new box version may not accept old configs. if we restore location, it should be validated here as well
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
||||
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
|
||||
if (error) return callback(error);
|
||||
// ## should probably query new location, access restriction from user
|
||||
values = {
|
||||
manifest: restoreConfig.manifest,
|
||||
portBindings: restoreConfig.portBindings,
|
||||
|
||||
// ## should probably query new location, access restriction from user
|
||||
var values = {
|
||||
manifest: restoreConfig.manifest,
|
||||
portBindings: restoreConfig.portBindings,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
manifest: app.manifest
|
||||
}
|
||||
};
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
manifest: app.manifest
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
@@ -573,6 +575,8 @@ function stop(appId, callback) {
|
||||
}
|
||||
|
||||
function checkManifestConstraints(manifest) {
|
||||
if (!manifest.dockerImage) return new Error('Missing dockerImage'); // dockerImage is optional in manifest
|
||||
|
||||
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
|
||||
return new Error('Box version exceeds Apps maxBoxVersion');
|
||||
}
|
||||
@@ -664,7 +668,7 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
update(appId, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
|
||||
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
|
||||
if (error) debug('Error initiating autoupdate of %s', appId);
|
||||
|
||||
iteratorDone(null);
|
||||
@@ -700,7 +704,7 @@ function backupApp(app, addonsToBackup, callback) {
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
backups.getBackupUrl(app, null, function (error, result) {
|
||||
backups.getBackupUrl(app, function (error, result) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -709,7 +713,7 @@ function backupApp(app, addonsToBackup, callback) {
|
||||
async.series([
|
||||
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
|
||||
addons.backupAddons.bind(null, app, addonsToBackup),
|
||||
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey ]),
|
||||
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ]),
|
||||
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
|
||||
], function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -756,7 +760,7 @@ function restoreApp(app, addonsToRestore, callback) {
|
||||
|
||||
debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
|
||||
|
||||
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey ], function (error) {
|
||||
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
addons.restoreAddons(app, addonsToRestore, callback);
|
||||
|
||||
+50
-67
@@ -46,6 +46,7 @@ var addons = require('./addons.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
@@ -248,7 +249,7 @@ function deleteImage(app, manifest, callback) {
|
||||
noprune: false
|
||||
};
|
||||
|
||||
// delete image by id because docker pull pulls down all the tags and this is the only way to delete all tags
|
||||
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
|
||||
docker.getImage(result.Id).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
@@ -331,8 +332,12 @@ function startContainer(app, callback) {
|
||||
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
|
||||
|
||||
var startOptions = {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
Memory: memoryLimit / 2,
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: dockerPortBindings,
|
||||
PublishAllPorts: false,
|
||||
Links: addons.getLinksSync(app, app.manifest.addons),
|
||||
@@ -355,6 +360,11 @@ function startContainer(app, callback) {
|
||||
}
|
||||
|
||||
function stopContainer(app, callback) {
|
||||
if (!app.containerId) {
|
||||
debugApp(app, 'No previous container to stop');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
debugApp(app, 'Stopping container %s', container.id);
|
||||
|
||||
@@ -420,43 +430,27 @@ function registerSubdomain(app, callback) {
|
||||
// need to register it so that we have a dnsRecordId to wait for it to complete
|
||||
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/subdomains')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.send({ records: [ record ] })
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(error);
|
||||
subdomains.add(record, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'Registered subdomain status: %s', res.status);
|
||||
debugApp(app, 'Registered subdomain.');
|
||||
|
||||
if (res.status === 409) return callback(null); // already registered
|
||||
if (res.status !== 201) return callback(new Error(util.format('Subdomain Registration failed. %s %j', res.status, res.body)));
|
||||
|
||||
updateApp(app, { dnsRecordId: res.body.ids[0] }, callback);
|
||||
});
|
||||
updateApp(app, { dnsRecordId: changeId }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSubdomain(app, callback) {
|
||||
debugApp(app, 'Unregistering subdomain: dnsRecordId=%s', app.dnsRecordId);
|
||||
|
||||
if (!app.dnsRecordId) return callback(null);
|
||||
function unregisterSubdomain(app, location, callback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', location);
|
||||
|
||||
// do not unregister bare domain because we show a error/cloudron info page there
|
||||
if (app.location === '') return updateApp(app, { dnsRecordId: null }, callback);
|
||||
if (location === '') return callback(null);
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId)
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, res) {
|
||||
if (error) {
|
||||
debugApp(app, 'Error making request: %s', error);
|
||||
} else if (res.status !== 204) {
|
||||
debugApp(app, 'Error unregistering subdomain:', res.status, res.body);
|
||||
}
|
||||
var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
|
||||
subdomains.remove(record, function (error) {
|
||||
if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function removeIcon(app, callback) {
|
||||
@@ -477,21 +471,15 @@ function waitForDnsPropagation(app, callback) {
|
||||
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
|
||||
}
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId + '/status')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, res) {
|
||||
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
|
||||
subdomains.status(app.dnsRecordId, function (error, result) {
|
||||
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
|
||||
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, res.status);
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
|
||||
|
||||
if (res.status !== 200) return retry(new Error(util.format('Error getting record status: %s %j', res.status, res.body)));
|
||||
if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
|
||||
|
||||
if (res.body.status !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, res.body.status)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
@@ -530,9 +518,9 @@ function install(app, callback) {
|
||||
deleteContainer.bind(null, app),
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
deleteVolume.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
|
||||
@@ -617,7 +605,11 @@ function restore(app, callback) {
|
||||
// oldConfig can be null during upgrades
|
||||
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null),
|
||||
deleteVolume.bind(null, app),
|
||||
deleteImage.bind(null, app, app.manifest),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
|
||||
|
||||
deleteImage(app, app.oldConfig.manifest, done);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -671,16 +663,15 @@ function restore(app, callback) {
|
||||
|
||||
// note that configure is called after an infra update as well
|
||||
function configure(app, callback) {
|
||||
var locationChanged = app.oldConfig ? app.oldConfig.location !== app.location : true;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
unregisterSubdomain(app, next);
|
||||
// oldConfig can be null during an infra update
|
||||
if (!app.oldConfig || app.oldConfig.location === app.location) return next();
|
||||
unregisterSubdomain(app, app.oldConfig.location, next);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -691,14 +682,8 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app)
|
||||
], next);
|
||||
},
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app),
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
|
||||
@@ -712,14 +697,8 @@ function configure(app, callback) {
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app)
|
||||
], next);
|
||||
},
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -753,7 +732,11 @@ function update(app, callback) {
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
addons.teardownAddons.bind(null, app, unusedAddons),
|
||||
deleteImage.bind(null, app, app.manifest), // delete image even if did not change (see df158b111f)
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
|
||||
|
||||
deleteImage(app, app.oldConfig.manifest, done);
|
||||
},
|
||||
// removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time...
|
||||
|
||||
function (next) {
|
||||
@@ -819,7 +802,7 @@ function uninstall(app, callback) {
|
||||
deleteImage.bind(null, app, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
unregisterSubdomain.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
AWSError: AWSError,
|
||||
|
||||
getAWSCredentials: getAWSCredentials,
|
||||
|
||||
getSignedUploadUrl: getSignedUploadUrl,
|
||||
getSignedDownloadUrl: getSignedDownloadUrl,
|
||||
|
||||
addSubdomain: addSubdomain,
|
||||
delSubdomain: delSubdomain,
|
||||
getChangeStatus: getChangeStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:aws'),
|
||||
SubdomainError = require('./subdomainerror.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
function AWSError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(AWSError, Error);
|
||||
AWSError.INTERNAL_ERROR = 'Internal Error';
|
||||
AWSError.MISSING_CREDENTIALS = 'Missing AWS credentials';
|
||||
|
||||
function getAWSCredentials(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// CaaS
|
||||
if (config.token()) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
|
||||
superagent.get(url).query({ token: config.token() }).end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode !== 201) return callback(new Error(result.text));
|
||||
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
|
||||
|
||||
return callback(null, {
|
||||
accessKeyId: result.body.credentials.AccessKeyId,
|
||||
secretAccessKey: result.body.credentials.SecretAccessKey,
|
||||
sessionToken: result.body.credentials.SessionToken,
|
||||
region: 'us-east-1'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new AWSError(AWSError.MISSING_CREDENTIALS));
|
||||
|
||||
callback(null, {
|
||||
accessKeyId: config.aws().accessKeyId,
|
||||
secretAccessKey: config.aws().secretAccessKey,
|
||||
region: 'us-east-1'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getSignedUploadUrl(filename, callback) {
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getSignedUploadUrl()');
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: config.aws().backupBucket,
|
||||
Key: config.aws().backupPrefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('putObject', params);
|
||||
|
||||
callback(null, { url : url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function getSignedDownloadUrl(filename, callback) {
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getSignedDownloadUrl()');
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var s3 = new AWS.S3(credentials);
|
||||
|
||||
var params = {
|
||||
Bucket: config.aws().backupBucket,
|
||||
Key: config.aws().backupPrefix + '/' + filename,
|
||||
Expires: 60 * 30 /* 30 minutes */
|
||||
};
|
||||
|
||||
var url = s3.getSignedUrl('getObject', params);
|
||||
|
||||
callback(null, { url: url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function getZoneByName(zoneName, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getZoneByName: %s', zoneName);
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.listHostedZones({}, function (error, result) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
|
||||
var zone = result.HostedZones.filter(function (zone) {
|
||||
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
|
||||
})[0];
|
||||
|
||||
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
debug('getZoneByName: found zone', zone);
|
||||
|
||||
callback(null, zone);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
|
||||
|
||||
getZoneByName(zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'UPSERT',
|
||||
ResourceRecordSet: {
|
||||
Type: type,
|
||||
Name: fqdn,
|
||||
ResourceRecords: [{
|
||||
Value: value
|
||||
}],
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'PriorRequestNotComplete') {
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
} else if (error) {
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
}
|
||||
|
||||
debug('addSubdomain: success. changeInfoId:%j', result);
|
||||
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
|
||||
|
||||
getZoneByName(zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var resourceRecordSet = {
|
||||
Name: fqdn,
|
||||
Type: type,
|
||||
ResourceRecords: [{
|
||||
Value: value
|
||||
}],
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
};
|
||||
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'DELETE',
|
||||
ResourceRecordSet: resourceRecordSet
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('delSubdomain: resource record set not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('delSubdomain: hosted zone not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('delSubdomain: resource is still busy', error);
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('delSubdomain: invalid change batch. No such record to be deleted.');
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error) {
|
||||
debug('delSubdomain: error', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
}
|
||||
|
||||
debug('delSubdomain: success');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getChangeStatus(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (changeId === '') return callback(null, 'INSYNC');
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.getChange({ Id: changeId }, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result.ChangeInfo.Status);
|
||||
});
|
||||
});
|
||||
}
|
||||
+31
-22
@@ -10,6 +10,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
aws = require('./aws.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:backups'),
|
||||
superagent = require('superagent'),
|
||||
@@ -54,42 +55,50 @@ function getAllPaged(page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupUrl(app, appBackupIds, callback) {
|
||||
function getBackupUrl(app, callback) {
|
||||
assert(!app || typeof app === 'object');
|
||||
assert(!appBackupIds || util.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupurl';
|
||||
var filename = '';
|
||||
if (app) {
|
||||
filename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
} else {
|
||||
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
|
||||
}
|
||||
|
||||
var data = {
|
||||
boxVersion: config.version(),
|
||||
appId: app ? app.id : null,
|
||||
appVersion: app ? app.manifest.version : null,
|
||||
appBackupIds: appBackupIds
|
||||
};
|
||||
aws.getSignedUploadUrl(filename, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent.put(url).query({ token: config.token() }).send(data).end(function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
||||
var obj = {
|
||||
id: filename,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: config.backupKey()
|
||||
};
|
||||
|
||||
return callback(null, result.body);
|
||||
debug('getBackupUrl: ', obj);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
}
|
||||
|
||||
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
|
||||
function getRestoreUrl(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/restoreurl';
|
||||
aws.getSignedDownloadUrl(backupId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent.put(url).query({ token: config.token(), backupId: backupId }).end(function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
||||
var obj = {
|
||||
id: backupId,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: config.backupKey()
|
||||
};
|
||||
|
||||
return callback(null, result.body);
|
||||
debug('getRestoreUrl: ', obj);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
+27
-33
@@ -39,6 +39,7 @@ var apps = require('./apps.js'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
shell = require('./shell.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
@@ -46,7 +47,8 @@ var apps = require('./apps.js'),
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util');
|
||||
util = require('util'),
|
||||
webhooks = require('./webhooks.js');
|
||||
|
||||
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
@@ -271,8 +273,7 @@ function getConfig(callback) {
|
||||
function sendHeartbeat() {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
|
||||
// TODO: this must be a POST
|
||||
superagent.get(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
if (error) debug('Error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
@@ -305,22 +306,10 @@ function sendMailDnsRecordsRequest(callback) {
|
||||
|
||||
debug('sendMailDnsRecords request:%s', JSON.stringify(records));
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/subdomains')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.send({ records: records })
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendMailDnsRecords status: %s', res.status);
|
||||
|
||||
if (res.status === 409) return callback(null); // already registered
|
||||
|
||||
if (res.status !== 201) return callback(new Error(util.format('Failed to add Mail DNS records: %s %j', res.status, res.body)));
|
||||
|
||||
return callback(null, res.body.ids);
|
||||
});
|
||||
subdomains.addMany(records, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function addMailDnsRecords() {
|
||||
@@ -334,6 +323,7 @@ function addMailDnsRecords() {
|
||||
}
|
||||
|
||||
debug('Added Mail DNS records successfully');
|
||||
|
||||
config.set('mailDnsRecordIds', ids);
|
||||
});
|
||||
}
|
||||
@@ -492,19 +482,20 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
var args = {
|
||||
sourceTarballUrl: result.body.url,
|
||||
|
||||
// IMPORTANT: if you change this, fix up argparser.sh as well. keep these sorted for readability
|
||||
// this data is opaque to the installer
|
||||
data: {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
boxVersionsUrl: config.get('boxVersionsUrl'),
|
||||
fqdn: config.fqdn(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
restoreKey: null,
|
||||
restoreUrl: null,
|
||||
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||
token: config.token(),
|
||||
version: boxUpdateInfo.version,
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
token: config.token(),
|
||||
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
restoreUrl: null,
|
||||
restoreKey: null,
|
||||
aws: config.aws()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -561,7 +552,7 @@ function ensureBackup(callback) {
|
||||
function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
assert(util.isArray(appBackupIds));
|
||||
|
||||
backups.getBackupUrl(null /* app */, appBackupIds, function (error, result) {
|
||||
backups.getBackupUrl(null /* app */, function (error, result) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -569,14 +560,17 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
|
||||
async.series([
|
||||
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
|
||||
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey ]),
|
||||
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey, result.sessionToken ]),
|
||||
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
|
||||
], function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('backup: successful');
|
||||
|
||||
callback(null, result.id);
|
||||
webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, result.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -609,7 +603,7 @@ function backupBoxAndApps(callback) {
|
||||
++processed;
|
||||
|
||||
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up app at ' + app.location);
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
|
||||
|
||||
if (error && error.reason === AppsError.BAD_STATE) {
|
||||
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
|
||||
|
||||
@@ -30,6 +30,9 @@ exports = module.exports = {
|
||||
|
||||
isDev: isDev,
|
||||
|
||||
backupKey: backupKey,
|
||||
aws: aws,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: initConfig
|
||||
};
|
||||
@@ -70,6 +73,13 @@ function initConfig() {
|
||||
data.webServerOrigin = null;
|
||||
data.internalPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.backupKey = 'backupKey';
|
||||
data.aws = {
|
||||
backupBucket: null,
|
||||
backupPrefix: null,
|
||||
accessKeyId: null, // selfhosting only
|
||||
secretAccessKey: null // selfhosting only
|
||||
};
|
||||
|
||||
if (exports.CLOUDRON) {
|
||||
data.port = 3000;
|
||||
@@ -172,3 +182,10 @@ function isDev() {
|
||||
return /dev/i.test(get('boxVersionsUrl'));
|
||||
}
|
||||
|
||||
function backupKey() {
|
||||
return get('backupKey');
|
||||
}
|
||||
|
||||
function aws() {
|
||||
return get('aws');
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ var gLogger = {
|
||||
};
|
||||
|
||||
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
|
||||
var GROUP_ADMINS_DN = 'cn=admin,ou=groups,dc=cloudron';
|
||||
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
|
||||
|
||||
function start(callback) {
|
||||
assert(typeof callback === 'function');
|
||||
|
||||
@@ -171,6 +171,8 @@ function sendErrorPageOrRedirect(req, res, message) {
|
||||
}
|
||||
}
|
||||
|
||||
// use this instead of sendErrorPageOrRedirect(), in case we have a returnTo provided in the query, to avoid login loops
|
||||
// This usually happens when the OAuth client ID is wrong
|
||||
function sendError(req, res, message) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof res, 'object');
|
||||
|
||||
@@ -119,8 +119,8 @@ describe('Backups API', function () {
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] })
|
||||
.reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' });
|
||||
.get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.reply(201, { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' });
|
||||
|
||||
request.post(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
|
||||
@@ -450,8 +450,18 @@ describe('Cloudron', function () {
|
||||
});
|
||||
|
||||
it('fails when in wrong state', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { size: 'small', region: 'sfo', restoreKey: 'someid' }).reply(409, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }).reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' });
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
|
||||
.reply(201, { credentials: { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' } });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function () { return false; })
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN')
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope1 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function () { return false; })
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { }).reply(409, {});
|
||||
|
||||
injectShellMock();
|
||||
|
||||
@@ -463,7 +473,7 @@ describe('Cloudron', function () {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone()) {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
fi
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: backup.sh <appid> <url> <key>"
|
||||
echo "Usage: backupapp.sh <appid> <url> <key> [aws session token]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -22,6 +22,7 @@ readonly DATA_DIR="${HOME}/data"
|
||||
app_id="$1"
|
||||
backup_url="$2"
|
||||
backup_key="$3"
|
||||
session_token="$4"
|
||||
readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
|
||||
readonly app_data_dir="${DATA_DIR}/${app_id}"
|
||||
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
|
||||
@@ -31,9 +32,17 @@ btrfs subvolume snapshot -r "${app_data_dir}" "${app_data_snapshot}"
|
||||
for try in `seq 1 5`; do
|
||||
echo "Uploading backup to ${backup_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
|
||||
headers=("-H" "Content-Type:")
|
||||
|
||||
# federated tokens in CaaS case need session token
|
||||
if [ ! -z "$session_token" ]; then
|
||||
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
|
||||
fi
|
||||
|
||||
if tar -cvzf - -C "${app_data_snapshot}" . \
|
||||
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
|
||||
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
| curl --fail -X PUT "${headers[@]}" --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
|
||||
@@ -13,12 +13,13 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
fi
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: backupbox.sh <url> <key>"
|
||||
echo "Usage: backupbox.sh <url> <key> [aws session token]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backup_url="$1"
|
||||
backup_key="$2"
|
||||
session_token="$3"
|
||||
now=$(date "+%Y-%m-%dT%H:%M:%S")
|
||||
BOX_DATA_DIR="${HOME}/data/box"
|
||||
box_snapshot_dir="${HOME}/data/snapshots/box-${now}"
|
||||
@@ -32,9 +33,17 @@ btrfs subvolume snapshot -r "${BOX_DATA_DIR}" "${box_snapshot_dir}"
|
||||
for try in `seq 1 5`; do
|
||||
echo "Uploading backup to ${backup_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
|
||||
headers=("-H" "Content-Type:")
|
||||
|
||||
# federated tokens in CaaS case need session token
|
||||
if [ ! -z "$session_token" ]; then
|
||||
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
|
||||
fi
|
||||
|
||||
if tar -cvzf - -C "${box_snapshot_dir}" . \
|
||||
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
|
||||
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
| curl --fail -X PUT ${headers[@]} --data-binary @- "${backup_url}" 2>"${error_log}"; then
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
|
||||
@@ -13,7 +13,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
fi
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: restoreapp.sh <appid> <url> <key>"
|
||||
echo "Usage: restoreapp.sh <appid> <url> <key> [aws session token]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -23,6 +23,7 @@ readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max
|
||||
app_id="$1"
|
||||
restore_url="$2"
|
||||
restore_key="$3"
|
||||
session_token="$4"
|
||||
|
||||
echo "Downloading backup: ${restore_url} and key: ${restore_key}"
|
||||
|
||||
@@ -30,7 +31,14 @@ for try in `seq 1 5`; do
|
||||
echo "Download backup from ${restore_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
|
||||
if $curl -L "${restore_url}" \
|
||||
headers=("") # empty element required (http://stackoverflow.com/questions/7577052/bash-empty-array-expansion-with-set-u)
|
||||
|
||||
# federated tokens in CaaS case need session token
|
||||
if [[ ! -z "${session_token}" ]]; then
|
||||
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
|
||||
fi
|
||||
|
||||
if $curl -L "${headers[@]}" "${restore_url}" \
|
||||
| openssl aes-256-cbc -d -pass "pass:${restore_key}" \
|
||||
| tar -zxf - -C "${DATA_DIR}/${app_id}" 2>"${error_log}"; then
|
||||
chown -R yellowtent:yellowtent "${DATA_DIR}/${app_id}"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = SubdomainError;
|
||||
|
||||
function SubdomainError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(SubdomainError, Error);
|
||||
|
||||
SubdomainError.NOT_FOUND = 'No such domain';
|
||||
SubdomainError.INTERNAL_ERROR = 'Internal error';
|
||||
SubdomainError.EXTERNAL_ERROR = 'External error';
|
||||
SubdomainError.STILL_BUSY = 'Still busy';
|
||||
SubdomainError.FAILED_TOO_OFTEN = 'Failed too often';
|
||||
SubdomainError.ALREADY_EXISTS = 'Domain already exists';
|
||||
SubdomainError.BAD_FIELD = 'Bad Field';
|
||||
SubdomainError.BAD_STATE = 'Bad State';
|
||||
SubdomainError.INVALID_ZONE_NAME = 'Invalid domain name';
|
||||
SubdomainError.INVALID_TASK = 'Invalid task';
|
||||
@@ -0,0 +1,68 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
aws = require('./aws.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('server:subdomains'),
|
||||
util = require('util'),
|
||||
SubdomainError = require('./subdomainerror.js');
|
||||
|
||||
module.exports = exports = {
|
||||
add: add,
|
||||
addMany: addMany,
|
||||
remove: remove,
|
||||
status: status
|
||||
};
|
||||
|
||||
function add(record, callback) {
|
||||
assert.strictEqual(typeof record, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: ', record);
|
||||
|
||||
aws.addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
callback(null, changeId);
|
||||
});
|
||||
}
|
||||
|
||||
function addMany(records, callback) {
|
||||
assert(util.isArray(records));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addMany: ', records);
|
||||
|
||||
async.eachSeries(function (record, callback) {
|
||||
add(record, callback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function remove(record, callback) {
|
||||
assert.strictEqual(typeof record, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('remove: ', record);
|
||||
|
||||
aws.delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
|
||||
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
|
||||
|
||||
debug('deleteSubdomain: successfully deleted subdomain from aws.');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function status(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('status: ', changeId);
|
||||
|
||||
aws.getChangeStatus(changeId, function (error, status) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
callback(null, status === 'INSYNC' ? 'done' : 'pending');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
backupDone: backupDone
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:webhooks'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
function backupDone(filename, app, appBackupIds, callback) {
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
assert(!app || typeof app === 'object');
|
||||
assert(!appBackupIds || util.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('backupDone():', filename);
|
||||
|
||||
// CaaS
|
||||
if (config.token()) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupDone';
|
||||
var data = {
|
||||
boxVersion: config.version(),
|
||||
restoreKey: filename,
|
||||
appId: app ? app.id : null,
|
||||
appVersion: app ? app.manifest.version : null,
|
||||
appBackupIds: appBackupIds
|
||||
};
|
||||
|
||||
superagent.post(url).send(data).query({ token: config.token() }).end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new Error(result.text));
|
||||
if (!result.body) return callback(new Error('Unexpected response'));
|
||||
|
||||
debug('backupDone()', filename);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
} else {
|
||||
// TODO call custom webhook
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,13 @@
|
||||
|
||||
<title> Cloudron App Error </title>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- external fonts and CSS -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
|
||||
<title> Cloudron Error </title>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- external fonts and CSS -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
@@ -48,9 +51,6 @@
|
||||
<!-- Main Application -->
|
||||
<script src="js/index.js"></script>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -81,7 +81,7 @@
|
||||
<li ng-repeat="change in config.update.box.changelog">{{change}}</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<fieldset>
|
||||
<fieldset ng-show="installedApps | readyToUpdate">
|
||||
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
|
||||
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
|
||||
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy" ng-show="installedApps | readyToUpdate"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
|
||||
<title> Cloudron </title>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- external fonts and CSS -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
<title> Cloudron Setup </title>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
@@ -31,9 +34,6 @@
|
||||
<!-- Setup Application -->
|
||||
<script src="js/setup.js"></script>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupController">
|
||||
|
||||
@@ -195,8 +195,8 @@ html {
|
||||
|
||||
.appstore-item-badge-testing {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
right: 0;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.appstore-item-content-testing {
|
||||
@@ -330,6 +330,10 @@ html {
|
||||
background-color: #5CB85C;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #EFBD48;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: $brand-danger;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
<title> Cloudron Update </title>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
|
||||
@@ -23,9 +26,6 @@
|
||||
<!-- Update Application -->
|
||||
<script src="js/update.js"></script>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link href="theme.css" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="Controller" style="background-color: #7F7F7F">
|
||||
|
||||
@@ -269,3 +269,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offset the footer -->
|
||||
<br/><br/>
|
||||
|
||||
@@ -147,8 +147,9 @@
|
||||
<div class="col-md-10" ng-show="ready && apps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-1 appstore-item" ng-repeat="app in apps">
|
||||
<div class="appstore-item-content highlight" ng-click="showInstall(app)" ng-class="{ 'appstore-item-content-testing': app.publishState === 'testing' }">
|
||||
<div class="appstore-item-content highlight" ng-click="showInstall(app)" ng-class="{ 'appstore-item-content-testing': (app.publishState === 'testing' || app.publishState === 'pending_approval') }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.publishState === 'testing'">Testing</span>
|
||||
<span class="badge badge-warning appstore-item-badge-testing" ng-show="app.publishState === 'pending_approval'">Pending Approval</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
var options = {
|
||||
scaleOverride: true,
|
||||
scaleSteps: 10,
|
||||
scaleStepWidth: $scope.activeApp === 'system' ? 100 : 10,
|
||||
scaleStepWidth: $scope.activeApp === 'system' ? 200 : 20,
|
||||
scaleStartValue: 0
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user