docker.js and services.js: async'ify

This commit is contained in:
Girish Ramakrishnan
2021-08-25 19:41:46 -07:00
parent 1cc11fece8
commit 42774eac8c
24 changed files with 1618 additions and 2130 deletions

View File

@@ -18,10 +18,8 @@ exports = module.exports = {
deleteImage,
deleteContainers,
createSubcontainer,
getContainerIdByIp,
inspect,
getContainerIp,
inspectByName: inspect,
execContainer,
getEvents,
memoryUsage,
@@ -32,11 +30,11 @@ exports = module.exports = {
};
const apps = require('./apps.js'),
async = require('async'),
assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker'),
delay = require('delay'),
Docker = require('dockerode'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
@@ -45,7 +43,6 @@ const apps = require('./apps.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
system = require('./system.js'),
util = require('util'),
volumes = require('./volumes.js'),
_ = require('underscore');
@@ -55,17 +52,13 @@ const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
function testRegistryConfig(config, callback) {
async function testRegistryConfig(config) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.provider === 'noop') return callback();
if (config.provider === 'noop') return;
gConnection.checkAuth(config, function (error /*, data */) { // this returns a 500 even for auth errors
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error, { field: 'serverAddress' }));
callback();
});
const [error] = await safe(gConnection.checkAuth(config)); // this returns a 500 even for auth errors
if (error) throw new BoxError(BoxError.BAD_FIELD, error, { field: 'serverAddress' });
}
function injectPrivateFields(newConfig, currentConfig) {
@@ -95,83 +88,77 @@ function ping(callback) {
});
}
function getRegistryConfig(image, callback) {
async function getRegistryConfig(image) {
// https://github.com/docker/distribution/blob/release/2.7/reference/normalize.go#L62
const parts = image.split('/');
if (parts.length === 1 || (parts[0].match(/[.:]/) === null)) return callback(null, null); // public docker registry
if (parts.length === 1 || (parts[0].match(/[.:]/) === null)) return null; // public docker registry
util.callbackify(settings.getRegistryConfig)(function (error, registryConfig) {
if (error) return callback(error);
const registryConfig = await settings.getRegistryConfig();
// 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
};
// 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);
});
return auth;
}
function pullImage(manifest, callback) {
getRegistryConfig(manifest.dockerImage, function (error, config) {
if (error) return callback(error);
async function pullImage(manifest) {
const config = await getRegistryConfig(manifest.dockerImage);
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${config ? 'yes' : 'no'}`);
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${config ? 'yes' : 'no'}`);
gConnection.pull(manifest.dockerImage, { authconfig: config }, 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}`));
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}`));
const [error, stream] = await safe(gConnection.pull(manifest.dockerImage, { authconfig: config }));
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${manifest.dockerImage}. message: ${error.message} statusCode: ${error.statusCode}`);
if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${manifest.dockerImage}. Please check the network or if the image needs authentication. 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: %j', data);
return new Promise((resolve, reject) => {
// 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);
// 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);
}
});
// 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);
}
});
stream.on('end', function () {
debug('downloaded image %s', manifest.dockerImage);
stream.on('end', function () {
debug('downloaded image %s', manifest.dockerImage);
resolve();
});
callback(null);
});
stream.on('error', function (error) {
debug('error pulling image %s: %j', manifest.dockerImage, error);
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
});
stream.on('error', function (error) {
debug('error pulling image %s: %j', manifest.dockerImage, error);
reject(new BoxError(BoxError.DOCKER_ERROR, error.message));
});
});
}
function downloadImage(manifest, callback) {
async function downloadImage(manifest) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
debug('downloadImage %s', manifest.dockerImage);
debug(`downloadImage ${manifest.dockerImage}`);
const image = gConnection.getImage(manifest.dockerImage);
image.inspect(function (error, result) {
if (!error && result) return callback(null); // image is already present locally
const [error, result] = await safe(image.inspect());
if (!error && result) return; // image is already present locally
let attempt = 1;
for (let times = 0; times < 10; times++) {
debug(`downloadImage: pulling image. attempt ${times+1}`);
const [pullError] = await safe(pullImage(manifest));
if (pullError && pullError.reason === BoxError.NOT_FOUND) throw pullError;
if (!pullError) break;
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);
});
await delay(5000);
}
}
async function getVolumeMounts(app) {
@@ -200,16 +187,15 @@ async function getVolumeMounts(app) {
return mounts;
}
function getAddonMounts(app, callback) {
async function getAddonMounts(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
let mounts = [];
const addons = app.manifest.addons;
if (!addons) return callback(null, mounts);
if (!addons) return mounts;
async.eachSeries(Object.keys(addons), async function (addon) {
for (const addon of Object.keys(addons)) {
switch (addon) {
case 'localstorage':
mounts.push({
@@ -219,7 +205,7 @@ function getAddonMounts(app, callback) {
ReadOnly: false
});
return;
break;
case 'tls': {
const bundle = await reverseProxy.getCertificatePath(app.fqdn, app.domain);
@@ -237,28 +223,22 @@ function getAddonMounts(app, callback) {
ReadOnly: true
});
return;
break;
}
default:
return;
break;
}
}, function (error) {
callback(error, mounts);
});
}
return mounts;
}
async function getMounts(app, callback) {
async function getMounts(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const [error, volumeMounts] = await safe(getVolumeMounts(app));
if (error) return callback(error);
getAddonMounts(app, function (error, addonMounts) {
if (error) return callback(error);
callback(null, volumeMounts.concat(addonMounts));
});
const volumeMounts = await getVolumeMounts(app);
const addonMounts = await getAddonMounts(app);
return volumeMounts.concat(addonMounts);
}
function getAddresses() {
@@ -278,22 +258,20 @@ function getAddresses() {
return addresses;
}
function createSubcontainer(app, name, cmd, options, callback) {
async function createSubcontainer(app, name, cmd, options) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert(!cmd || Array.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let isAppContainer = !cmd; // non app-containers are like scheduler
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
const manifest = app.manifest;
const exposedPorts = {}, dockerPortBindings = { };
const domain = app.fqdn;
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
let stdEnv = [
const stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${app.id}`,
@@ -303,13 +281,13 @@ function createSubcontainer(app, name, cmd, options, callback) {
`${envPrefix}APP_DOMAIN=${domain}`
];
var portEnv = [];
for (let portName in app.portBindings) {
const portEnv = [];
for (const 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;
var containerPort = ports[portName].containerPort || hostPort;
const containerPort = ports[portName].containerPort || hostPort;
// docker portBindings requires ports to be exposed
exposedPorts[`${containerPort}/${portType}`] = {};
@@ -319,7 +297,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
dockerPortBindings[`${containerPort}/${portType}`] = hostIps.map(hip => { return { HostIp: hip, HostPort: hostPort + '' }; });
}
let appEnv = [];
const appEnv = [];
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
let memoryLimit = apps.getMemoryLimit(app);
@@ -328,219 +306,194 @@ function createSubcontainer(app, name, cmd, options, callback) {
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
getMounts(app, async function (error, mounts) {
if (error) return callback(error);
const mounts = await getMounts(app);
const [getEnvError, addonEnv] = await safe(services.getEnvironment(app));
if (getEnvError) return callback(getEnvError);
const addonEnv = await services.getEnvironment(app);
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': {}
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)
},
HostConfig: {
Mounts: mounts,
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
Memory: system.getMemoryAllocation(memoryLimit),
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
HostConfig: {
Mounts: mounts,
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Memory: system.getMemoryAllocation(memoryLimit),
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: []
CpuShares: app.cpuShares,
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
}
};
// 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.HostConfig.Dns = ['172.18.0.1']; // use internal dns
containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
IPAMConfig: {
IPv4Address: app.containerIp
},
Aliases: [ name ] // adds hostname entry with container name
}
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth
}
// 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.HostConfig.Dns = ['172.18.0.1']; // use internal dns
containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns
const capabilities = manifest.capabilities || [];
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
IPAMConfig: {
IPv4Address: app.containerIp
},
Aliases: [ name ] // adds hostname entry with container name
}
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth
}
// 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
var capabilities = manifest.capabilities || [];
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
// 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
containerOptions = _.extend(containerOptions, options);
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
const [createError, container] = await safe(gConnection.createContainer(containerOptions));
if (createError && createError.statusCode === 409) throw new BoxError(BoxError.ALREADY_EXISTS, createError);
if (createError) throw new BoxError(BoxError.DOCKER_ERROR, createError);
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));
callback(null, container);
});
});
return container;
}
function createContainer(app, callback) {
createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */, callback);
async function createContainer(app) {
return await createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */);
}
function startContainer(containerId, callback) {
async function startContainer(containerId) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
const container = gConnection.getContainer(containerId);
container.start(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 !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); // 304 means already started
return callback(null);
});
const [error] = await safe(container.start());
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND);
if (error && error.statusCode === 400) throw new BoxError(BoxError.BAD_FIELD, error); // e.g start.sh is not executable
if (error && error.statusCode !== 304) throw new BoxError(BoxError.DOCKER_ERROR, error); // 304 means already started
}
function restartContainer(containerId, callback) {
async function restartContainer(containerId) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
const 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));
return callback(null);
});
const [error] = await safe(container.restart());
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND);
if (error && error.statusCode === 400) throw new BoxError(BoxError.BAD_FIELD, error); // e.g start.sh is not executable
if (error && error.statusCode !== 204) throw new BoxError(BoxError.DOCKER_ERROR, error);
}
function stopContainer(containerId, callback) {
async function stopContainer(containerId) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
if (!containerId) {
debug('No previous container to stop');
return callback();
return;
}
var container = gConnection.getContainer(containerId);
const container = gConnection.getContainer(containerId);
var options = {
const 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 BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
let [error] = await safe(container.stop(options));
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) throw new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message);
container.wait(function (error/*, data */) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
return callback(null);
});
});
[error] = await safe(container.wait());
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) throw new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message);
}
function deleteContainer(containerId, callback) { // id can also be name
async function deleteContainer(containerId) { // id can also be name
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
if (containerId === null) return callback(null);
if (containerId === null) return null;
var container = gConnection.getContainer(containerId);
const container = gConnection.getContainer(containerId);
var removeOptions = {
const 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);
const [error] = await safe(container.remove(removeOptions));
if (error && error.statusCode === 404) return;
if (error) {
debug('Error removing container %s : %j', containerId, error);
return callback(new BoxError(BoxError.DOCKER_ERROR, error));
}
callback(null);
});
if (error) {
debug('Error removing container %s : %j', containerId, error);
throw new BoxError(BoxError.DOCKER_ERROR, error);
}
}
function deleteContainers(appId, options, callback) {
async function deleteContainers(appId, options) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let labels = [ 'appId=' + appId ];
const labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
const [error, containers] = await safe(gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
async.eachSeries(containers, function (container, iteratorDone) {
deleteContainer(container.Id, iteratorDone);
}, callback);
});
for (const container of containers) {
await deleteContainer(container.Id);
}
}
function stopContainers(appId, callback) {
async function stopContainers(appId) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
const [error, containers] = await safe(gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
async.eachSeries(containers, function (container, iteratorDone) {
stopContainer(container.Id, iteratorDone);
}, callback);
});
for (const container of containers) {
await stopContainer(container.Id);
}
}
function deleteImage(manifest, callback) {
@@ -573,114 +526,79 @@ function deleteImage(manifest, callback) {
});
}
function getContainerIdByIp(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
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'));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
var containerId;
for (var id in bridge.Containers) {
if (bridge.Containers[id].IPv4Address.indexOf(ip + '/16') === 0) {
containerId = id;
break;
}
}
if (!containerId) return callback(new BoxError(BoxError.DOCKER_ERROR, 'No container with that ip'));
callback(null, containerId);
});
}
function inspect(containerId, callback) {
async function inspect(containerId) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
const container = gConnection.getContainer(containerId);
container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
const [error, result] = await safe(container.inspect());
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`);
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
callback(null, result);
});
return result;
}
function getContainerIp(containerId, callback) {
async function getContainerIp(containerId) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
if (constants.TEST) return callback(null, '127.0.5.5');
if (constants.TEST) return '127.0.5.5';
inspect(containerId, function (error, result) {
if (error) return callback(error);
const result = await inspect(containerId);
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP'));
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) throw new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP');
callback(null, ip);
});
return ip;
}
function execContainer(containerId, options, callback) {
async function execContainer(containerId, options) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
const container = gConnection.getContainer(containerId);
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));
const [error, exec] = await safe(container.exec(options.execOptions));
if (error && error.statusCode === 409) throw new BoxError(BoxError.BAD_STATE, error.message); // container restarting/not running
if (error) throw 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));
const [startError, stream] = await safe(exec.start(options.startOptions)); /* in hijacked mode, stream is a net.socket */
if (startError) throw new BoxError(BoxError.DOCKER_ERROR, startError);
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);
}
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);
});
});
return stream;
}
function getEvents(options, callback) {
async function getEvents(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
gConnection.getEvents(options, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, stream);
});
const [error, stream] = await safe(gConnection.getEvents(options));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
return stream;
}
function memoryUsage(containerId, callback) {
async function memoryUsage(containerId) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
const container = gConnection.getContainer(containerId);
container.stats({ stream: false }, function (error, result) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
const [error, result] = await safe(container.stats({ stream: false }));
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND);
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
callback(null, result);
});
return result;
}
function createVolume(name, volumeDataDir, labels, callback) {
async function createVolume(name, volumeDataDir, labels) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof labels, 'object');
assert.strictEqual(typeof callback, 'function');
const volumeOptions = {
Name: name,
@@ -694,69 +612,56 @@ function createVolume(name, volumeDataDir, labels, callback) {
};
// 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}`));
let [error] = await safe(shell.promises.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}));
if (error) throw new BoxError(BoxError.FS_ERROR, `Error creating app data dir: ${error.message}`);
gConnection.createVolume(volumeOptions, function (error) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback();
});
});
[error] = await safe(gConnection.createVolume(volumeOptions));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
}
function clearVolume(name, options, callback) {
async function clearVolume(name, options) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let volume = gConnection.getVolume(name);
volume.inspect(function (error, v) {
if (error && error.statusCode === 404) return callback();
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
let [error, v] = await safe(volume.inspect());
if (error && error.statusCode === 404) return;
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
const volumeDataDir = v.Options.device;
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
callback();
});
});
const volumeDataDir = v.Options.device;
[error] = await shell.promises.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {});
if (error) throw new BoxError(BoxError.FS_ERROR, error);
}
// this only removes the volume and not the data
function removeVolume(name, callback) {
async function removeVolume(name) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
let volume = gConnection.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
callback();
});
const [error] = await safe(volume.remove());
if (error && error.statusCode !== 404) throw new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`);
}
function info(callback) {
assert.strictEqual(typeof callback, 'function');
gConnection.info(function (error, result) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker'));
callback(null, result);
});
async function info() {
const [error, result] = await safe(gConnection.info());
if (error) throw new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker');
return result;
}
function update(name, memory, memorySwap, callback) {
async function update(name, memory, memorySwap) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof memory, 'number');
assert.strictEqual(typeof memorySwap, 'number');
assert.strictEqual(typeof callback, 'function');
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${name}`.split(' ');
// scale back db containers, if possible. this is retried because updating memory constraints can fail
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
async.retry({ times: 10, interval: 60 * 1000 }, function (retryCallback) {
shell.spawn(`update(${name})`, '/usr/bin/docker', args, { }, retryCallback);
}, callback);
for (let times = 0; times < 10; times++) {
const [error] = await safe(shell.promises.spawn(`update(${name})`, '/usr/bin/docker', args, { }));
if (!error) return;
await delay(60 * 1000);
}
throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to update container');
}