Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f19113f88e | |||
| 3837bee51f | |||
| 89c3296632 | |||
| db55f0696e | |||
| 03d4ae9058 | |||
| f8b41b703c | |||
| 2a989e455c | |||
| cd24decca0 | |||
| f39842a001 | |||
| 2a39526a4c | |||
| ded5d4c98b | |||
| a0ca59c3f2 | |||
| 53cfc49807 | |||
| 942eb579e4 | |||
| 5819cfe412 | |||
| 5cb62ca412 | |||
| df10c245de | |||
| 4a804dc52b | |||
| ed2f25a998 | |||
| 7510c9fe29 | |||
| 78a1d53728 | |||
| e9b078cd58 | |||
| dd8b928684 | |||
| 185b574bdc | |||
| a89726a8c6 | |||
| c80aca27e6 | |||
| 029acab333 | |||
| 4f9f10e130 | |||
| 9ba11d2e14 | |||
| 23a5a1f79f | |||
| e8dc617d40 | |||
| d56794e846 | |||
| 2663ec7da0 | |||
| eec4ae98cd | |||
| c31a0f4e09 | |||
| 739db23514 | |||
| 8598fb444b | |||
| 0b630ff504 | |||
| 84169dea3d | |||
| d83b5de47a | |||
| 2719c4240f | |||
| d749756b53 | |||
| 0401c61c15 | |||
| 34f45da2de | |||
| baecbf783c |
@@ -4,6 +4,12 @@
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
@@ -30,7 +36,7 @@ async.series([
|
||||
server.start,
|
||||
ldap.start,
|
||||
appHealthMonitor.start,
|
||||
oauthproxy.start.bind(null, 4000 /* port */)
|
||||
oauthproxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:janitor'),
|
||||
async = require('async'),
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
"engines": [
|
||||
"node >= 0.12.0"
|
||||
],
|
||||
"bin": {
|
||||
"cloudron": "./app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"aws-sdk": "^2.1.46",
|
||||
|
||||
+8
-8
@@ -3,15 +3,15 @@
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=9
|
||||
INFRA_VERSION=11
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
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
|
||||
BASE_IMAGE=cloudron/base:0.4.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.4.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.4.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.4.0
|
||||
REDIS_IMAGE=cloudron/redis:0.4.0 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.4.0
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.4.0
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
|
||||
rm -rf /etc/collectd
|
||||
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
||||
|
||||
########## apparmor docker profile
|
||||
cp "${container_files}/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
systemctl restart apparmor
|
||||
|
||||
########## nginx
|
||||
# link nginx config to system config
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#include <tunables/global>
|
||||
|
||||
|
||||
profile docker-cloudron-app flags=(attach_disconnected,mediate_deleted) {
|
||||
|
||||
#include <abstractions/base>
|
||||
|
||||
ptrace peer=@{profile_name},
|
||||
|
||||
network,
|
||||
capability,
|
||||
file,
|
||||
umount,
|
||||
|
||||
deny @{PROC}/sys/fs/** wklx,
|
||||
deny @{PROC}/sysrq-trigger rwklx,
|
||||
deny @{PROC}/mem rwklx,
|
||||
deny @{PROC}/kmem rwklx,
|
||||
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
|
||||
deny @{PROC}/sys/kernel/*/** wklx,
|
||||
|
||||
deny mount,
|
||||
|
||||
deny /sys/[^f]*/** wklx,
|
||||
deny /sys/f[^s]*/** wklx,
|
||||
deny /sys/fs/[^c]*/** wklx,
|
||||
deny /sys/fs/c[^g]*/** wklx,
|
||||
deny /sys/fs/cg[^r]*/** wklx,
|
||||
deny /sys/firmware/efi/efivars/** rwklx,
|
||||
deny /sys/kernel/security/** rwklx,
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ server {
|
||||
}
|
||||
|
||||
<% } else if ( endpoint === 'oauthproxy' ) { %>
|
||||
proxy_pass http://127.0.0.1:4000;
|
||||
proxy_pass http://127.0.0.1:3003;
|
||||
proxy_set_header X-Cloudron-Proxy-Port <%= port %>;
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
|
||||
+4
-2
@@ -289,7 +289,9 @@ function setupLdap(app, callback) {
|
||||
'LDAP_PORT=3002',
|
||||
'LDAP_URL=ldap://172.17.42.1:3002',
|
||||
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron'
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
|
||||
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
|
||||
'LDAP_BIND_PASSWORD=' + hat(256) // this is ignored
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up LDAP');
|
||||
@@ -664,7 +666,7 @@ function setupRedis(app, callback) {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Image: 'cloudron/redis:0.4.0', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Cmd: null,
|
||||
Volumes: {},
|
||||
VolumesFrom: []
|
||||
|
||||
+68
-28
@@ -150,6 +150,8 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
2020, /* install server */
|
||||
config.get('port'), /* app server (lo) */
|
||||
config.get('internalPort'), /* internal app server (lo) */
|
||||
config.get('ldapPort'), /* ldap server (lo) */
|
||||
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
|
||||
3306, /* mysql (lo) */
|
||||
8000 /* graphite (lo) */
|
||||
];
|
||||
@@ -688,33 +690,35 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function backupApp(app, addonsToBackup, callback) {
|
||||
function canBackupApp(app) {
|
||||
// only backup apps that are installed or pending configure. Rest of them are in some
|
||||
// state not good for consistent backup (i.e addons may not have been setup completely)
|
||||
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|
||||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|
||||
app.installationState === appdb.ISTATE_PENDING_BACKUP ||
|
||||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
|
||||
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
|
||||
function reuseOldBackup(app, callback) {
|
||||
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
backups.copyLastBackup(app, function (error, newBackupId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'reuseOldBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
|
||||
|
||||
callback(null, newBackupId);
|
||||
});
|
||||
}
|
||||
|
||||
function createNewBackup(app, addonsToBackup, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addonsToBackup || typeof addonsToBackup, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function canBackupApp(app) {
|
||||
// only backup apps that are installed or pending configure. Rest of them are in some
|
||||
// state not good for consistent backup (i.e addons may not have been setup completely)
|
||||
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|
||||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|
||||
app.installationState === appdb.ISTATE_PENDING_BACKUP ||
|
||||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy'));
|
||||
|
||||
var appConfig = {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction
|
||||
};
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -729,13 +733,49 @@ function backupApp(app, addonsToBackup, callback) {
|
||||
], function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'backupApp: successful id:%s', result.id);
|
||||
callback(null, result.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setRestorePoint(app.id, result.id, appConfig, function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
function backupApp(app, addonsToBackup, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addonsToBackup || typeof addonsToBackup, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback(null, result.id);
|
||||
});
|
||||
var appConfig = null, backupFunction;
|
||||
|
||||
if (!canBackupApp(app)) {
|
||||
if (!app.lastBackupId) {
|
||||
debugApp(app, 'backupApp: cannot backup app');
|
||||
return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy and never backed up previously'));
|
||||
}
|
||||
|
||||
appConfig = app.lastBackupConfig;
|
||||
backupFunction = reuseOldBackup.bind(null, app);
|
||||
} else {
|
||||
appConfig = {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction
|
||||
};
|
||||
backupFunction = createNewBackup.bind(null, app, addonsToBackup);
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
return callback(safe.error);
|
||||
}
|
||||
}
|
||||
|
||||
backupFunction(function (error, backupId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'backupApp: successful id:%s', backupId);
|
||||
|
||||
setRestorePoint(app.id, backupId, appConfig, function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, backupId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+27
-17
@@ -25,6 +25,12 @@ exports = module.exports = {
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
@@ -126,11 +132,9 @@ function unconfigureNginx(app, callback) {
|
||||
vbox.unforwardFromHostToVirtualBox(app.id + '-http');
|
||||
}
|
||||
|
||||
function downloadImage(app, callback) {
|
||||
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
|
||||
|
||||
function pullImage(app, callback) {
|
||||
docker.pull(app.manifest.dockerImage, function (err, stream) {
|
||||
if (err) return callback(new Error('Error connecting to docker'));
|
||||
if (err) return callback(new Error('Error connecting to docker. statusCode: %s' + err.statusCode));
|
||||
|
||||
// https://github.com/dotcloud/docker/issues/1074 says each status message
|
||||
// is emitted as a chunk
|
||||
@@ -152,25 +156,30 @@ function downloadImage(app, callback) {
|
||||
var image = docker.getImage(app.manifest.dockerImage);
|
||||
|
||||
image.inspect(function (err, data) {
|
||||
if (err) {
|
||||
return callback(new Error('Error inspecting image:' + err.message));
|
||||
}
|
||||
|
||||
if (!data || !data.Config) {
|
||||
return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
|
||||
}
|
||||
|
||||
if (!data.Config.Entrypoint && !data.Config.Cmd) {
|
||||
return callback(new Error('Only images with entry point are allowed'));
|
||||
}
|
||||
if (err) return callback(new Error('Error inspecting image:' + err.message));
|
||||
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'));
|
||||
|
||||
debugApp(app, 'This image exposes ports: %j', data.Config.ExposedPorts);
|
||||
return callback(null);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function downloadImage(app, callback) {
|
||||
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
|
||||
|
||||
var attempt = 1;
|
||||
|
||||
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
|
||||
debugApp(app, 'Downloading image. attempt: %s', attempt++);
|
||||
|
||||
pullImage(app, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
appdb.getPortBindings(app.id, function (error, portBindings) {
|
||||
if (error) return callback(error);
|
||||
@@ -346,7 +355,8 @@ function startContainer(app, callback) {
|
||||
"Name": "always",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
CpuShares: 512 // relative to 1024 for system processes
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
|
||||
};
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
|
||||
+22
-1
@@ -8,7 +8,9 @@ exports = module.exports = {
|
||||
|
||||
addSubdomain: addSubdomain,
|
||||
delSubdomain: delSubdomain,
|
||||
getChangeStatus: getChangeStatus
|
||||
getChangeStatus: getChangeStatus,
|
||||
|
||||
copyObject: copyObject
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -259,3 +261,22 @@ function getChangeStatus(changeId, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyObject(from, to, callback) {
|
||||
assert.strictEqual(typeof from, 'string');
|
||||
assert.strictEqual(typeof to, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAWSCredentials(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);
|
||||
});
|
||||
}
|
||||
|
||||
+17
-2
@@ -6,7 +6,9 @@ exports = module.exports = {
|
||||
getAllPaged: getAllPaged,
|
||||
|
||||
getBackupUrl: getBackupUrl,
|
||||
getRestoreUrl: getRestoreUrl
|
||||
getRestoreUrl: getRestoreUrl,
|
||||
|
||||
copyLastBackup: copyLastBackup
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -76,7 +78,7 @@ function getBackupUrl(app, callback) {
|
||||
backupKey: config.backupKey()
|
||||
};
|
||||
|
||||
debug('getBackupUrl: ', obj);
|
||||
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
@@ -102,3 +104,16 @@ function getRestoreUrl(backupId, callback) {
|
||||
callback(null, obj);
|
||||
});
|
||||
}
|
||||
|
||||
function copyLastBackup(app, callback) {
|
||||
assert(app && typeof app === 'object');
|
||||
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
+4
-2
@@ -22,7 +22,9 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||
|
||||
debug('addSubdomain: zoneName: %s subdomain: %s type: %s value: %s fqdn: %s', zoneName, subdomain, type, value, fqdn);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
@@ -30,7 +32,7 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
};
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
|
||||
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||
.query({ token: config.token() })
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
|
||||
+24
-28
@@ -19,7 +19,8 @@ exports = module.exports = {
|
||||
reboot: reboot,
|
||||
migrate: migrate,
|
||||
backup: backup,
|
||||
ensureBackup: ensureBackup};
|
||||
ensureBackup: ensureBackup
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
AppsError = require('./apps.js').AppsError,
|
||||
@@ -138,7 +139,7 @@ function setTimeZone(ip, callback) {
|
||||
}
|
||||
|
||||
if (!result.body.timezone) {
|
||||
debug('No timezone in geoip response');
|
||||
debug('No timezone in geoip response : %j', result.body);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ function activate(username, password, email, name, ip, callback) {
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { });
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
if (!name) name = settings.getDefaultSync(settings.CLOUDRON_NAME_KEY);
|
||||
|
||||
@@ -323,31 +324,23 @@ function addDnsRecords() {
|
||||
function checkIfInSync() {
|
||||
debug('addDnsRecords: Check if admin DNS record is in sync.');
|
||||
|
||||
var allDone = true;
|
||||
|
||||
async.each(changeIds, function (changeId, callback) {
|
||||
async.eachSeries(changeIds, function (changeId, callback) {
|
||||
subdomains.status(changeId, function (error, result) {
|
||||
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
|
||||
|
||||
if (result !== 'done') allDone = false;
|
||||
if (result !== 'done') return callback(new Error(changeId + ' is not in sync. result:' + result));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// retry if needed
|
||||
if (error || !allDone) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
config.set('dnsInSync', true);
|
||||
|
||||
// send heartbeat after the dns records are done
|
||||
sendHeartbeat();
|
||||
|
||||
debug('addDnsRecords: done');
|
||||
config.set('dnsInSync', true);
|
||||
sendHeartbeat(); // send heartbeat after the dns records are done
|
||||
});
|
||||
}
|
||||
|
||||
@@ -463,7 +456,7 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Create app and box backup for upgrade');
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
@@ -473,7 +466,7 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.end(function (error, result) {
|
||||
if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
|
||||
if (result.status !== 202) return upgradeError(new Error('Server not ready to upgrade: ' + result.body));
|
||||
if (result.status !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
@@ -492,9 +485,9 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Create box backup for update');
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for update');
|
||||
|
||||
backupBox(function (error) {
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// fetch a signed sourceTarballUrl
|
||||
@@ -503,7 +496,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
.end(function (error, result) {
|
||||
if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
|
||||
if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
|
||||
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + result.body));
|
||||
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + JSON.stringify(result.body)));
|
||||
|
||||
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
||||
var args = {
|
||||
@@ -531,7 +524,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
|
||||
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
|
||||
if (error) return updateError(error);
|
||||
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + result.body));
|
||||
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
||||
|
||||
@@ -549,6 +542,9 @@ function backup(callback) {
|
||||
var error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
// clearing backup ensures tools can 'wait' on progress
|
||||
progress.clear(progress.BACKUP);
|
||||
|
||||
// start the backup operation in the background
|
||||
backupBoxAndApps(function (error) {
|
||||
if (error) console.error('backup failed.', error);
|
||||
@@ -633,12 +629,12 @@ function backupBoxAndApps(callback) {
|
||||
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
|
||||
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);
|
||||
backupId = app.lastBackupId;
|
||||
if (error && error.reason !== AppsError.BAD_STATE) {
|
||||
debugApp(app, 'Unable to backup', error);
|
||||
return iteratorCallback(error);
|
||||
}
|
||||
|
||||
return iteratorCallback(null, backupId);
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
}, function appsBackedUp(error, backupIds) {
|
||||
if (error) {
|
||||
@@ -646,7 +642,7 @@ function backupBoxAndApps(callback) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
|
||||
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
|
||||
@@ -25,6 +25,7 @@ exports = module.exports = {
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
|
||||
@@ -73,6 +74,7 @@ function initConfig() {
|
||||
data.webServerOrigin = null;
|
||||
data.internalPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.oauthProxyPort = 3003;
|
||||
data.backupKey = 'backupKey';
|
||||
data.aws = {
|
||||
backupBucket: null,
|
||||
@@ -162,6 +164,10 @@ function adminOrigin() {
|
||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||
}
|
||||
|
||||
function internalAdminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('port');
|
||||
}
|
||||
|
||||
function token() {
|
||||
return get('token');
|
||||
}
|
||||
|
||||
+3
-1
@@ -48,6 +48,8 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
if (typeof unusedTimeZone === 'function') callback = unusedTimeZone;
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
debug('Creating jobs with timezone %s', allSettings[settings.TIME_ZONE_KEY]);
|
||||
|
||||
if (gHeartbeatJob) gHeartbeatJob.stop();
|
||||
gHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
@@ -110,7 +112,7 @@ function autoupdatePatternChanged(pattern) {
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: gBoxUpdateCheckerJob.cronTime.timeZone // hack
|
||||
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+8
-4
@@ -63,7 +63,6 @@ function start(callback) {
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap user send:', tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,7 +99,6 @@ function start(callback) {
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap group send:', tmp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,8 +106,14 @@ function start(callback) {
|
||||
});
|
||||
});
|
||||
|
||||
gServer.bind('dc=cloudron', function(req, res, next) {
|
||||
debug('ldap bind: %s', req.dn.toString());
|
||||
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
|
||||
// TODO: validate password
|
||||
debug('ldap application bind: %s', req.dn.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
|
||||
debug('ldap user bind: %s', req.dn.toString());
|
||||
|
||||
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
|
||||
+12
-8
@@ -18,9 +18,6 @@ var appdb = require('./appdb.js'),
|
||||
url = require('url'),
|
||||
uuid = require('node-uuid');
|
||||
|
||||
// Allow self signed certs!
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
var gSessions = {};
|
||||
var gProxyMiddlewareCache = {};
|
||||
var gHttpServer = null;
|
||||
@@ -49,7 +46,11 @@ function verifySession(req, res, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
superagent.get(config.adminOrigin() + '/api/v1/profile').query({ access_token: req.sessionData.accessToken}).end(function (error, result) {
|
||||
// use http admin origin so that it works with self-signed certs
|
||||
superagent
|
||||
.get(config.internalAdminOrigin() + '/api/v1/profile')
|
||||
.query({ access_token: req.sessionData.accessToken})
|
||||
.end(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
req.authenticated = false;
|
||||
@@ -83,7 +84,11 @@ function authenticate(req, res, next) {
|
||||
client_secret: req.sessionData.clientSecret
|
||||
};
|
||||
|
||||
superagent.post(config.adminOrigin() + '/api/v1/oauth/token').query(query).send(data).end(function (error, result) {
|
||||
// use http admin origin so that it works with self-signed certs
|
||||
superagent
|
||||
.post(config.internalAdminOrigin() + '/api/v1/oauth/token')
|
||||
.query(query).send(data)
|
||||
.end(function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return res.send(500, 'Unable to contact the oauth server.');
|
||||
@@ -172,13 +177,12 @@ function initializeServer() {
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function start(port, callback) {
|
||||
assert.strictEqual(typeof port, 'number');
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gHttpServer = initializeServer();
|
||||
|
||||
gHttpServer.listen(port, callback);
|
||||
gHttpServer.listen(config.get('oauthProxyPort'), callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
|
||||
+3
-1
@@ -24,5 +24,7 @@ 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')
|
||||
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico'),
|
||||
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var cloudron = require('../cloudron.js'),
|
||||
CloudronError = require('../cloudron.js').CloudronError,
|
||||
debug = require('debug')('box:routes/internal'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
@@ -14,10 +15,12 @@ var cloudron = require('../cloudron.js'),
|
||||
function backup(req, res, next) {
|
||||
debug('trigger backup');
|
||||
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
cloudron.backup(function (error) {
|
||||
if (error) debug('Internal route backup failed', error);
|
||||
});
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// we always succeed to trigger a backup
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ var APP_LOCATION = 'appslocation';
|
||||
var APP_LOCATION_2 = 'appslocationtwo';
|
||||
var APP_LOCATION_NEW = 'appslocationnew';
|
||||
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
|
||||
APP_MANIFEST.dockerImage = 'girish/test:0.2.0';
|
||||
APP_MANIFEST.dockerImage = 'cloudron/test:2.0.0';
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='admin@me.com';
|
||||
var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
@@ -591,8 +591,8 @@ describe('Clients', function () {
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
createdAt: (new Date()).toISOString(),
|
||||
modifiedAt: (new Date()).toISOString(),
|
||||
resetToken: hat(256)
|
||||
};
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ function initializeExpressSync() {
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
// provides hooks for the 'installer'
|
||||
function initializeInternalExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
|
||||
+1
-4
@@ -42,12 +42,9 @@ var assert = require('assert'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gDefaults = (function () {
|
||||
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
|
||||
tz = tz ? tz.trim() : 'America/Los_Angeles';
|
||||
|
||||
var result = { };
|
||||
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
|
||||
result[exports.TIME_ZONE_KEY] = tz;
|
||||
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
|
||||
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
|
||||
result[exports.DEVELOPER_MODE_KEY] = false;
|
||||
|
||||
|
||||
@@ -80,8 +80,6 @@ function status(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('status: ', changeId);
|
||||
|
||||
api().getChangeStatus(changeId, function (error, status) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
callback(null, status === 'INSYNC' ? 'done' : 'pending');
|
||||
|
||||
@@ -29,7 +29,7 @@ var MANIFEST = {
|
||||
"contactEmail": "support@cloudron.io",
|
||||
"version": "0.1.0",
|
||||
"manifestVersion": 1,
|
||||
"dockerImage": "girish/test:0.2.0",
|
||||
"dockerImage": "cloudron/test:2.0.0",
|
||||
"healthCheckPath": "/",
|
||||
"httpPort": 7777,
|
||||
"tcpPorts": {
|
||||
|
||||
@@ -34,8 +34,8 @@ for script in "${scripts[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull girish/test:0.2.0 for tests to run"
|
||||
if ! docker inspect cloudron/test:2.0.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/test:2.0.0 for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
require('supererror', { splatchError: true});
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
|
||||
+29
-6
@@ -14,14 +14,23 @@ var apps = require('./apps.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:updatechecker'),
|
||||
mailer = require('./mailer.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest }
|
||||
gBoxUpdateInfo = null,
|
||||
gMailedUser = { };
|
||||
gBoxUpdateInfo = null;
|
||||
|
||||
function loadState() {
|
||||
var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
|
||||
return state || { };
|
||||
}
|
||||
|
||||
function saveState(mailedUser) {
|
||||
safe.fs.writeFileSync(paths.UPDATE_CHECKER_FILE, JSON.stringify(mailedUser, null, 4), 'utf8');
|
||||
}
|
||||
|
||||
function getUpdateInfo() {
|
||||
return {
|
||||
@@ -116,13 +125,21 @@ function getBoxUpdates(callback) {
|
||||
function checkAppUpdates() {
|
||||
debug('Checking App Updates');
|
||||
|
||||
var oldState = loadState();
|
||||
var newState = { box: oldState.box }; // creaee new state so that old app ids are removed
|
||||
|
||||
getAppUpdates(function (error, result) {
|
||||
if (error) debug('Error checking app updates: ', error);
|
||||
|
||||
gAppUpdateInfo = error ? {} : result;
|
||||
|
||||
async.eachSeries(Object.keys(gAppUpdateInfo), function iterator(id, iteratorDone) {
|
||||
if (gMailedUser[id]) return iteratorDone();
|
||||
newState[id] = gAppUpdateInfo[id].manifest.version;
|
||||
|
||||
if (oldState[id] === gAppUpdateInfo[id].manifest.version) {
|
||||
debug('Skipping notification of app update %s since user was already notified', id);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
apps.get(id, function (error, app) {
|
||||
if (error) {
|
||||
@@ -131,9 +148,10 @@ function checkAppUpdates() {
|
||||
}
|
||||
|
||||
mailer.appUpdateAvailable(app, gAppUpdateInfo[id]);
|
||||
gMailedUser[id] = true;
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
saveState(newState);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -141,14 +159,19 @@ function checkAppUpdates() {
|
||||
function checkBoxUpdates() {
|
||||
debug('Checking Box Updates');
|
||||
|
||||
var state = loadState();
|
||||
|
||||
getBoxUpdates(function (error, result) {
|
||||
if (error) debug('Error checking box updates: ', error);
|
||||
|
||||
gBoxUpdateInfo = error ? null : result;
|
||||
|
||||
if (gBoxUpdateInfo && !gMailedUser['box']) {
|
||||
if (gBoxUpdateInfo && state.box !== gBoxUpdateInfo.version) {
|
||||
mailer.boxUpdateAvailable(gBoxUpdateInfo.version, gBoxUpdateInfo.changelog);
|
||||
gMailedUser['box'] = true;
|
||||
state.box = gBoxUpdateInfo.version;
|
||||
saveState(state);
|
||||
} else {
|
||||
debug('Skipping notification of box update as user was already notified');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -134,7 +134,7 @@ function createUser(username, password, email, admin, invitor, callback) {
|
||||
crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var now = (new Date()).toUTCString();
|
||||
var now = (new Date()).toISOString();
|
||||
var user = {
|
||||
id: username,
|
||||
username: username,
|
||||
@@ -331,7 +331,7 @@ function setPassword(userId, newPassword, callback) {
|
||||
crypto.pbkdf2(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
user.modifiedAt = (new Date()).toUTCString();
|
||||
user.modifiedAt = (new Date()).toISOString();
|
||||
user.password = new Buffer(derivedKey, 'binary').toString('hex');
|
||||
user.resetToken = '';
|
||||
|
||||
|
||||
+6
-18
@@ -120,7 +120,6 @@ html {
|
||||
.grid-item {
|
||||
padding: 10px;
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-item:hover .grid-item-bottom {
|
||||
@@ -175,6 +174,12 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.app-update-badge {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Appstore view
|
||||
// ----------------------------
|
||||
@@ -354,23 +359,6 @@ html {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.app-update-badge {
|
||||
font-size: $font-size-h4;
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
width: $font-size-h4 + 6px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.app-update-badge:hover {
|
||||
width: inherit;
|
||||
background-color: #5CB85C;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #5CB85C;
|
||||
}
|
||||
|
||||
@@ -55,10 +55,6 @@
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
@@ -244,26 +240,27 @@
|
||||
</div>
|
||||
<div class="grid-item-bottom" ng-show="user.admin">
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<a href="" ng-click="showUninstall(app)"><i class="fa fa-remove scale"></i></a>
|
||||
<a href="" ng-click="showUninstall(app)" title="Uninstall App"><i class="fa fa-remove scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app | installError) === true">
|
||||
<a href="" ng-click="showRestore(app)"><i class="fa fa-undo scale"></i></a>
|
||||
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app | installSuccess) == true">
|
||||
<a href="" ng-click="showConfigure(app)"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<a href="" ng-click="showUpdate(app)"><i class="fa fa-arrow-up text-success scale"></i></a>
|
||||
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<a href="" ng-click="showUpdate(app)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -313,22 +313,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.startApp = function (app) {
|
||||
$scope.appConfigure.runStateBusy = true;
|
||||
app.runState = 'pending_start'; // we assume we will end up there
|
||||
Client.startApp(app.id, function () {
|
||||
$scope.appConfigure.runStateBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stopApp = function (app) {
|
||||
$scope.appConfigure.runStateBusy = true;
|
||||
app.runState = 'pending_stop'; // we assume we will end up there
|
||||
Client.stopApp(app.id, function () {
|
||||
$scope.appConfigure.runStateBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user