refactor container code into docker.js
This commit is contained in:
252
src/docker.js
252
src/docker.js
@@ -1,9 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
var Docker = require('dockerode');
|
||||
var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:src/appcontainer.js'),
|
||||
Docker = require('dockerode'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
connection: connectionInstance()
|
||||
connection: connectionInstance(),
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
stopContainer: stopContainer,
|
||||
deleteContainer: deleteContainer,
|
||||
deleteImage: deleteImage
|
||||
};
|
||||
|
||||
function connectionInstance() {
|
||||
@@ -21,3 +35,237 @@ function connectionInstance() {
|
||||
|
||||
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(app, callback) {
|
||||
var docker = exports.connection;
|
||||
|
||||
docker.pull(app.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) || { };
|
||||
debugApp(app, '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) {
|
||||
debugApp(app, 'pullImage error detail: %s', data.errorDetail.message);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
debugApp(app, 'download image successfully');
|
||||
|
||||
var image = docker.getImage(app.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'));
|
||||
|
||||
debugApp(app, 'This image exposes ports: %j', data.Config.ExposedPorts);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debugApp(app, 'pullImage error : %j', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function downloadImage(app, callback) {
|
||||
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
|
||||
|
||||
var attempt = 1;
|
||||
|
||||
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
|
||||
debugApp(app, 'Downloading image. attempt: %s', attempt++);
|
||||
|
||||
pullImage(app, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, env, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(util.isArray(env));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'WEBADMIN_ORIGIN' + '=' + config.adminOrigin(),
|
||||
'API_ORIGIN' + '=' + config.adminOrigin()
|
||||
];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
exposedPorts[manifest.httpPort + '/tcp'] = {};
|
||||
|
||||
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
|
||||
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
|
||||
|
||||
for (var e in app.portBindings) {
|
||||
var hostPort = app.portBindings[e];
|
||||
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
|
||||
|
||||
exposedPorts[containerPort + '/tcp'] = {};
|
||||
env.push(e + '=' + hostPort);
|
||||
|
||||
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
|
||||
|
||||
var containerOptions = {
|
||||
name: app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Tty: true,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: null,
|
||||
Env: stdEnv.concat(env),
|
||||
ExposedPorts: exposedPorts,
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
'/tmp': {},
|
||||
'/run': {}
|
||||
},
|
||||
HostConfig: {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
Memory: memoryLimit / 2,
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: 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": "always",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
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 with options: %j', app.manifest.dockerImage, containerOptions);
|
||||
|
||||
docker.createContainer(containerOptions, callback);
|
||||
}
|
||||
|
||||
function startContainer(app, callback) {
|
||||
var docker = exports.connection;
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
debugApp(app, 'Starting container %s', container.id);
|
||||
|
||||
container.start(function (error) {
|
||||
if (error && error.statusCode !== 304) return callback(new Error('Error starting container:' + error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function stopContainer(app, callback) {
|
||||
if (!app.containerId) {
|
||||
debugApp(app, 'No previous container to stop');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var docker = exports.connection;
|
||||
var container = docker.getContainer(app.containerId);
|
||||
debugApp(app, 'Stopping container %s', container.id);
|
||||
|
||||
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));
|
||||
|
||||
debugApp(app, 'Waiting for container ' + container.id);
|
||||
|
||||
container.wait(function (error, data) {
|
||||
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
|
||||
|
||||
debugApp(app, 'Container stopped with status code [%s]', data ? String(data.StatusCode) : '');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainer(app, callback) {
|
||||
if (app.containerId === null) return callback(null);
|
||||
|
||||
var docker = exports.connection;
|
||||
var container = docker.getContainer(app.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) debugApp(app, 'Error removing container', error);
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteImage(app, manifest, callback) {
|
||||
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) debugApp(app, 'Error removing image', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user