UNPKG

iobroker.kisshome-defender

Version:
1,009 lines (1,008 loc) 111 kB
"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);