2015-07-20 00:09:47 -07:00
|
|
|
'use strict';
|
|
|
|
|
|
2015-10-19 11:24:21 -07:00
|
|
|
exports = module.exports = {
|
2018-11-19 10:19:46 +01:00
|
|
|
DockerError: DockerError,
|
|
|
|
|
|
2015-10-19 11:40:19 -07:00
|
|
|
connection: connectionInstance(),
|
2018-10-12 17:04:04 -07:00
|
|
|
setRegistryConfig: setRegistryConfig,
|
|
|
|
|
|
2018-11-19 10:19:46 +01:00
|
|
|
ping: ping,
|
|
|
|
|
|
2015-10-19 11:40:19 -07:00
|
|
|
downloadImage: downloadImage,
|
|
|
|
|
createContainer: createContainer,
|
|
|
|
|
startContainer: startContainer,
|
|
|
|
|
stopContainer: stopContainer,
|
2015-12-23 13:23:47 -08:00
|
|
|
stopContainerByName: stopContainer,
|
2015-10-20 00:05:07 -07:00
|
|
|
stopContainers: stopContainers,
|
2015-10-19 11:40:19 -07:00
|
|
|
deleteContainer: deleteContainer,
|
2015-12-23 13:23:47 -08:00
|
|
|
deleteContainerByName: deleteContainer,
|
2015-10-19 18:48:56 -07:00
|
|
|
deleteImage: deleteImage,
|
2015-10-20 09:36:30 -07:00
|
|
|
deleteContainers: deleteContainers,
|
2016-02-18 15:39:27 +01:00
|
|
|
createSubcontainer: createSubcontainer,
|
2016-04-18 10:32:22 -07:00
|
|
|
getContainerIdByIp: getContainerIdByIp,
|
2017-08-11 22:04:40 +02:00
|
|
|
inspect: inspect,
|
2018-02-27 13:21:38 -08:00
|
|
|
inspectByName: inspect,
|
2019-03-06 11:54:37 -08:00
|
|
|
getEvents: getEvents,
|
2018-11-28 10:39:12 +01:00
|
|
|
memoryUsage: memoryUsage,
|
2018-09-13 13:55:49 -07:00
|
|
|
execContainer: execContainer,
|
|
|
|
|
createVolume: createVolume,
|
2018-09-15 17:05:04 -07:00
|
|
|
removeVolume: removeVolume,
|
|
|
|
|
clearVolume: clearVolume
|
2015-10-19 11:24:21 -07:00
|
|
|
};
|
2015-10-19 11:08:23 -07:00
|
|
|
|
2018-11-23 15:49:47 +01:00
|
|
|
// timeout is optional
|
|
|
|
|
function connectionInstance(timeout) {
|
2016-04-18 16:30:58 -07:00
|
|
|
var Docker = require('dockerode');
|
2015-07-20 00:09:47 -07:00
|
|
|
var docker;
|
|
|
|
|
|
2015-07-24 01:42:28 -07:00
|
|
|
if (process.env.BOX_ENV === 'test') {
|
2015-07-20 00:09:47 -07:00
|
|
|
// test code runs a docker proxy on this port
|
2018-11-23 15:49:47 +01:00
|
|
|
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
|
2015-10-19 11:08:23 -07:00
|
|
|
|
|
|
|
|
// proxy code uses this to route to the real docker
|
|
|
|
|
docker.options = { socketPath: '/var/run/docker.sock' };
|
2015-07-20 00:09:47 -07:00
|
|
|
} else {
|
2018-11-23 15:49:47 +01:00
|
|
|
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
|
2015-07-20 00:09:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return docker;
|
|
|
|
|
}
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2016-04-18 16:30:58 -07:00
|
|
|
var addons = require('./addons.js'),
|
|
|
|
|
async = require('async'),
|
|
|
|
|
assert = require('assert'),
|
|
|
|
|
child_process = require('child_process'),
|
|
|
|
|
constants = require('./constants.js'),
|
2017-04-23 21:53:59 -07:00
|
|
|
debug = require('debug')('box:docker.js'),
|
2016-04-18 16:30:58 -07:00
|
|
|
once = require('once'),
|
2018-09-13 13:55:49 -07:00
|
|
|
path = require('path'),
|
2019-07-26 10:49:29 -07:00
|
|
|
settings = require('./settings.js'),
|
2018-04-12 11:32:49 -07:00
|
|
|
shell = require('./shell.js'),
|
2019-08-06 09:45:16 -07:00
|
|
|
safe = require('safetydance'),
|
2016-04-18 16:30:58 -07:00
|
|
|
spawn = child_process.spawn,
|
|
|
|
|
util = require('util'),
|
|
|
|
|
_ = require('underscore');
|
|
|
|
|
|
2019-01-18 14:48:31 -08:00
|
|
|
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
|
2018-12-20 14:33:29 -08:00
|
|
|
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
|
2018-09-13 13:55:49 -07:00
|
|
|
|
2018-10-12 17:04:04 -07:00
|
|
|
function DockerError(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(DockerError, Error);
|
|
|
|
|
DockerError.INTERNAL_ERROR = 'Internal Error';
|
2018-11-19 10:19:46 +01:00
|
|
|
DockerError.NOT_FOUND = 'Not found';
|
2018-10-12 17:04:04 -07:00
|
|
|
DockerError.BAD_FIELD = 'Bad field';
|
|
|
|
|
|
2019-07-26 10:49:29 -07:00
|
|
|
function debugApp(app) {
|
2018-02-08 15:07:49 +01:00
|
|
|
assert(typeof app === 'object');
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2018-02-08 15:07:49 +01:00
|
|
|
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
2015-10-19 11:40:19 -07:00
|
|
|
}
|
|
|
|
|
|
2018-10-12 17:04:04 -07:00
|
|
|
function setRegistryConfig(auth, callback) {
|
|
|
|
|
assert.strictEqual(typeof auth, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
const isLogin = !!auth.password;
|
|
|
|
|
|
|
|
|
|
// currently, auth info is not stashed in the db but maybe it should for restore to work?
|
|
|
|
|
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
|
|
|
|
|
|
|
|
|
|
child_process.exec(cmd, { }, function (error, stdout, stderr) {
|
|
|
|
|
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
|
|
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-19 10:19:46 +01:00
|
|
|
function ping(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2018-11-23 15:49:47 +01:00
|
|
|
// do not let the request linger
|
|
|
|
|
var docker = connectionInstance(1000);
|
2018-11-19 10:19:46 +01:00
|
|
|
|
|
|
|
|
docker.ping(function (error, result) {
|
|
|
|
|
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
2018-11-20 13:07:41 +01:00
|
|
|
if (result !== 'OK') return callback(new DockerError(DockerError.INTERNAL_ERROR, 'Unable to ping the docker daemon'));
|
2018-11-19 10:19:46 +01:00
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-19 15:37:57 -07:00
|
|
|
function pullImage(manifest, callback) {
|
2015-10-19 11:40:19 -07:00
|
|
|
var docker = exports.connection;
|
|
|
|
|
|
2018-04-12 11:32:49 -07:00
|
|
|
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
|
|
|
|
|
// https://github.com/apocas/dockerode#pull-from-private-repos
|
2019-08-06 09:45:16 -07:00
|
|
|
docker.pull(manifest.dockerImage, function (error, stream) {
|
|
|
|
|
if (error) return callback(new DockerError(DockerError.EXTERNAL_ERROR, 'Error connecting to docker. statusCode: ' + error.statusCode));
|
|
|
|
|
|
|
|
|
|
// 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) || { };
|
|
|
|
|
debug('pullImage %s: %j', manifest.id, data);
|
|
|
|
|
|
|
|
|
|
// The data.status here is useless because this is per layer as opposed to per image
|
|
|
|
|
if (!data.status && data.error) {
|
|
|
|
|
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
|
|
|
|
|
}
|
|
|
|
|
});
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2019-08-06 09:45:16 -07:00
|
|
|
stream.on('end', function () {
|
|
|
|
|
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2019-08-06 09:45:16 -07:00
|
|
|
callback(null);
|
|
|
|
|
});
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2019-08-06 09:45:16 -07:00
|
|
|
stream.on('error', function (error) {
|
|
|
|
|
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2019-08-06 09:45:16 -07:00
|
|
|
callback(new DockerError(DockerError.EXTERNAL_ERROR, error.message));
|
2015-10-19 11:40:19 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-19 15:37:57 -07:00
|
|
|
function downloadImage(manifest, callback) {
|
2015-10-19 15:51:02 -07:00
|
|
|
assert.strictEqual(typeof manifest, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2015-11-12 15:58:39 -08:00
|
|
|
debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
var attempt = 1;
|
|
|
|
|
|
2015-11-12 16:22:53 -08:00
|
|
|
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
2015-11-12 15:58:39 -08:00
|
|
|
debug('Downloading image %s %s. attempt: %s', manifest.id, manifest.dockerImage, attempt++);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2015-10-19 15:37:57 -07:00
|
|
|
pullImage(manifest, function (error) {
|
2015-10-19 11:40:19 -07:00
|
|
|
if (error) console.error(error);
|
|
|
|
|
|
|
|
|
|
retryCallback(error);
|
|
|
|
|
});
|
|
|
|
|
}, callback);
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-10 12:47:48 -08:00
|
|
|
function createSubcontainer(app, name, cmd, options, callback) {
|
2015-10-19 11:40:19 -07:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
2015-11-02 09:34:31 -08:00
|
|
|
assert.strictEqual(typeof name, 'string');
|
2015-10-19 16:22:35 -07:00
|
|
|
assert(!cmd || util.isArray(cmd));
|
2015-11-10 12:47:48 -08:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
2015-10-19 11:40:19 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2015-10-20 10:51:43 -07:00
|
|
|
var docker = exports.connection,
|
2019-06-26 14:13:26 -07:00
|
|
|
isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
var manifest = app.manifest;
|
|
|
|
|
var exposedPorts = {}, dockerPortBindings = { };
|
2018-02-08 15:07:49 +01:00
|
|
|
var domain = app.fqdn;
|
2019-06-26 14:21:00 -07:00
|
|
|
const hostname = isAppContainer ? app.id : name;
|
2019-06-03 13:45:03 -07:00
|
|
|
|
|
|
|
|
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
|
|
|
|
|
|
|
|
|
let stdEnv = [
|
2015-10-19 11:40:19 -07:00
|
|
|
'CLOUDRON=1',
|
2018-11-22 16:50:02 -08:00
|
|
|
'CLOUDRON_PROXY_IP=172.18.0.1',
|
2019-06-26 14:18:39 -07:00
|
|
|
`CLOUDRON_APP_HOSTNAME=${app.id}`,
|
2019-07-12 14:29:35 -07:00
|
|
|
`CLOUDRON_ADMIN_EMAIL=${app.adminEmail}`,
|
2019-07-26 10:49:29 -07:00
|
|
|
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
|
|
|
|
|
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
|
2019-06-03 13:45:03 -07:00
|
|
|
`${envPrefix}APP_ORIGIN=https://${domain}`,
|
|
|
|
|
`${envPrefix}APP_DOMAIN=${domain}`
|
2015-10-19 11:40:19 -07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// docker portBindings requires ports to be exposed
|
|
|
|
|
exposedPorts[manifest.httpPort + '/tcp'] = {};
|
|
|
|
|
|
|
|
|
|
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
|
|
|
|
|
|
2015-10-19 16:00:40 -07:00
|
|
|
var portEnv = [];
|
2018-08-12 19:33:11 -07:00
|
|
|
for (let portName in app.portBindings) {
|
2018-08-12 22:47:59 -07:00
|
|
|
const hostPort = app.portBindings[portName];
|
|
|
|
|
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
|
|
|
|
|
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2018-08-12 22:47:59 -07:00
|
|
|
var containerPort = ports[portName].containerPort || hostPort;
|
|
|
|
|
|
|
|
|
|
exposedPorts[`${containerPort}/${portType}`] = {};
|
2018-08-12 19:33:11 -07:00
|
|
|
portEnv.push(`${portName}=${hostPort}`);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2018-08-12 22:47:59 -07:00
|
|
|
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
2015-10-19 11:40:19 -07:00
|
|
|
}
|
2018-10-11 16:18:38 -07:00
|
|
|
|
2018-10-11 14:07:43 -07:00
|
|
|
let appEnv = [];
|
|
|
|
|
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2016-02-11 18:13:42 +01:00
|
|
|
// first check db record, then manifest
|
2016-09-03 21:02:22 -07:00
|
|
|
var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0;
|
2016-02-11 18:13:42 +01:00
|
|
|
|
2017-01-19 15:02:12 -08:00
|
|
|
if (memoryLimit === -1) { // unrestricted
|
2016-09-03 21:02:22 -07:00
|
|
|
memoryLimit = 0;
|
|
|
|
|
} else if (memoryLimit === 0 || memoryLimit < constants.DEFAULT_MEMORY_LIMIT) { // ensure we never go below minimum (in case we change the default)
|
|
|
|
|
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
|
|
|
|
|
}
|
2016-02-11 17:00:21 +01:00
|
|
|
|
2018-02-27 10:51:21 -08:00
|
|
|
// give scheduler tasks twice the memory limit since background jobs take more memory
|
|
|
|
|
// if required, we can make this a manifest and runtime argument later
|
|
|
|
|
if (!isAppContainer) memoryLimit *= 2;
|
|
|
|
|
|
2015-10-19 16:00:40 -07:00
|
|
|
addons.getEnvironment(app, function (error, addonEnv) {
|
|
|
|
|
if (error) return callback(new Error('Error getting addon environment : ' + error));
|
|
|
|
|
|
2016-06-18 17:53:54 -05:00
|
|
|
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
|
|
|
|
|
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
|
|
|
|
|
// name to look up the internal docker ip. this makes curl from within container fail
|
|
|
|
|
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
|
|
|
|
|
// Hostname cannot be set with container NetworkMode
|
2015-10-19 16:00:40 -07:00
|
|
|
var containerOptions = {
|
2019-06-01 09:36:35 -07:00
|
|
|
name: name, // for referencing containers
|
2015-10-20 10:51:43 -07:00
|
|
|
Tty: isAppContainer,
|
2019-06-26 14:21:00 -07:00
|
|
|
Hostname: hostname,
|
2015-10-19 16:00:40 -07:00
|
|
|
Image: app.manifest.dockerImage,
|
2017-01-20 05:48:25 -08:00
|
|
|
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
2018-10-11 14:07:43 -07:00
|
|
|
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
2015-10-20 10:51:43 -07:00
|
|
|
ExposedPorts: isAppContainer ? exposedPorts : { },
|
2015-10-19 16:00:40 -07:00
|
|
|
Volumes: { // see also ReadonlyRootfs
|
|
|
|
|
'/tmp': {},
|
|
|
|
|
'/run': {}
|
2015-10-19 11:40:19 -07:00
|
|
|
},
|
2015-10-19 16:01:04 -07:00
|
|
|
Labels: {
|
2018-02-08 15:07:49 +01:00
|
|
|
'fqdn': app.fqdn,
|
2017-09-30 18:17:50 -07:00
|
|
|
'appId': app.id,
|
2018-11-10 18:57:50 -08:00
|
|
|
'isSubcontainer': String(!isAppContainer),
|
|
|
|
|
'isCloudronManaged': String(true)
|
2015-10-19 16:01:04 -07:00
|
|
|
},
|
2015-10-19 16:00:40 -07:00
|
|
|
HostConfig: {
|
2018-09-13 13:55:49 -07:00
|
|
|
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
2018-01-16 17:41:18 +01:00
|
|
|
LogConfig: {
|
|
|
|
|
Type: 'syslog',
|
|
|
|
|
Config: {
|
|
|
|
|
'tag': app.id,
|
2018-06-04 21:12:55 +02:00
|
|
|
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
|
2018-01-16 17:41:18 +01:00
|
|
|
'syslog-format': 'rfc5424'
|
|
|
|
|
}
|
|
|
|
|
},
|
2015-10-19 16:00:40 -07:00
|
|
|
Memory: memoryLimit / 2,
|
|
|
|
|
MemorySwap: memoryLimit, // Memory + Swap
|
2015-10-20 10:51:43 -07:00
|
|
|
PortBindings: isAppContainer ? dockerPortBindings : { },
|
2015-10-19 16:00:40 -07:00
|
|
|
PublishAllPorts: false,
|
2017-01-20 05:48:25 -08:00
|
|
|
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
|
2015-10-19 16:00:40 -07:00
|
|
|
RestartPolicy: {
|
2017-09-30 18:17:50 -07:00
|
|
|
'Name': isAppContainer ? 'always' : 'no',
|
|
|
|
|
'MaximumRetryCount': 0
|
2015-10-19 16:00:40 -07:00
|
|
|
},
|
|
|
|
|
CpuShares: 512, // relative to 1024 for system processes
|
2017-09-30 18:17:50 -07:00
|
|
|
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
2019-06-01 09:36:35 -07:00
|
|
|
NetworkMode: 'cloudron', // user defined bridge network
|
2017-04-20 17:02:28 +00:00
|
|
|
Dns: ['172.18.0.1'], // use internal dns
|
2017-04-20 21:28:20 +00:00
|
|
|
DnsSearch: ['.'], // use internal dns
|
2018-11-23 11:23:33 -08:00
|
|
|
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
2019-06-01 10:48:17 -07:00
|
|
|
},
|
|
|
|
|
NetworkingConfig: {
|
|
|
|
|
EndpointsConfig: {
|
|
|
|
|
cloudron: {
|
|
|
|
|
Aliases: [ name ] // this allows sub-containers reach app containers by name
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-10-20 17:34:47 -07:00
|
|
|
}
|
2015-10-19 16:00:40 -07:00
|
|
|
};
|
2017-08-11 23:22:48 +01:00
|
|
|
|
|
|
|
|
var capabilities = manifest.capabilities || [];
|
|
|
|
|
if (capabilities.includes('net_admin')) {
|
|
|
|
|
containerOptions.HostConfig.CapAdd = [
|
|
|
|
|
'NET_ADMIN'
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-10 12:47:48 -08:00
|
|
|
containerOptions = _.extend(containerOptions, options);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2017-09-30 18:17:50 -07:00
|
|
|
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2015-10-19 16:00:40 -07:00
|
|
|
docker.createContainer(containerOptions, callback);
|
|
|
|
|
});
|
2015-10-19 11:40:19 -07:00
|
|
|
}
|
|
|
|
|
|
2015-10-19 21:33:53 -07:00
|
|
|
function createContainer(app, callback) {
|
2015-11-10 12:47:48 -08:00
|
|
|
createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */, callback);
|
2015-10-19 21:33:53 -07:00
|
|
|
}
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
function startContainer(containerId, callback) {
|
2015-10-19 15:51:02 -07:00
|
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2015-10-19 11:40:19 -07:00
|
|
|
var docker = exports.connection;
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
var container = docker.getContainer(containerId);
|
|
|
|
|
debug('Starting container %s', containerId);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
container.start(function (error) {
|
2015-10-19 15:39:26 -07:00
|
|
|
if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error));
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
return callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
function stopContainer(containerId, callback) {
|
2015-10-19 15:51:02 -07:00
|
|
|
assert(!containerId || typeof containerId === 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
if (!containerId) {
|
|
|
|
|
debug('No previous container to stop');
|
2015-10-19 11:40:19 -07:00
|
|
|
return callback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var docker = exports.connection;
|
2015-10-19 15:39:26 -07:00
|
|
|
var container = docker.getContainer(containerId);
|
|
|
|
|
debug('Stopping container %s', containerId);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
var options = {
|
|
|
|
|
t: 10 // wait for 10 seconds before killing it
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
container.stop(options, function (error) {
|
|
|
|
|
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
debug('Waiting for container ' + containerId);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
container.wait(function (error, data) {
|
|
|
|
|
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
return callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
function deleteContainer(containerId, callback) {
|
2015-10-19 15:51:02 -07:00
|
|
|
assert(!containerId || typeof containerId === 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2015-10-19 18:48:56 -07:00
|
|
|
debug('deleting container %s', containerId);
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
if (containerId === null) return callback(null);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
var docker = exports.connection;
|
2015-10-19 15:39:26 -07:00
|
|
|
var container = docker.getContainer(containerId);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
|
|
|
|
var removeOptions = {
|
|
|
|
|
force: true, // kill container if it's running
|
|
|
|
|
v: true // removes volumes associated with the container (but not host mounts)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
container.remove(removeOptions, function (error) {
|
|
|
|
|
if (error && error.statusCode === 404) return callback(null);
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
if (error) debug('Error removing container %s : %j', containerId, error);
|
|
|
|
|
|
2015-10-19 11:40:19 -07:00
|
|
|
callback(error);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-17 23:32:24 -08:00
|
|
|
function deleteContainers(appId, options, callback) {
|
2015-10-19 18:48:56 -07:00
|
|
|
assert.strictEqual(typeof appId, 'string');
|
2019-01-17 23:32:24 -08:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
2015-10-19 18:48:56 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var docker = exports.connection;
|
|
|
|
|
|
|
|
|
|
debug('deleting containers of %s', appId);
|
|
|
|
|
|
2019-01-17 23:32:24 -08:00
|
|
|
let labels = [ 'appId=' + appId ];
|
|
|
|
|
if (options.managedOnly) labels.push('isCloudronManaged=true');
|
|
|
|
|
|
|
|
|
|
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
|
2015-10-19 18:48:56 -07:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
async.eachSeries(containers, function (container, iteratorDone) {
|
|
|
|
|
deleteContainer(container.Id, iteratorDone);
|
|
|
|
|
}, callback);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-20 00:05:07 -07:00
|
|
|
function stopContainers(appId, callback) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var docker = exports.connection;
|
|
|
|
|
|
|
|
|
|
debug('stopping containers of %s', appId);
|
|
|
|
|
|
|
|
|
|
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
async.eachSeries(containers, function (container, iteratorDone) {
|
|
|
|
|
stopContainer(container.Id, iteratorDone);
|
|
|
|
|
}, callback);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-19 15:39:26 -07:00
|
|
|
function deleteImage(manifest, callback) {
|
2015-10-19 15:51:02 -07:00
|
|
|
assert(!manifest || typeof manifest === 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2015-10-19 11:40:19 -07:00
|
|
|
var dockerImage = manifest ? manifest.dockerImage : null;
|
|
|
|
|
if (!dockerImage) return callback(null);
|
|
|
|
|
|
|
|
|
|
var docker = exports.connection;
|
|
|
|
|
|
2016-01-21 14:59:24 -08:00
|
|
|
var removeOptions = {
|
|
|
|
|
force: false, // might be shared with another instance of this app
|
|
|
|
|
noprune: false // delete untagged parents
|
|
|
|
|
};
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2016-01-21 14:59:24 -08:00
|
|
|
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
|
|
|
|
|
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
|
|
|
|
|
// not created anymore after https://github.com/docker/docker/pull/10571
|
|
|
|
|
docker.getImage(dockerImage).remove(removeOptions, function (error) {
|
2018-10-24 13:09:35 -07:00
|
|
|
if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image
|
|
|
|
|
if (error && error.statusCode === 404) return callback(null); // not found
|
2016-01-21 14:59:24 -08:00
|
|
|
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2016-01-21 14:59:24 -08:00
|
|
|
if (error) debug('Error removing image %s : %j', dockerImage, error);
|
2015-10-19 11:40:19 -07:00
|
|
|
|
2016-01-21 14:59:24 -08:00
|
|
|
callback(error);
|
2015-10-19 11:40:19 -07:00
|
|
|
});
|
|
|
|
|
}
|
2016-02-18 15:39:27 +01:00
|
|
|
|
|
|
|
|
function getContainerIdByIp(ip, callback) {
|
|
|
|
|
assert.strictEqual(typeof ip, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var docker = exports.connection;
|
|
|
|
|
|
2017-11-11 16:54:43 -08:00
|
|
|
docker.getNetwork('cloudron').inspect(function (error, bridge) {
|
|
|
|
|
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
|
2016-02-18 15:39:27 +01:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
var containerId;
|
|
|
|
|
for (var id in bridge.Containers) {
|
2017-11-11 16:54:43 -08:00
|
|
|
if (bridge.Containers[id].IPv4Address.indexOf(ip + '/16') === 0) {
|
2016-02-18 15:39:27 +01:00
|
|
|
containerId = id;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!containerId) return callback(new Error('No container with that ip'));
|
|
|
|
|
|
|
|
|
|
callback(null, containerId);
|
|
|
|
|
});
|
|
|
|
|
}
|
2016-04-18 10:32:22 -07:00
|
|
|
|
2017-08-11 22:04:40 +02:00
|
|
|
function inspect(containerId, callback) {
|
|
|
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var container = exports.connection.getContainer(containerId);
|
|
|
|
|
|
|
|
|
|
container.inspect(function (error, result) {
|
2018-11-19 10:19:46 +01:00
|
|
|
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
|
|
|
|
|
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2018-11-28 10:39:12 +01:00
|
|
|
callback(null, result);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-06 11:54:37 -08:00
|
|
|
function getEvents(options, callback) {
|
|
|
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
let docker = exports.connection;
|
|
|
|
|
|
|
|
|
|
docker.getEvents(options, function (error, stream) {
|
|
|
|
|
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
callback(null, stream);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 10:39:12 +01:00
|
|
|
function memoryUsage(containerId, callback) {
|
|
|
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var container = exports.connection.getContainer(containerId);
|
|
|
|
|
|
|
|
|
|
container.stats({ stream: false }, function (error, result) {
|
|
|
|
|
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
|
|
|
|
|
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
|
|
|
|
|
2017-08-11 22:04:40 +02:00
|
|
|
callback(null, result);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-04-18 11:42:34 -07:00
|
|
|
function execContainer(containerId, cmd, options, callback) {
|
2016-04-18 10:32:22 -07:00
|
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
|
assert(util.isArray(cmd));
|
2016-04-18 15:02:31 -07:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
2016-04-18 10:32:22 -07:00
|
|
|
|
|
|
|
|
callback = once(callback); // ChildProcess exit may or may not be called after error
|
|
|
|
|
|
|
|
|
|
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', containerId ].concat(cmd));
|
2016-04-18 12:22:42 -07:00
|
|
|
|
2016-04-18 11:02:02 -07:00
|
|
|
var chunks = [ ];
|
2016-04-18 12:22:42 -07:00
|
|
|
|
|
|
|
|
if (options.stdout) {
|
|
|
|
|
cp.stdout.pipe(options.stdout);
|
2016-04-18 15:02:31 -07:00
|
|
|
} else if (options.bufferStdout) {
|
2016-04-18 12:22:42 -07:00
|
|
|
cp.stdout.on('data', function (chunk) { chunks.push(chunk); });
|
2016-04-18 15:02:31 -07:00
|
|
|
} else {
|
|
|
|
|
cp.stdout.pipe(process.stdout);
|
2016-04-18 12:22:42 -07:00
|
|
|
}
|
2016-04-18 11:02:02 -07:00
|
|
|
|
2016-04-18 11:42:34 -07:00
|
|
|
cp.on('error', callback);
|
2016-04-18 10:32:22 -07:00
|
|
|
cp.on('exit', function (code, signal) {
|
|
|
|
|
debug('execContainer code: %s signal: %s', code, signal);
|
2016-04-18 11:02:02 -07:00
|
|
|
if (!callback.called) callback(code ? 'Failed with status ' + code : null, Buffer.concat(chunks));
|
2016-04-18 10:32:22 -07:00
|
|
|
});
|
|
|
|
|
|
2016-04-18 11:42:34 -07:00
|
|
|
cp.stderr.pipe(options.stderr || process.stderr);
|
2016-04-18 10:32:22 -07:00
|
|
|
|
2016-04-18 11:42:34 -07:00
|
|
|
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
|
2016-04-18 10:32:22 -07:00
|
|
|
}
|
2018-09-13 13:55:49 -07:00
|
|
|
|
2019-01-13 19:18:06 -08:00
|
|
|
function createVolume(app, name, volumeDataDir, callback) {
|
2018-09-13 13:55:49 -07:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof name, 'string');
|
2019-01-13 19:18:06 -08:00
|
|
|
assert.strictEqual(typeof volumeDataDir, 'string');
|
2018-09-13 13:55:49 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
let docker = exports.connection;
|
|
|
|
|
|
|
|
|
|
const volumeOptions = {
|
|
|
|
|
Name: name,
|
|
|
|
|
Driver: 'local',
|
|
|
|
|
DriverOpts: { // https://github.com/moby/moby/issues/19990#issuecomment-248955005
|
|
|
|
|
type: 'none',
|
|
|
|
|
device: volumeDataDir,
|
|
|
|
|
o: 'bind'
|
|
|
|
|
},
|
|
|
|
|
Labels: {
|
|
|
|
|
'fqdn': app.fqdn,
|
|
|
|
|
'appId': app.id
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2018-12-20 14:33:29 -08:00
|
|
|
// requires sudo because the path can be outside appsdata
|
|
|
|
|
shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) {
|
2018-09-13 13:55:49 -07:00
|
|
|
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
|
|
|
|
|
|
|
|
|
|
docker.createVolume(volumeOptions, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-18 14:48:31 -08:00
|
|
|
function clearVolume(app, name, options, callback) {
|
2018-09-15 17:05:04 -07:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof name, 'string');
|
2019-01-18 14:48:31 -08:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
2018-09-15 17:05:04 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-01-13 19:18:06 -08:00
|
|
|
let docker = exports.connection;
|
|
|
|
|
let volume = docker.getVolume(name);
|
|
|
|
|
volume.inspect(function (error, v) {
|
|
|
|
|
if (error && error.statusCode === 404) return callback();
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
const volumeDataDir = v.Options.device;
|
2019-01-18 14:48:31 -08:00
|
|
|
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, callback);
|
2019-01-13 19:18:06 -08:00
|
|
|
});
|
2018-09-15 17:05:04 -07:00
|
|
|
}
|
|
|
|
|
|
2019-01-17 09:53:51 -08:00
|
|
|
// this only removes the volume and not the data
|
2019-01-13 19:18:06 -08:00
|
|
|
function removeVolume(app, name, callback) {
|
2018-09-13 13:55:49 -07:00
|
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
|
assert.strictEqual(typeof name, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
let docker = exports.connection;
|
|
|
|
|
|
2019-01-15 09:36:32 -08:00
|
|
|
let volume = docker.getVolume(name);
|
|
|
|
|
volume.remove(function (error) {
|
|
|
|
|
if (error && error.statusCode !== 404) return callback(new Error(`removeVolume: Error removing volume of ${app.id} ${error.message}`));
|
2019-01-13 19:18:06 -08:00
|
|
|
|
2019-01-15 09:36:32 -08:00
|
|
|
callback();
|
2018-09-13 13:55:49 -07:00
|
|
|
});
|
|
|
|
|
}
|