Files
cloudron-box/src/docker.js

696 lines
27 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
testRegistryConfig,
setRegistryConfig,
injectPrivateFields,
removePrivateFields,
ping,
info,
downloadImage,
createContainer,
startContainer,
restartContainer,
stopContainer,
stopContainerByName: stopContainer,
stopContainers,
deleteContainer,
deleteImage,
deleteContainers,
createSubcontainer,
getContainerIdByIp,
inspect,
getContainerIp,
inspectByName: inspect,
execContainer,
getEvents,
memoryUsage,
createVolume,
removeVolume,
clearVolume
};
2016-04-18 16:30:58 -07:00
var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
2019-09-23 12:13:21 -07:00
BoxError = require('./boxerror.js'),
2016-04-18 16:30:58 -07:00
child_process = require('child_process'),
constants = require('./constants.js'),
2019-12-06 13:52:43 -08:00
debug = require('debug')('box:docker'),
2019-12-04 13:17:58 -08:00
Docker = require('dockerode'),
os = require('os'),
path = require('path'),
settings = require('./settings.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
2016-04-18 16:30:58 -07:00
util = require('util'),
volumes = require('./volumes.js'),
_ = require('underscore');
2016-04-18 16:30:58 -07:00
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
2019-12-04 14:37:00 -08:00
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
2019-12-04 13:17:58 -08:00
2019-10-22 22:07:44 -07:00
function testRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
2019-12-04 13:17:58 -08:00
gConnection.checkAuth(auth, function (error /*, data */) { // this returns a 500 even for auth errors
2019-10-23 06:11:17 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error, { field: 'serverAddress' }));
callback();
});
2019-10-22 22:07:44 -07:00
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
2019-10-22 22:07:44 -07:00
}
function removePrivateFields(registryConfig) {
assert.strictEqual(typeof registryConfig, 'object');
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
2019-10-22 22:07:44 -07:00
return registryConfig;
}
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?
2019-10-22 22:07:44 -07:00
const cmd = isLogin ? `docker login ${auth.serverAddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serverAddress}`;
2019-09-23 12:13:21 -07:00
child_process.exec(cmd, { }, function (error /*, stdout, stderr */) {
if (error) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
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
2019-12-04 14:37:00 -08:00
const connection = new Docker({ socketPath: DOCKER_SOCKET_PATH, timeout: 1000 });
2018-11-19 10:19:46 +01:00
2019-12-04 13:17:58 -08:00
connection.ping(function (error, result) {
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2020-01-30 16:06:11 -08:00
if (Buffer.isBuffer(result) && result.toString('utf8') === 'OK') return callback(null); // sometimes it returns buffer
if (result === 'OK') return callback(null);
2018-11-19 10:19:46 +01:00
2020-01-30 16:06:11 -08:00
callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
2018-11-19 10:19:46 +01:00
});
}
2019-10-27 12:14:27 -07:00
function getRegistryConfig(image, callback) {
// https://github.com/docker/distribution/blob/release/2.7/reference/normalize.go#L62
2019-10-27 12:14:27 -07:00
const parts = image.split('/');
if (parts.length === 1 || (parts[0].match(/[.:]/) === null)) return callback(null, null); // public docker registry
2019-10-27 12:14:27 -07:00
settings.getRegistryConfig(function (error, registryConfig) {
if (error) return callback(error);
// https://github.com/apocas/dockerode#pull-from-private-repos
const auth = {
username: registryConfig.username,
password: registryConfig.password,
auth: registryConfig.auth || '', // the auth token at login time
email: registryConfig.email || '',
serveraddress: registryConfig.serverAddress
};
callback(null, auth);
});
}
function pullImage(manifest, callback) {
2019-10-27 12:14:27 -07:00
getRegistryConfig(manifest.dockerImage, function (error, authConfig) {
if (error) return callback(error);
2015-10-19 11:40:19 -07:00
2019-10-27 12:14:27 -07:00
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
2015-10-19 11:40:19 -07:00
2019-12-04 13:17:58 -08:00
gConnection.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${manifest.dockerImage}. message: ${error.message} statusCode: ${error.statusCode}`));
2020-02-10 21:54:08 -08:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${manifest.dockerImage}. Please check the network or if the image needs authentication. statusCode: ${error.statusCode}`));
2019-10-27 12:14:27 -07:00
// 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: %j', data);
2019-10-27 12:14:27 -07:00
// 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.dockerImage, data.errorDetail.message);
2019-10-27 12:14:27 -07:00
}
});
stream.on('end', function () {
debug('downloaded image %s', manifest.dockerImage);
2019-10-27 12:14:27 -07:00
callback(null);
});
2015-10-19 11:40:19 -07:00
2019-10-27 12:14:27 -07:00
stream.on('error', function (error) {
debug('error pulling image %s: %j', manifest.dockerImage, error);
2015-10-19 11:40:19 -07:00
2019-10-27 12:14:27 -07:00
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
});
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');
debug('downloadImage %s', manifest.dockerImage);
2015-10-19 11:40:19 -07:00
const image = gConnection.getImage(manifest.dockerImage);
2015-10-19 11:40:19 -07:00
image.inspect(function (error, result) {
if (!error && result) return callback(null); // image is already present locally
2015-10-19 11:40:19 -07:00
let attempt = 1;
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
pullImage(manifest, retryCallback);
}, callback);
});
2015-10-19 11:40:19 -07:00
}
function getBinds(app, callback) {
2020-04-29 21:55:21 -07:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2020-10-29 11:47:37 -07:00
if (app.mounts.length === 0) return callback(null);
2020-04-29 21:55:21 -07:00
let binds = [];
volumes.list(function (error, result) {
if (error) return callback(error);
2020-10-29 11:47:37 -07:00
let volumesById = {};
result.forEach(r => volumesById[r.id] = r);
2020-10-29 11:47:37 -07:00
for (const mount of app.mounts) {
const volume = volumesById[mount.volumeId];
binds.push(`${volume.hostPath}:/media/${volume.name}:${mount.readOnly ? 'ro' : 'rw'}`);
}
2020-04-29 21:55:21 -07:00
2020-10-29 11:47:37 -07:00
callback(null, binds);
});
2020-04-29 21:55:21 -07:00
}
function getLowerUpIp() { // see getifaddrs and IFF_LOWER_UP and netdevice
const ni = os.networkInterfaces(); // { lo: [], eth0: [] }
for (const iname of Object.keys(ni)) {
if (iname === 'lo') continue;
for (const address of ni[iname]) {
if (!address.internal && address.family === 'IPv4') return address.address;
}
}
return null;
}
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');
let isAppContainer = !cmd; // non app-containers are like scheduler
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;
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',
`CLOUDRON_APP_HOSTNAME=${app.id}`,
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
`${envPrefix}APP_DOMAIN=${domain}`
2015-10-19 11:40:19 -07:00
];
2015-10-19 16:00:40 -07:00
var portEnv = [];
for (let portName in app.portBindings) {
const hostPort = app.portBindings[portName];
const portType = (manifest.tcpPorts && portName in manifest.tcpPorts) ? 'tcp' : 'udp';
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
2015-10-19 11:40:19 -07:00
var containerPort = ports[portName].containerPort || hostPort;
// docker portBindings requires ports to be exposed
exposedPorts[`${containerPort}/${portType}`] = {};
portEnv.push(`${portName}=${hostPort}`);
2015-10-19 11:40:19 -07:00
const hostIp = hostPort === 53 ? getLowerUpIp() : '0.0.0.0'; // port 53 is special because it is possibly taken by systemd-resolved
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: hostIp, HostPort: hostPort + '' } ];
2015-10-19 11:40:19 -07:00
}
2018-10-11 16:18:38 -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
// 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;
addons.getEnvironment(app, function (error, addonEnv) {
2019-10-24 20:31:45 -07:00
if (error) return callback(error);
2015-10-19 16:00:40 -07:00
getBinds(app, function (error, binds) {
if (error) return callback(error);
let containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
2015-10-19 16:00:40 -07:00
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: binds, // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
CpuShares: app.cpuShares,
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
}
};
2017-08-11 23:22:48 +01: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. Subcontainers run is the network space of the app container
// This is done to prevent lots of up/down events and iptables locking
if (isAppContainer) {
containerOptions.Hostname = app.id;
containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // adds hostname entry with container name
}
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`;
}
var capabilities = manifest.capabilities || [];
2020-06-30 07:31:24 -07:00
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
2017-08-11 23:22:48 +01:00
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
2020-08-14 18:48:53 -07:00
containerOptions = _.extend(containerOptions, options);
gConnection.createContainer(containerOptions, function (error, container) {
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2019-10-22 21:46:32 -07:00
callback(null, container);
});
2019-10-22 21:46:32 -07:00
});
2015-10-19 16:00:40 -07:00
});
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');
2019-12-04 13:17:58 -08:00
var container = gConnection.getContainer(containerId);
2015-10-19 11:40:19 -07:00
container.start(function (error) {
2019-09-23 12:13:21 -07:00
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
2019-10-20 13:35:19 -07:00
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
2019-12-20 10:29:29 -08:00
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); // 304 means already started
return callback(null);
});
}
function restartContainer(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
container.restart(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
if (error && error.statusCode !== 204) return callback(new BoxError(BoxError.DOCKER_ERROR, 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();
}
2019-12-04 13:17:58 -08:00
var container = gConnection.getContainer(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) {
2019-10-22 21:46:32 -07:00
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
2015-10-19 11:40:19 -07:00
2020-05-24 12:30:48 -07:00
container.wait(function (error/*, data */) {
2019-10-22 21:46:32 -07:00
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
2015-10-19 11:40:19 -07:00
return callback(null);
});
});
}
function deleteContainer(containerId, callback) { // id can also be name
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 === null) return callback(null);
2015-10-19 11:40:19 -07:00
2019-12-04 13:17:58 -08:00
var container = gConnection.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);
2019-10-23 09:53:46 -07:00
if (error) {
debug('Error removing container %s : %j', containerId, error);
return callback(new BoxError(BoxError.DOCKER_ERROR, error));
}
2015-10-19 15:39:26 -07:00
2019-10-23 09:53:46 -07:00
callback(null);
2015-10-19 11:40:19 -07:00
});
}
function deleteContainers(appId, options, callback) {
2015-10-19 18:48:56 -07:00
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof options, 'object');
2015-10-19 18:48:56 -07:00
assert.strictEqual(typeof callback, 'function');
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
2019-12-04 13:17:58 -08:00
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2015-10-19 18:48:56 -07:00
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');
2019-12-04 13:17:58 -08:00
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2015-10-20 00:05:07 -07:00
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 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
2019-12-04 13:17:58 -08:00
gConnection.getImage(dockerImage).remove(removeOptions, function (error) {
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
if (error && error.statusCode === 409) return callback(null); // another container using the image
2015-10-19 11:40:19 -07:00
2019-10-23 09:24:51 -07:00
if (error) {
debug('Error removing image %s : %j', dockerImage, error);
return callback(new BoxError(BoxError.DOCKER_ERROR, error));
}
2015-10-19 11:40:19 -07:00
2019-10-23 09:24:51 -07:00
callback(null);
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');
2019-12-04 13:17:58 -08:00
gConnection.getNetwork('cloudron').inspect(function (error, bridge) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to find the cloudron network'));
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2016-02-18 15:39:27 +01:00
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 BoxError(BoxError.DOCKER_ERROR, 'No container with that ip'));
2016-02-18 15:39:27 +01:00
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');
2019-12-04 13:17:58 -08:00
var container = gConnection.getContainer(containerId);
2017-08-11 22:04:40 +02:00
container.inspect(function (error, result) {
2020-07-30 11:29:43 -07:00
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`));
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2018-11-19 10:19:46 +01:00
2018-11-28 10:39:12 +01:00
callback(null, result);
});
}
function getContainerIp(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
if (constants.TEST) return callback(null, '127.0.5.5');
inspect(containerId, function (error, result) {
if (error) return callback(error);
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP'));
callback(null, ip);
});
}
2019-12-04 13:17:58 -08:00
function execContainer(containerId, options, callback) {
assert.strictEqual(typeof containerId, 'string');
2019-03-06 11:54:37 -08:00
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
2019-12-04 13:17:58 -08:00
var container = gConnection.getContainer(containerId);
2019-03-06 11:54:37 -08:00
2019-12-04 13:17:58 -08:00
container.exec(options.execOptions, function (error, exec) {
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
exec.start(options.startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (options.rows && options.columns) {
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout(function () {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}, 2000);
}
callback(null, stream);
});
});
}
function getEvents(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
gConnection.getEvents(options, function (error, stream) {
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2019-03-06 11:54:37 -08:00
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');
2019-12-04 13:17:58 -08:00
var container = gConnection.getContainer(containerId);
2018-11-28 10:39:12 +01:00
container.stats({ stream: false }, function (error, result) {
2019-09-23 12:13:21 -07:00
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
2018-11-28 10:39:12 +01:00
2017-08-11 22:04:40 +02:00
callback(null, result);
});
}
2020-04-29 21:28:46 -07:00
function createVolume(name, volumeDataDir, labels, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
2020-04-29 21:28:46 -07:00
assert.strictEqual(typeof labels, 'object');
assert.strictEqual(typeof callback, 'function');
const volumeOptions = {
Name: name,
Driver: 'local',
DriverOpts: { // https://github.com/moby/moby/issues/19990#issuecomment-248955005
type: 'none',
device: volumeDataDir,
o: 'bind'
},
2020-04-29 21:28:46 -07:00
Labels: labels
};
// requires sudo because the path can be outside appsdata
shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating app data dir: ${error.message}`));
2019-12-04 13:17:58 -08:00
gConnection.createVolume(volumeOptions, function (error) {
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback();
});
});
}
2020-04-29 21:28:46 -07:00
function clearVolume(name, options, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
2019-12-04 13:17:58 -08:00
let volume = gConnection.getVolume(name);
volume.inspect(function (error, v) {
if (error && error.statusCode === 404) return callback();
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
const volumeDataDir = v.Options.device;
2019-10-22 21:46:32 -07:00
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
callback();
});
});
}
// this only removes the volume and not the data
2020-04-29 21:28:46 -07:00
function removeVolume(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
2019-12-04 13:17:58 -08:00
let volume = gConnection.getVolume(name);
2019-01-15 09:36:32 -08:00
volume.remove(function (error) {
2020-04-29 21:28:46 -07:00
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
2019-01-15 09:36:32 -08:00
callback();
});
}
function info(callback) {
assert.strictEqual(typeof callback, 'function');
2019-12-04 13:17:58 -08:00
gConnection.info(function (error, result) {
2019-09-23 23:27:32 -07:00
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker'));
callback(null, result);
});
}