iobroker.kisshome-defender
Version:
Collection of information for KISSHome defender
1,009 lines (1,008 loc) • 111 kB
JavaScript
"use strict";
// This class implements docker commands using CLI and
// it monitors periodically the docker daemon status.
// It manages containers defined in adapter.config.containers and monitors other containers
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _DockerManager_instances, _a, _DockerManager_waitReady, _DockerManager_waitAllChecked, _DockerManager_waitAllCheckedResolve, _DockerManager_ownContainers, _DockerManager_monitoringInterval, _DockerManager_ownContainersStats, _DockerManager_driver, _DockerManager_dockerode, _DockerManager_cliAvailable, _DockerManager_tarPack, _DockerManager_init, _DockerManager_isDockerDaemonRunning, _DockerManager_ensureActualConfiguration, _DockerManager_checkOwnContainers, _DockerManager_monitorOwnContainers, _DockerManager_exec, _DockerManager_isDockerInstalled, _DockerManager_isNeedSudo, _DockerManager_parseSize;
Object.defineProperty(exports, "__esModule", { value: true });
const node_util_1 = require("node:util");
const node_child_process_1 = require("node:child_process");
const node_net_1 = require("node:net");
const dockerode_1 = __importDefault(require("dockerode"));
const execPromise = (0, node_util_1.promisify)(node_child_process_1.exec);
const dockerDefaults = {
tty: false,
stdinOpen: false,
attachStdin: false,
attachStdout: false,
attachStderr: false,
openStdin: false,
publishAllPorts: false,
readOnly: false,
user: '',
workdir: '',
domainname: '',
macAddress: '',
networkMode: 'bridge',
};
function isDefault(value, def) {
return JSON.stringify(value) === JSON.stringify(def);
}
function deepCompare(object1, object2) {
if (typeof object1 === 'number') {
object1 = object1.toString();
}
if (typeof object2 === 'number') {
object2 = object2.toString();
}
if (typeof object1 !== typeof object2) {
return false;
}
if (typeof object1 !== 'object' || object1 === null || object2 === null) {
return object1 === object2;
}
if (Array.isArray(object1)) {
if (!Array.isArray(object2) || object1.length !== object2.length) {
return false;
}
for (let i = 0; i < object1.length; i++) {
if (!deepCompare(object1[i], object2[i])) {
return false;
}
}
return true;
}
const keys1 = Object.keys(object1);
for (const key of keys1) {
// ignore iob* properties as they belong to ioBroker configuration
// ignore hostname
if (key.startsWith('iob') || key === 'hostname') {
continue;
}
if (!deepCompare(object1[key], object2[key])) {
return false;
}
}
return true;
}
function compareConfigs(desired, existing) {
const diffs = [];
const keys = Object.keys(desired);
// We only compare keys that are in the desired config
for (const key of keys) {
// ignore iob* properties as they belong to ioBroker configuration
// ignore hostname
if (key.startsWith('iob') || key === 'hostname') {
continue;
}
if (typeof desired[key] === 'object' && desired[key] !== null) {
if (Array.isArray(desired[key])) {
if (!Array.isArray(existing[key]) || desired[key].length !== existing[key].length) {
diffs.push(key);
}
else {
for (let i = 0; i < desired[key].length; i++) {
if (!deepCompare(desired[key][i], existing[key][i])) {
diffs.push(`${key}[${i}]`);
}
}
}
}
else {
Object.keys(desired[key]).forEach((subKey) => {
if (!deepCompare(desired[key][subKey], existing[key][subKey])) {
diffs.push(`${key}.${subKey}`);
}
});
}
}
else if (desired[key] !== existing[key]) {
diffs.push(key);
}
}
return diffs;
}
// remove undefined entries recursively
function removeUndefined(obj) {
if (Array.isArray(obj)) {
const arr = obj.map(v => (v && typeof v === 'object' ? removeUndefined(v) : v)).filter(v => v !== undefined);
if (!arr.length) {
return undefined;
}
return arr;
}
if (obj && typeof obj === 'object') {
const _obj = Object.fromEntries(Object.entries(obj)
.map(([k, v]) => [k, v && typeof v === 'object' ? removeUndefined(v) : v])
.filter(([_, v]) => v !== undefined &&
v !== null &&
v !== '' &&
!(Array.isArray(v) && v.length === 0) &&
!(typeof v === 'object' && Object.keys(v).length === 0)));
if (Object.keys(_obj).length === 0) {
return undefined;
}
return _obj;
}
if (obj === '') {
return undefined;
}
return obj;
}
function cleanContainerConfig(obj, mayChange) {
var _b;
obj = removeUndefined(obj);
Object.keys(obj).forEach(name => {
var _b, _c, _d, _e;
if (isDefault(obj[name], dockerDefaults[name])) {
delete obj[name];
}
if (name === 'mounts') {
if (!obj.mounts) {
delete obj.mounts;
return;
}
obj.mounts = obj.mounts.map((mount) => {
const m = { ...mount };
// /var/lib/docker/volumes/influxdb_0_flux_config/_data
if (mayChange && m.source.includes('/docker/volumes') && m.source.endsWith('/_data')) {
const parts = m.source.split('/');
m.source = parts[parts.length - 2];
}
delete m.readOnly;
return m;
});
if (!obj.mounts.length) {
delete obj.mounts;
return;
}
(_b = obj.mounts) === null || _b === void 0 ? void 0 : _b.sort((a, b) => a.target.localeCompare(b.target));
}
if (name === 'ports') {
if (!obj.ports) {
delete obj.ports;
return;
}
obj.ports = obj.ports.map((port) => {
const p = { ...port };
if (p.protocol === 'tcp') {
delete p.protocol;
}
return p;
});
if (!obj.ports.length) {
delete obj.ports;
return;
}
(_c = obj.ports) === null || _c === void 0 ? void 0 : _c.sort((a, b) => {
var _b;
if (a.hostPort !== b.hostPort) {
return parseInt(a.containerPort, 10) - parseInt(b.containerPort, 10);
}
if (a.hostIP !== b.hostIP && a.hostIP && b.hostIP) {
return (_b = a.hostIP) === null || _b === void 0 ? void 0 : _b.localeCompare(b.hostIP);
}
return 0;
});
}
if (name === 'environment') {
if (!obj.environment) {
delete obj.environment;
return;
}
const env = obj.environment;
if (Object.keys(env).length) {
obj.environment = {};
Object.keys(env)
.sort()
.forEach(key => {
if (key && env[key]) {
obj.environment[key] = env[key];
}
});
}
else {
delete obj.environment;
}
if (!Object.keys(env).length) {
delete obj.environment;
}
}
if (name === 'labels') {
if (!obj.labels) {
delete obj.labels;
return;
}
const labels = obj.labels;
if (Object.keys(labels).length) {
obj.labels = {};
Object.keys(labels)
.sort()
.forEach(key => {
if (key && labels[key]) {
obj.labels[key] = labels[key];
}
});
}
else {
delete obj.labels;
}
if (!Object.keys(labels).length) {
delete obj.labels;
}
}
if (name === 'volumes') {
if (!((_d = obj.volumes) === null || _d === void 0 ? void 0 : _d.length)) {
delete obj.volumes;
return;
}
obj.volumes = obj.volumes.map(v => v.trim()).filter(v => v);
obj.volumes.sort();
if (!((_e = obj.volumes) === null || _e === void 0 ? void 0 : _e.length)) {
delete obj.volumes;
}
}
});
(_b = obj.volumes) === null || _b === void 0 ? void 0 : _b.sort();
return obj;
}
function size2string(size) {
if (size < 1024) {
return `${size} B`;
}
if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(2)} KB`;
}
if (size < 1024 * 1024 * 1024) {
return `${(size / (1024 * 1024)).toFixed(2)} MB`;
}
return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
class DockerManager {
constructor(adapter, options, containers) {
_DockerManager_instances.add(this);
this.installed = false;
this.dockerVersion = '';
this.needSudo = false;
_DockerManager_waitReady.set(this, void 0);
_DockerManager_waitAllChecked.set(this, void 0);
_DockerManager_waitAllCheckedResolve.set(this, void 0);
_DockerManager_ownContainers.set(this, []);
_DockerManager_monitoringInterval.set(this, null);
_DockerManager_ownContainersStats.set(this, {});
_DockerManager_driver.set(this, 'cli');
_DockerManager_dockerode.set(this, null);
_DockerManager_cliAvailable.set(this, false);
_DockerManager_tarPack.set(this, null);
this.adapter = adapter;
this.options = options || {};
__classPrivateFieldSet(this, _DockerManager_ownContainers, containers || [], "f");
__classPrivateFieldSet(this, _DockerManager_waitReady, new Promise(resolve => __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_init).call(this).then(() => resolve())), "f");
__classPrivateFieldSet(this, _DockerManager_waitAllChecked, new Promise(resolve => (__classPrivateFieldSet(this, _DockerManager_waitAllCheckedResolve, resolve, "f"))), "f");
}
/** Wait till the check if docker is installed and the daemon is running is ready */
isReady() {
return __classPrivateFieldGet(this, _DockerManager_waitReady, "f");
}
/**
* Convert information from inspect to docker configuration to start it
*
* @param inspect Inspect information
*/
static mapInspectToConfig(inspect) {
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
const obj = {
image: inspect.Config.Image,
name: inspect.Name.replace(/^\//, ''),
command: (_b = inspect.Config.Cmd) !== null && _b !== void 0 ? _b : undefined,
entrypoint: (_c = inspect.Config.Entrypoint) !== null && _c !== void 0 ? _c : undefined,
user: (_d = inspect.Config.User) !== null && _d !== void 0 ? _d : undefined,
workdir: (_e = inspect.Config.WorkingDir) !== null && _e !== void 0 ? _e : undefined,
hostname: (_f = inspect.Config.Hostname) !== null && _f !== void 0 ? _f : undefined,
domainname: (_g = inspect.Config.Domainname) !== null && _g !== void 0 ? _g : undefined,
macAddress: (_h = inspect.NetworkSettings.MacAddress) !== null && _h !== void 0 ? _h : undefined,
environment: inspect.Config.Env
? Object.fromEntries(inspect.Config.Env.map(e => {
const [key, ...rest] = e.split('=');
return [key, rest.join('=')];
}))
: undefined,
labels: (_j = inspect.Config.Labels) !== null && _j !== void 0 ? _j : undefined,
tty: inspect.Config.Tty,
stdinOpen: inspect.Config.OpenStdin,
attachStdin: inspect.Config.AttachStdin,
attachStdout: inspect.Config.AttachStdout,
attachStderr: inspect.Config.AttachStderr,
openStdin: inspect.Config.OpenStdin,
publishAllPorts: inspect.HostConfig.PublishAllPorts,
ports: inspect.HostConfig.PortBindings
? Object.entries(inspect.HostConfig.PortBindings).flatMap(([containerPort, bindings]) => bindings.map(binding => ({
containerPort: containerPort.split('/')[0],
protocol: containerPort.split('/')[1] || 'tcp',
hostPort: binding.HostPort,
hostIP: binding.HostIp,
})))
: undefined,
mounts: (_k = inspect.Mounts) === null || _k === void 0 ? void 0 : _k.map(mount => ({
type: mount.Type,
source: mount.Source,
target: mount.Destination,
readOnly: mount.RW,
})),
volumes: inspect.Config.Volumes ? Object.keys(inspect.Config.Volumes) : inspect.HostConfig.Binds,
extraHosts: (_l = inspect.HostConfig.ExtraHosts) !== null && _l !== void 0 ? _l : undefined,
dns: {
servers: inspect.HostConfig.Dns,
search: inspect.HostConfig.DnsSearch,
options: inspect.HostConfig.DnsOptions,
},
networkMode: inspect.HostConfig.NetworkMode,
networks: inspect.NetworkSettings.Networks
? Object.entries(inspect.NetworkSettings.Networks).map(([name, net]) => {
var _b, _c;
return ({
name,
aliases: (_b = net.Aliases) !== null && _b !== void 0 ? _b : undefined,
ipv4Address: net.IPAddress,
ipv6Address: net.GlobalIPv6Address,
driverOpts: (_c = net.DriverOpts) !== null && _c !== void 0 ? _c : undefined,
});
})
: undefined,
restart: {
policy: inspect.HostConfig.RestartPolicy.Name,
maxRetries: inspect.HostConfig.RestartPolicy.MaximumRetryCount,
},
resources: {
cpuShares: inspect.HostConfig.CpuShares,
cpuQuota: inspect.HostConfig.CpuQuota,
cpuPeriod: inspect.HostConfig.CpuPeriod,
cpusetCpus: inspect.HostConfig.CpusetCpus,
memory: inspect.HostConfig.Memory,
memorySwap: inspect.HostConfig.MemorySwap,
memoryReservation: inspect.HostConfig.MemoryReservation,
pidsLimit: (_m = inspect.HostConfig.PidsLimit) !== null && _m !== void 0 ? _m : undefined,
shmSize: inspect.HostConfig.ShmSize,
readOnlyRootFilesystem: inspect.HostConfig.ReadonlyRootfs,
},
logging: {
driver: inspect.HostConfig.LogConfig.Type,
options: inspect.HostConfig.LogConfig.Config,
},
security: {
privileged: inspect.HostConfig.Privileged,
capAdd: (_o = inspect.HostConfig.CapAdd) !== null && _o !== void 0 ? _o : undefined,
capDrop: (_p = inspect.HostConfig.CapDrop) !== null && _p !== void 0 ? _p : undefined,
usernsMode: (_q = inspect.HostConfig.UsernsMode) !== null && _q !== void 0 ? _q : undefined,
ipc: inspect.HostConfig.IpcMode,
pid: inspect.HostConfig.PidMode,
seccomp: (_t = (_s = (_r = inspect.HostConfig.SecurityOpt) === null || _r === void 0 ? void 0 : _r.find(opt => opt.startsWith('seccomp='))) === null || _s === void 0 ? void 0 : _s.split('=')[1]) !== null && _t !== void 0 ? _t : undefined,
apparmor: (_w = (_v = (_u = inspect.HostConfig.SecurityOpt) === null || _u === void 0 ? void 0 : _u.find(opt => opt.startsWith('apparmor='))) === null || _v === void 0 ? void 0 : _v.split('=')[1]) !== null && _w !== void 0 ? _w : undefined,
groupAdd: (_x = inspect.HostConfig.GroupAdd) !== null && _x !== void 0 ? _x : undefined,
noNewPrivileges: undefined, // Nicht direkt verfügbar
},
sysctls: (_y = inspect.HostConfig.Sysctls) !== null && _y !== void 0 ? _y : undefined,
init: (_z = inspect.HostConfig.Init) !== null && _z !== void 0 ? _z : undefined,
stop: {
signal: (_0 = inspect.Config.StopSignal) !== null && _0 !== void 0 ? _0 : undefined,
gracePeriodSec: (_1 = inspect.Config.StopTimeout) !== null && _1 !== void 0 ? _1 : undefined,
},
readOnly: inspect.HostConfig.ReadonlyRootfs,
timezone: undefined, // Nicht direkt verfügbar
__meta: undefined, // Eigene Metadaten
};
return cleanContainerConfig(obj, true);
}
/**
* Get information about the Docker daemon: is it running and which version
*
* @returns Object with version and daemonRunning
*/
async getDockerDaemonInfo() {
await this.isReady();
const daemonRunning = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_isDockerDaemonRunning).call(this);
return {
version: this.dockerVersion,
daemonRunning,
removeSupported: !__classPrivateFieldGet(this, _DockerManager_dockerode, "f") || __classPrivateFieldGet(this, _DockerManager_cliAvailable, "f"),
driver: __classPrivateFieldGet(this, _DockerManager_driver, "f"),
};
}
static checkDockerSocket() {
return new Promise(resolve => {
const socket = (0, node_net_1.createConnection)({ path: '/var/run/docker.sock' }, () => {
socket.end();
resolve(true);
});
socket.on('error', e => {
console.error(`Cannot connect to docker socket: ${e.message}`);
resolve(false);
});
});
}
static async isDockerApiRunningOnPort(port, host = 'localhost') {
return new Promise(resolve => {
const socket = (0, node_net_1.createConnection)({ port, host }, () => {
socket.write('GET /version HTTP/1.0\r\nHost: localhost\r\n\r\n');
});
let data = '';
socket.on('data', chunk => (data += chunk.toString()));
socket.on('end', () => {
resolve(data.includes('Docker') || data.includes('Api-Version'));
});
socket.on('error', () => resolve(false));
});
}
getDefaultContainerName() {
return `iob_${this.adapter.namespace.replace(/[-.]/g, '_')}`;
}
allOwnContainersChecked() {
return __classPrivateFieldGet(this, _DockerManager_waitAllChecked, "f");
}
/** Read own container stats */
getOwnContainerStats() {
return __classPrivateFieldGet(this, _DockerManager_ownContainersStats, "f");
}
async containerGetRamAndCpuUsage(containerNameOrId) {
try {
const { stdout } = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `stats ${containerNameOrId} --no-stream --format "{{.CPUPerc}};{{.MemUsage}};{{.NetIO}};{{.BlockIO}};{{.PIDs}}"`);
// Example: "0.15%;12.34MiB / 512MiB;1.2kB / 2.3kB;0B / 0B;5"
const [cpuStr, memStr, netStr, blockIoStr, pid] = stdout.trim().split(';');
const [memUsed, memMax] = memStr.split('/').map(it => it.trim());
const [netRead, netWrite] = netStr.split('/').map(it => it.trim());
const [blockIoRead, blockIoWrite] = blockIoStr.split('/').map(it => it.trim());
return {
ts: Date.now(),
cpu: parseFloat(cpuStr.replace('%', '').replace(',', '.')),
memUsed: __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, memUsed.replace('iB', 'B')),
memMax: __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, memMax.replace('iB', 'B')),
netRead: __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, netRead.replace('iB', 'B')),
netWrite: __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, netWrite.replace('iB', 'B')),
processes: parseInt(pid, 10),
blockIoRead: __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, blockIoRead.replace('iB', 'B')),
blockIoWrite: __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, blockIoWrite.replace('iB', 'B')),
};
}
catch (e) {
this.adapter.log.debug(`Cannot get stats: ${e.message}`);
return null;
}
}
/**
* Update the image if a newer version is available
*
* @param image Image name with tag
* @param ignoreIfNotExist If true, do not throw error if image does not exist
* @returns New image info if image was updated, null if no update was necessary
*/
async imageUpdate(image, ignoreIfNotExist) {
const list = await this.imageList();
if (!image.includes(':')) {
image += ':latest';
}
const existingImage = list.find(it => `${it.repository}:${it.tag}` === image);
if (!existingImage && !ignoreIfNotExist) {
throw new Error(`Image ${image} not found`);
}
// Pull the image
const result = await this.imagePull(image);
if (result.stderr) {
throw new Error(`Cannot pull image ${image}: ${result.stderr}`);
}
const newList = await this.imageList();
const newImage = newList.find(it => `${it.repository}:${it.tag}` === image);
if (!newImage) {
throw new Error(`Image ${image} not found after pull`);
}
// If image ID has changed, image was updated
return !existingImage || existingImage.id !== newImage.id ? newImage : null;
}
/** Get disk usage information */
async discUsage() {
var _b;
if (__classPrivateFieldGet(this, _DockerManager_dockerode, "f")) {
const info = await __classPrivateFieldGet(this, _DockerManager_dockerode, "f").df();
const result = { total: { size: 0, reclaimable: 0 } };
if (info.Images) {
let size = 0;
let reclaimable = 0;
for (const image of info.Images) {
size += image.Size;
reclaimable += image.SharedSize + image.VirtualSize;
}
result.images = {
total: info.Images.length,
// @ts-expect-error todo
active: info.Images.filter(img => img.Containers > 0).length,
size,
reclaimable,
};
result.total.size += size;
result.total.reclaimable += reclaimable;
}
if (info.Containers) {
let size = 0;
for (const container of info.Containers) {
size += container.SizeRootFs || 0;
}
result.containers = {
total: info.Containers.length,
// @ts-expect-error todo
active: info.Containers.filter(cont => cont.State === 'running').length,
size,
reclaimable: 0, // Not available
};
result.total.size += size;
}
if (info.Volumes) {
let size = 0;
for (const volume of info.Volumes) {
size += ((_b = volume.UsageData) === null || _b === void 0 ? void 0 : _b.Size) || 0;
}
result.volumes = {
total: info.Volumes.length,
// @ts-expect-error todo
active: info.Volumes.filter(vol => { var _b; return ((_b = vol.UsageData) === null || _b === void 0 ? void 0 : _b.RefCount) && vol.UsageData.RefCount > 0; }).length,
size,
reclaimable: 0, // Not available
};
result.total.size += size;
}
// Build cache not available
return result;
}
const { stdout } = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `system df`);
const result = { total: { size: 0, reclaimable: 0 } };
// parse the output
// TYPE TOTAL ACTIVE SIZE RECLAIMABLE
// Images 2 1 2.715GB 2.715GB (99%)
// Containers 1 1 26.22MB 0B (0%)
// Local Volumes 0 0 0B 0B
// Build Cache 0 0 0B 0B
const lines = stdout.split('\n');
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 5 && parts[0] !== 'TYPE') {
let size;
let reclaimable;
if (parts[0] === 'Images') {
const sizeStr = parts[3];
const reclaimableStr = parts[4].split(' ')[0];
size = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, sizeStr);
reclaimable = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, reclaimableStr);
result.images = {
total: parseInt(parts[1], 10),
active: parseInt(parts[2], 10),
size,
reclaimable: reclaimable,
};
}
else if (parts[0] === 'Containers') {
const sizeStr = parts[3];
const reclaimableStr = parts[4].split(' ')[0];
size = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, sizeStr);
reclaimable = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, reclaimableStr);
result.containers = {
total: parseInt(parts[1], 10),
active: parseInt(parts[2], 10),
size,
reclaimable: reclaimable,
};
}
else if (parts[0] === 'Local' && parts[1] === 'Volumes') {
const sizeStr = parts[4];
const reclaimableStr = parts[5].split(' ')[0];
size = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, sizeStr);
reclaimable = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, reclaimableStr);
result.volumes = {
total: parseInt(parts[2], 10),
active: parseInt(parts[3], 10),
size,
reclaimable: reclaimable,
};
}
else if (parts[0] === 'Build' && parts[1] === 'Cache') {
const sizeStr = parts[4];
const reclaimableStr = parts[5].split(' ')[0];
size = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, sizeStr);
reclaimable = __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, reclaimableStr);
result.buildCache = {
total: parseInt(parts[2], 10),
active: parseInt(parts[3], 10),
size,
reclaimable: reclaimable,
};
}
result.total.size += size || 0;
result.total.reclaimable += reclaimable || 0;
}
}
return result;
}
/** Pull an image from the registry */
async imagePull(image) {
if (!image.includes(':')) {
image += ':latest';
}
if (__classPrivateFieldGet(this, _DockerManager_dockerode, "f")) {
const stream = await __classPrivateFieldGet(this, _DockerManager_dockerode, "f").pull(image);
if (!stream) {
throw new Error('No stream returned');
}
return new Promise((resolve, reject) => {
const onFinished = (err) => {
if (err) {
return reject(err);
}
this.imageList()
.then(images => resolve({ stdout: `Image ${image} pulled`, stderr: '', images }))
.catch(reject);
};
const lastShownProgress = {};
const onProgress = (event) => {
// {"status":"Downloading","progressDetail":{"current":109494080,"total":689664036},"progress":"[=======> ] 109.5MB/689.7MB","id":"29bce3058cea"}
// {"status":"Download complete","progressDetail":{},"id":"6859c690a072"}
// {"status":"Verifying Checksum","progressDetail":{},"id":"6859c690a072"}
// {"status":"Extracting","progressDetail":{"current":32,"total":32},"progress":"[======>] 32B/32B","id":"4f4fb700ef54"}
// {"status":"Pull complete","progressDetail":{},"id":"4f4fb700ef54"}
if (!lastShownProgress || Date.now() - lastShownProgress[event.id] > 4000) {
if (event.status === 'Download complete' ||
event.status === 'Pull complete' ||
event.status === 'Verifying Checksum') {
this.adapter.log.debug(`Image ${image}/${event.id}: ${event.status}`);
}
else if (event.status === 'Downloading' || event.status === 'Extracting') {
this.adapter.log.debug(`Pulling image ${image}/${event.id}: ${event.status} ${Math.round((event.progressDetail.current / event.progressDetail.total) * 1000) / 10}% of ${size2string(event.progressDetail.total)}`);
}
else {
this.adapter.log.debug(`Pulling image ${image}/${event.id}: ${JSON.stringify(event)}`);
}
lastShownProgress[event.id] = Date.now();
}
};
__classPrivateFieldGet(this, _DockerManager_dockerode, "f").modem.followProgress(stream, onFinished, onProgress);
});
}
try {
const result = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `pull ${image}`);
const images = await this.imageList();
if (!images.find(it => `${it.repository}:${it.tag}` === image)) {
throw new Error(`Image ${image} not found after pull`);
}
return { ...result, images };
}
catch (e) {
return { stdout: '', stderr: e.message.toString() };
}
}
/** Autocomplete image names from Docker Hub */
async imageNameAutocomplete(partialName) {
try {
// Read stars and descriptions
const { stdout } = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `search ${partialName} --format "{{.Name}};{{.Description}};{{.IsOfficial}};{{.StarCount}}" --limit 50`);
return stdout
.split('\n')
.filter(line => line.trim() !== '')
.map(line => {
const [name, description, isOfficial, starCount] = line.split(';');
return {
name,
description,
isOfficial: isOfficial === 'true',
starCount: parseInt(starCount, 10) || 0,
};
});
}
catch (e) {
this.adapter.log.debug(`Cannot search images: ${e.message}`);
return [];
}
}
static getDockerodeConfig(config) {
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
let mounts;
if (config.mounts) {
for (const mount of config.mounts) {
let volumeOptions;
if (mount.volumeOptions) {
if (mount.volumeOptions.nocopy !== undefined) {
volumeOptions || (volumeOptions = {});
volumeOptions.NoCopy = mount.volumeOptions.nocopy;
}
if (mount.volumeOptions.labels) {
volumeOptions || (volumeOptions = {});
volumeOptions.Labels = mount.volumeOptions.labels;
}
}
let bindOptions;
if (mount.bindOptions) {
if (mount.bindOptions.propagation) {
bindOptions || (bindOptions = {});
bindOptions.Propagation = mount.bindOptions.propagation;
}
}
let tmpfsOptions;
if (mount.tmpfsOptions) {
if (mount.tmpfsOptions.size !== undefined) {
tmpfsOptions || (tmpfsOptions = {});
tmpfsOptions.SizeBytes = mount.tmpfsOptions.size;
}
if (mount.tmpfsOptions.mode !== undefined) {
tmpfsOptions || (tmpfsOptions = {});
tmpfsOptions.Mode = mount.tmpfsOptions.mode;
}
}
if (mount.source === true) {
throw new Error(`Mount source must be a string, but got boolean true`);
}
const m = {
Target: mount.target,
Source: mount.source || '',
Type: mount.type,
ReadOnly: mount.readOnly,
Consistency: mount.consistency,
VolumeOptions: volumeOptions,
BindOptions: bindOptions,
TmpfsOptions: tmpfsOptions,
};
mounts || (mounts = []);
mounts.push(m);
}
}
if (!config.name) {
throw new Error(`Container name must be a string, but got boolean true`);
}
return {
name: config.name,
Image: config.image,
Cmd: Array.isArray(config.command)
? config.command
: typeof config.command === 'string'
? [config.command]
: undefined,
Entrypoint: config.entrypoint,
Env: config.environment
? Object.keys(config.environment).map(key => `${key}=${config.environment[key]}`)
: undefined,
// WorkingDir: config.workingDir,
// { '/data': {} }
Labels: config.labels,
ExposedPorts: config.ports
? config.ports.reduce((acc, port) => {
acc[`${port.containerPort}/${port.protocol || 'tcp'}`] = {};
return acc;
}, {})
: undefined,
HostConfig: {
// https://github.com/apocas/dockerode/issues/265#issuecomment-462786936
Binds: config.volumes,
PortBindings: config.ports
? config.ports.reduce((acc, port) => {
acc[`${port.containerPort}/${port.protocol || 'tcp'}`] = [
{
HostPort: port.hostPort ? port.hostPort.toString() : undefined,
HostIp: port.hostIP || undefined,
},
];
return acc;
}, {})
: undefined,
Mounts: mounts,
NetworkMode: config.networkMode === true ? '' : config.networkMode || undefined,
// Links: config.links,
// Dns: config.dns,
// DnsOptions: config.dnsOptions,
// DnsSearch: config.dnsSearch,
ExtraHosts: config.extraHosts,
// VolumesFrom: config.volumesFrom,
Privileged: (_b = config.security) === null || _b === void 0 ? void 0 : _b.privileged,
CapAdd: (_c = config.security) === null || _c === void 0 ? void 0 : _c.capAdd,
CapDrop: (_d = config.security) === null || _d === void 0 ? void 0 : _d.capDrop,
UsernsMode: (_e = config.security) === null || _e === void 0 ? void 0 : _e.usernsMode,
IpcMode: (_f = config.security) === null || _f === void 0 ? void 0 : _f.ipc,
PidMode: (_g = config.security) === null || _g === void 0 ? void 0 : _g.pid,
GroupAdd: (_j = (_h = config.security) === null || _h === void 0 ? void 0 : _h.groupAdd) === null || _j === void 0 ? void 0 : _j.map(g => g.toString()),
ReadonlyRootfs: config.readOnly,
RestartPolicy: {
Name: ((_k = config.restart) === null || _k === void 0 ? void 0 : _k.policy) || 'no',
MaximumRetryCount: ((_l = config.restart) === null || _l === void 0 ? void 0 : _l.maxRetries) || 0,
},
CpuShares: (_m = config.resources) === null || _m === void 0 ? void 0 : _m.cpuShares,
CpuPeriod: (_o = config.resources) === null || _o === void 0 ? void 0 : _o.cpuPeriod,
CpuQuota: (_p = config.resources) === null || _p === void 0 ? void 0 : _p.cpuQuota,
CpusetCpus: (_r = (_q = config.resources) === null || _q === void 0 ? void 0 : _q.cpus) === null || _r === void 0 ? void 0 : _r.toString(),
Memory: (_s = config.resources) === null || _s === void 0 ? void 0 : _s.memory,
MemorySwap: (_t = config.resources) === null || _t === void 0 ? void 0 : _t.memorySwap,
MemoryReservation: (_u = config.resources) === null || _u === void 0 ? void 0 : _u.memoryReservation,
// OomKillDisable: config.resources?.oomKillDisable,
// OomScoreAdj: config.resources?.oomScoreAdj,
LogConfig: config.logging
? {
Type: config.logging.driver || 'json-file',
Config: config.logging.options || {},
}
: undefined,
SecurityOpt: [
...(((_v = config.security) === null || _v === void 0 ? void 0 : _v.seccomp) ? [`seccomp=${config.security.seccomp}`] : []),
...(((_w = config.security) === null || _w === void 0 ? void 0 : _w.apparmor) ? [`apparmor=${config.security.apparmor}`] : []),
...(((_x = config.security) === null || _x === void 0 ? void 0 : _x.noNewPrivileges) ? ['no-new-privileges:true'] : []),
],
Sysctls: config.sysctls,
Init: config.init,
},
StopSignal: (_y = config.stop) === null || _y === void 0 ? void 0 : _y.signal,
StopTimeout: (_z = config.stop) === null || _z === void 0 ? void 0 : _z.gracePeriodSec,
Tty: config.tty,
OpenStdin: config.openStdin,
};
}
/**
* Create and start a container with the given configuration. No checks are done.
*/
async containerRun(config) {
if (__classPrivateFieldGet(this, _DockerManager_dockerode, "f")) {
const container = await __classPrivateFieldGet(this, _DockerManager_dockerode, "f").createContainer(_a.getDockerodeConfig(config));
await container.start();
return { stdout: `Container ${config.name} started`, stderr: '' };
}
try {
return await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `run ${_a.toDockerRun(config)}`);
}
catch (e) {
return { stdout: '', stderr: e.message.toString() };
}
}
/**
* Create a container with the given configuration without starting it. No checks are done.
*/
async containerCreate(config) {
if (__classPrivateFieldGet(this, _DockerManager_dockerode, "f")) {
const container = await __classPrivateFieldGet(this, _DockerManager_dockerode, "f").createContainer(_a.getDockerodeConfig(config));
return { stdout: `Container ${container.id} created`, stderr: '' };
}
try {
return await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `create ${_a.toDockerRun(config, true)}`);
}
catch (e) {
return { stdout: '', stderr: e.message.toString() };
}
}
/**
* Recreate a container
*
* This function checks if a container is running, stops it if necessary,
* removes it and creates a new one with the given configuration.
* The container is not started after creation.
*
* @param config new configuration
* @returns stdout and stderr of the create command
*/
async containerReCreate(config) {
if (__classPrivateFieldGet(this, _DockerManager_dockerode, "f")) {
// Get if the container is running
let containers = await this.containerList();
// find ID of container
const containerInfo = containers.find(it => it.names === config.name);
if (containerInfo) {
const container = __classPrivateFieldGet(this, _DockerManager_dockerode, "f").getContainer(containerInfo.id);
if (containerInfo.status === 'running' || containerInfo.status === 'restarting') {
await container.stop();
containers = await this.containerList();
if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
this.adapter.log.warn(`Cannot remove container: still running`);
throw new Error(`Container ${containerInfo.id} still running after stop`);
}
}
// Remove container
await container.remove();
containers = await this.containerList();
if (containers.find(it => it.id === containerInfo.id)) {
this.adapter.log.warn(`Cannot remove container: still existing`);
throw new Error(`Container ${containerInfo.id} still found after remove`);
}
}
const dockerodeConfig = _a.getDockerodeConfig(config);
const newContainer = await __classPrivateFieldGet(this, _DockerManager_dockerode, "f").createContainer(dockerodeConfig);
return { stdout: `Container ${newContainer.id} created`, stderr: '' };
}
try {
// Get if the container is running
let containers = await this.containerList();
// find ID of container
const containerInfo = containers.find(it => it.names === config.name);
if (containerInfo) {
if (containerInfo.status === 'running' || containerInfo.status === 'restarting') {
const stopResult = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `stop ${containerInfo.id}`);
containers = await this.containerList();
if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
this.adapter.log.warn(`Cannot remove container: ${stopResult.stderr || stopResult.stdout}`);
throw new Error(`Container ${containerInfo.id} still running after stop`);
}
}
// Remove container
const rmResult = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `rm ${containerInfo.id}`);
containers = await this.containerList();
if (containers.find(it => it.id === containerInfo.id)) {
this.adapter.log.warn(`Cannot remove container: ${rmResult.stderr || rmResult.stdout}`);
throw new Error(`Container ${containerInfo.id} still found after remove`);
}
}
return await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `create ${_a.toDockerRun(config, true)}`);
}
catch (e) {
return { stdout: '', stderr: e.message.toString() };
}
}
async containerCreateCompose(compose) {
try {
return await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, `compose -f ${compose} create`);
}
catch (e) {
return { stdout: '', stderr: e.message.toString() };
}
}
/** List all images */
async imageList() {
if (__classPrivateFieldGet(this, _DockerManager_dockerode, "f")) {
const images = await __classPrivateFieldGet(this, _DockerManager_dockerode, "f").listImages();
return images.map(img => {
const repoTag = img.RepoTags && img.RepoTags.length ? img.RepoTags[0] : '<none>:<none>';
const [repository, tag] = repoTag.split(':');
return {
repository,
tag,
id: img.Id.startsWith('sha256:') ? img.Id.substring(7, 19) : img.Id.substring(0, 12),
createdSince: new Date(img.Created * 1000).toISOString(),
size: img.Size,
};
});
}
try {
const { stdout } = await __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_exec).call(this, 'images --format "{{.Repository}}:{{.Tag}};{{.ID}};{{.CreatedAt}};{{.Size}}"');
return stdout
.split('\n')
.filter(line => line.trim() !== '')
.map(line => {
const [repositoryTag, id, createdSince, size] = line.split(';');
const [repository, tag] = repositoryTag.split(':');
return {
repository,
tag,
id,
createdSince,
size: __classPrivateFieldGet(this, _DockerManager_instances, "m", _DockerManager_parseSize).call(this, size),
};
});
}
catch (e) {
this.adapter.log.debug(`Cannot list images: ${e.message}`);
return [];
}
}
/** Build an image from a Dockerfile */
async imageBuild(dockerfilePath, tag) {
if (__classPrivateFieldGet(this, _DockerManager_dockerode, "f")) {
try {
const stream = await __classPrivateFieldGet(this, _DockerManager_dockerode, "f").buildImage(dockerfilePath, {
t: tag,
dockerfile: dockerfilePath,
});
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
const onFinished = (err) => {
if (err) {
return reject(err);