2015-07-20 00:09:47 -07:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
|
|
|
|
AppsError: AppsError,
|
|
|
|
|
|
2015-10-15 15:06:34 +02:00
|
|
|
hasAccessTo: hasAccessTo,
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
get: get,
|
|
|
|
|
getBySubdomain: getBySubdomain,
|
2016-02-18 15:43:46 +01:00
|
|
|
getByIpAddress: getByIpAddress,
|
2015-07-20 00:09:47 -07:00
|
|
|
getAll: getAll,
|
2016-02-25 11:28:29 +01:00
|
|
|
getAllByUser: getAllByUser,
|
2015-07-20 00:09:47 -07:00
|
|
|
purchase: purchase,
|
|
|
|
|
install: install,
|
|
|
|
|
configure: configure,
|
|
|
|
|
uninstall: uninstall,
|
|
|
|
|
|
|
|
|
|
restore: restore,
|
|
|
|
|
|
|
|
|
|
update: update,
|
|
|
|
|
|
|
|
|
|
backup: backup,
|
2016-01-19 13:35:18 +01:00
|
|
|
listBackups: listBackups,
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
getLogs: getLogs,
|
|
|
|
|
|
|
|
|
|
start: start,
|
|
|
|
|
stop: stop,
|
|
|
|
|
|
|
|
|
|
exec: exec,
|
|
|
|
|
|
|
|
|
|
checkManifestConstraints: checkManifestConstraints,
|
|
|
|
|
|
|
|
|
|
autoupdateApps: autoupdateApps,
|
|
|
|
|
|
|
|
|
|
// exported for testing
|
|
|
|
|
_validateHostname: validateHostname,
|
2015-10-15 12:26:48 +02:00
|
|
|
_validatePortBindings: validatePortBindings,
|
2015-10-28 14:35:39 +01:00
|
|
|
_validateAccessRestriction: validateAccessRestriction
|
2015-07-20 00:09:47 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var addons = require('./addons.js'),
|
|
|
|
|
appdb = require('./appdb.js'),
|
|
|
|
|
assert = require('assert'),
|
|
|
|
|
async = require('async'),
|
|
|
|
|
backups = require('./backups.js'),
|
2015-12-11 12:24:52 -08:00
|
|
|
certificates = require('./certificates.js'),
|
2015-07-20 00:09:47 -07:00
|
|
|
config = require('./config.js'),
|
|
|
|
|
constants = require('./constants.js'),
|
|
|
|
|
DatabaseError = require('./databaseerror.js'),
|
|
|
|
|
debug = require('debug')('box:apps'),
|
2015-11-10 12:47:48 -08:00
|
|
|
docker = require('./docker.js'),
|
2016-05-01 21:37:08 -07:00
|
|
|
eventlog = require('./eventlog.js'),
|
2015-07-20 00:09:47 -07:00
|
|
|
fs = require('fs'),
|
2016-02-09 13:03:52 -08:00
|
|
|
groups = require('./groups.js'),
|
2015-07-20 00:09:47 -07:00
|
|
|
manifestFormat = require('cloudron-manifestformat'),
|
|
|
|
|
path = require('path'),
|
|
|
|
|
paths = require('./paths.js'),
|
|
|
|
|
safe = require('safetydance'),
|
|
|
|
|
semver = require('semver'),
|
2015-11-02 11:20:50 -08:00
|
|
|
spawn = require('child_process').spawn,
|
2015-07-20 00:09:47 -07:00
|
|
|
split = require('split'),
|
|
|
|
|
superagent = require('superagent'),
|
|
|
|
|
taskmanager = require('./taskmanager.js'),
|
|
|
|
|
util = require('util'),
|
2015-10-28 14:35:39 +01:00
|
|
|
validator = require('validator');
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
// http://dustinsenos.com/articles/customErrorsInNode
|
|
|
|
|
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
|
|
|
|
function AppsError(reason, errorOrMessage) {
|
|
|
|
|
assert.strictEqual(typeof reason, 'string');
|
|
|
|
|
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
|
|
|
|
|
|
|
|
|
Error.call(this);
|
|
|
|
|
Error.captureStackTrace(this, this.constructor);
|
|
|
|
|
|
|
|
|
|
this.name = this.constructor.name;
|
|
|
|
|
this.reason = reason;
|
|
|
|
|
if (typeof errorOrMessage === 'undefined') {
|
|
|
|
|
this.message = reason;
|
|
|
|
|
} else if (typeof errorOrMessage === 'string') {
|
|
|
|
|
this.message = errorOrMessage;
|
|
|
|
|
} else {
|
|
|
|
|
this.message = 'Internal error';
|
|
|
|
|
this.nestedError = errorOrMessage;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
util.inherits(AppsError, Error);
|
|
|
|
|
AppsError.INTERNAL_ERROR = 'Internal Error';
|
|
|
|
|
AppsError.EXTERNAL_ERROR = 'External Error';
|
|
|
|
|
AppsError.ALREADY_EXISTS = 'Already Exists';
|
|
|
|
|
AppsError.NOT_FOUND = 'Not Found';
|
|
|
|
|
AppsError.BAD_FIELD = 'Bad Field';
|
|
|
|
|
AppsError.BAD_STATE = 'Bad State';
|
|
|
|
|
AppsError.PORT_RESERVED = 'Port Reserved';
|
|
|
|
|
AppsError.PORT_CONFLICT = 'Port Conflict';
|
|
|
|
|
AppsError.BILLING_REQUIRED = 'Billing Required';
|
2015-10-15 15:18:40 +02:00
|
|
|
AppsError.ACCESS_DENIED = 'Access denied';
|
2015-10-16 20:01:45 +02:00
|
|
|
AppsError.USER_REQUIRED = 'User required';
|
2015-10-27 16:36:09 +01:00
|
|
|
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
// Hostname validation comes from RFC 1123 (section 2.1)
|
|
|
|
|
// Domain name validation comes from RFC 2181 (Name syntax)
|
|
|
|
|
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
|
|
|
|
// We are validating the validity of the location-fqdn as host name
|
|
|
|
|
function validateHostname(location, fqdn) {
|
2016-05-04 15:35:59 -07:00
|
|
|
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION ];
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new Error(location + ' is reserved');
|
|
|
|
|
|
|
|
|
|
if (location === '') return null; // bare location
|
|
|
|
|
|
|
|
|
|
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 63) return new Error('Hostname length cannot be greater than 63');
|
|
|
|
|
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new Error('Hostname can only contain alphanumerics and hyphen');
|
|
|
|
|
if (location[0] === '-' || location[location.length-1] === '-') return new Error('Hostname cannot start or end with hyphen');
|
|
|
|
|
if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new Error('FQDN length exceeds 253 characters');
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validate the port bindings
|
|
|
|
|
function validatePortBindings(portBindings, tcpPorts) {
|
|
|
|
|
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
|
|
|
|
|
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
|
|
|
|
|
// for custom tcp ports
|
|
|
|
|
var RESERVED_PORTS = [
|
|
|
|
|
25, /* smtp */
|
|
|
|
|
53, /* dns */
|
|
|
|
|
80, /* http */
|
|
|
|
|
443, /* https */
|
2015-09-10 10:08:31 -07:00
|
|
|
919, /* ssh */
|
2015-07-20 00:09:47 -07:00
|
|
|
2003, /* graphite (lo) */
|
|
|
|
|
2004, /* graphite (lo) */
|
|
|
|
|
2020, /* install server */
|
|
|
|
|
config.get('port'), /* app server (lo) */
|
2016-04-15 12:33:54 -07:00
|
|
|
config.get('sysadminPort'), /* sysadmin app server (lo) */
|
2015-09-16 10:12:59 -07:00
|
|
|
config.get('ldapPort'), /* ldap server (lo) */
|
|
|
|
|
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
|
2015-10-11 13:19:25 +02:00
|
|
|
config.get('simpleAuthPort'), /* simple auth server (lo) */
|
2015-07-20 00:09:47 -07:00
|
|
|
3306, /* mysql (lo) */
|
|
|
|
|
8000 /* graphite (lo) */
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (!portBindings) return null;
|
|
|
|
|
|
|
|
|
|
var env;
|
|
|
|
|
for (env in portBindings) {
|
|
|
|
|
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
|
|
|
|
|
|
|
|
|
|
if (!Number.isInteger(portBindings[env])) return new Error(portBindings[env] + ' is not an integer');
|
|
|
|
|
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new Error(portBindings[env] + ' is out of range');
|
|
|
|
|
|
2015-11-25 13:49:20 +01:00
|
|
|
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
|
2015-07-20 00:09:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
|
|
|
|
// that the user wants the service disabled
|
|
|
|
|
tcpPorts = tcpPorts || { };
|
|
|
|
|
for (env in portBindings) {
|
|
|
|
|
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-15 12:26:48 +02:00
|
|
|
function validateAccessRestriction(accessRestriction) {
|
2015-10-16 15:11:54 +02:00
|
|
|
assert.strictEqual(typeof accessRestriction, 'object');
|
2015-10-15 12:26:48 +02:00
|
|
|
|
2015-10-16 15:11:54 +02:00
|
|
|
if (accessRestriction === null) return null;
|
2015-10-15 12:26:48 +02:00
|
|
|
|
2016-02-09 13:03:52 -08:00
|
|
|
var noUsers = true, noGroups = true;
|
|
|
|
|
|
|
|
|
|
if (accessRestriction.users) {
|
|
|
|
|
if (!Array.isArray(accessRestriction.users)) return new Error('users array property required');
|
|
|
|
|
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
|
|
|
|
|
noUsers = accessRestriction.users.length === 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (accessRestriction.groups) {
|
|
|
|
|
if (!Array.isArray(accessRestriction.groups)) return new Error('groups array property required');
|
|
|
|
|
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new Error('All groups have to be strings');
|
|
|
|
|
noGroups = accessRestriction.groups.length === 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (noUsers && noGroups) return new Error('users and groups array cannot both be empty');
|
2015-10-15 12:26:48 +02:00
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-11 17:39:15 +01:00
|
|
|
function validateMemoryLimit(manifest, memoryLimit) {
|
|
|
|
|
assert.strictEqual(typeof manifest, 'object');
|
|
|
|
|
assert.strictEqual(typeof memoryLimit, 'number');
|
|
|
|
|
|
2016-02-14 12:13:49 +01:00
|
|
|
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
2016-02-11 17:39:15 +01:00
|
|
|
var max = (4096 * 1024 * 1024);
|
|
|
|
|
|
|
|
|
|
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
|
|
|
|
|
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
|
|
|
|
|
if (memoryLimit === 0) return null;
|
|
|
|
|
|
|
|
|
|
if (memoryLimit < min) return new Error('memoryLimit too small');
|
|
|
|
|
if (memoryLimit > max) return new Error('memoryLimit too large');
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
function getDuplicateErrorDetails(location, portBindings, error) {
|
|
|
|
|
assert.strictEqual(typeof location, 'string');
|
|
|
|
|
assert.strictEqual(typeof portBindings, 'object');
|
|
|
|
|
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
|
|
|
|
|
|
|
|
|
|
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key/);
|
|
|
|
|
if (!match) {
|
|
|
|
|
console.error('Unexpected SQL error message.', error);
|
|
|
|
|
return new AppsError(AppsError.INTERNAL_ERROR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check if the location conflicts
|
|
|
|
|
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
|
|
|
|
|
|
|
|
|
|
// check if any of the port bindings conflict
|
|
|
|
|
for (var env in portBindings) {
|
|
|
|
|
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new AppsError(AppsError.ALREADY_EXISTS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getIconUrlSync(app) {
|
|
|
|
|
var iconPath = paths.APPICONS_DIR + '/' + app.id + '.png';
|
|
|
|
|
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-09 12:48:21 -08:00
|
|
|
function hasAccessTo(app, user, callback) {
|
2015-10-15 15:06:34 +02:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
2016-02-09 12:48:21 -08:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
2015-10-15 15:06:34 +02:00
|
|
|
|
2016-02-09 12:48:21 -08:00
|
|
|
if (app.accessRestriction === null) return callback(null, true);
|
|
|
|
|
|
2016-02-09 13:03:52 -08:00
|
|
|
// check user access
|
|
|
|
|
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
|
|
|
|
|
|
|
|
|
|
// check group access
|
|
|
|
|
if (!app.accessRestriction.groups) return callback(null, false);
|
|
|
|
|
|
|
|
|
|
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
|
|
|
|
|
groups.isMember(groupId, user.id, function (error, member) {
|
|
|
|
|
iteratorDone(!error && member); // async.some does not take error argument in callback
|
|
|
|
|
});
|
|
|
|
|
}, function (result) {
|
|
|
|
|
callback(null, result);
|
|
|
|
|
});
|
2015-10-15 15:06:34 +02:00
|
|
|
}
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
function get(appId, callback) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
appdb.get(appId, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
app.iconUrl = getIconUrlSync(app);
|
2016-04-22 23:46:45 -07:00
|
|
|
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
callback(null, app);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getBySubdomain(subdomain, callback) {
|
|
|
|
|
assert.strictEqual(typeof subdomain, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
appdb.getBySubdomain(subdomain, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
app.iconUrl = getIconUrlSync(app);
|
2016-04-22 23:46:45 -07:00
|
|
|
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
callback(null, app);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-18 15:43:46 +01:00
|
|
|
function getByIpAddress(ip, callback) {
|
|
|
|
|
assert.strictEqual(typeof ip, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
docker.getContainerIdByIp(ip, function (error, containerId) {
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
appdb.getByContainerId(containerId, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
app.iconUrl = getIconUrlSync(app);
|
2016-04-22 23:46:45 -07:00
|
|
|
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
2016-02-18 15:43:46 +01:00
|
|
|
|
|
|
|
|
callback(null, app);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
function getAll(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
appdb.getAll(function (error, apps) {
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
apps.forEach(function (app) {
|
|
|
|
|
app.iconUrl = getIconUrlSync(app);
|
2016-04-22 23:46:45 -07:00
|
|
|
app.fqdn = app.altDomain || config.appFqdn(app.location);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
callback(null, apps);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-25 11:28:29 +01:00
|
|
|
function getAllByUser(user, callback) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
getAll(function (error, result) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
async.filter(result, function (app, callback) {
|
|
|
|
|
hasAccessTo(app, user, function (error, hasAccess) {
|
|
|
|
|
callback(hasAccess);
|
|
|
|
|
});
|
|
|
|
|
}, callback.bind(null, null)); // never error
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
function purchase(appStoreId, callback) {
|
|
|
|
|
assert.strictEqual(typeof appStoreId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
// Skip purchase if appStoreId is empty
|
|
|
|
|
if (appStoreId === '') return callback(null);
|
|
|
|
|
|
2015-12-31 09:15:27 +01:00
|
|
|
// Skip if we don't have an appstore token
|
|
|
|
|
if (config.token() === '') return callback(null);
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
var url = config.apiServerOrigin() + '/api/v1/apps/' + appStoreId + '/purchase';
|
|
|
|
|
|
|
|
|
|
superagent.post(url).query({ token: config.token() }).end(function (error, res) {
|
2015-12-15 09:12:52 -08:00
|
|
|
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
|
|
|
|
|
if (res.statusCode === 402) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
|
|
|
|
if (res.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND));
|
|
|
|
|
if (res.statusCode !== 201 && res.statusCode !== 200) return callback(new Error(util.format('App purchase failed. %s %j', res.status, res.body)));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-01 21:37:08 -07:00
|
|
|
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, altDomain, auditSource, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof appStoreId, 'string');
|
|
|
|
|
assert(manifest && typeof manifest === 'object');
|
|
|
|
|
assert.strictEqual(typeof location, 'string');
|
|
|
|
|
assert.strictEqual(typeof portBindings, 'object');
|
2015-10-16 15:11:54 +02:00
|
|
|
assert.strictEqual(typeof accessRestriction, 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert(!icon || typeof icon === 'string');
|
2015-10-28 22:09:19 +01:00
|
|
|
assert(cert === null || typeof cert === 'string');
|
|
|
|
|
assert(key === null || typeof key === 'string');
|
2016-02-11 17:00:21 +01:00
|
|
|
assert.strictEqual(typeof memoryLimit, 'number');
|
2016-04-26 14:40:21 -07:00
|
|
|
assert(altDomain === null || typeof altDomain === 'string');
|
2016-05-01 21:37:08 -07:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var error = manifestFormat.parse(manifest);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error: ' + error.message));
|
|
|
|
|
|
|
|
|
|
error = checkManifestConstraints(manifest);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
|
|
|
|
|
|
|
|
|
error = validateHostname(location, config.fqdn());
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
|
|
|
|
|
|
|
|
|
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
error = validateAccessRestriction(accessRestriction);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
|
|
|
|
|
2016-02-11 17:39:15 +01:00
|
|
|
error = validateMemoryLimit(manifest, memoryLimit);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
2016-02-05 15:07:27 +01:00
|
|
|
|
2016-02-11 18:14:16 +01:00
|
|
|
// memoryLimit might come in as 0 if not specified
|
2016-02-14 12:13:49 +01:00
|
|
|
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
2016-02-11 18:14:16 +01:00
|
|
|
|
2016-04-26 14:40:21 -07:00
|
|
|
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
|
|
|
|
|
|
2015-10-16 20:01:45 +02:00
|
|
|
// singleUser mode requires accessRestriction to contain exactly one user
|
|
|
|
|
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
|
|
|
|
|
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
if (icon) {
|
|
|
|
|
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
|
|
|
|
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
|
|
|
|
|
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-11 12:24:52 -08:00
|
|
|
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
2015-10-28 22:09:19 +01:00
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
debug('Will install app with id : ' + appId);
|
|
|
|
|
|
|
|
|
|
purchase(appStoreId, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2016-04-26 14:40:21 -07:00
|
|
|
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, altDomain, function (error) {
|
2015-07-20 00:09:47 -07:00
|
|
|
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2015-10-28 22:09:19 +01:00
|
|
|
// save cert to data/box/certs
|
|
|
|
|
if (cert && key) {
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
taskmanager.restartAppTask(appId);
|
|
|
|
|
|
2016-05-02 09:32:39 -07:00
|
|
|
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
|
2016-05-01 21:37:08 -07:00
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-01 21:37:08 -07:00
|
|
|
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, altDomain, auditSource, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof location, 'string');
|
|
|
|
|
assert.strictEqual(typeof portBindings, 'object');
|
2015-10-16 15:11:54 +02:00
|
|
|
assert.strictEqual(typeof accessRestriction, 'object');
|
2015-10-27 16:36:09 +01:00
|
|
|
assert(cert === null || typeof cert === 'string');
|
|
|
|
|
assert(key === null || typeof key === 'string');
|
2016-02-11 17:00:21 +01:00
|
|
|
assert.strictEqual(typeof memoryLimit, 'number');
|
2016-04-19 00:22:56 -07:00
|
|
|
assert(altDomain === null || typeof altDomain === 'string');
|
2016-05-01 21:37:08 -07:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var error = validateHostname(location, config.fqdn());
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
|
|
|
|
|
|
|
|
|
error = validateAccessRestriction(accessRestriction);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
|
|
|
|
|
2015-12-11 12:24:52 -08:00
|
|
|
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
2015-10-27 16:36:09 +01:00
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
|
|
|
|
|
2016-04-19 00:22:56 -07:00
|
|
|
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
appdb.get(appId, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
2016-02-11 17:39:15 +01:00
|
|
|
|
|
|
|
|
error = validateMemoryLimit(app.manifest, memoryLimit);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2016-02-11 18:14:16 +01:00
|
|
|
// memoryLimit might come in as 0 if not specified
|
2016-02-14 12:13:49 +01:00
|
|
|
memoryLimit = memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
2016-02-11 18:14:16 +01:00
|
|
|
|
2015-10-28 12:24:59 +01:00
|
|
|
// save cert to data/box/certs
|
|
|
|
|
if (cert && key) {
|
2015-10-28 12:42:04 +01:00
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
2015-10-28 12:24:59 +01:00
|
|
|
}
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
var values = {
|
|
|
|
|
location: location.toLowerCase(),
|
|
|
|
|
accessRestriction: accessRestriction,
|
|
|
|
|
portBindings: portBindings,
|
2016-02-11 17:00:21 +01:00
|
|
|
memoryLimit: memoryLimit,
|
2016-04-19 00:22:56 -07:00
|
|
|
altDomain: altDomain,
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
oldConfig: {
|
|
|
|
|
location: app.location,
|
|
|
|
|
accessRestriction: app.accessRestriction,
|
2015-10-13 09:49:05 +02:00
|
|
|
portBindings: app.portBindings,
|
2016-04-25 21:08:19 -07:00
|
|
|
memoryLimit: app.memoryLimit,
|
|
|
|
|
altDomain: altDomain
|
2015-07-20 00:09:47 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
debug('Will configure app with id:%s values:%j', appId, values);
|
|
|
|
|
|
|
|
|
|
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
taskmanager.restartAppTask(appId);
|
|
|
|
|
|
2016-05-02 09:32:39 -07:00
|
|
|
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
|
2016-05-01 21:37:08 -07:00
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-01 21:37:08 -07:00
|
|
|
function update(appId, force, manifest, portBindings, icon, auditSource, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof force, 'boolean');
|
|
|
|
|
assert(manifest && typeof manifest === 'object');
|
2016-04-15 21:26:18 -07:00
|
|
|
assert(typeof portBindings === 'object'); // can be null
|
2015-07-20 00:09:47 -07:00
|
|
|
assert(!icon || typeof icon === 'string');
|
2016-05-01 21:37:08 -07:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debug('Will update app with id:%s', appId);
|
|
|
|
|
|
|
|
|
|
var error = manifestFormat.parse(manifest);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
|
|
|
|
|
|
|
|
|
|
error = checkManifestConstraints(manifest);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed:' + error.message));
|
|
|
|
|
|
|
|
|
|
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
|
|
|
|
|
|
|
|
|
if (icon) {
|
|
|
|
|
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
|
|
|
|
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
|
|
|
|
|
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appdb.get(appId, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2016-03-25 11:35:47 -07:00
|
|
|
var appStoreId = app.appStoreId;
|
|
|
|
|
|
|
|
|
|
// prevent user from installing a app with different manifest id over an existing app
|
|
|
|
|
// this allows cloudron install -f --app <appid> for an app installed from the appStore
|
|
|
|
|
if (app.manifest.id !== manifest.id) {
|
|
|
|
|
if (!force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
|
|
|
|
|
// clear appStoreId so that this app does not get updates anymore. this will mark is a dev app
|
|
|
|
|
appStoreId = '';
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-11 18:14:16 +01:00
|
|
|
// Ensure we update the memory limit in case the new app requires more memory as a minimum
|
|
|
|
|
var memoryLimit = manifest.memoryLimit ? (app.memoryLimit < manifest.memoryLimit ? manifest.memoryLimit : app.memoryLimit) : app.memoryLimit;
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
var values = {
|
2016-03-25 11:35:47 -07:00
|
|
|
appStoreId: appStoreId,
|
2015-07-20 00:09:47 -07:00
|
|
|
manifest: manifest,
|
|
|
|
|
portBindings: portBindings,
|
2016-02-11 18:14:16 +01:00
|
|
|
memoryLimit: memoryLimit,
|
2016-02-14 12:10:22 +01:00
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
oldConfig: {
|
|
|
|
|
manifest: app.manifest,
|
2015-10-16 14:15:43 +02:00
|
|
|
portBindings: app.portBindings,
|
|
|
|
|
accessRestriction: app.accessRestriction,
|
2016-04-25 21:08:19 -07:00
|
|
|
memoryLimit: app.memoryLimit,
|
|
|
|
|
altDomain: app.altDomain
|
2015-07-20 00:09:47 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
appdb.setInstallationCommand(appId, force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
|
|
|
|
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, portBindings, error));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
taskmanager.restartAppTask(appId);
|
|
|
|
|
|
2016-05-02 09:32:39 -07:00
|
|
|
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest });
|
2016-05-01 21:37:08 -07:00
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
function appLogFilter(app) {
|
2015-11-02 14:23:02 -08:00
|
|
|
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
|
2015-11-02 11:20:50 -08:00
|
|
|
|
|
|
|
|
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getLogs(appId, lines, follow, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
2015-11-02 11:20:50 -08:00
|
|
|
assert.strictEqual(typeof lines, 'number');
|
|
|
|
|
assert.strictEqual(typeof follow, 'boolean');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debug('Getting logs for %s', appId);
|
2015-11-02 11:20:50 -08:00
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
appdb.get(appId, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
|
|
|
|
|
if (follow) args.push('--follow');
|
|
|
|
|
args = args.concat(appLogFilter(app));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
var cp = spawn('/bin/journalctl', args);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
var transformStream = split(function mapper(line) {
|
|
|
|
|
var obj = safe.JSON.parse(line);
|
|
|
|
|
if (!obj) return undefined;
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
|
2015-11-02 14:26:15 -08:00
|
|
|
return JSON.stringify({
|
|
|
|
|
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
|
|
|
|
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
|
|
|
|
message: obj.MESSAGE,
|
|
|
|
|
source: source || 'main'
|
2015-11-10 11:31:05 +01:00
|
|
|
}) + '\n';
|
2015-11-02 11:20:50 -08:00
|
|
|
});
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
cp.stdout.pipe(transformStream);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-11-02 11:20:50 -08:00
|
|
|
return callback(null, transformStream);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-01 21:37:08 -07:00
|
|
|
function restore(appId, auditSource, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
2016-05-01 21:37:08 -07:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debug('Will restore app with id:%s', appId);
|
|
|
|
|
|
|
|
|
|
appdb.get(appId, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2015-08-19 10:54:39 -07:00
|
|
|
// restore without a backup is the same as re-install
|
|
|
|
|
var restoreConfig = app.lastBackupConfig, values = { };
|
|
|
|
|
if (restoreConfig) {
|
|
|
|
|
// re-validate because this new box version may not accept old configs.
|
|
|
|
|
// if we restore location, it should be validated here as well
|
|
|
|
|
error = checkManifestConstraints(restoreConfig.manifest);
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
|
|
|
|
|
|
|
|
|
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
// ## should probably query new location, access restriction from user
|
|
|
|
|
values = {
|
|
|
|
|
manifest: restoreConfig.manifest,
|
|
|
|
|
portBindings: restoreConfig.portBindings,
|
2016-02-14 12:10:22 +01:00
|
|
|
memoryLimit: restoreConfig.memoryLimit,
|
2015-08-19 10:54:39 -07:00
|
|
|
|
|
|
|
|
oldConfig: {
|
|
|
|
|
location: app.location,
|
|
|
|
|
accessRestriction: app.accessRestriction,
|
|
|
|
|
portBindings: app.portBindings,
|
2016-02-14 12:10:22 +01:00
|
|
|
memoryLimit: app.memoryLimit,
|
2016-04-25 21:08:19 -07:00
|
|
|
manifest: app.manifest,
|
|
|
|
|
altDomain: app.altDomain
|
2015-08-19 10:54:39 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
taskmanager.restartAppTask(appId);
|
|
|
|
|
|
2016-05-01 21:37:08 -07:00
|
|
|
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-01 21:37:08 -07:00
|
|
|
function uninstall(appId, auditSource, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
2016-05-01 21:37:08 -07:00
|
|
|
assert.strictEqual(typeof auditSource, 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debug('Will uninstall app with id:%s', appId);
|
|
|
|
|
|
2016-02-09 11:41:59 -08:00
|
|
|
taskmanager.stopAppTask(appId, function () {
|
|
|
|
|
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2016-05-01 21:37:08 -07:00
|
|
|
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
|
|
|
|
|
|
2016-02-09 12:14:04 -08:00
|
|
|
taskmanager.startAppTask(appId, callback);
|
2016-02-09 11:41:59 -08:00
|
|
|
});
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function start(appId, callback) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debug('Will start app with id:%s', appId);
|
|
|
|
|
|
|
|
|
|
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
taskmanager.restartAppTask(appId);
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stop(appId, callback) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
debug('Will stop app with id:%s', appId);
|
|
|
|
|
|
|
|
|
|
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
taskmanager.restartAppTask(appId);
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function checkManifestConstraints(manifest) {
|
2015-08-19 11:08:45 -07:00
|
|
|
if (!manifest.dockerImage) return new Error('Missing dockerImage'); // dockerImage is optional in manifest
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
|
|
|
|
|
return new Error('Box version exceeds Apps maxBoxVersion');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
|
|
|
|
|
return new Error('minBoxVersion exceeds Box version');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exec(appId, options, callback) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert(options && typeof options === 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var cmd = options.cmd || [ '/bin/bash' ];
|
|
|
|
|
assert(util.isArray(cmd) && cmd.length > 0);
|
|
|
|
|
|
|
|
|
|
appdb.get(appId, function (error, app) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2016-02-12 12:32:51 -08:00
|
|
|
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
|
|
|
|
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
|
|
|
|
|
}
|
|
|
|
|
|
2016-01-15 15:15:24 -08:00
|
|
|
var container = docker.connection.getContainer(app.containerId);
|
|
|
|
|
|
|
|
|
|
var execOptions = {
|
2015-07-20 00:09:47 -07:00
|
|
|
AttachStdin: true,
|
|
|
|
|
AttachStdout: true,
|
2016-01-18 21:36:05 -08:00
|
|
|
AttachStderr: true,
|
2016-01-18 11:16:06 -08:00
|
|
|
Tty: options.tty,
|
2016-01-15 15:15:24 -08:00
|
|
|
Cmd: cmd
|
2015-07-20 00:09:47 -07:00
|
|
|
};
|
|
|
|
|
|
2016-01-15 15:15:24 -08:00
|
|
|
container.exec(execOptions, function (error, exec) {
|
2015-07-20 00:09:47 -07:00
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
2016-01-15 15:15:24 -08:00
|
|
|
var startOptions = {
|
|
|
|
|
Detach: false,
|
2016-01-18 11:16:06 -08:00
|
|
|
Tty: options.tty,
|
2016-01-15 15:15:24 -08:00
|
|
|
stdin: true // this is a dockerode option that enabled openStdin in the modem
|
|
|
|
|
};
|
|
|
|
|
exec.start(startOptions, function(error, stream) {
|
2015-07-20 00:09:47 -07:00
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2016-01-15 15:15:24 -08:00
|
|
|
if (options.rows && options.columns) {
|
|
|
|
|
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
|
|
|
|
}
|
2015-11-10 21:56:17 -08:00
|
|
|
|
2016-01-15 15:15:24 -08:00
|
|
|
return callback(null, stream);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { manifest } }
|
|
|
|
|
assert.strictEqual(typeof updateInfo, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
function canAutoupdateApp(app, newManifest) {
|
2015-09-10 11:39:03 -07:00
|
|
|
var tcpPorts = newManifest.tcpPorts || { };
|
|
|
|
|
var portBindings = app.portBindings; // this is never null
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-09-10 11:39:03 -07:00
|
|
|
if (Object.keys(tcpPorts).length === 0 && Object.keys(portBindings).length === 0) return null;
|
|
|
|
|
if (Object.keys(tcpPorts).length === 0) return new Error('tcpPorts is now empty but portBindings is not');
|
|
|
|
|
if (Object.keys(portBindings).length === 0) return new Error('portBindings is now empty but tcpPorts is not');
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-09-10 11:39:03 -07:00
|
|
|
for (var env in tcpPorts) {
|
|
|
|
|
if (!(env in portBindings)) return new Error(env + ' is required from user');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// it's fine if one or more keys got removed
|
|
|
|
|
return null;
|
2015-07-20 00:09:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!updateInfo) return callback(null);
|
|
|
|
|
|
|
|
|
|
async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) {
|
|
|
|
|
get(appId, function (error, app) {
|
2015-09-10 11:39:03 -07:00
|
|
|
if (error) {
|
|
|
|
|
debug('Cannot autoupdate app %s : %s', appId, error.message);
|
|
|
|
|
return iteratorDone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error = canAutoupdateApp(app, updateInfo[appId].manifest);
|
|
|
|
|
if (error) {
|
|
|
|
|
debug('app %s requires manual update. %s', appId, error.message);
|
2015-07-20 00:09:47 -07:00
|
|
|
return iteratorDone();
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-03 23:36:27 -07:00
|
|
|
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings,
|
|
|
|
|
null /* icon */, { userId: null, username: 'autoupdater' }, function (error) {
|
2015-09-10 11:39:03 -07:00
|
|
|
if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
iteratorDone(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}, callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function backup(appId, callback) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2016-01-17 16:05:47 +01:00
|
|
|
appdb.exists(appId, function (error, exists) {
|
2015-07-20 00:09:47 -07:00
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
2016-01-17 16:05:47 +01:00
|
|
|
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
|
|
|
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
taskmanager.restartAppTask(appId);
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-01-19 13:35:18 +01:00
|
|
|
|
2016-03-08 08:57:28 -08:00
|
|
|
function listBackups(page, perPage, appId, callback) {
|
|
|
|
|
assert(typeof page === 'number' && page > 0);
|
|
|
|
|
assert(typeof perPage === 'number' && perPage > 0);
|
|
|
|
|
|
2016-01-19 13:35:18 +01:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
appdb.exists(appId, function (error, exists) {
|
|
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
|
|
|
|
|
|
2016-03-08 08:57:28 -08:00
|
|
|
backups.getByAppIdPaged(page, perPage, appId, function (error, results) {
|
2016-01-19 13:35:18 +01:00
|
|
|
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2016-03-08 08:57:28 -08:00
|
|
|
callback(null, results);
|
2016-01-19 13:35:18 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|