'use strict'; var addons = require('./addons.js'), async = require('async'), assert = require('assert'), config = require('./config.js'), debug = require('debug')('box:src/docker.js'), Docker = require('dockerode'), safe = require('safetydance'), semver = require('semver'), util = require('util'); exports = module.exports = { connection: connectionInstance(), downloadImage: downloadImage, createContainer: createContainer, startContainer: startContainer, stopContainer: stopContainer, stopContainers: stopContainers, deleteContainer: deleteContainer, deleteImage: deleteImage, deleteContainers: deleteContainers, createSubcontainer: createSubcontainer }; function connectionInstance() { 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; } function debugApp(app, args) { assert(!app || typeof app === 'object'); var prefix = app ? (app.location || '(bare)') : '(no app)'; debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); } function targetBoxVersion(manifest) { if ('targetBoxVersion' in manifest) return manifest.targetBoxVersion; if ('minBoxVersion' in manifest) return manifest.minBoxVersion; return '0.0.1'; } function pullImage(manifest, callback) { var docker = exports.connection; docker.pull(manifest.dockerImage, function (err, stream) { if (err) return callback(new Error('Error connecting to docker. statusCode: %s' + err.statusCode)); // https://github.com/dotcloud/docker/issues/1074 says each status message // is emitted as a chunk stream.on('data', function (chunk) { var data = safe.JSON.parse(chunk) || { }; debug('pullImage data: %j', data); // The information here is useless because this is per layer as opposed to per image if (data.status) { // debugApp(app, 'progress: %s', data.status); // progressDetail { current, total } } else if (data.error) { debug('pullImage error detail: %s', data.errorDetail.message); } }); stream.on('end', function () { debug('downloaded image %s successfully', manifest.dockerImage); var image = docker.getImage(manifest.dockerImage); image.inspect(function (err, data) { if (err) return callback(new Error('Error inspecting image:' + err.message)); if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4))); if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed')); debug('This image exposes ports: %j', data.Config.ExposedPorts); callback(null); }); }); stream.on('error', function (error) { debug('error pulling image %s : %j', manifest.dockerImage, error); callback(error); }); }); } function downloadImage(manifest, callback) { assert.strictEqual(typeof manifest, 'object'); assert.strictEqual(typeof callback, 'function'); debug('downloadImage %s', manifest.dockerImage); var attempt = 1; async.retry({ times: 5, interval: 15000 }, function (retryCallback) { debug('Downloading image. attempt: %s', attempt++); pullImage(manifest, function (error) { if (error) console.error(error); retryCallback(error); }); }, callback); } function createSubcontainer(app, name, cmd, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof name, 'string'); assert(!cmd || util.isArray(cmd)); assert.strictEqual(typeof callback, 'function'); var docker = exports.connection, isAppContainer = !cmd; var manifest = app.manifest; var exposedPorts = {}, dockerPortBindings = { }; var stdEnv = [ 'CLOUDRON=1', 'WEBADMIN_ORIGIN=' + config.adminOrigin(), 'API_ORIGIN=' + config.adminOrigin(), 'APP_ORIGIN=https://' + config.appFqdn(app.location), 'APP_DOMAIN=' + config.appFqdn(app.location) ]; // docker portBindings requires ports to be exposed exposedPorts[manifest.httpPort + '/tcp'] = {}; dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ]; var portEnv = []; for (var e in app.portBindings) { var hostPort = app.portBindings[e]; var containerPort = manifest.tcpPorts[e].containerPort || hostPort; exposedPorts[containerPort + '/tcp'] = {}; portEnv.push(e + '=' + hostPort); dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ]; } var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default addons.getEnvironment(app, function (error, addonEnv) { if (error) return callback(new Error('Error getting addon environment : ' + error)); var containerOptions = { // do _not_ set hostname to app fqdn. doing so sets up the dns name to look up the internal docker ip. this makes curl from within container fail Hostname: semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location), Name: name, // used for filtering logs Tty: isAppContainer, Image: app.manifest.dockerImage, Cmd: cmd, Env: stdEnv.concat(addonEnv).concat(portEnv), ExposedPorts: isAppContainer ? exposedPorts : { }, Volumes: { // see also ReadonlyRootfs '/tmp': {}, '/run': {} }, Labels: { "location": app.location, "appId": app.id, "isSubcontainer": String(!isAppContainer) }, HostConfig: { Binds: addons.getBindsSync(app, app.manifest.addons), Memory: memoryLimit / 2, MemorySwap: memoryLimit, // Memory + Swap PortBindings: isAppContainer ? dockerPortBindings : { }, PublishAllPorts: false, ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer Links: addons.getLinksSync(app, app.manifest.addons), RestartPolicy: { "Name": isAppContainer ? "always" : "no", "MaximumRetryCount": 0 }, CpuShares: 512, // relative to 1024 for system processes VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ], SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron } }; // older versions wanted a writable /var/log if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {}; debugApp(app, 'Creating container for %s', app.manifest.dockerImage); docker.createContainer(containerOptions, callback); }); } function createContainer(app, callback) { createSubcontainer(app, app.id /* name */, null /* cmd */, callback); } function startContainer(containerId, callback) { assert.strictEqual(typeof containerId, 'string'); assert.strictEqual(typeof callback, 'function'); var docker = exports.connection; var container = docker.getContainer(containerId); debug('Starting container %s', containerId); container.start(function (error) { if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error)); return callback(null); }); } function stopContainer(containerId, callback) { assert(!containerId || typeof containerId === 'string'); assert.strictEqual(typeof callback, 'function'); if (!containerId) { debug('No previous container to stop'); return callback(); } var docker = exports.connection; var container = docker.getContainer(containerId); debug('Stopping container %s', containerId); 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)); debug('Waiting for container ' + containerId); container.wait(function (error, data) { if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error)); debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : ''); return callback(null); }); }); } function deleteContainer(containerId, callback) { assert(!containerId || typeof containerId === 'string'); assert.strictEqual(typeof callback, 'function'); debug('deleting container %s', containerId); if (containerId === null) return callback(null); var docker = exports.connection; var container = docker.getContainer(containerId); 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); if (error) debug('Error removing container %s : %j', containerId, error); callback(error); }); } 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); }); } 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); }); } function deleteImage(manifest, callback) { assert(!manifest || typeof manifest === 'object'); assert.strictEqual(typeof callback, 'function'); var dockerImage = manifest ? manifest.dockerImage : null; if (!dockerImage) return callback(null); var docker = exports.connection; docker.getImage(dockerImage).inspect(function (error, result) { if (error && error.statusCode === 404) return callback(null); if (error) return callback(error); var removeOptions = { force: true, noprune: false }; // delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags docker.getImage(result.Id).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 if (error) debug('Error removing image %s : %j', dockerImage, error); callback(error); }); }); }