Files
cloudron-box/src/docker.js

426 lines
15 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2015-10-19 11:40:19 -07:00
connection: connectionInstance(),
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
stopContainer: stopContainer,
stopContainerByName: stopContainer,
2015-10-20 00:05:07 -07:00
stopContainers: stopContainers,
2015-10-19 11:40:19 -07:00
deleteContainer: deleteContainer,
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,
inspectByName: inspect,
2016-04-18 10:32:22 -07:00
execContainer: execContainer
};
function connectionInstance() {
2016-04-18 16:30:58 -07:00
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687 });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker({ socketPath: '/var/run/docker.sock' });
}
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'),
config = require('./config.js'),
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'),
safe = require('safetydance'),
shell = require('./shell.js'),
2016-04-18 16:30:58 -07:00
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
2015-10-19 11:40:19 -07:00
function debugApp(app, args) {
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
}
function pullImage(manifest, callback) {
2015-10-19 11:40:19 -07:00
var docker = exports.connection;
// 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
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
}
2015-10-19 11:40:19 -07:00
var image = docker.getImage(manifest.dockerImage);
2015-10-19 11:40:19 -07:00
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
2015-10-19 11:40:19 -07:00
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
2015-10-19 11:40:19 -07:00
callback(null);
2015-10-19 11:40:19 -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
pullImage(manifest, function (error) {
2015-10-19 11:40:19 -07:00
if (error) console.error(error);
retryCallback(error);
});
}, callback);
}
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));
assert.strictEqual(typeof options, 'object');
2015-10-19 11:40:19 -07:00
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection,
isAppContainer = !cmd; // non app-containers are like scheduler 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;
2015-10-19 11:40:19 -07:00
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + domain,
'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 = [];
for (let portName in app.portBindings) {
var hostPort = app.portBindings[portName];
var containerPort = manifest.tcpPorts[portName].containerPort || hostPort;
2015-10-19 11:40:19 -07:00
exposedPorts[containerPort + '/tcp'] = {};
portEnv.push(`${portName}=${hostPort}`);
2015-10-19 11:40:19 -07:00
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
// first check db record, then manifest
var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted
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
// 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;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
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));
// 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 = {
2015-11-02 10:31:40 -08:00
name: name, // used for filtering logs
Tty: isAppContainer,
2015-10-19 16:00:40 -07:00
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
2015-10-19 16:00:40 -07:00
Env: stdEnv.concat(addonEnv).concat(portEnv),
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,
'isSubcontainer': String(!isAppContainer)
2015-10-19 16:01:04 -07:00
},
2015-10-19 16:00:40 -07:00
HostConfig: {
Binds: addons.getBindsSync(app, app.manifest.addons),
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()
'syslog-format': 'rfc5424'
}
},
2015-10-19 16:00:40 -07:00
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
2015-10-19 16:00:40 -07:00
PublishAllPorts: false,
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' ],
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
2017-09-30 18:17:50 -07:00
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
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'
];
}
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) {
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);
});
}
2015-10-19 18:48:56 -07:00
function deleteContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('deleting 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) {
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;
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
// 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) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
2015-10-19 11:40:19 -07:00
if (error) debug('Error removing image %s : %j', dockerImage, error);
2015-10-19 11:40:19 -07: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;
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) {
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) {
if (error) return callback(error);
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
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: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);
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
}