Files
cloudron-box/src/apptask.js
T

983 lines
35 KiB
JavaScript
Raw Normal View History

2014-06-19 19:50:13 -07:00
#!/usr/bin/env node
2014-05-23 13:18:59 -07:00
/* jslint node:true */
'use strict';
2014-11-23 00:16:00 -08:00
require('supererror')({ splatchError: true });
2014-11-28 10:04:42 -08:00
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
tokendb = require('./tokendb.js'),
2015-02-27 13:19:15 -08:00
apps = require('./apps.js'),
2014-10-23 14:12:46 -07:00
assert = require('assert'),
2014-05-23 13:18:59 -07:00
async = require('async'),
clientdb = require('./clientdb.js'),
2014-05-31 12:45:27 -07:00
child_process = require('child_process'),
2014-10-17 20:47:55 -07:00
cloudron = require('./cloudron.js'),
2014-06-19 21:12:26 -07:00
config = require('../config.js'),
constants = require('../constants.js'),
2014-06-25 20:23:14 -07:00
database = require('./database.js'),
2014-08-23 20:33:31 -07:00
DatabaseError = require('./databaseerror.js'),
2014-10-23 14:12:46 -07:00
debug = require('debug')('box:apptask'),
dns = require('native-dns'),
docker = require('./docker.js'),
2014-07-30 13:49:28 -07:00
ejs = require('ejs'),
2014-10-23 14:12:46 -07:00
execFile = child_process.execFile,
fs = require('fs'),
hat = require('hat'),
2015-03-14 11:45:34 -07:00
net = require('net'),
os = require('os'),
2014-10-23 14:12:46 -07:00
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
2014-10-23 14:12:46 -07:00
superagent = require('superagent'),
2014-11-22 10:34:07 -08:00
util = require('util'),
2015-02-04 01:03:30 -08:00
uuid = require('node-uuid'),
vbox = require('./vbox.js');
2014-05-23 13:18:59 -07:00
exports = module.exports = {
initialize: initialize,
2014-08-21 18:14:50 -07:00
startTask: startTask,
writeNginxNakedDomainConfig: writeNginxNakedDomainConfig,
2014-08-01 10:26:55 -07:00
// exported for testing
2014-08-01 14:33:20 -07:00
_getFreePort: getFreePort,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_allocateOAuthProxyCredentials: allocateOAuthProxyCredentials,
_removeOAuthProxyCredentials: removeOAuthProxyCredentials,
_allocateAccessToken: allocateAccessToken,
_removeAccessToken: removeAccessToken,
_verifyManifest: verifyManifest,
2014-08-01 14:33:20 -07:00
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
2014-10-17 20:47:55 -07:00
_reloadNginx: reloadNginx,
2015-02-27 13:19:15 -08:00
_waitForDnsPropagation: waitForDnsPropagation
2014-05-23 13:18:59 -07:00
};
2015-02-11 15:46:41 -08:00
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
2014-10-09 22:43:13 -07:00
SUDO = '/usr/bin/sudo',
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
RELOAD_COLLECTD_CMD = path.join(__dirname, 'scripts/reloadcollectd.sh'),
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
2014-05-29 11:29:26 -07:00
2014-08-01 10:26:55 -07:00
function initialize(callback) {
database.initialize(callback);
2014-05-23 13:18:59 -07:00
}
2015-02-12 01:38:58 -08:00
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
2014-05-31 16:13:43 -07:00
function getFreePort(callback) {
var server = net.createServer();
server.listen(0, function () {
var port = server.address().port;
server.close(function () {
return callback(null, port);
});
});
2014-05-31 12:45:27 -07:00
}
function reloadNginx(callback) {
2014-10-09 22:43:13 -07:00
execFile(SUDO, [ RELOAD_NGINX_CMD ], { timeout: 10000 }, callback);
}
2014-08-23 03:59:10 -07:00
function configureNginx(app, callback) {
getFreePort(function (error, freePort) {
if (error) return callback(error);
2014-05-31 12:45:27 -07:00
2014-08-23 03:59:10 -07:00
var sourceDir = path.resolve(__dirname, '..');
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, vhost: config.appFqdn(app.location), isAdmin: false, port: freePort, accessRestriction: app.accessRestriction });
2014-05-31 12:45:27 -07:00
2014-10-21 22:55:07 -07:00
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
2014-08-23 03:59:10 -07:00
debug('writing config to ' + nginxConfigFilename);
2014-05-31 12:45:27 -07:00
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
console.error('Error creating nginx config ' + safe.error);
return callback(safe.error);
}
async.series([
exports._reloadNginx,
updateApp.bind(null, app, { httpPort: freePort })
], callback);
2014-08-23 03:59:10 -07:00
vbox.forwardFromHostToVirtualBox(app.id + '-http', freePort);
2014-06-28 18:48:29 -07:00
});
}
2014-07-02 08:45:07 -07:00
function unconfigureNginx(app, callback) {
2014-10-21 22:55:07 -07:00
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
2014-07-02 08:45:07 -07:00
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
console.error('Error removing nginx configuration ' + safe.error);
2014-08-23 20:19:12 -07:00
return callback(null);
2014-07-02 08:45:07 -07:00
}
exports._reloadNginx(callback);
2015-02-04 01:03:30 -08:00
vbox.unforwardFromHostToVirtualBox(app.id + '-http');
2014-07-02 08:45:07 -07:00
}
function writeNginxNakedDomainConfig(app, callback) {
assert(app === null || typeof app === 'object');
assert(typeof callback === 'function');
var sourceDir = path.resolve(__dirname, '..');
var nginxConf;
if (app === null) { // admin
nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, vhost: config.fqdn(), isAdmin: true });
} else {
nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, vhost: config.fqdn(), isAdmin: false, port: app.httpPort, accessRestriction: app.accessRestriction });
}
2014-06-28 18:48:29 -07:00
2014-10-21 22:55:07 -07:00
var nginxNakedDomainFilename = path.join(paths.NGINX_CONFIG_DIR, 'naked_domain.conf');
2014-06-28 18:48:29 -07:00
debug('writing naked domain config to ' + nginxNakedDomainFilename);
fs.writeFile(nginxNakedDomainFilename, nginxConf, function (error) {
if (error) return callback(error);
exports._reloadNginx(callback);
2014-05-31 12:45:27 -07:00
});
}
function configureNakedDomain(app, callback) {
assert(typeof app === 'object');
assert(typeof callback === 'function');
settings.getNakedDomain(function (error, nakedDomainAppId) {
if (error) return callback(error);
if (nakedDomainAppId !== app.id) return callback(null);
debug('configureNakedDomain: writing nginx config for %s', app.id);
writeNginxNakedDomainConfig(app, callback);
});
}
function unconfigureNakedDomain(app, callback) {
assert(typeof app === 'object');
assert(typeof callback === 'function');
settings.getNakedDomain(function (error, nakedDomainAppId) {
if (error) return callback(error);
if (nakedDomainAppId !== app.id) return callback(null);
debug('unconfigureNakedDomain: resetting to admin');
settings.setNakedDomain(constants.ADMIN_APPID, callback);
});
}
2014-05-23 13:18:59 -07:00
function downloadImage(app, callback) {
debug('Will download app now');
2014-07-30 22:53:28 -07:00
var manifest = app.manifest;
2014-05-23 13:18:59 -07:00
docker.pull(manifest.dockerImage, function (err, stream) {
2014-06-26 22:07:18 -07:00
if (err) return callback(new Error('Error connecting to docker'));
2014-05-23 13:18:59 -07:00
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
2014-07-02 11:37:11 -07:00
debug('downloadImage:', JSON.stringify(data));
2014-06-21 01:06:13 -07:00
2014-06-26 22:07:18 -07:00
// The information here is useless because this is per layer as opposed to per image
2014-05-23 13:18:59 -07:00
if (data.status) {
debug('Progress: ' + data.status); // progressDetail { current, total }
} else if (data.error) {
console.error('Error detail:' + data.errorDetail.message);
2014-05-23 13:18:59 -07:00
}
});
stream.on('end', function () {
debug('pulled successfully');
var image = docker.getImage(manifest.dockerImage);
2014-05-23 13:18:59 -07:00
image.inspect(function (err, data) {
2014-09-10 15:13:33 -07:00
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)));
2014-05-23 13:18:59 -07:00
}
2014-06-26 22:07:18 -07:00
2014-06-08 10:33:35 -07:00
if (!data.Config.Entrypoint && !data.Config.Cmd) {
2014-06-26 22:07:18 -07:00
return callback(new Error('Only images with entry point are allowed'));
2014-05-23 13:18:59 -07:00
}
2014-06-08 10:33:35 -07:00
debug('This image exposes ports: ' + JSON.stringify(data.Config.ExposedPorts));
2014-06-26 22:07:18 -07:00
return callback(null);
2014-05-23 13:18:59 -07:00
});
});
});
}
2014-05-23 13:18:59 -07:00
2014-08-23 03:59:10 -07:00
function createContainer(app, callback) {
appdb.getPortBindings(app.id, function (error, portBindings) {
if (error) return callback(error);
2015-03-09 19:11:54 -07:00
var manifest = app.manifest;
2015-03-13 16:06:11 +01:00
var exposedPorts = {};
var env = [];
2015-03-14 09:52:35 -07:00
for (var e in portBindings) {
var hostPort = portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
2015-03-13 16:06:11 +01:00
exposedPorts[containerPort + '/tcp'] = {};
2015-03-09 19:11:54 -07:00
2015-03-08 19:34:27 -07:00
env.push(e + '=' + hostPort);
2014-08-23 03:59:10 -07:00
}
2015-02-08 22:12:39 -08:00
env.push('CLOUDRON=1');
2014-10-24 18:02:39 -07:00
env.push('ADMIN_ORIGIN' + '=' + config.adminOrigin());
tokendb.getByIdentifier(tokendb.PREFIX_APP + app.id, function (error, results) {
if (error || results.length === 0) return callback(new Error('No access token found: ' + error));
env.push('CLOUDRON_TOKEN' + '=' + results[0].accessToken);
addons.getEnvironment(app.id, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon env: ' + error));
var containerOptions = {
name: app.id,
Hostname: config.appFqdn(app.location),
Tty: true,
Image: manifest.dockerImage,
Cmd: null,
Volumes: { },
VolumesFrom: '',
Env: env.concat(addonEnv),
ExposedPorts: exposedPorts
};
debug('Creating container for %s', manifest.dockerImage);
docker.createContainer(containerOptions, function (error, container) {
2015-03-17 17:50:56 -07:00
if (error) return callback(new Error('Error creating container: ' + error));
updateApp(app, { containerId: container.id }, callback);
});
2014-08-23 03:59:10 -07:00
});
});
});
}
2014-05-29 11:29:26 -07:00
2014-07-02 08:45:07 -07:00
function deleteContainer(app, callback) {
if (app.containerId === null) return callback(null);
var container = docker.getContainer(app.containerId);
2014-07-02 08:45:07 -07:00
var removeOptions = {
force: true, // kill container if it's running
v: false // removes volumes associated with the container
2014-07-02 08:45:07 -07:00
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return updateApp(app, { containerId: null }, callback);
2014-08-23 18:27:37 -07:00
if (error) console.error('Error removing container', error);
2014-07-02 08:45:07 -07:00
callback(error);
});
}
2014-08-08 21:24:37 -07:00
function deleteImage(app, callback) {
2014-08-23 13:59:17 -07:00
var docker_image = app.manifest ? app.manifest.dockerImage : '';
var image = docker.getImage(docker_image);
2014-08-08 21:24:37 -07:00
var removeOptions = {
force: true,
noprune: false
};
image.remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
2014-10-04 00:35:05 -07:00
if (error && error.statusCode === 409) return callback(null); // another container using the image
2014-08-23 18:27:37 -07:00
if (error) console.error('Error removing image', error);
2014-08-08 21:24:37 -07:00
callback(error);
});
}
2014-06-10 23:02:56 -07:00
function createVolume(app, callback) {
2014-10-21 22:55:07 -07:00
var appDataDir = path.join(paths.APPDATA_DIR, app.id);
2014-08-27 22:53:15 -07:00
if (!safe.fs.mkdirSync(appDataDir)) {
return callback(new Error('Error creating app data directory ' + appDataDir + ' ' + safe.error));
}
2014-08-27 22:53:15 -07:00
return callback(null);
}
2014-07-02 08:45:07 -07:00
function deleteVolume(app, callback) {
2015-02-04 00:21:20 -08:00
execFile(SUDO, [ RMAPPDIR_CMD, 'appdata/' + app.id ], { }, function (error, stdout, stderr) {
if (error) console.error('Error removing volume', error, stdout, stderr);
2014-07-02 08:45:07 -07:00
return callback(error);
});
}
function allocateOAuthProxyCredentials(app, callback) {
assert(typeof app === 'object');
assert(typeof callback === 'function');
if (!app.accessRestriction) return callback(null);
var appId = 'proxy-' + app.id;
2015-02-24 01:54:34 -08:00
var id = 'cid-proxy-' + uuid.v4();
var clientSecret = hat();
2015-01-24 08:26:04 -08:00
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile,' + app.accessRestriction;
clientdb.add(id, appId, clientSecret, redirectURI, scope, callback);
}
function removeOAuthProxyCredentials(app, callback) {
assert(typeof app === 'object');
assert(typeof callback === 'function');
clientdb.delByAppId('proxy-' + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) {
console.error('Error removing OAuth client id', error);
return callback(error);
}
callback(null);
});
}
function allocateAccessToken(app, callback) {
assert(typeof app === 'object');
assert(typeof callback === 'function');
var token = tokendb.generateToken();
var expiresAt = Number.MAX_SAFE_INTEGER; // basically never expire
var scopes = 'profile,users'; // TODO This should be put into the manifest and the user should know those
var clientId = ''; // meaningless for apps so far
tokendb.add(token, tokendb.PREFIX_APP + app.id, clientId, expiresAt, scopes, callback);
}
function removeAccessToken(app, callback) {
assert(typeof app === 'object');
assert(typeof callback === 'function');
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) {
console.error('Error removing access token', error);
return callback(error);
}
callback(null);
});
}
function addCollectdProfile(app, callback) {
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
2014-10-21 22:55:07 -07:00
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
2014-10-09 22:43:13 -07:00
execFile(SUDO, [ RELOAD_COLLECTD_CMD ], { timeout: 10000 }, callback);
});
}
function removeCollectdProfile(app, callback) {
2014-10-21 22:55:07 -07:00
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error, stdout, stderr) {
if (error) console.error('Error removing collectd profile', error, stdout, stderr);
2014-10-09 22:43:13 -07:00
execFile(SUDO, [ RELOAD_COLLECTD_CMD ], { timeout: 10000 }, callback);
});
}
2014-08-23 03:59:10 -07:00
function startContainer(app, callback) {
appdb.getPortBindings(app.id, function (error, portBindings) {
2014-08-23 03:59:10 -07:00
if (error) return callback(error);
2014-05-29 11:29:26 -07:00
2014-08-23 03:59:10 -07:00
var manifest = app.manifest;
2014-10-21 22:55:07 -07:00
var appDataDir = path.join(paths.APPDATA_DIR, app.id);
var dockerPortBindings = { };
2015-03-14 11:45:34 -07:00
var isMac = os.platform() === 'darwin';
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: isMac ? '0.0.0.0' : '127.0.0.1', HostPort: app.httpPort + '' } ];
for (var env in portBindings) {
var hostPort = portBindings[env];
2015-03-08 19:34:27 -07:00
var containerPort = manifest.tcpPorts[env].containerPort || hostPort;
2015-03-14 09:52:35 -07:00
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
2015-03-08 19:34:27 -07:00
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
2014-08-23 03:59:10 -07:00
}
2014-08-23 03:59:10 -07:00
var startOptions = {
Binds: [ appDataDir + ':/app/data:rw' ],
PortBindings: dockerPortBindings,
2015-02-03 13:26:32 -08:00
PublishAllPorts: false,
2015-02-04 20:03:58 -08:00
Links: addons.getLinksSync(app),
RestartPolicy: {
"Name": "on-failure",
"MaximumRetryCount": 100
}
2014-08-23 03:59:10 -07:00
};
var container = docker.getContainer(app.containerId);
2014-08-23 03:59:10 -07:00
debug('Starting container ' + container.id + ' with options: ' + JSON.stringify(startOptions));
2014-08-23 03:59:10 -07:00
container.start(startOptions, function (error, data) {
2014-08-24 17:10:02 -07:00
if (error && error.statusCode !== 304) return callback(new Error('Error starting container:' + error));
2014-08-23 03:59:10 -07:00
return callback(null);
});
2014-05-23 13:18:59 -07:00
});
}
2014-05-23 13:18:59 -07:00
2014-08-21 18:14:50 -07:00
function stopContainer(app, callback) {
var container = docker.getContainer(app.containerId);
2014-08-21 18:14:50 -07:00
debug('Stopping container ' + container.id);
var options = {
t: 10 // wait for 10 seconds before killing it
};
container.stop(options, function (error) {
2014-10-10 08:53:32 -07:00
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
2014-08-21 18:14:50 -07:00
var tcpPorts = safe.query(app, 'manifest.tcpPorts', { });
2015-02-11 15:59:36 -08:00
for (var containerPort in tcpPorts) {
2015-02-04 01:03:30 -08:00
vbox.unforwardFromHostToVirtualBox(app.id + '-tcp' + containerPort);
}
2015-02-27 01:11:14 -08:00
debug('Waiting for container ' + container.id);
container.wait(function (error, data) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
debug('Container stopped with status code [%s]', data ? String(data.StatusCode) : '');
return callback(null);
});
2014-08-21 18:14:50 -07:00
});
}
function verifyManifest(app, callback) {
debug('Verifying manifest for :', app.id);
2014-05-23 13:18:59 -07:00
var manifest = app.manifest;
var error = apps.validateManifest(manifest);
if (error) return callback(new Error(util.format('Manifest error: %s', error.message)));
error = apps.checkManifestConstraints(manifest);
if (error) return callback(error);
2015-02-27 18:40:23 -08:00
if (!manifest.iconUrl) return callback(null);
2015-02-27 18:40:23 -08:00
superagent
.get(manifest.iconUrl)
.buffer(true)
.end(function (error, res) {
if (error) return callback(new Error('Error downloading icon:' + error.message));
2015-02-27 19:42:17 -08:00
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return callback(new Error('Error saving icon:' + safe.error.message));
2015-02-27 18:40:23 -08:00
callback(null);
});
}
2014-05-23 13:18:59 -07:00
2014-06-11 10:28:54 -07:00
function registerSubdomain(app, callback) {
2014-10-24 18:02:39 -07:00
if (!config.token()) {
2014-06-29 18:04:40 -07:00
debug('Skipping subdomain registration for development');
return callback(null);
}
2014-07-30 13:49:28 -07:00
debug('Registering subdomain for ' + app.id + ' at ' + app.location);
2014-06-24 15:34:58 -07:00
var record = { subdomain: app.location, type: 'A' };
2014-06-11 10:28:54 -07:00
superagent
2015-02-10 18:23:03 -08:00
.post(config.apiServerOrigin() + '/api/v1/subdomains')
2014-06-11 10:28:54 -07:00
.set('Accept', 'application/json')
2014-10-24 18:02:39 -07:00
.query({ token: config.token() })
.send({ records: [ record ] })
2014-06-11 10:28:54 -07:00
.end(function (error, res) {
2014-06-26 22:07:18 -07:00
if (error) return callback(error);
2014-08-28 03:32:59 -07:00
debug('Registered subdomain for ' + app.id + ' ' + res.status);
2014-06-11 10:28:54 -07:00
2014-08-28 03:32:59 -07:00
if (res.status === 409) return callback(null); // already registered
2014-11-22 10:34:07 -08:00
if (res.status !== 201) return callback(new Error(util.format('Subdomain Registration failed. %s %j', res.status, res.body)));
2014-06-24 15:34:58 -07:00
2014-09-30 17:08:11 -07:00
updateApp(app, { dnsRecordId: res.body.ids[0] }, callback);
2014-06-11 10:28:54 -07:00
});
}
2014-06-25 20:23:14 -07:00
function unregisterSubdomain(app, callback) {
2014-10-24 18:02:39 -07:00
if (!config.token()) {
2014-07-01 22:14:18 -07:00
debug('Skipping subdomain unregistration for development');
return callback(null);
}
2014-07-30 13:49:28 -07:00
debug('Unregistering subdomain for ' + app.id + ' at ' + app.location);
2014-06-25 20:23:14 -07:00
superagent
2015-02-10 18:23:03 -08:00
.del(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId)
2014-10-24 18:02:39 -07:00
.query({ token: config.token() })
2014-06-25 20:23:14 -07:00
.end(function (error, res) {
if (error) {
2014-08-06 22:46:41 -07:00
console.error('Error making request: ', error);
2014-11-06 18:18:20 -08:00
} else if (res.status !== 204) {
2014-08-06 22:46:41 -07:00
console.error('Error unregistering subdomain:', res.status, res.body);
2014-06-25 20:23:14 -07:00
}
2014-09-30 17:08:11 -07:00
updateApp(app, { dnsRecordId: null }, function (error) {
if (error) console.error(error);
callback(null);
});
2014-06-25 20:23:14 -07:00
});
}
function removeIcon(app, callback) {
2014-10-21 22:55:07 -07:00
fs.unlink(path.join(paths.APPICONS_DIR, app.id + '.png'), function (error) {
if (error && error.code !== 'ENOENT') console.error(error);
callback(null);
});
}
2014-10-17 20:47:55 -07:00
function waitForDnsPropagation(app, callback) {
if (process.env.NODE_ENV === 'test') {
debug('Skipping dns propagation check for development');
return callback(null);
}
var ip = cloudron.getIp(),
zoneName = config.zoneName(),
2015-01-24 08:26:04 -08:00
fqdn = config.appFqdn(app.location);
2014-10-17 20:47:55 -07:00
function retry(error) {
console.error(error);
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
}
2014-10-17 22:01:05 -07:00
debug('Checking if DNS is setup for %s to resolve to %s (zone: %s)', fqdn, ip, zoneName);
// localhost is always known
if (zoneName === 'localhost') return callback(null);
2014-10-17 20:47:55 -07:00
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || nameservers.length === 0) return retry(new Error('Failed to get NS of ' + zoneName));
debug('checkARecord: %s should resolve to %s by %s', fqdn, ip, nameservers[0]);
2014-10-17 22:01:05 -07:00
dns.resolve4(nameservers[0], function (error, dnsIps) {
2014-10-17 20:47:55 -07:00
if (error || dnsIps.length === 0) return retry(new Error('Failed to query DNS'));
var req = dns.Request({
question: dns.Question({ name: fqdn, type: 'A' }),
server: { address: dnsIps[0] },
timeout: 5000
});
req.on('timeout', function () { return retry(new Error('Timedout')); });
req.on('message', function (error, message) {
debug('checkARecord:', message.answer);
2014-10-17 22:01:05 -07:00
if (error || !message.answer || message.answer.length === 0) return retry(new Error('Nothing yet'));
2014-10-17 20:47:55 -07:00
if (message.answer[0].address !== ip) return retry(new Error('DNS resolved to another IP'));
callback(null);
});
req.send();
});
});
}
2014-06-28 02:28:04 -07:00
// updates the app object and the database
function updateApp(app, values, callback) {
for (var value in values) {
app[value] = values[value];
}
2014-06-26 22:07:18 -07:00
2015-02-24 00:38:55 -08:00
debug(app.id + ' installationState:' + app.installationState + ' progress: ' + app.installationProgress);
2014-06-26 22:07:18 -07:00
2014-06-28 02:28:04 -07:00
appdb.update(app.id, values, callback);
}
2014-06-26 22:07:18 -07:00
2014-06-28 08:48:54 -07:00
function install(app, callback) {
async.series([
// configure nginx
2014-08-23 03:59:10 -07:00
configureNginx.bind(null, app),
2014-06-26 22:07:18 -07:00
2014-06-28 08:48:54 -07:00
// register subdomain
updateApp.bind(null, app, { installationProgress: 'Registering subdomain' }),
2014-08-23 03:59:10 -07:00
registerSubdomain.bind(null, app),
// verify manifest
updateApp.bind(null, app, { installationProgress: 'Verifying manifest' }),
verifyManifest.bind(null, app),
2014-06-26 22:07:18 -07:00
// create proxy OAuth credentials
updateApp.bind(null, app, { installationProgress: 'Creating OAuth proxy credentials' }),
allocateOAuthProxyCredentials.bind(null, app),
// allocate access token
updateApp.bind(null, app, { installationProgress: 'Allocate access token' }),
allocateAccessToken.bind(null, app),
2014-06-28 08:48:54 -07:00
// download the image
updateApp.bind(null, app, { installationProgress: 'Downloading image' }),
2014-08-23 03:59:10 -07:00
downloadImage.bind(null, app),
2014-11-28 10:48:53 -08:00
// setup addons
updateApp.bind(null, app, { installationProgress: 'Setting up addons' }),
addons.teardownAddons.bind(null, app),
addons.setupAddons.bind(null, app),
// recreate container
updateApp.bind(null, app, { installationProgress: 'Creating container' }),
deleteContainer.bind(null, app),
2014-08-23 03:59:10 -07:00
createContainer.bind(null, app),
2014-08-27 22:53:15 -07:00
// recreate data volume
updateApp.bind(null, app, { installationProgress: 'Creating volume' }),
2014-08-27 22:53:15 -07:00
deleteVolume.bind(null, app),
2014-08-23 03:59:10 -07:00
createVolume.bind(null, app),
2014-06-21 00:32:40 -07:00
// add collectd profile
updateApp.bind(null, app, { installationProgress: 'Setting up collectd profile' }),
addCollectdProfile.bind(null, app),
2014-10-17 20:47:55 -07:00
// wait until dns propagated
updateApp.bind(null, app, { installationProgress: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
2014-06-28 08:48:54 -07:00
// done!
function (callback) {
2014-07-02 11:37:11 -07:00
debug('App ' + app.id + ' installed');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '' }, callback);
2014-06-24 15:34:58 -07:00
}
2014-08-21 18:03:59 -07:00
], function seriesDone(error) {
if (error) {
console.error('Error installing app:', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
2014-08-21 18:03:59 -07:00
}
callback(null);
});
2014-06-21 00:32:40 -07:00
}
2014-08-27 21:22:22 -07:00
function restore(app, callback) {
2015-02-17 16:57:14 -08:00
var oldManifest = app.manifest; // TODO: this won't be correct all the time should we crash after download manifest
2015-02-04 20:25:44 -08:00
2014-08-27 21:22:22 -07:00
async.series([
// configure nginx
updateApp.bind(null, app, { installationProgress: 'Configuring nginx' }),
configureNginx.bind(null, app),
configureNakedDomain.bind(null, app),
2014-08-27 21:22:22 -07:00
// register subdomain
updateApp.bind(null, app, { installationProgress: 'Registering subdomain' }),
registerSubdomain.bind(null, app),
// verify manifest
updateApp.bind(null, app, { installationProgress: 'Verify manifest' }),
verifyManifest.bind(null, app),
2015-02-17 16:57:14 -08:00
// setup oauth proxy
updateApp.bind(null, app, { installationProgress: 'Setting up OAuth proxy credentials' }),
removeOAuthProxyCredentials.bind(null, app),
allocateOAuthProxyCredentials.bind(null, app),
// allocate access token
updateApp.bind(null, app, { installationProgress: 'Allocate access token' }),
removeAccessToken.bind(null, app),
allocateAccessToken.bind(null, app),
2014-08-27 21:22:22 -07:00
// download the image
updateApp.bind(null, app, { installationProgress: 'Downloading image' }),
downloadImage.bind(null, app),
2014-11-28 10:48:53 -08:00
// setup addons
updateApp.bind(null, app, { installationProgress: 'Setting up addons' }),
2015-02-04 20:25:44 -08:00
addons.updateAddons.bind(null, app, oldManifest),
2014-08-27 21:22:22 -07:00
2014-11-28 21:50:31 -08:00
// create container (old containers are deleted by update script)
2014-08-27 21:22:22 -07:00
updateApp.bind(null, app, { installationProgress: 'Creating container' }),
2015-03-17 17:50:56 -07:00
deleteContainer.bind(null, app),
2014-08-27 21:22:22 -07:00
createContainer.bind(null, app),
// add collectd profile
updateApp.bind(null, app, { installationProgress: 'Add collectd profile' }),
addCollectdProfile.bind(null, app),
2014-10-17 20:47:55 -07:00
// wait until dns propagated
updateApp.bind(null, app, { installationProgress: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
2014-08-27 21:22:22 -07:00
// done!
function (callback) {
debug('App ' + app.id + ' installed');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '' }, callback);
}
], function seriesDone(error) {
if (error) {
console.error('Error installing app:', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
}
postInstall(app, callback);
});
}
2014-08-22 14:05:48 -07:00
// TODO: optimize by checking if location actually changed
function configure(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: 'Stopping app' }),
2014-08-22 14:05:48 -07:00
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Deleting container' }),
2014-08-22 14:05:48 -07:00
deleteContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Unregistering subdomain' }),
2014-08-22 14:05:48 -07:00
unregisterSubdomain.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Remove OAuth credentials' }),
removeOAuthProxyCredentials.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Remove access token' }),
removeAccessToken.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Configuring Nginx' }),
2014-08-22 14:05:48 -07:00
configureNginx.bind(null, app),
configureNakedDomain.bind(null, app),
2014-08-22 14:05:48 -07:00
updateApp.bind(null, app, { installationProgress: 'Registering subdomain' }),
2014-08-22 14:05:48 -07:00
registerSubdomain.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Create OAuth proxy credentials' }),
allocateOAuthProxyCredentials.bind(null, app),
// allocate access token
updateApp.bind(null, app, { installationProgress: 'Allocate access token' }),
allocateAccessToken.bind(null, app),
2014-11-28 16:28:30 -08:00
// addons like oauth might rely on the app's fqdn
2014-11-28 10:48:53 -08:00
updateApp.bind(null, app, { installationProgress: 'Setting up addons' }),
addons.setupAddons.bind(null, app),
2014-08-22 14:05:48 -07:00
updateApp.bind(null, app, { installationProgress: 'Creating container' }),
2014-08-22 14:05:48 -07:00
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Add collectd profile' }),
addCollectdProfile.bind(null, app),
2014-08-22 14:05:48 -07:00
runApp.bind(null, app),
2014-10-17 20:47:55 -07:00
updateApp.bind(null, app, { installationProgress: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
2014-08-22 14:05:48 -07:00
// done!
function (callback) {
debug('App ' + app.id + ' installed');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '' }, callback);
2014-08-22 14:05:48 -07:00
}
], function seriesDone(error) {
if (error) {
console.error('Error reconfiguring app:', app, error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
2014-08-22 14:05:48 -07:00
}
callback(null);
});
}
// nginx and naked domain configuration is skipped because app.httpPort is expected to be available
// TODO: old image should probably be deleted, but what if it's used by another app instance
2014-08-24 22:18:04 -07:00
function update(app, callback) {
2015-02-17 16:57:14 -08:00
var oldManifest = app.manifest; // TODO: this won't be correct all the time should we crash after download manifest
2015-03-10 10:37:00 -07:00
debug('Updating %s to %s', app.id, safe.query(app, 'manifest.version'));
2014-08-24 22:18:04 -07:00
async.series([
updateApp.bind(null, app, { installationProgress: 'Stopping app' }),
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Deleting container' }),
deleteContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Verify manifest' }),
verifyManifest.bind(null, app),
2014-08-24 22:18:04 -07:00
updateApp.bind(null, app, { installationProgress: 'Downloading image' }),
downloadImage.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Updating addons' }),
addons.updateAddons.bind(null, app, oldManifest),
2014-11-28 16:28:30 -08:00
2014-08-24 22:18:04 -07:00
updateApp.bind(null, app, { installationProgress: 'Creating container' }),
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Add collectd profile' }),
addCollectdProfile.bind(null, app),
2014-08-24 22:18:04 -07:00
runApp.bind(null, app),
// done!
function (callback) {
debug('App ' + app.id + ' updated');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '' }, callback);
}
], function seriesDone(error) {
if (error) {
console.error('Error updating app:', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
}
callback(null);
});
}
2014-07-02 08:45:07 -07:00
function uninstall(app, callback) {
2015-03-14 11:45:34 -07:00
debug('uninstalling ' + app.id);
2014-07-02 08:45:07 -07:00
// TODO: figure what happens if one of the steps fail
async.series([
2014-07-31 02:39:13 -07:00
// unset naked domain
function (callback) {
2014-08-07 06:04:18 -07:00
if (config.nakedDomain !== app.id) return callback(null);
2014-07-31 02:39:13 -07:00
2014-08-07 06:04:18 -07:00
config.set('nakedDomain', null);
2014-08-05 15:56:12 -07:00
callback(null);
2014-07-31 02:39:13 -07:00
},
updateApp.bind(null, app, { installationProgress: 'Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
unconfigureNakedDomain.bind(null, app),
2014-07-02 08:45:07 -07:00
2014-10-08 23:42:32 -07:00
updateApp.bind(null, app, { installationProgress: 'Stopping app' }),
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Deleting container' }),
deleteContainer.bind(null, app),
2014-07-02 08:45:07 -07:00
updateApp.bind(null, app, { installationProgress: 'Add collectd profile' }),
removeCollectdProfile.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Deleting image' }),
deleteImage.bind(null, app),
2014-08-08 21:24:37 -07:00
2014-11-28 10:48:53 -08:00
updateApp.bind(null, app, { installationProgress: 'Teardown addons' }),
addons.teardownAddons.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Deleting volume' }),
deleteVolume.bind(null, app),
2014-07-02 08:45:07 -07:00
updateApp.bind(null, app, { installationProgress: 'Unregistering subdomain' }),
unregisterSubdomain.bind(null, app),
2014-07-02 08:45:07 -07:00
updateApp.bind(null, app, { installationProgress: 'Remove access token' }),
removeAccessToken.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Remove OAuth credentials' }),
removeOAuthProxyCredentials.bind(null, app),
updateApp.bind(null, app, { installationProgress: 'Cleanup manifest' }),
removeIcon.bind(null, app),
2014-08-23 10:59:13 -07:00
appdb.del.bind(null, app.id)
2014-07-02 08:45:07 -07:00
], callback);
}
2014-06-28 02:28:04 -07:00
function runApp(app, callback) {
2014-08-23 03:59:10 -07:00
startContainer(app, function (error) {
if (error) {
2014-08-24 16:18:31 -07:00
console.error('Error starting container.', error);
2014-08-23 03:59:10 -07:00
return updateApp(app, { runState: appdb.RSTATE_ERROR }, callback);
}
2014-06-28 02:28:04 -07:00
2014-08-23 03:59:10 -07:00
updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback);
2014-06-28 02:28:04 -07:00
});
}
2014-08-21 18:14:50 -07:00
function stopApp(app, callback) {
stopContainer(app, function (error) {
if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
});
}
2014-08-27 21:22:22 -07:00
function postInstall(app, callback) {
if (app.runState === appdb.RSTATE_PENDING_STOP) {
return stopApp(app, callback);
}
2014-09-06 20:18:20 -07:00
if (app.runState !== appdb.RSTATE_STOPPED) {
2014-08-27 21:22:22 -07:00
debug('Resuming app with state : %s %s', app.runState, app.id);
return runApp(app, callback);
}
2014-08-28 03:36:37 -07:00
debug('postInstall - doing nothing: %s %s', app.runState, app.id);
2014-08-27 21:22:22 -07:00
return callback(null);
}
2014-08-21 18:14:50 -07:00
function startTask(appId, callback) {
// determine what to do
2014-06-24 15:34:58 -07:00
appdb.get(appId, function (error, app) {
if (error) return callback(error);
2014-07-01 21:27:42 -07:00
2014-08-21 18:14:50 -07:00
debug('ISTATE:' + app.installationState + ' RSTATE:' + app.runState);
2014-07-01 21:27:42 -07:00
if (app.installationState === appdb.ISTATE_PENDING_UNINSTALL) {
2014-08-24 16:18:31 -07:00
return uninstall(app, callback);
2014-06-28 08:48:54 -07:00
}
2014-08-22 14:05:48 -07:00
if (app.installationState === appdb.ISTATE_PENDING_CONFIGURE) {
2014-08-24 16:18:31 -07:00
return configure(app, callback);
2014-08-22 14:05:48 -07:00
}
2014-08-24 22:18:04 -07:00
if (app.installationState === appdb.ISTATE_PENDING_UPDATE) {
return update(app, callback);
}
2014-08-27 21:22:22 -07:00
if (app.installationState === appdb.ISTATE_PENDING_RESTORE) {
return restore(app, callback);
}
2014-08-21 18:14:50 -07:00
2014-08-27 21:22:22 -07:00
if (app.installationState === appdb.ISTATE_INSTALLED) {
return postInstall(app, callback);
2014-08-21 18:14:50 -07:00
}
if (app.installationState === appdb.ISTATE_PENDING_INSTALL) {
install(app, function (error) {
if (error) return callback(error);
2014-06-28 02:28:04 -07:00
runApp(app, callback);
});
return;
}
console.error('Apptask launched but nothing to do.', app);
return callback(null);
2014-05-23 13:18:59 -07:00
});
}
2014-06-19 21:12:26 -07:00
if (require.main === module) {
2014-06-24 15:34:58 -07:00
assert(process.argv.length === 3, 'Pass the appid as argument');
debug('Apptask for ' + process.argv[2]);
2014-08-01 10:26:55 -07:00
initialize(function (error) {
if (error) throw error;
2014-06-19 21:12:26 -07:00
2014-08-21 18:14:50 -07:00
startTask(process.argv[2], function (error) {
2014-08-23 01:49:43 -07:00
debug('Apptask completed for ' + process.argv[2], error);
2014-08-01 10:26:55 -07:00
process.exit(error ? 1 : 0);
});
2014-06-19 21:12:26 -07:00
});
}