691 lines
26 KiB
JavaScript
691 lines
26 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
removePrivateFields,
|
|
getRegistryConfig,
|
|
setRegistryConfig,
|
|
|
|
ping,
|
|
|
|
info,
|
|
df,
|
|
downloadImage,
|
|
createContainer,
|
|
startContainer,
|
|
restartContainer,
|
|
stopContainer,
|
|
stopContainers,
|
|
deleteContainer,
|
|
deleteImage,
|
|
deleteContainers,
|
|
createSubcontainer,
|
|
inspect,
|
|
getContainerIp,
|
|
getEvents,
|
|
memoryUsage,
|
|
|
|
update,
|
|
|
|
parseImageName,
|
|
|
|
createExec,
|
|
startExec,
|
|
getExec,
|
|
resizeExec
|
|
};
|
|
|
|
const apps = require('./apps.js'),
|
|
assert = require('assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
constants = require('./constants.js'),
|
|
dashboard = require('./dashboard.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'),
|
|
semver = require('semver'),
|
|
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 getAuthConfig(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 getRegistryConfig();
|
|
|
|
// https://github.com/apocas/dockerode#pull-from-private-repos
|
|
const autoConfig = {
|
|
username: registryConfig.username,
|
|
password: registryConfig.password,
|
|
auth: registryConfig.auth || '', // the auth token at login time
|
|
email: registryConfig.email || '',
|
|
serveraddress: registryConfig.serverAddress
|
|
};
|
|
|
|
return autoConfig;
|
|
}
|
|
|
|
async function pullImage(manifest) {
|
|
const authConfig = await getAuthConfig(manifest.dockerImage);
|
|
|
|
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
|
|
|
|
const [error, stream] = await safe(gConnection.pull(manifest.dockerImage, { authconfig: authConfig }));
|
|
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.message.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);
|
|
}
|
|
|
|
// This only returns ipv4 addresses
|
|
// We dont bind to ipv6 interfaces, public prefix changes and container restarts wont work
|
|
function getAddressesForPort53() {
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 { fqdn:dashboardFqdn } = await dashboard.getLocation();
|
|
|
|
const stdEnv = [
|
|
'CLOUDRON=1',
|
|
'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}`
|
|
];
|
|
|
|
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;
|
|
|
|
let portCount = 1;
|
|
if (portName === 'sfuTcp' || portName === 'sfuUdp') portCount = 100;
|
|
|
|
const containerPort = ports[portName].containerPort || hostPort;
|
|
const hostIps = hostPort === 53 ? getAddressesForPort53() : [ '0.0.0.0', '::0' ]; // port 53 is special because it is possibly taken by systemd-resolved
|
|
|
|
portEnv.push(`${portName}=${hostPort}`);
|
|
|
|
// 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: (hostPort + i) + '' }; });
|
|
}
|
|
}
|
|
|
|
console.log('=== env', portEnv)
|
|
console.log('=== bindings', dockerPortBindings)
|
|
console.log('=== exposedPorts', exposedPorts)
|
|
|
|
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': {},
|
|
};
|
|
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');
|
|
}
|
|
|
|
async function getRegistryConfig() {
|
|
const value = await settings.getJson(settings.REGISTRY_CONFIG_KEY);
|
|
return value || { provider: 'noop' };
|
|
}
|
|
|
|
async function setRegistryConfig(registryConfig) {
|
|
assert.strictEqual(typeof registryConfig, 'object');
|
|
|
|
const currentConfig = await getRegistryConfig();
|
|
|
|
injectPrivateFields(registryConfig, currentConfig);
|
|
|
|
await testRegistryConfig(registryConfig);
|
|
|
|
await settings.setJson(settings.REGISTRY_CONFIG_KEY, registryConfig);
|
|
}
|
|
|
|
function parseImageName(imageName) {
|
|
const repository = imageName.split(':', 1)[0];
|
|
const tag = imageName.substr(repository.length + 1).split('@', 1)[0];
|
|
const digest = imageName.substr(repository.length + 1 + tag.length + 1).split(':', 2)[1];
|
|
|
|
return { repository, tag, version: semver.parse(tag), digest };
|
|
}
|