Compare commits

..

26 Commits

Author SHA1 Message Date
Girish Ramakrishnan f19113f88e rename test iamge under cloudron/ 2015-09-29 12:52:54 -07:00
Girish Ramakrishnan 3837bee51f retry pulling image
fixes #497
2015-09-29 12:47:03 -07:00
Girish Ramakrishnan 89c3296632 debug the status code as well 2015-09-28 23:18:50 -07:00
Girish Ramakrishnan db55f0696e stringify object when appending to string 2015-09-28 23:10:09 -07:00
Girish Ramakrishnan 03d4ae9058 new base image 0.4.0 2015-09-28 19:33:58 -07:00
Girish Ramakrishnan f8b41b703c Use fqdn to generate domain name of txt records 2015-09-28 17:20:59 -07:00
Girish Ramakrishnan 2a989e455c Ensure TXT records are added as dotted domains
Fixes #498
2015-09-28 16:35:58 -07:00
Girish Ramakrishnan cd24decca0 Send dns status requests in series
And abort status checking after the first one fails. Otherwise, this
bombards the appstore unnecessarily. And checks for status of other
things unnecessarily.
2015-09-28 16:23:39 -07:00
Girish Ramakrishnan f39842a001 ldap: allow non-anonymous searches
Add LDAP_BIND_DN and LDAP_BIND_PASSWORD that allow
apps to bind before a search. There appear to be two kinds of
ldap flows:

1. App simply binds using cn=<username>,$LDAP_USERS_BASE_DN. This
   works swimmingly today.

2. App searches the username under a "bind_dn" using some admin
   credentials. It takes the result and uses the first dn in the
   result as the user dn. It then binds as step 1.

This commit tries to help out the case 2) apps. These apps really
insist on having some credentials for searching.
2015-09-25 21:28:47 -07:00
Girish Ramakrishnan 2a39526a4c Remove old app ids from updatechecker state
Fixes #472
2015-09-22 22:46:14 -07:00
Girish Ramakrishnan ded5d4c98b debug message when notification is skipped 2015-09-22 22:41:42 -07:00
Girish Ramakrishnan a0ca59c3f2 Fix typo 2015-09-22 20:22:17 -07:00
Girish Ramakrishnan 53cfc49807 Save version instead of boolean so we get notified when version changes
part of #472
2015-09-22 16:11:15 -07:00
Girish Ramakrishnan 942eb579e4 save/restore notification state of updatechecker
part of #472
2015-09-22 16:11:04 -07:00
Girish Ramakrishnan 5819cfe412 Fix progress message 2015-09-22 13:02:09 -07:00
Johannes Zellner 5cb62ca412 Remove start/stop buttons in webadmin
Fixes #495
2015-09-22 22:00:42 +02:00
Johannes Zellner df10c245de app.js is no more 2015-09-22 22:00:42 +02:00
Girish Ramakrishnan 4a804dc52b Do a complete backup for updates
The backup cron job ensures backups every 4 hours which simply does
a 'box' backup listing. If we do only a 'box' backup during update,
this means that this cron job skips doing a backup and thus the apps
are not backed up.

This results in the janitor on the CaaS side complaining that the
app backups are too old.

Since we don't stop apps anymore during updates, it makes sense
to simply backup everything for updates as well. This is probably
what the user wants anyway.
2015-09-22 12:51:58 -07:00
Girish Ramakrishnan ed2f25a998 better debugs 2015-09-21 16:02:58 -07:00
Girish Ramakrishnan 7510c9fe29 Fix typo 2015-09-21 15:57:06 -07:00
Girish Ramakrishnan 78a1d53728 copy old backup as failed/errored apps
This ensures that
a) we don't get emails from janitor about bad app backups
b) that the backups are persisted over the s3 lifecycle

Fixes #493
2015-09-21 15:03:10 -07:00
Girish Ramakrishnan e9b078cd58 add backups.copyLastBackup 2015-09-21 14:14:43 -07:00
Girish Ramakrishnan dd8b928684 aws: add copyObject 2015-09-21 14:02:00 -07:00
Girish Ramakrishnan 185b574bdc Add custom apparmor profile for cloudron apps
Docker generates an apparmor profile on the fly under /etc/apparmor.d/docker.
This profile gets overwritten on every docker daemon start.

This profile allows processes to ptrace themselves. This is required by
circus (python process manager) for reasons unknown to me. It floods the logs
with
    audit[7623]: <audit-1400> apparmor="DENIED" operation="ptrace" profile="docker-default" pid=7623 comm="python3.4" requested_mask="trace" denied_mask="trace" peer="docker-default"

This is easily tested using:
    docker run -it cloudron/base:0.3.3 /bin/bash
        a) now do ps
        b) journalctl should show error log as above

    docker run --security-opt=apparmor:docker-cloudron-app -it cloudron/base:0.3.3 /bin/bash
        a) now do ps
        b) no error!

Note that despite this, the process may not have ability to ptrace since it does not
have CAP_PTRACE. Also, security-opt is the profile name (inside the apparmor config file)
and not the filename.

References:
    https://groups.google.com/forum/#!topic/docker-user/xvxpaceTCyw
    https://github.com/docker/docker/issues/7276
    https://bugs.launchpad.net/ubuntu/+source/docker.io/+bug/1320869

This is an infra update because we need to recreate containers to get the right profile.

Fixes #492
2015-09-21 11:01:44 -07:00
Girish Ramakrishnan a89726a8c6 Add custom debug.formatArgs to remove timestamp prefix in logs
Fixes #490

See also:
https://github.com/visionmedia/debug/issues/216
2015-09-21 09:05:14 -07:00
Girish Ramakrishnan c80aca27e6 remove unnecessary supererror call 2015-09-21 09:04:16 -07:00
22 changed files with 258 additions and 125 deletions
+6
View File
@@ -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'),
+6
View File
@@ -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'),
-3
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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,
}
+4 -2
View File
@@ -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: []
+66 -28
View File
@@ -690,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));
@@ -731,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
View File
@@ -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
View File
@@ -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);
});
}
+16 -1
View File
@@ -6,7 +6,9 @@ exports = module.exports = {
getAllPaged: getAllPaged,
getBackupUrl: getBackupUrl,
getRestoreUrl: getRestoreUrl
getRestoreUrl: getRestoreUrl,
copyLastBackup: copyLastBackup
};
var assert = require('assert'),
@@ -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
View File
@@ -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) {
+19 -26
View File
@@ -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,
@@ -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');
@@ -636,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) {
@@ -649,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 : '');
+8 -4
View File
@@ -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()));
+3 -1
View File
@@ -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')
};
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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": {
+2 -2
View File
@@ -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
-2
View File
@@ -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
View File
@@ -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');
}
});
}
-4
View File
@@ -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>
-16
View File
@@ -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();
};