9c8f78a059 already fixed many of the cert issues.
However, some issues were caught in the CI:
* The TLS addon has to be rebuilt and not just restarted. For this reason, we now
move things to a directory instead of mounting files. This way the container is just restarted.
* Cleanups must be driven by the database and not the filesystem . Deleting files on disk or after a restore,
the certs are left dangling forever in the db.
* Separate the db cert logic and disk cert logic. This way we can sync as many times as we want and whenever we want.
656 lines
24 KiB
JavaScript
656 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
testRegistryConfig,
|
|
injectPrivateFields,
|
|
removePrivateFields,
|
|
|
|
ping,
|
|
|
|
info,
|
|
df,
|
|
downloadImage,
|
|
createContainer,
|
|
startContainer,
|
|
restartContainer,
|
|
stopContainer,
|
|
stopContainers,
|
|
deleteContainer,
|
|
deleteImage,
|
|
deleteContainers,
|
|
createSubcontainer,
|
|
inspect,
|
|
getContainerIp,
|
|
getEvents,
|
|
memoryUsage,
|
|
|
|
update,
|
|
|
|
createExec,
|
|
startExec,
|
|
getExec,
|
|
resizeExec
|
|
};
|
|
|
|
const apps = require('./apps.js'),
|
|
assert = require('assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
constants = require('./constants.js'),
|
|
debug = require('debug')('box:docker'),
|
|
delay = require('./delay.js'),
|
|
Docker = require('dockerode'),
|
|
paths = require('./paths.js'),
|
|
services = require('./services.js'),
|
|
settings = require('./settings.js'),
|
|
shell = require('./shell.js'),
|
|
safe = require('safetydance'),
|
|
system = require('./system.js'),
|
|
volumes = require('./volumes.js'),
|
|
_ = require('underscore');
|
|
|
|
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
|
|
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
|
|
|
|
async function testRegistryConfig(config) {
|
|
assert.strictEqual(typeof config, 'object');
|
|
|
|
if (config.provider === 'noop') return;
|
|
|
|
const [error] = await safe(gConnection.checkAuth(config)); // this returns a 500 even for auth errors
|
|
if (error) throw new BoxError(BoxError.BAD_FIELD, `Invalid serverAddress: ${error.message}`);
|
|
}
|
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
|
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
|
}
|
|
|
|
function removePrivateFields(registryConfig) {
|
|
assert.strictEqual(typeof registryConfig, 'object');
|
|
|
|
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
|
|
|
|
return registryConfig;
|
|
}
|
|
|
|
async function ping() {
|
|
// do not let the request linger
|
|
const connection = new Docker({ socketPath: DOCKER_SOCKET_PATH, timeout: 1000 });
|
|
|
|
const [error, result] = await safe(connection.ping());
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
if (Buffer.isBuffer(result) && result.toString('utf8') === 'OK') return; // sometimes it returns buffer
|
|
if (result === 'OK') return;
|
|
|
|
throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon');
|
|
}
|
|
|
|
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 null; // public docker registry
|
|
|
|
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
|
|
};
|
|
|
|
return auth;
|
|
}
|
|
|
|
async function pullImage(manifest) {
|
|
const config = await getRegistryConfig(manifest.dockerImage);
|
|
|
|
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${config ? 'yes' : 'no'}`);
|
|
|
|
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}`);
|
|
|
|
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) {
|
|
const 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);
|
|
}
|
|
});
|
|
|
|
stream.on('end', function () {
|
|
debug('downloaded image %s', manifest.dockerImage);
|
|
resolve();
|
|
});
|
|
|
|
stream.on('error', function (error) {
|
|
debug('error pulling image %s: %j', manifest.dockerImage, error);
|
|
reject(new BoxError(BoxError.DOCKER_ERROR, error.message));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function downloadImage(manifest) {
|
|
assert.strictEqual(typeof manifest, 'object');
|
|
|
|
debug(`downloadImage ${manifest.dockerImage}`);
|
|
|
|
const image = gConnection.getImage(manifest.dockerImage);
|
|
|
|
const [error, result] = await safe(image.inspect());
|
|
if (!error && result) return; // image is already present locally
|
|
|
|
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;
|
|
|
|
await delay(5000);
|
|
}
|
|
}
|
|
|
|
async function getVolumeMounts(app) {
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
let mounts = [];
|
|
|
|
if (app.mounts.length === 0) return [];
|
|
|
|
const result = await volumes.list();
|
|
|
|
let volumesById = {};
|
|
result.forEach(r => volumesById[r.id] = r);
|
|
|
|
for (const mount of app.mounts) {
|
|
const volume = volumesById[mount.volumeId];
|
|
|
|
mounts.push({
|
|
Source: volume.hostPath,
|
|
Target: `/media/${volume.name}`,
|
|
Type: 'bind',
|
|
ReadOnly: mount.readOnly
|
|
});
|
|
}
|
|
|
|
return mounts;
|
|
}
|
|
|
|
async function getAddonMounts(app) {
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
let mounts = [];
|
|
|
|
const addons = app.manifest.addons;
|
|
if (!addons) return mounts;
|
|
|
|
for (const addon of Object.keys(addons)) {
|
|
switch (addon) {
|
|
case 'localstorage': {
|
|
const storageDir = await apps.getStorageDir(app);
|
|
mounts.push({
|
|
Target: '/app/data',
|
|
Source: storageDir,
|
|
Type: 'bind',
|
|
ReadOnly: false
|
|
});
|
|
|
|
break;
|
|
}
|
|
case 'tls': {
|
|
const certificateDir = `${paths.PLATFORM_DATA_DIR}/tls/${app.id}`;
|
|
|
|
mounts.push({
|
|
Target: '/etc/certs',
|
|
Source: certificateDir,
|
|
Type: 'bind',
|
|
ReadOnly: true
|
|
});
|
|
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return mounts;
|
|
}
|
|
|
|
async function getMounts(app) {
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
const volumeMounts = await getVolumeMounts(app);
|
|
const addonMounts = await getAddonMounts(app);
|
|
return volumeMounts.concat(addonMounts);
|
|
}
|
|
|
|
function getAddresses() {
|
|
const deviceLinks = safe.fs.readdirSync('/sys/class/net'); // https://man7.org/linux/man-pages/man5/sysfs.5.html
|
|
if (!deviceLinks) return [];
|
|
|
|
const devices = deviceLinks.map(d => { return { name: d, link: safe.fs.readlinkSync(`/sys/class/net/${d}`) }; });
|
|
const physicalDevices = devices.filter(d => d.link && !d.link.includes('virtual'));
|
|
|
|
const addresses = [];
|
|
for (const phy of physicalDevices) {
|
|
const inet = safe.JSON.parse(safe.child_process.execSync(`ip -f inet -j addr show dev ${phy.name} scope global`, { encoding: 'utf8' }));
|
|
for (const r of inet) {
|
|
const address = safe.query(r, 'addr_info[0].local');
|
|
if (address) addresses.push(address);
|
|
}
|
|
const inet6 = safe.JSON.parse(safe.child_process.execSync(`ip -f inet6 -j addr show dev ${phy.name} scope global`, { encoding: 'utf8' }));
|
|
for (const r of inet6) {
|
|
const address = safe.query(r, 'addr_info[0].local');
|
|
if (address) addresses.push(address);
|
|
}
|
|
}
|
|
|
|
return addresses;
|
|
}
|
|
|
|
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');
|
|
|
|
let isAppContainer = !cmd; // non app-containers are like scheduler
|
|
|
|
const manifest = app.manifest;
|
|
const exposedPorts = {}, dockerPortBindings = { };
|
|
const domain = app.fqdn;
|
|
|
|
const stdEnv = [
|
|
'CLOUDRON=1',
|
|
'CLOUDRON_PROXY_IP=172.18.0.1',
|
|
`CLOUDRON_APP_HOSTNAME=${app.id}`,
|
|
`CLOUDRON_WEBADMIN_ORIGIN=${settings.dashboardOrigin()}`,
|
|
`CLOUDRON_API_ORIGIN=${settings.dashboardOrigin()}`,
|
|
`CLOUDRON_APP_ORIGIN=https://${domain}`,
|
|
`CLOUDRON_APP_DOMAIN=${domain}`
|
|
];
|
|
|
|
const secondaryDomainsEnv = app.secondaryDomains.map(sd => `${sd.environmentVariable}=${sd.fqdn}`);
|
|
|
|
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;
|
|
|
|
const containerPort = ports[portName].containerPort || hostPort;
|
|
|
|
// docker portBindings requires ports to be exposed
|
|
exposedPorts[`${containerPort}/${portType}`] = {};
|
|
portEnv.push(`${portName}=${hostPort}`);
|
|
|
|
const hostIps = hostPort === 53 ? getAddresses() : [ '0.0.0.0', '::0' ]; // port 53 is special because it is possibly taken by systemd-resolved
|
|
dockerPortBindings[`${containerPort}/${portType}`] = hostIps.map(hip => { return { HostIp: hip, HostPort: hostPort + '' }; });
|
|
}
|
|
|
|
const appEnv = [];
|
|
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
|
|
|
|
let memoryLimit = apps.getMemoryLimit(app);
|
|
|
|
// 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;
|
|
|
|
const mounts = await getMounts(app);
|
|
|
|
const addonEnv = await services.getEnvironment(app);
|
|
const runtimeVolumes = {
|
|
'/tmp': {},
|
|
'/run': {},
|
|
'/home/cloudron/.cache': {},
|
|
'/root/.cache': {}
|
|
};
|
|
if (app.manifest.runtimeDirs) {
|
|
app.manifest.runtimeDirs.forEach(dir => runtimeVolumes[dir] = {});
|
|
}
|
|
|
|
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).concat(secondaryDomainsEnv),
|
|
ExposedPorts: isAppContainer ? exposedPorts : { },
|
|
Volumes: runtimeVolumes,
|
|
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'
|
|
}
|
|
},
|
|
Memory: await 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: [],
|
|
Sysctls: {}
|
|
}
|
|
};
|
|
|
|
// 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
|
|
}
|
|
|
|
const capabilities = manifest.capabilities || [];
|
|
|
|
// 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');
|
|
// ipv6 for new interfaces is disabled in the container. this prevents the openvpn tun device having ipv6
|
|
// See https://github.com/moby/moby/issues/20569 and https://github.com/moby/moby/issues/33099
|
|
containerOptions.HostConfig.Sysctls['net.ipv6.conf.all.disable_ipv6'] = '0';
|
|
containerOptions.HostConfig.Sysctls['net.ipv6.conf.all.forwarding'] = '1';
|
|
}
|
|
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
|
|
|
|
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
|
|
containerOptions.HostConfig.Devices = [
|
|
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
|
|
];
|
|
}
|
|
|
|
containerOptions = _.extend(containerOptions, options);
|
|
|
|
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);
|
|
|
|
return container;
|
|
}
|
|
|
|
async function createContainer(app) {
|
|
return await createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */);
|
|
}
|
|
|
|
async function startContainer(containerId) {
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
const container = gConnection.getContainer(containerId);
|
|
|
|
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
|
|
}
|
|
|
|
async function restartContainer(containerId) {
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
const container = gConnection.getContainer(containerId);
|
|
|
|
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);
|
|
}
|
|
|
|
async function stopContainer(containerId) {
|
|
assert(!containerId || typeof containerId === 'string');
|
|
|
|
if (!containerId) {
|
|
debug('No previous container to stop');
|
|
return;
|
|
}
|
|
|
|
const container = gConnection.getContainer(containerId);
|
|
|
|
const options = {
|
|
t: 10 // wait for 10 seconds before killing it
|
|
};
|
|
|
|
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);
|
|
|
|
[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);
|
|
}
|
|
|
|
async function deleteContainer(containerId) { // id can also be name
|
|
assert(!containerId || typeof containerId === 'string');
|
|
|
|
if (containerId === null) return null;
|
|
|
|
const container = gConnection.getContainer(containerId);
|
|
|
|
const removeOptions = {
|
|
force: true, // kill container if it's running
|
|
v: true // removes volumes associated with the container (but not host mounts)
|
|
};
|
|
|
|
const [error] = await safe(container.remove(removeOptions));
|
|
if (error && error.statusCode === 404) return;
|
|
|
|
if (error) {
|
|
debug('Error removing container %s : %j', containerId, error);
|
|
throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
}
|
|
}
|
|
|
|
async function deleteContainers(appId, options) {
|
|
assert.strictEqual(typeof appId, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const labels = [ 'appId=' + appId ];
|
|
if (options.managedOnly) labels.push('isCloudronManaged=true');
|
|
|
|
const [error, containers] = await safe(gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }));
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
|
|
for (const container of containers) {
|
|
await deleteContainer(container.Id);
|
|
}
|
|
}
|
|
|
|
async function stopContainers(appId) {
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
const [error, containers] = await safe(gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }));
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
|
|
for (const container of containers) {
|
|
await stopContainer(container.Id);
|
|
}
|
|
}
|
|
|
|
async function deleteImage(manifest) {
|
|
assert(!manifest || typeof manifest === 'object');
|
|
|
|
const dockerImage = manifest ? manifest.dockerImage : null;
|
|
if (!dockerImage) return;
|
|
if (dockerImage.includes('//') || dockerImage.startsWith('/')) return; // a common mistake is to paste a https:// as docker image. this results in a crash at runtime in dockerode module (https://github.com/apocas/dockerode/issues/548)
|
|
|
|
const removeOptions = {
|
|
force: false, // might be shared with another instance of this app
|
|
noprune: false // delete untagged parents
|
|
};
|
|
|
|
// 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
|
|
const [error] = await safe(gConnection.getImage(dockerImage).remove(removeOptions));
|
|
if (error && error.statusCode === 400) return; // invalid image format. this can happen if user installed with a bad --docker-image
|
|
if (error && error.statusCode === 404) return; // not found
|
|
if (error && error.statusCode === 409) return; // another container using the image
|
|
|
|
if (error) {
|
|
debug('Error removing image %s : %j', dockerImage, error);
|
|
throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
}
|
|
}
|
|
|
|
async function inspect(containerId) {
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
const container = gConnection.getContainer(containerId);
|
|
|
|
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);
|
|
|
|
return result;
|
|
}
|
|
|
|
async function getContainerIp(containerId) {
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
if (constants.TEST) return '127.0.5.5';
|
|
|
|
const result = await inspect(containerId);
|
|
|
|
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
|
|
if (!ip) throw new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP');
|
|
|
|
return ip;
|
|
}
|
|
|
|
async function createExec(containerId, options) {
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const container = gConnection.getContainer(containerId);
|
|
const [error, exec] = await safe(container.exec(options));
|
|
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND);
|
|
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);
|
|
|
|
return exec.id;
|
|
}
|
|
|
|
async function startExec(execId, options) {
|
|
assert.strictEqual(typeof execId, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const exec = gConnection.getExec(execId);
|
|
const [error, stream] = await safe(exec.start(options)); /* in hijacked mode, stream is a net.socket */
|
|
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND);
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
return stream;
|
|
}
|
|
|
|
async function getExec(execId) {
|
|
assert.strictEqual(typeof execId, 'string');
|
|
|
|
const exec = gConnection.getExec(execId);
|
|
const [error, result] = await safe(exec.inspect());
|
|
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Unable to find exec container ${execId}`);
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
|
|
return { exitCode: result.ExitCode, running: result.Running };
|
|
}
|
|
|
|
async function resizeExec(execId, options) {
|
|
assert.strictEqual(typeof execId, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const exec = gConnection.getExec(execId);
|
|
const [error] = await safe(exec.resize(options)); // { h, w }
|
|
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND);
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
}
|
|
|
|
async function getEvents(options) {
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
const [error, stream] = await safe(gConnection.getEvents(options));
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
|
|
return stream;
|
|
}
|
|
|
|
async function memoryUsage(containerId) {
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
const container = gConnection.getContainer(containerId);
|
|
|
|
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);
|
|
|
|
return 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;
|
|
}
|
|
|
|
async function df() {
|
|
const [error, result] = await safe(gConnection.df());
|
|
if (error) throw new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker');
|
|
return result;
|
|
}
|
|
|
|
async function update(name, memory, memorySwap) {
|
|
assert.strictEqual(typeof name, 'string');
|
|
assert.strictEqual(typeof memory, 'number');
|
|
assert.strictEqual(typeof memorySwap, 'number');
|
|
|
|
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
|
|
|
|
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');
|
|
}
|