a5e83a4d84
This can be useful for app to set them in trusted hosts. Or alternately, show different text when accessed from different domains.
657 lines
25 KiB
JavaScript
657 lines
25 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'),
|
|
Docker = require('dockerode'),
|
|
paths = require('./paths.js'),
|
|
promiseRetry = require('./promise-retry.js'),
|
|
services = require('./services.js'),
|
|
settings = require('./settings.js'),
|
|
shell = require('./shell.js'),
|
|
safe = require('safetydance'),
|
|
system = require('./system.js'),
|
|
timers = require('timers/promises'),
|
|
volumes = require('./volumes.js');
|
|
|
|
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
|
|
let layerError = null;
|
|
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) { // data is { errorDetail: { message: xx } , error: xx }
|
|
debug(`pullImage error ${manifest.dockerImage}: ${data.errorDetail.message}`);
|
|
layerError = data.errorDetail;
|
|
}
|
|
});
|
|
|
|
stream.on('end', function () {
|
|
debug(`downloaded image ${manifest.dockerImage} . error: ${!!layerError}`);
|
|
|
|
if (!layerError) return resolve();
|
|
|
|
reject(new BoxError(layerError.includes('no space') ? BoxError.FS_ERROR : BoxError.DOCKER_ERROR, layerError.message));
|
|
});
|
|
|
|
stream.on('error', function (error) { // this is only hit for stream error and not for some download error
|
|
debug('error pulling image %s: %o', 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
|
|
|
|
await promiseRetry({ times: 10, interval: 5000, debug, retry: (pullError) => pullError.reason !== BoxError.NOT_FOUND && pullError.reason !== BoxError.FS_ERROR }, async () => {
|
|
await pullImage(manifest);
|
|
});
|
|
}
|
|
|
|
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}`
|
|
];
|
|
|
|
if (app.manifest.multiDomain) stdEnv.push(`CLOUDRON_ALIAS_DOMAINS=${app.aliasDomains.map(ad => ad.fqdn).join(',')}`);
|
|
|
|
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] = {});
|
|
}
|
|
|
|
const 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' }
|
|
];
|
|
}
|
|
|
|
const mergedOptions = Object.assign({}, containerOptions, options);
|
|
|
|
const [createError, container] = await safe(gConnection.createContainer(mergedOptions));
|
|
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 : %o', 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 : %o', 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 timers.setTimeout(60 * 1000);
|
|
}
|
|
|
|
throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to update container');
|
|
}
|