Files
cloudron-box/src/docker.js

701 lines
28 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
ping,
info,
2022-10-11 22:38:26 +02:00
df,
downloadImage,
createContainer,
startContainer,
restartContainer,
stopContainer,
stopContainers,
deleteContainer,
deleteImage,
deleteContainers,
createSubcontainer,
inspect,
getContainerIp,
getEvents,
2025-05-21 15:37:31 +02:00
stats,
2022-05-16 10:26:30 -07:00
update,
2022-05-16 10:26:30 -07:00
2024-12-14 14:00:05 +01:00
parseImageRef,
2022-05-16 10:26:30 -07:00
createExec,
startExec,
getExec,
resizeExec
};
const apps = require('./apps.js'),
2016-04-18 16:30:58 -07:00
assert = require('assert'),
2019-09-23 12:13:21 -07:00
BoxError = require('./boxerror.js'),
2016-04-18 16:30:58 -07:00
constants = require('./constants.js'),
dashboard = require('./dashboard.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'),
dockerRegistries = require('./dockerregistries.js'),
2024-02-20 23:09:49 +01:00
fs = require('fs'),
mailServer = require('./mailserver.js'),
os = require('os'),
paths = require('./paths.js'),
promiseRetry = require('./promise-retry.js'),
services = require('./services.js'),
2024-10-14 19:10:31 +02:00
shell = require('./shell.js')('docker'),
safe = require('safetydance'),
2023-05-14 10:53:50 +02:00
timers = require('timers/promises'),
volumes = require('./volumes.js');
2016-04-18 16:30:58 -07:00
const gConnection = new Docker({ socketPath: paths.DOCKER_SOCKET_PATH });
2019-10-22 22:07:44 -07:00
function parseImageRef(imageRef) {
assert.strictEqual(typeof imageRef, 'string');
// a ref is like registry.docker.com/cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4
// registry.docker.com is registry name . cloudron is (optional) namespace . base is image name . cloudron/base is repository path
// registry.docker.com/cloudron/base is fullRepositoryName
const result = { fullRepositoryName: null, registry: null, tag: null, digest: null };
result.fullRepositoryName = imageRef.split(/[:@]/)[0];
const parts = result.fullRepositoryName.split('/');
result.registry = parts[0].includes('.') ? parts[0] : null; // https://docs.docker.com/admin/faqs/general-faqs/#what-is-a-docker-id
let remaining = imageRef.substr(result.fullRepositoryName.length);
if (remaining.startsWith(':')) {
result.tag = remaining.substr(1).split('@', 1)[0];
remaining = remaining.substr(result.tag.length + 1); // also ':'
}
if (remaining.startsWith('@sha256:')) result.digest = remaining.substr(8);
return result;
}
2021-08-26 21:14:49 -07:00
async function ping() {
2018-11-23 15:49:47 +01:00
// do not let the request linger
const connection = new Docker({ socketPath: paths.DOCKER_SOCKET_PATH, timeout: 1000 });
2018-11-19 10:19:46 +01:00
2021-08-26 21:14:49 -07:00
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;
2018-11-19 10:19:46 +01:00
2021-08-26 21:14:49 -07:00
throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon');
2018-11-19 10:19:46 +01:00
}
async function getAuthConfig(imageRef) {
assert.strictEqual(typeof imageRef, 'string');
const parsedRef = parseImageRef(imageRef);
// images in our cloudron namespace are always unauthenticated to not interfere with any user limits
if (parsedRef.registry === null && parsedRef.fullRepositoryName.startsWith('cloudron/')) return null;
2019-10-27 12:14:27 -07:00
const registries = await dockerRegistries.list();
for (const registry of registries) {
if (registry.serverAddress !== parsedRef.registry) { // ideally they match but there's too many docker registry domains!
if (!registry.serverAddress.includes('.docker.')) continue;
if (parsedRef.registry !== null && !parsedRef.includes('.docker.')) continue;
}
2019-10-27 12:14:27 -07:00
// https://github.com/apocas/dockerode#pull-from-private-repos
const authConfig = {
username: registry.username,
password: registry.password,
auth: registry.auth || '', // the auth token at login time
email: registry.email || '',
serveraddress: registry.serverAddress
};
return authConfig;
}
2015-10-19 11:40:19 -07:00
return null;
2021-08-25 19:41:46 -07:00
}
2015-10-19 11:40:19 -07:00
async function pullImage(imageRef) {
assert.strictEqual(typeof imageRef, 'string');
2019-10-27 12:14:27 -07:00
const authConfig = await getAuthConfig(imageRef);
2019-10-27 12:14:27 -07:00
debug(`pullImage: will pull ${imageRef}. auth: ${authConfig ? 'yes' : 'no'}`);
2024-12-14 14:05:53 +01:00
const [error, stream] = await safe(gConnection.pull(imageRef, { authconfig: authConfig }));
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${imageRef}. message: ${error.message} statusCode: ${error.statusCode}`);
// toomanyrequests is flagged as a 500. dockerhub appears to have 10 pulls her hour per IP limit
if (error && error.statusCode === 500) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${imageRef}. registry error: ${JSON.stringify(error)}`);
if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${imageRef}. Please check the network or if the image needs authentication. statusCode: ${error.statusCode}`);
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
return new Promise((resolve, reject) => {
// https://github.com/dotcloud/docker/issues/1074 says each status message is emitted as a chunk
let layerError = null;
2021-08-25 19:41:46 -07:00
stream.on('data', function (chunk) {
2022-04-14 17:41:41 -05:00
const data = safe.JSON.parse(chunk) || { };
2021-08-25 19:41:46 -07:00
debug('pullImage: %j', data);
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
// 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 ${imageRef}: ${data.errorDetail.message}`);
layerError = data.errorDetail;
2021-08-25 19:41:46 -07:00
}
});
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
stream.on('end', function () {
debug(`downloaded image ${imageRef} . error: ${!!layerError}`);
if (!layerError) return resolve();
reject(new BoxError(layerError.message.includes('no space') ? BoxError.FS_ERROR : BoxError.DOCKER_ERROR, layerError.message));
2021-08-25 19:41:46 -07:00
});
2015-10-19 11:40:19 -07:00
stream.on('error', function (error) { // this is only hit for stream error and not for some download error
debug(`error pulling image ${imageRef}: %o`, error);
2021-08-25 19:41:46 -07:00
reject(new BoxError(BoxError.DOCKER_ERROR, error.message));
2015-10-19 11:40:19 -07:00
});
});
}
2021-08-25 19:41:46 -07:00
async function downloadImage(manifest) {
2015-10-19 15:51:02 -07:00
assert.strictEqual(typeof manifest, 'object');
debug(`downloadImage: ${manifest.dockerImage}`);
2015-10-19 11:40:19 -07:00
const image = gConnection.getImage(manifest.dockerImage);
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [error, result] = await safe(image.inspect());
if (!error && result) return; // image is already present locally
2015-10-19 11:40:19 -07:00
const parsedManifestRef = parseImageRef(manifest.dockerImage);
await promiseRetry({ times: 10, interval: 5000, debug, retry: (pullError) => pullError.reason !== BoxError.FS_ERROR }, async () => {
2024-12-14 23:28:00 +01:00
if (parsedManifestRef.registry !== null || !parsedManifestRef.fullRepositoryName.startsWith('cloudron/')) return await pullImage(manifest.dockerImage);
let upstreamRef = null;
for (const registry of [ 'registry.docker.com', 'registry.ipv4.docker.com', 'quay.io' ]) {
upstreamRef = `${registry}/${manifest.dockerImage}`;
const [pullError] = await safe(pullImage(upstreamRef));
if (!pullError) break;
}
if (!upstreamRef) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${manifest.dockerImage} from dockerhub or quay`);
// retag the downloaded image to not have the registry name. this prevents 'docker run' from redownloading it
debug(`downloadImage: tagging ${upstreamRef} as ${parsedManifestRef.fullRepositoryName}:${parsedManifestRef.tag}`);
await gConnection.getImage(upstreamRef).tag({ repo: parsedManifestRef.fullRepositoryName, tag: parsedManifestRef.tag });
debug(`downloadImage: untagging ${upstreamRef}`);
await deleteImage(upstreamRef);
});
2015-10-19 11:40:19 -07:00
}
2021-05-11 17:50:48 -07:00
async function getVolumeMounts(app) {
2020-04-29 21:55:21 -07:00
assert.strictEqual(typeof app, 'object');
2021-05-11 17:50:48 -07:00
if (app.mounts.length === 0) return [];
2020-04-29 21:55:21 -07:00
2021-05-11 17:50:48 -07:00
const result = await volumes.list();
const volumesById = {};
2021-05-11 17:50:48 -07:00
result.forEach(r => volumesById[r.id] = r);
const mounts = [];
2021-05-11 17:50:48 -07:00
for (const mount of app.mounts) {
const volume = volumesById[mount.volumeId];
2021-02-17 22:53:50 -08:00
2021-05-11 17:50:48 -07:00
mounts.push({
Source: volume.hostPath,
Target: `/media/${volume.name}`,
Type: 'bind',
ReadOnly: mount.readOnly
});
}
2020-04-29 21:55:21 -07:00
2021-05-11 17:50:48 -07:00
return mounts;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
async function getAddonMounts(app) {
2021-02-17 22:53:50 -08:00
assert.strictEqual(typeof app, 'object');
const mounts = [];
2021-02-17 22:53:50 -08:00
const addons = app.manifest.addons;
2021-08-25 19:41:46 -07:00
if (!addons) return mounts;
2021-02-17 22:53:50 -08:00
2021-08-25 19:41:46 -07:00
for (const addon of Object.keys(addons)) {
2021-02-17 22:53:50 -08:00
switch (addon) {
case 'localstorage': {
const storageDir = await apps.getStorageDir(app);
2021-02-17 22:53:50 -08:00
mounts.push({
Target: '/app/data',
Source: storageDir,
Type: 'bind',
2021-02-17 22:53:50 -08:00
ReadOnly: false
});
2021-08-25 19:41:46 -07:00
break;
}
2021-08-17 14:04:29 -07:00
case 'tls': {
const certificateDir = `${paths.PLATFORM_DATA_DIR}/tls/${app.id}`;
2021-08-17 14:04:29 -07:00
mounts.push({
Target: '/etc/certs',
Source: certificateDir,
2021-08-17 14:04:29 -07:00
Type: 'bind',
ReadOnly: true
2021-02-17 22:53:50 -08:00
});
2021-08-25 19:41:46 -07:00
break;
2021-08-17 14:04:29 -07:00
}
2021-02-17 22:53:50 -08:00
default:
2021-08-25 19:41:46 -07:00
break;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
}
return mounts;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
async function getMounts(app) {
2021-02-17 22:53:50 -08:00
assert.strictEqual(typeof app, 'object');
2021-08-25 19:41:46 -07:00
const volumeMounts = await getVolumeMounts(app);
const addonMounts = await getAddonMounts(app);
return volumeMounts.concat(addonMounts);
2020-04-29 21:55:21 -07:00
}
// This only returns ipv4 addresses
// We dont bind to ipv6 interfaces, public prefix changes and container restarts wont work
2024-02-20 23:09:49 +01:00
async function getAddressesForPort53() {
const [error, deviceLinks] = await safe(fs.promises.readdir('/sys/class/net')); // https://man7.org/linux/man-pages/man5/sysfs.5.html
if (error) 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) {
2024-11-05 14:24:40 +01:00
const [error, output] = await safe(shell.spawn('ip', ['-f', 'inet', '-j', 'addr', 'show', 'dev', phy.name, 'scope', 'global'], { encoding: 'utf8' }));
2024-02-20 23:09:49 +01:00
if (error) continue;
const inet = safe.JSON.parse(output) || [];
2022-02-17 11:56:08 -08:00
for (const r of inet) {
const address = safe.query(r, 'addr_info[0].local');
if (address) addresses.push(address);
}
}
return addresses;
}
2021-08-25 19:41:46 -07:00
async function createSubcontainer(app, name, cmd, options) {
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');
2021-05-02 11:26:08 -07:00
assert(!cmd || Array.isArray(cmd));
assert.strictEqual(typeof options, 'object');
2015-10-19 11:40:19 -07:00
const isAppContainer = !cmd; // non app-containers are like scheduler
2021-08-25 19:41:46 -07:00
const manifest = app.manifest;
const exposedPorts = {}, dockerPortBindings = { };
const domain = app.fqdn;
const { fqdn:dashboardFqdn } = await dashboard.getLocation();
2021-08-25 19:41:46 -07:00
const 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}`,
`CLOUDRON_WEBADMIN_ORIGIN=https://${dashboardFqdn}`,
`CLOUDRON_API_ORIGIN=https://${dashboardFqdn}`,
`CLOUDRON_APP_ORIGIN=https://${domain}`,
`CLOUDRON_APP_DOMAIN=${domain}`
2015-10-19 11:40:19 -07:00
];
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}`);
2021-08-25 19:41:46 -07:00
const portEnv = [];
for (const portName in app.portBindings) {
const { hostPort, type:portType, count:portCount } = app.portBindings[portName];
const portSpec = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
const containerPort = portSpec[portName].containerPort || hostPort;
// port 53 is special. systemd-resolved is listening on 127.0.0.x port 53 and another process cannot listen to 0.0.0.0 port 53
// for port 53 alone, we listen explicitly on the server's interface IP
const hostIps = hostPort === 53 ? await getAddressesForPort53() : [ '0.0.0.0', '::0' ];
portEnv.push(`${portName}=${hostPort}`);
if (portCount > 1) portEnv.push(`${portName}_COUNT=${portCount}`);
2015-10-19 11:40:19 -07:00
2024-02-06 16:10:34 +01:00
// docker portBindings requires ports to be exposed
for (let i = 0; i < portCount; ++i) {
exposedPorts[`${containerPort+i}/${portType}`] = {};
dockerPortBindings[`${containerPort+i}/${portType}`] = hostIps.map(hip => { return { HostIp: hip, HostPort: String(hostPort + i) }; });
2024-02-06 16:10:34 +01:00
}
2015-10-19 11:40:19 -07:00
}
2018-10-11 16:18:38 -07:00
2021-08-25 19:41:46 -07:00
const appEnv = [];
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
2015-10-19 11:40:19 -07:00
2021-01-20 12:12:14 -08:00
let memoryLimit = apps.getMemoryLimit(app);
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;
2021-08-25 19:41:46 -07:00
const mounts = await getMounts(app);
const addonEnv = await services.getEnvironment(app);
2022-10-24 22:34:06 +02:00
const runtimeVolumes = {
'/tmp': {},
2022-10-24 23:58:20 +02:00
'/run': {},
2022-10-24 22:34:06 +02:00
};
if (app.manifest.runtimeDirs) {
app.manifest.runtimeDirs.forEach(dir => runtimeVolumes[dir] = {});
}
2021-08-25 19:41:46 -07:00
const containerOptions = {
2021-08-25 19:41:46 -07:00
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),
2021-08-25 19:41:46 -07:00
ExposedPorts: isAppContainer ? exposedPorts : { },
2022-10-24 22:34:06 +02:00
Volumes: runtimeVolumes,
2021-08-25 19:41:46 -07:00
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': `unix://${paths.SYSLOG_SOCKET_FILE}`,
2021-08-25 19:41:46 -07:00
'syslog-format': 'rfc5424'
}
},
Memory: memoryLimit,
MemorySwap: -1, // Unlimited swap
PortBindings: isAppContainer ? dockerPortBindings : {},
2021-08-25 19:41:46 -07:00
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
// CpuPeriod (100000 microseconds) and CpuQuota(app.cpuQuota% of CpuPeriod)
// 1000000000 is one core https://github.com/moby/moby/issues/24713#issuecomment-233167619 and https://stackoverflow.com/questions/52391877/set-the-number-of-cpu-cores-of-a-container-using-docker-engine-api
NanoCPUs: app.cpuQuota === 100 ? 0 : Math.round(os.cpus().length * app.cpuQuota/100 * 1000000000),
2021-08-25 19:41:46 -07:00
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: [],
Sysctls: {},
ExtraHosts: []
2021-08-25 19:41:46 -07:00
}
};
2017-08-11 23:22:48 +01:00
2021-08-25 19:41:46 -07: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
// Do not inject for AdGuard. It ends up resolving the dashboard domain as the docker bridge IP
if (manifest.id !== 'com.adguard.home.cloudronapp') containerOptions.HostConfig.ExtraHosts.push(`${dashboardFqdn}:172.18.0.1`);
if (manifest.addons?.sendmail?.requiresValidCertificate) {
const { fqdn:mailFqdn } = await mailServer.getLocation();
containerOptions.HostConfig.ExtraHosts.push(`${mailFqdn}:${constants.MAIL_SERVICE_IPv4}`);
}
2021-08-25 19:41:46 -07:00
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
IPAMConfig: {
IPv4Address: app.containerIp
},
Aliases: [ name ] // adds hostname entry with container name
}
2021-08-25 19:41:46 -07:00
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth
}
2021-08-25 19:41:46 -07:00
const capabilities = manifest.capabilities || [];
2020-06-30 07:31:24 -07:00
2021-08-25 19:41:46 -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');
// 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
2024-12-05 13:47:59 +01:00
containerOptions.HostConfig.Devices = Object.keys(app.devices).map((d) => {
if (!safe.fs.existsSync(d)) {
debug(`createSubcontainer: device ${d} does not exist. Skipping...`);
return null;
}
return { PathOnHost: d, PathInContainer: d, CgroupPermissions: 'rwm' };
}).filter(d => d);
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
2024-12-05 13:47:59 +01:00
containerOptions.HostConfig.Devices.push({ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' });
}
2020-08-14 18:48:53 -07:00
const mergedOptions = Object.assign({}, containerOptions, options);
const [createError, container] = await safe(gConnection.createContainer(mergedOptions));
2021-08-25 19:41:46 -07:00
if (createError && createError.statusCode === 409) throw new BoxError(BoxError.ALREADY_EXISTS, createError);
if (createError) throw new BoxError(BoxError.DOCKER_ERROR, createError);
2019-10-22 21:46:32 -07:00
2021-08-25 19:41:46 -07:00
return container;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function createContainer(app) {
return await createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */);
2015-10-19 21:33:53 -07:00
}
2021-08-25 19:41:46 -07:00
async function startContainer(containerId) {
2015-10-19 15:51:02 -07:00
assert.strictEqual(typeof containerId, 'string');
2021-08-25 19:41:46 -07:00
const container = gConnection.getContainer(containerId);
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [error] = await safe(container.start());
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Container ${containerId} not found`);
2021-08-25 19:41:46 -07:00
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
2019-12-20 10:29:29 -08:00
}
2021-08-25 19:41:46 -07:00
async function restartContainer(containerId) {
2019-12-20 10:29:29 -08:00
assert.strictEqual(typeof containerId, 'string');
2021-08-25 19:41:46 -07:00
const container = gConnection.getContainer(containerId);
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [error] = await safe(container.restart());
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Contanier ${containerId} not found`);
2021-08-25 19:41:46 -07:00
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);
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function stopContainer(containerId) {
2015-10-19 15:51:02 -07:00
assert(!containerId || typeof containerId === 'string');
2015-10-19 15:39:26 -07:00
if (!containerId) {
debug('No previous container to stop');
2021-08-25 19:41:46 -07:00
return;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
const container = gConnection.getContainer(containerId);
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const options = {
2015-10-19 11:40:19 -07:00
t: 10 // wait for 10 seconds before killing it
};
2021-08-25 19:41:46 -07:00
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);
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
[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);
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function deleteContainer(containerId) { // id can also be name
2015-10-19 15:51:02 -07:00
assert(!containerId || typeof containerId === 'string');
2021-08-25 19:41:46 -07:00
if (containerId === null) return null;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const container = gConnection.getContainer(containerId);
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const removeOptions = {
2015-10-19 11:40:19 -07:00
force: true, // kill container if it's running
v: true // removes volumes associated with the container (but not host mounts)
};
2021-08-25 19:41:46 -07:00
const [error] = await safe(container.remove(removeOptions));
if (error && error.statusCode === 404) return;
2015-10-19 15:39:26 -07:00
2021-08-25 19:41:46 -07:00
if (error) {
debug('Error removing container %s : %o', containerId, error);
2021-08-25 19:41:46 -07:00
throw new BoxError(BoxError.DOCKER_ERROR, error);
}
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function deleteContainers(appId, options) {
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
2021-08-25 19:41:46 -07:00
const labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
2025-05-27 13:55:36 +02:00
const [error, containers] = await safe(gConnection.listContainers({ all: typeof options.all === 'undefined' ? 1 : options.all, filters: JSON.stringify({ label: labels }) }));
2021-08-25 19:41:46 -07:00
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
2015-10-19 18:48:56 -07:00
2021-08-25 19:41:46 -07:00
for (const container of containers) {
await deleteContainer(container.Id);
}
2015-10-19 18:48:56 -07:00
}
2021-08-25 19:41:46 -07:00
async function stopContainers(appId) {
2015-10-20 00:05:07 -07:00
assert.strictEqual(typeof appId, 'string');
2021-08-25 19:41:46 -07:00
const [error, containers] = await safe(gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
2015-10-20 00:05:07 -07:00
2021-08-25 19:41:46 -07:00
for (const container of containers) {
await stopContainer(container.Id);
}
2015-10-20 00:05:07 -07:00
}
async function deleteImage(imageRef) {
assert.strictEqual(typeof imageRef, 'string');
2015-10-19 15:51:02 -07:00
if (!imageRef) return;
if (imageRef.includes('//') || imageRef.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)
2015-10-19 11:40:19 -07:00
const 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
debug(`deleteImage: removing ${imageRef}`);
const [error] = await safe(gConnection.getImage(imageRef.replace(/@sha256:.*/,'')).remove(removeOptions)); // can't have the manifest id. won't remove anythin
2021-08-26 18:34:32 -07:00
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
2015-10-19 11:40:19 -07:00
2021-08-26 18:34:32 -07:00
if (error) {
debug(`Error removing image ${imageRef} : %o`, error);
2021-08-26 18:34:32 -07:00
throw new BoxError(BoxError.DOCKER_ERROR, error);
}
2015-10-19 11:40:19 -07:00
}
2016-02-18 15:39:27 +01:00
2021-08-25 19:41:46 -07:00
async function inspect(containerId) {
2017-08-11 22:04:40 +02:00
assert.strictEqual(typeof containerId, 'string');
2021-08-25 19:41:46 -07:00
const container = gConnection.getContainer(containerId);
2017-08-11 22:04:40 +02:00
2021-08-25 19:41:46 -07:00
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);
2018-11-19 10:19:46 +01:00
2021-08-25 19:41:46 -07:00
return result;
2018-11-28 10:39:12 +01:00
}
2021-08-25 19:41:46 -07:00
async function getContainerIp(containerId) {
assert.strictEqual(typeof containerId, 'string');
2021-08-25 19:41:46 -07:00
if (constants.TEST) return '127.0.5.5';
2021-08-25 19:41:46 -07:00
const result = await inspect(containerId);
2021-08-25 19:41:46 -07:00
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) throw new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP');
2021-08-25 19:41:46 -07:00
return ip;
}
2022-05-16 10:26:30 -07:00
async function createExec(containerId, options) {
2019-12-04 13:17:58 -08:00
assert.strictEqual(typeof containerId, 'string');
2019-03-06 11:54:37 -08:00
assert.strictEqual(typeof options, 'object');
2021-08-25 19:41:46 -07:00
const container = gConnection.getContainer(containerId);
2022-05-16 10:26:30 -07:00
const [error, exec] = await safe(container.exec(options));
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Container ${containerId} not found`);
2021-08-25 19:41:46 -07:00
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);
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
return exec.id;
}
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
async function startExec(execId, options) {
assert.strictEqual(typeof execId, 'string');
assert.strictEqual(typeof options, 'object');
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
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, `Exec container ${execId} not found`);
2022-05-16 10:26:30 -07:00
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
2021-08-25 19:41:46 -07:00
return stream;
2019-12-04 13:17:58 -08:00
}
2022-05-16 10:26:30 -07:00
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, `Exec container ${execId} not found`);
2022-05-16 10:26:30 -07:00
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
}
2021-08-25 19:41:46 -07:00
async function getEvents(options) {
2019-12-04 13:17:58 -08:00
assert.strictEqual(typeof options, 'object');
2021-08-25 19:41:46 -07:00
const [error, stream] = await safe(gConnection.getEvents(options));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
return stream;
2019-03-06 11:54:37 -08:00
}
2025-05-21 15:37:31 +02:00
async function stats(containerId) {
2018-11-28 10:39:12 +01:00
assert.strictEqual(typeof containerId, 'string');
2021-08-25 19:41:46 -07:00
const container = gConnection.getContainer(containerId);
2018-11-28 10:39:12 +01:00
2021-08-25 19:41:46 -07:00
const [error, result] = await safe(container.stats({ stream: false }));
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Container ${containerId} not found`);
2021-08-25 19:41:46 -07:00
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
2018-11-28 10:39:12 +01:00
2021-08-25 19:41:46 -07:00
return result;
2017-08-11 22:04:40 +02:00
}
2021-08-25 19:41:46 -07:00
async function info() {
const [error, result] = await safe(gConnection.info());
2025-02-05 16:23:31 +01:00
if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Error connecting to docker: ${error.message}`);
2021-08-25 19:41:46 -07:00
return result;
}
2021-01-20 12:01:15 -08:00
2022-10-11 22:38:26 +02:00
async function df() {
const [error, result] = await safe(gConnection.df());
2025-02-05 16:23:31 +01:00
if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Error connecting to docker: ${error.message}`);
2022-10-11 22:38:26 +02:00
return result;
}
async function update(name, memory) {
2021-01-20 12:01:15 -08:00
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof memory, 'number');
// 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
2021-08-25 19:41:46 -07:00
for (let times = 0; times < 10; times++) {
const [error] = await safe(shell.spawn('docker', ['update', '--memory', memory, '--memory-swap', '-1', name], {}));
2021-08-25 19:41:46 -07:00
if (!error) return;
2023-05-14 10:53:50 +02:00
await timers.setTimeout(60 * 1000);
2021-08-25 19:41:46 -07:00
}
throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to update container');
2021-01-20 12:01:15 -08:00
}