iobroker.docker-manager 0.1.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2277 +0,0 @@
1
- "use strict";
2
- // This class implements docker commands using CLI and
3
- // it monitors periodically the docker daemon status.
4
- // It manages containers defined in adapter.config.containers and monitors other containers
5
- var __importDefault = (this && this.__importDefault) || function (mod) {
6
- return (mod && mod.__esModule) ? mod : { "default": mod };
7
- };
8
- var _a;
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- const node_util_1 = require("node:util");
11
- const node_child_process_1 = require("node:child_process");
12
- const node_net_1 = require("node:net");
13
- const dockerode_1 = __importDefault(require("dockerode"));
14
- const execPromise = (0, node_util_1.promisify)(node_child_process_1.exec);
15
- const dockerDefaults = {
16
- tty: false,
17
- stdinOpen: false,
18
- attachStdin: false,
19
- attachStdout: false,
20
- attachStderr: false,
21
- openStdin: false,
22
- publishAllPorts: false,
23
- readOnly: false,
24
- user: '',
25
- workdir: '',
26
- domainname: '',
27
- macAddress: '',
28
- networkMode: 'bridge',
29
- };
30
- function isDefault(value, def) {
31
- return JSON.stringify(value) === JSON.stringify(def);
32
- }
33
- function deepCompare(object1, object2) {
34
- if (typeof object1 === 'number') {
35
- object1 = object1.toString();
36
- }
37
- if (typeof object2 === 'number') {
38
- object2 = object2.toString();
39
- }
40
- if (typeof object1 !== typeof object2) {
41
- return false;
42
- }
43
- if (typeof object1 !== 'object' || object1 === null || object2 === null) {
44
- return object1 === object2;
45
- }
46
- if (Array.isArray(object1)) {
47
- if (!Array.isArray(object2) || object1.length !== object2.length) {
48
- return false;
49
- }
50
- for (let i = 0; i < object1.length; i++) {
51
- if (!deepCompare(object1[i], object2[i])) {
52
- return false;
53
- }
54
- }
55
- return true;
56
- }
57
- const keys1 = Object.keys(object1);
58
- for (const key of keys1) {
59
- // ignore iob* properties as they belong to ioBroker configuration
60
- // ignore hostname
61
- if (key.startsWith('iob') || key === 'hostname') {
62
- continue;
63
- }
64
- if (!deepCompare(object1[key], object2[key])) {
65
- return false;
66
- }
67
- }
68
- return true;
69
- }
70
- function compareConfigs(desired, existing) {
71
- const diffs = [];
72
- const keys = Object.keys(desired);
73
- // We only compare keys that are in the desired config
74
- for (const key of keys) {
75
- // ignore iob* properties as they belong to ioBroker configuration
76
- // ignore hostname
77
- if (key.startsWith('iob') || key === 'hostname') {
78
- continue;
79
- }
80
- if (typeof desired[key] === 'object' && desired[key] !== null) {
81
- if (Array.isArray(desired[key])) {
82
- if (!Array.isArray(existing[key]) || desired[key].length !== existing[key].length) {
83
- diffs.push(key);
84
- }
85
- else {
86
- for (let i = 0; i < desired[key].length; i++) {
87
- if (!deepCompare(desired[key][i], existing[key][i])) {
88
- diffs.push(`${key}[${i}]`);
89
- }
90
- }
91
- }
92
- }
93
- else {
94
- Object.keys(desired[key]).forEach((subKey) => {
95
- if (!deepCompare(desired[key][subKey], existing[key][subKey])) {
96
- diffs.push(`${key}.${subKey}`);
97
- }
98
- });
99
- }
100
- }
101
- else if (desired[key] !== existing[key]) {
102
- diffs.push(key);
103
- }
104
- }
105
- return diffs;
106
- }
107
- // remove undefined entries recursively
108
- function removeUndefined(obj) {
109
- if (Array.isArray(obj)) {
110
- const arr = obj.map(v => (v && typeof v === 'object' ? removeUndefined(v) : v)).filter(v => v !== undefined);
111
- if (!arr.length) {
112
- return undefined;
113
- }
114
- return arr;
115
- }
116
- if (obj && typeof obj === 'object') {
117
- const _obj = Object.fromEntries(Object.entries(obj)
118
- .map(([k, v]) => [k, v && typeof v === 'object' ? removeUndefined(v) : v])
119
- .filter(([_, v]) => v !== undefined &&
120
- v !== null &&
121
- v !== '' &&
122
- !(Array.isArray(v) && v.length === 0) &&
123
- !(typeof v === 'object' && Object.keys(v).length === 0)));
124
- if (Object.keys(_obj).length === 0) {
125
- return undefined;
126
- }
127
- return _obj;
128
- }
129
- if (obj === '') {
130
- return undefined;
131
- }
132
- return obj;
133
- }
134
- function cleanContainerConfig(obj, mayChange) {
135
- obj = removeUndefined(obj);
136
- Object.keys(obj).forEach(name => {
137
- if (isDefault(obj[name], dockerDefaults[name])) {
138
- delete obj[name];
139
- }
140
- if (name === 'mounts') {
141
- if (!obj.mounts) {
142
- delete obj.mounts;
143
- return;
144
- }
145
- obj.mounts = obj.mounts.map((mount) => {
146
- const m = { ...mount };
147
- // /var/lib/docker/volumes/influxdb_0_flux_config/_data
148
- if (mayChange && m.source.includes('/docker/volumes') && m.source.endsWith('/_data')) {
149
- const parts = m.source.split('/');
150
- m.source = parts[parts.length - 2];
151
- }
152
- delete m.readOnly;
153
- return m;
154
- });
155
- if (!obj.mounts.length) {
156
- delete obj.mounts;
157
- return;
158
- }
159
- obj.mounts?.sort((a, b) => a.target.localeCompare(b.target));
160
- }
161
- if (name === 'ports') {
162
- if (!obj.ports) {
163
- delete obj.ports;
164
- return;
165
- }
166
- obj.ports = obj.ports.map((port) => {
167
- const p = { ...port };
168
- if (p.protocol === 'tcp') {
169
- delete p.protocol;
170
- }
171
- return p;
172
- });
173
- if (!obj.ports.length) {
174
- delete obj.ports;
175
- return;
176
- }
177
- obj.ports?.sort((a, b) => {
178
- if (a.hostPort !== b.hostPort) {
179
- return parseInt(a.containerPort, 10) - parseInt(b.containerPort, 10);
180
- }
181
- if (a.hostIP !== b.hostIP && a.hostIP && b.hostIP) {
182
- return a.hostIP?.localeCompare(b.hostIP);
183
- }
184
- return 0;
185
- });
186
- }
187
- if (name === 'environment') {
188
- if (!obj.environment) {
189
- delete obj.environment;
190
- return;
191
- }
192
- const env = obj.environment;
193
- if (Object.keys(env).length) {
194
- obj.environment = {};
195
- Object.keys(env)
196
- .sort()
197
- .forEach(key => {
198
- if (key && env[key]) {
199
- obj.environment[key] = env[key];
200
- }
201
- });
202
- }
203
- else {
204
- delete obj.environment;
205
- }
206
- if (!Object.keys(env).length) {
207
- delete obj.environment;
208
- }
209
- }
210
- if (name === 'labels') {
211
- if (!obj.labels) {
212
- delete obj.labels;
213
- return;
214
- }
215
- const labels = obj.labels;
216
- if (Object.keys(labels).length) {
217
- obj.labels = {};
218
- Object.keys(labels)
219
- .sort()
220
- .forEach(key => {
221
- if (key && labels[key]) {
222
- obj.labels[key] = labels[key];
223
- }
224
- });
225
- }
226
- else {
227
- delete obj.labels;
228
- }
229
- if (!Object.keys(labels).length) {
230
- delete obj.labels;
231
- }
232
- }
233
- if (name === 'volumes') {
234
- if (!obj.volumes?.length) {
235
- delete obj.volumes;
236
- return;
237
- }
238
- obj.volumes = obj.volumes.map(v => v.trim()).filter(v => v);
239
- obj.volumes.sort();
240
- if (!obj.volumes?.length) {
241
- delete obj.volumes;
242
- }
243
- }
244
- });
245
- obj.volumes?.sort();
246
- return obj;
247
- }
248
- function size2string(size) {
249
- if (size < 1024) {
250
- return `${size} B`;
251
- }
252
- if (size < 1024 * 1024) {
253
- return `${(size / 1024).toFixed(2)} KB`;
254
- }
255
- if (size < 1024 * 1024 * 1024) {
256
- return `${(size / (1024 * 1024)).toFixed(2)} MB`;
257
- }
258
- return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
259
- }
260
- class DockerManager {
261
- installed = false;
262
- dockerVersion = '';
263
- needSudo = false;
264
- #waitReady;
265
- #waitAllChecked;
266
- #waitAllCheckedResolve;
267
- adapter;
268
- #ownContainers = [];
269
- #monitoringInterval = null;
270
- #ownContainersStats = {};
271
- #driver = 'cli';
272
- #dockerode = null;
273
- #cliAvailable = false;
274
- options;
275
- #tarPack = null;
276
- constructor(adapter, options, containers) {
277
- this.adapter = adapter;
278
- this.options = options || {};
279
- this.#ownContainers = containers || [];
280
- this.#waitReady = new Promise(resolve => this.#init().then(() => resolve()));
281
- this.#waitAllChecked = new Promise(resolve => (this.#waitAllCheckedResolve = resolve));
282
- }
283
- /** Wait till the check if docker is installed and the daemon is running is ready */
284
- isReady() {
285
- return this.#waitReady;
286
- }
287
- /**
288
- * Convert information from inspect to docker configuration to start it
289
- *
290
- * @param inspect Inspect information
291
- */
292
- static mapInspectToConfig(inspect) {
293
- const obj = {
294
- image: inspect.Config.Image,
295
- name: inspect.Name.replace(/^\//, ''),
296
- command: inspect.Config.Cmd ?? undefined,
297
- entrypoint: inspect.Config.Entrypoint ?? undefined,
298
- user: inspect.Config.User ?? undefined,
299
- workdir: inspect.Config.WorkingDir ?? undefined,
300
- hostname: inspect.Config.Hostname ?? undefined,
301
- domainname: inspect.Config.Domainname ?? undefined,
302
- macAddress: inspect.NetworkSettings.MacAddress ?? undefined,
303
- environment: inspect.Config.Env
304
- ? Object.fromEntries(inspect.Config.Env.map(e => {
305
- const [key, ...rest] = e.split('=');
306
- return [key, rest.join('=')];
307
- }))
308
- : undefined,
309
- labels: inspect.Config.Labels ?? undefined,
310
- tty: inspect.Config.Tty,
311
- stdinOpen: inspect.Config.OpenStdin,
312
- attachStdin: inspect.Config.AttachStdin,
313
- attachStdout: inspect.Config.AttachStdout,
314
- attachStderr: inspect.Config.AttachStderr,
315
- openStdin: inspect.Config.OpenStdin,
316
- publishAllPorts: inspect.HostConfig.PublishAllPorts,
317
- ports: inspect.HostConfig.PortBindings
318
- ? Object.entries(inspect.HostConfig.PortBindings).flatMap(([containerPort, bindings]) => bindings.map(binding => ({
319
- containerPort: containerPort.split('/')[0],
320
- protocol: containerPort.split('/')[1] || 'tcp',
321
- hostPort: binding.HostPort,
322
- hostIP: binding.HostIp,
323
- })))
324
- : undefined,
325
- mounts: inspect.Mounts?.map(mount => ({
326
- type: mount.Type,
327
- source: mount.Source,
328
- target: mount.Destination,
329
- readOnly: mount.RW,
330
- })),
331
- volumes: inspect.Config.Volumes ? Object.keys(inspect.Config.Volumes) : undefined,
332
- extraHosts: inspect.HostConfig.ExtraHosts ?? undefined,
333
- dns: {
334
- servers: inspect.HostConfig.Dns,
335
- search: inspect.HostConfig.DnsSearch,
336
- options: inspect.HostConfig.DnsOptions,
337
- },
338
- networkMode: inspect.HostConfig.NetworkMode,
339
- networks: inspect.NetworkSettings.Networks
340
- ? Object.entries(inspect.NetworkSettings.Networks).map(([name, net]) => ({
341
- name,
342
- aliases: net.Aliases ?? undefined,
343
- ipv4Address: net.IPAddress,
344
- ipv6Address: net.GlobalIPv6Address,
345
- driverOpts: net.DriverOpts ?? undefined,
346
- }))
347
- : undefined,
348
- restart: {
349
- policy: inspect.HostConfig.RestartPolicy.Name,
350
- maxRetries: inspect.HostConfig.RestartPolicy.MaximumRetryCount,
351
- },
352
- resources: {
353
- cpuShares: inspect.HostConfig.CpuShares,
354
- cpuQuota: inspect.HostConfig.CpuQuota,
355
- cpuPeriod: inspect.HostConfig.CpuPeriod,
356
- cpusetCpus: inspect.HostConfig.CpusetCpus,
357
- memory: inspect.HostConfig.Memory,
358
- memorySwap: inspect.HostConfig.MemorySwap,
359
- memoryReservation: inspect.HostConfig.MemoryReservation,
360
- pidsLimit: inspect.HostConfig.PidsLimit ?? undefined,
361
- shmSize: inspect.HostConfig.ShmSize,
362
- readOnlyRootFilesystem: inspect.HostConfig.ReadonlyRootfs,
363
- },
364
- logging: {
365
- driver: inspect.HostConfig.LogConfig.Type,
366
- options: inspect.HostConfig.LogConfig.Config,
367
- },
368
- security: {
369
- privileged: inspect.HostConfig.Privileged,
370
- capAdd: inspect.HostConfig.CapAdd ?? undefined,
371
- capDrop: inspect.HostConfig.CapDrop ?? undefined,
372
- usernsMode: inspect.HostConfig.UsernsMode ?? undefined,
373
- ipc: inspect.HostConfig.IpcMode,
374
- pid: inspect.HostConfig.PidMode,
375
- seccomp: inspect.HostConfig.SecurityOpt?.find(opt => opt.startsWith('seccomp='))?.split('=')[1] ?? undefined,
376
- apparmor: inspect.HostConfig.SecurityOpt?.find(opt => opt.startsWith('apparmor='))?.split('=')[1] ??
377
- undefined,
378
- groupAdd: inspect.HostConfig.GroupAdd ?? undefined,
379
- noNewPrivileges: undefined, // Nicht direkt verfügbar
380
- },
381
- sysctls: inspect.HostConfig.Sysctls ?? undefined,
382
- init: inspect.HostConfig.Init ?? undefined,
383
- stop: {
384
- signal: inspect.Config.StopSignal ?? undefined,
385
- gracePeriodSec: inspect.Config.StopTimeout ?? undefined,
386
- },
387
- readOnly: inspect.HostConfig.ReadonlyRootfs,
388
- timezone: undefined, // Nicht direkt verfügbar
389
- __meta: undefined, // Eigene Metadaten
390
- };
391
- return cleanContainerConfig(obj, true);
392
- }
393
- /**
394
- * Get information about the Docker daemon: is it running and which version
395
- *
396
- * @returns Object with version and daemonRunning
397
- */
398
- async getDockerDaemonInfo() {
399
- await this.isReady();
400
- const daemonRunning = await this.#isDockerDaemonRunning();
401
- return {
402
- version: this.dockerVersion,
403
- daemonRunning,
404
- removeSupported: !this.#dockerode || this.#cliAvailable,
405
- driver: this.#driver,
406
- };
407
- }
408
- static checkDockerSocket() {
409
- return new Promise(resolve => {
410
- const socket = (0, node_net_1.createConnection)({ path: '/var/run/docker.sock' }, () => {
411
- socket.end();
412
- resolve(true);
413
- });
414
- socket.on('error', e => {
415
- console.error(`Cannot connect to docker socket: ${e.message}`);
416
- resolve(false);
417
- });
418
- });
419
- }
420
- static async isDockerApiRunningOnPort(port, host = 'localhost') {
421
- return new Promise(resolve => {
422
- const socket = (0, node_net_1.createConnection)({ port, host }, () => {
423
- socket.write('GET /version HTTP/1.0\r\nHost: localhost\r\n\r\n');
424
- });
425
- let data = '';
426
- socket.on('data', chunk => (data += chunk.toString()));
427
- socket.on('end', () => {
428
- resolve(data.includes('Docker') || data.includes('Api-Version'));
429
- });
430
- socket.on('error', () => resolve(false));
431
- });
432
- }
433
- async #init() {
434
- // first of all detects which way is available:
435
- // - '/var/run/docker.sock',
436
- // - http://localhost:2375,
437
- // - https://localhost:2376 or
438
- // - CLI
439
- // Probe the socket
440
- if (this.options?.dockerApi && this.options.dockerApiHost && this.options.dockerApiPort) {
441
- this.#driver = this.options.dockerApiProtocol || 'http';
442
- this.#dockerode = new dockerode_1.default({
443
- host: this.options.dockerApiHost,
444
- port: this.options.dockerApiPort,
445
- protocol: this.options.dockerApiProtocol || 'http',
446
- });
447
- }
448
- else if (await _a.checkDockerSocket()) {
449
- this.#driver = 'socket';
450
- this.#dockerode = new dockerode_1.default({ socketPath: '/var/run/docker.sock' });
451
- }
452
- else if (await _a.isDockerApiRunningOnPort(2375)) {
453
- this.#driver = 'http';
454
- this.#dockerode = new dockerode_1.default({ protocol: 'http', host: '127.0.0.1', port: 2375 });
455
- }
456
- else if (await _a.isDockerApiRunningOnPort(2376)) {
457
- this.#driver = 'http';
458
- this.#dockerode = new dockerode_1.default({ protocol: 'http', host: '127.0.0.1', port: 2376 });
459
- }
460
- else {
461
- this.#driver = 'cli';
462
- this.#cliAvailable = true;
463
- }
464
- if (!this.#cliAvailable) {
465
- try {
466
- const result = await execPromise('docker --version');
467
- if (!result.stderr && result.stdout) {
468
- this.#cliAvailable = true;
469
- }
470
- }
471
- catch {
472
- // ignore
473
- }
474
- }
475
- const version = await this.#isDockerInstalled();
476
- this.installed = !!version;
477
- if (version) {
478
- this.dockerVersion = version;
479
- }
480
- else {
481
- const daemonRunning = await this.#isDockerDaemonRunning();
482
- if (daemonRunning) {
483
- // Docker daemon is running, but docker command not found
484
- this.adapter.log.warn('Docker daemon is running, but docker command not found. May be "iobroker" user has no access to Docker. Run "iob fix" command to fix it.');
485
- }
486
- else {
487
- this.adapter.log.warn('Docker is not installed. Please install Docker.');
488
- }
489
- }
490
- if (this.installed) {
491
- // we still must check the sudo as autocompletion works only via CLI
492
- this.needSudo = await this.#isNeedSudo();
493
- await this.#checkOwnContainers();
494
- }
495
- else {
496
- this.#waitAllCheckedResolve?.();
497
- }
498
- }
499
- async #isDockerDaemonRunning() {
500
- if (this.#dockerode) {
501
- return true;
502
- }
503
- try {
504
- const { stdout, stderr } = await execPromise('systemctl status docker');
505
- // ● docker.service - Docker Application Container Engine
506
- // Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
507
- // Active: active (running) since Fri 2025-08-15 08:37:22 CEST; 3 weeks 2 days ago
508
- // TriggeredBy: ● docker.socket
509
- // Docs: https://docs.docker.com
510
- // Main PID: 785 (dockerd)
511
- // Tasks: 30
512
- // CPU: 4min 17.003s
513
- // CGroup: /system.slice/docker.service
514
- // ├─ 785 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
515
- // ├─97032 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5000 -container-ip 172.17.0.2 -container-port 5000 -use-listen-fd
516
- // └─97039 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 5000 -container-ip 172.17.0.2 -container-port 5000 -use-listen-fd
517
- if (stderr?.includes('could not be found') || stderr.includes('not-found')) {
518
- this.adapter.log.error(`Docker is not installed: ${stderr}`);
519
- return false;
520
- }
521
- return stdout.includes('(running)');
522
- }
523
- catch {
524
- return false;
525
- }
526
- }
527
- /**
528
- * Ensure that the given container is running with the actual configuration
529
- *
530
- * @param container Container configuration
531
- */
532
- async #ensureActualConfiguration(container) {
533
- if (!container.name) {
534
- throw new Error(`Container name must be a string, but got boolean true`);
535
- }
536
- // Check the configuration of the container
537
- const inspect = await this.containerInspect(container.name);
538
- if (inspect) {
539
- const existingConfig = _a.mapInspectToConfig(inspect);
540
- console.log('Compare existing config', existingConfig, ' and', container);
541
- container = cleanContainerConfig(container);
542
- const diffs = compareConfigs(container, existingConfig);
543
- if (diffs.length) {
544
- this.adapter.log.info(`Configuration of own container ${container.name} has changed: ${diffs.join(', ')}. Restarting container...`);
545
- const result = await this.containerReCreate(container);
546
- if (result.stderr) {
547
- this.adapter.log.warn(`Cannot recreate own container ${container.name}: ${result.stderr}`);
548
- }
549
- }
550
- else {
551
- this.adapter.log.debug(`Configuration of own container ${container.name} is up to date`);
552
- }
553
- // Check if container is running
554
- const status = await this.containerList(true);
555
- const containerInfo = status.find(it => it.names === container.name);
556
- if (containerInfo) {
557
- if (containerInfo.status !== 'running' && containerInfo.status !== 'restarting') {
558
- // Start the container
559
- this.adapter.log.info(`Starting own container ${container.name}`);
560
- try {
561
- const result = await this.containerStart(containerInfo.id);
562
- if (result.stderr) {
563
- this.adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
564
- }
565
- }
566
- catch (e) {
567
- this.adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
568
- }
569
- }
570
- else {
571
- this.adapter.log.debug(`Own container ${container.name} is already running`);
572
- }
573
- }
574
- else {
575
- this.adapter.log.warn(`Own container ${container.name} not found in container list after recreation`);
576
- }
577
- }
578
- }
579
- getDefaultContainerName() {
580
- return `iob_${this.adapter.namespace.replace(/[-.]/g, '_')}`;
581
- }
582
- async #checkOwnContainers() {
583
- if (!this.#ownContainers.length) {
584
- this.#waitAllCheckedResolve?.();
585
- return;
586
- }
587
- const status = await this.containerList(true);
588
- let images = await this.imageList();
589
- let anyStartedOrRunning = false;
590
- const networkChecked = [];
591
- const prefix = this.getDefaultContainerName();
592
- for (let c = 0; c < this.#ownContainers.length; c++) {
593
- const container = this.#ownContainers[c];
594
- if (container.iobEnabled !== false) {
595
- if (!container.image.includes(':')) {
596
- container.image += ':latest';
597
- }
598
- if (container.labels?.iobroker !== this.adapter.namespace) {
599
- container.labels = { ...container.labels, iobroker: this.adapter.namespace };
600
- }
601
- container.name ||= prefix;
602
- // Name of the container, name of the network and name of the volume must start with iob_<Adaptername>_<instance>_
603
- if (container.name !== prefix && !container.name.startsWith(`${prefix}_`)) {
604
- this.adapter.log.debug(`Renaming container ${container.name} to be prefixed with iob_${prefix}_`);
605
- container.name = `${prefix}_${container.name}`;
606
- }
607
- try {
608
- // create iobroker network if necessary
609
- if (container.networkMode &&
610
- container.networkMode !== 'container' &&
611
- container.networkMode !== 'host' &&
612
- container.networkMode !== 'bridge' &&
613
- container.networkMode !== 'none') {
614
- if (container.networkMode === true) {
615
- container.networkMode = prefix;
616
- }
617
- if (container.networkMode !== prefix && !container.networkMode.startsWith(`${prefix}_`)) {
618
- this.adapter.log.debug(`Renaming network ${container.networkMode} to be prefixed with ${prefix}_`);
619
- container.networkMode = `${prefix}_${container.networkMode}`;
620
- }
621
- if (!networkChecked.includes(container.networkMode)) {
622
- // check if the network exists
623
- const networks = await this.networkList();
624
- if (!networks.find(it => it.name === container.networkMode)) {
625
- this.adapter.log.info(`Creating docker network ${container.networkMode}`);
626
- await this.networkCreate(container.networkMode);
627
- }
628
- networkChecked.push(container.networkMode);
629
- }
630
- }
631
- // create all volumes ourselves, to have a static name
632
- if (container.mounts?.find(m => m.type === 'volume')) {
633
- // check if the volume exists
634
- const volumes = await this.volumeList();
635
- for (const mount of container.mounts) {
636
- if (mount.type === 'volume' && mount.source) {
637
- if (mount.source === true) {
638
- mount.source = prefix;
639
- }
640
- if (mount.source !== prefix && !mount.source.startsWith(`${prefix}_`)) {
641
- this.adapter.log.debug(`Renaming volume ${mount.source} to be prefixed with ${prefix}_`);
642
- mount.source = `${prefix}_${mount.source}`;
643
- }
644
- if (mount.iobBackup) {
645
- if (!container.labels.iob_backup) {
646
- container.labels = { ...container.labels, iob_backup: mount.source };
647
- }
648
- else {
649
- const volumes = container.labels.iob_backup
650
- .split(',')
651
- .map(v => v.trim())
652
- .filter(v => v);
653
- if (!volumes.includes(mount.source)) {
654
- volumes.push(mount.source);
655
- container.labels = { ...container.labels, iob_backup: volumes.join(',') };
656
- }
657
- }
658
- }
659
- const volume = volumes.find(v => v.name === mount.source);
660
- if (!volume) {
661
- this.adapter.log.info(`Creating docker volume ${mount.source}`);
662
- const result = await this.volumeCreate(mount.source);
663
- if (result.stderr) {
664
- this.adapter.log.warn(`Cannot create volume ${mount.source}: ${result.stderr}`);
665
- continue;
666
- }
667
- // Copy data from host to volume
668
- if (mount.iobAutoCopyFrom) {
669
- await this.volumeCopyTo(mount.source, mount.iobAutoCopyFrom);
670
- }
671
- }
672
- else if (mount.iobAutoCopyFromForce && mount.iobAutoCopyFrom) {
673
- // Copy data from host to volume
674
- await this.volumeCopyTo(mount.source, mount.iobAutoCopyFrom);
675
- }
676
- }
677
- }
678
- }
679
- let containerInfo = status.find(it => it.names === container.name);
680
- let image = images.find(it => `${it.repository}:${it.tag}` === container.image);
681
- if (container.iobAutoImageUpdate) {
682
- // ensure that the image is actual
683
- const newImage = await this.imageUpdate(container.image, true);
684
- if (newImage) {
685
- this.adapter.log.info(`Image ${container.image} for own container ${container.name} was updated`);
686
- if (containerInfo) {
687
- // destroy current container
688
- await this.containerRemove(containerInfo.id);
689
- containerInfo = undefined;
690
- }
691
- image = newImage;
692
- }
693
- }
694
- if (!image) {
695
- this.adapter.log.info(`Pulling image ${container.image} for own container ${container.name}`);
696
- try {
697
- const result = await this.imagePull(container.image);
698
- if (result.stderr) {
699
- this.adapter.log.warn(`Cannot pull image ${container.image}: ${result.stderr}`);
700
- continue;
701
- }
702
- }
703
- catch (e) {
704
- this.adapter.log.warn(`Cannot pull image ${container.image}: ${e.message}`);
705
- continue;
706
- }
707
- // Check that image is available now
708
- images = await this.imageList();
709
- image = images.find(it => `${it.repository}:${it.tag}` === container.image);
710
- if (!image) {
711
- this.adapter.log.warn(`Image ${container.image} for own container ${container.name} not found after pull`);
712
- continue;
713
- }
714
- }
715
- if (containerInfo) {
716
- await this.#ensureActualConfiguration(container);
717
- anyStartedOrRunning ||= !!container.iobMonitoringEnabled;
718
- }
719
- else {
720
- // Create and start the container, as the container was not found
721
- this.adapter.log.info(`Creating and starting own container ${container.name}`);
722
- try {
723
- const result = await this.containerRun(container);
724
- if (result.stderr) {
725
- this.adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
726
- }
727
- else {
728
- anyStartedOrRunning ||= !!container.iobMonitoringEnabled;
729
- }
730
- }
731
- catch (e) {
732
- this.adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
733
- }
734
- }
735
- }
736
- catch (e) {
737
- this.adapter.log.warn(`Cannot check own container ${container.name}: ${e.message}`);
738
- }
739
- }
740
- }
741
- if (anyStartedOrRunning) {
742
- this.#monitoringInterval ||= setInterval(() => this.#monitorOwnContainers(), 60000);
743
- }
744
- this.#waitAllCheckedResolve?.();
745
- }
746
- allOwnContainersChecked() {
747
- return this.#waitAllChecked;
748
- }
749
- async #monitorOwnContainers() {
750
- // get the status of containers
751
- const containers = await this.containerList();
752
- // Check the status of own containers
753
- for (let c = 0; c < this.#ownContainers.length; c++) {
754
- const container = this.#ownContainers[c];
755
- if (container.iobEnabled !== false && container.iobMonitoringEnabled && container.name) {
756
- // Check if container is running
757
- const running = containers.find(it => it.names === container.name);
758
- if (!running || (running.status !== 'running' && running.status !== 'restarting')) {
759
- this.adapter.log.warn(`Own container ${container.name} is not running. Restarting...`);
760
- try {
761
- const result = await this.containerStart(container.name);
762
- if (result.stderr) {
763
- this.adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
764
- this.#ownContainersStats[container.name] = {
765
- ...this.#ownContainersStats[container.name],
766
- status: running?.status || 'unknown',
767
- statusTs: Date.now(),
768
- };
769
- continue;
770
- }
771
- }
772
- catch (e) {
773
- this.adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
774
- this.#ownContainersStats[container.name] = {
775
- ...this.#ownContainersStats[container.name],
776
- status: running?.status || 'unknown',
777
- statusTs: Date.now(),
778
- };
779
- continue;
780
- }
781
- }
782
- // check the stats
783
- this.#ownContainersStats[container.name] = {
784
- ...((await this.containerGetRamAndCpuUsage(container.name)) || {}),
785
- status: running?.status || 'unknown',
786
- statusTs: Date.now(),
787
- };
788
- }
789
- }
790
- }
791
- /** Read own container stats */
792
- getOwnContainerStats() {
793
- return this.#ownContainersStats;
794
- }
795
- async containerGetRamAndCpuUsage(containerNameOrId) {
796
- try {
797
- const { stdout } = await this.#exec(`stats ${containerNameOrId} --no-stream --format "{{.CPUPerc}};{{.MemUsage}};{{.NetIO}};{{.BlockIO}};{{.PIDs}}"`);
798
- // Example: "0.15%;12.34MiB / 512MiB;1.2kB / 2.3kB;0B / 0B;5"
799
- const [cpuStr, memStr, netStr, blockIoStr, pid] = stdout.trim().split(';');
800
- const [memUsed, memMax] = memStr.split('/').map(it => it.trim());
801
- const [netRead, netWrite] = netStr.split('/').map(it => it.trim());
802
- const [blockIoRead, blockIoWrite] = blockIoStr.split('/').map(it => it.trim());
803
- return {
804
- ts: Date.now(),
805
- cpu: parseFloat(cpuStr.replace('%', '').replace(',', '.')),
806
- memUsed: this.#parseSize(memUsed.replace('iB', 'B')),
807
- memMax: this.#parseSize(memMax.replace('iB', 'B')),
808
- netRead: this.#parseSize(netRead.replace('iB', 'B')),
809
- netWrite: this.#parseSize(netWrite.replace('iB', 'B')),
810
- processes: parseInt(pid, 10),
811
- blockIoRead: this.#parseSize(blockIoRead.replace('iB', 'B')),
812
- blockIoWrite: this.#parseSize(blockIoWrite.replace('iB', 'B')),
813
- };
814
- }
815
- catch (e) {
816
- this.adapter.log.debug(`Cannot get stats: ${e.message}`);
817
- return null;
818
- }
819
- }
820
- /**
821
- * Update the image if a newer version is available
822
- *
823
- * @param image Image name with tag
824
- * @param ignoreIfNotExist If true, do not throw error if image does not exist
825
- * @returns New image info if image was updated, null if no update was necessary
826
- */
827
- async imageUpdate(image, ignoreIfNotExist) {
828
- const list = await this.imageList();
829
- if (!image.includes(':')) {
830
- image += ':latest';
831
- }
832
- const existingImage = list.find(it => `${it.repository}:${it.tag}` === image);
833
- if (!existingImage && !ignoreIfNotExist) {
834
- throw new Error(`Image ${image} not found`);
835
- }
836
- // Pull the image
837
- const result = await this.imagePull(image);
838
- if (result.stderr) {
839
- throw new Error(`Cannot pull image ${image}: ${result.stderr}`);
840
- }
841
- const newList = await this.imageList();
842
- const newImage = newList.find(it => `${it.repository}:${it.tag}` === image);
843
- if (!newImage) {
844
- throw new Error(`Image ${image} not found after pull`);
845
- }
846
- // If image ID has changed, image was updated
847
- return !existingImage || existingImage.id !== newImage.id ? newImage : null;
848
- }
849
- #exec(command) {
850
- if (!this.installed) {
851
- return Promise.reject(new Error('Docker is not installed'));
852
- }
853
- const finalCommand = this.needSudo ? `sudo docker ${command}` : `docker ${command}`;
854
- return execPromise(finalCommand);
855
- }
856
- async #isDockerInstalled() {
857
- if (this.#driver === 'cli') {
858
- try {
859
- const result = await execPromise('docker --version');
860
- if (!result.stderr && result.stdout) {
861
- // "Docker version 28.3.2, build 578ccf6\n"
862
- return result.stdout.split('\n')[0].trim();
863
- }
864
- this.adapter.log.debug(`Docker not installed: ${result.stderr}`);
865
- }
866
- catch (e) {
867
- this.adapter.log.debug(`Docker not installed: ${e.message}`);
868
- }
869
- }
870
- else if (this.#dockerode) {
871
- try {
872
- const info = await this.#dockerode.version();
873
- if (info?.Version) {
874
- return `Docker version ${info.Version}, api version: ${info.ApiVersion}`;
875
- }
876
- }
877
- catch (e) {
878
- this.adapter.log.debug(`Docker not installed: ${e.message}`);
879
- }
880
- }
881
- return false;
882
- }
883
- async #isNeedSudo() {
884
- try {
885
- await execPromise('docker ps');
886
- return false;
887
- }
888
- catch {
889
- return true;
890
- }
891
- }
892
- /** Get disk usage information */
893
- async discUsage() {
894
- if (this.#dockerode) {
895
- const info = await this.#dockerode.df();
896
- const result = { total: { size: 0, reclaimable: 0 } };
897
- if (info.Images) {
898
- let size = 0;
899
- let reclaimable = 0;
900
- for (const image of info.Images) {
901
- size += image.Size;
902
- reclaimable += image.SharedSize + image.VirtualSize;
903
- }
904
- result.images = {
905
- total: info.Images.length,
906
- // @ts-expect-error todo
907
- active: info.Images.filter(img => img.Containers > 0).length,
908
- size,
909
- reclaimable,
910
- };
911
- result.total.size += size;
912
- result.total.reclaimable += reclaimable;
913
- }
914
- if (info.Containers) {
915
- let size = 0;
916
- for (const container of info.Containers) {
917
- size += container.SizeRootFs || 0;
918
- }
919
- result.containers = {
920
- total: info.Containers.length,
921
- // @ts-expect-error todo
922
- active: info.Containers.filter(cont => cont.State === 'running').length,
923
- size,
924
- reclaimable: 0, // Not available
925
- };
926
- result.total.size += size;
927
- }
928
- if (info.Volumes) {
929
- let size = 0;
930
- for (const volume of info.Volumes) {
931
- size += volume.UsageData?.Size || 0;
932
- }
933
- result.volumes = {
934
- total: info.Volumes.length,
935
- // @ts-expect-error todo
936
- active: info.Volumes.filter(vol => vol.UsageData?.RefCount && vol.UsageData.RefCount > 0).length,
937
- size,
938
- reclaimable: 0, // Not available
939
- };
940
- result.total.size += size;
941
- }
942
- // Build cache not available
943
- return result;
944
- }
945
- const { stdout } = await this.#exec(`system df`);
946
- const result = { total: { size: 0, reclaimable: 0 } };
947
- // parse the output
948
- // TYPE TOTAL ACTIVE SIZE RECLAIMABLE
949
- // Images 2 1 2.715GB 2.715GB (99%)
950
- // Containers 1 1 26.22MB 0B (0%)
951
- // Local Volumes 0 0 0B 0B
952
- // Build Cache 0 0 0B 0B
953
- const lines = stdout.split('\n');
954
- for (const line of lines) {
955
- const parts = line.trim().split(/\s+/);
956
- if (parts.length >= 5 && parts[0] !== 'TYPE') {
957
- let size;
958
- let reclaimable;
959
- if (parts[0] === 'Images') {
960
- const sizeStr = parts[3];
961
- const reclaimableStr = parts[4].split(' ')[0];
962
- size = this.#parseSize(sizeStr);
963
- reclaimable = this.#parseSize(reclaimableStr);
964
- result.images = {
965
- total: parseInt(parts[1], 10),
966
- active: parseInt(parts[2], 10),
967
- size,
968
- reclaimable: reclaimable,
969
- };
970
- }
971
- else if (parts[0] === 'Containers') {
972
- const sizeStr = parts[3];
973
- const reclaimableStr = parts[4].split(' ')[0];
974
- size = this.#parseSize(sizeStr);
975
- reclaimable = this.#parseSize(reclaimableStr);
976
- result.containers = {
977
- total: parseInt(parts[1], 10),
978
- active: parseInt(parts[2], 10),
979
- size,
980
- reclaimable: reclaimable,
981
- };
982
- }
983
- else if (parts[0] === 'Local' && parts[1] === 'Volumes') {
984
- const sizeStr = parts[4];
985
- const reclaimableStr = parts[5].split(' ')[0];
986
- size = this.#parseSize(sizeStr);
987
- reclaimable = this.#parseSize(reclaimableStr);
988
- result.volumes = {
989
- total: parseInt(parts[2], 10),
990
- active: parseInt(parts[3], 10),
991
- size,
992
- reclaimable: reclaimable,
993
- };
994
- }
995
- else if (parts[0] === 'Build' && parts[1] === 'Cache') {
996
- const sizeStr = parts[4];
997
- const reclaimableStr = parts[5].split(' ')[0];
998
- size = this.#parseSize(sizeStr);
999
- reclaimable = this.#parseSize(reclaimableStr);
1000
- result.buildCache = {
1001
- total: parseInt(parts[2], 10),
1002
- active: parseInt(parts[3], 10),
1003
- size,
1004
- reclaimable: reclaimable,
1005
- };
1006
- }
1007
- result.total.size += size || 0;
1008
- result.total.reclaimable += reclaimable || 0;
1009
- }
1010
- }
1011
- return result;
1012
- }
1013
- /** Pull an image from the registry */
1014
- async imagePull(image) {
1015
- if (!image.includes(':')) {
1016
- image += ':latest';
1017
- }
1018
- if (this.#dockerode) {
1019
- const stream = await this.#dockerode.pull(image);
1020
- if (!stream) {
1021
- throw new Error('No stream returned');
1022
- }
1023
- return new Promise((resolve, reject) => {
1024
- const onFinished = (err) => {
1025
- if (err) {
1026
- return reject(err);
1027
- }
1028
- this.imageList()
1029
- .then(images => resolve({ stdout: `Image ${image} pulled`, stderr: '', images }))
1030
- .catch(reject);
1031
- };
1032
- const lastShownProgress = {};
1033
- const onProgress = (event) => {
1034
- // {"status":"Downloading","progressDetail":{"current":109494080,"total":689664036},"progress":"[=======> ] 109.5MB/689.7MB","id":"29bce3058cea"}
1035
- // {"status":"Download complete","progressDetail":{},"id":"6859c690a072"}
1036
- // {"status":"Verifying Checksum","progressDetail":{},"id":"6859c690a072"}
1037
- // {"status":"Extracting","progressDetail":{"current":32,"total":32},"progress":"[======>] 32B/32B","id":"4f4fb700ef54"}
1038
- // {"status":"Pull complete","progressDetail":{},"id":"4f4fb700ef54"}
1039
- if (!lastShownProgress || Date.now() - lastShownProgress[event.id] > 4000) {
1040
- if (event.status === 'Download complete' ||
1041
- event.status === 'Pull complete' ||
1042
- event.status === 'Verifying Checksum') {
1043
- this.adapter.log.debug(`Image ${image}/${event.id}: ${event.status}`);
1044
- }
1045
- else if (event.status === 'Downloading' || event.status === 'Extracting') {
1046
- 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)}`);
1047
- }
1048
- else {
1049
- this.adapter.log.debug(`Pulling image ${image}/${event.id}: ${JSON.stringify(event)}`);
1050
- }
1051
- lastShownProgress[event.id] = Date.now();
1052
- }
1053
- };
1054
- this.#dockerode.modem.followProgress(stream, onFinished, onProgress);
1055
- });
1056
- }
1057
- try {
1058
- const result = await this.#exec(`pull ${image}`);
1059
- const images = await this.imageList();
1060
- if (!images.find(it => `${it.repository}:${it.tag}` === image)) {
1061
- throw new Error(`Image ${image} not found after pull`);
1062
- }
1063
- return { ...result, images };
1064
- }
1065
- catch (e) {
1066
- return { stdout: '', stderr: e.message.toString() };
1067
- }
1068
- }
1069
- /** Autocomplete image names from Docker Hub */
1070
- async imageNameAutocomplete(partialName) {
1071
- try {
1072
- // Read stars and descriptions
1073
- const { stdout } = await this.#exec(`search ${partialName} --format "{{.Name}};{{.Description}};{{.IsOfficial}};{{.StarCount}}" --limit 50`);
1074
- return stdout
1075
- .split('\n')
1076
- .filter(line => line.trim() !== '')
1077
- .map(line => {
1078
- const [name, description, isOfficial, starCount] = line.split(';');
1079
- return {
1080
- name,
1081
- description,
1082
- isOfficial: isOfficial === 'true',
1083
- starCount: parseInt(starCount, 10) || 0,
1084
- };
1085
- });
1086
- }
1087
- catch (e) {
1088
- this.adapter.log.debug(`Cannot search images: ${e.message}`);
1089
- return [];
1090
- }
1091
- }
1092
- static getDockerodeConfig(config) {
1093
- let mounts;
1094
- if (config.mounts) {
1095
- for (const mount of config.mounts) {
1096
- let volumeOptions;
1097
- if (mount.volumeOptions) {
1098
- if (mount.volumeOptions.nocopy !== undefined) {
1099
- volumeOptions ||= {};
1100
- volumeOptions.NoCopy = mount.volumeOptions.nocopy;
1101
- }
1102
- if (mount.volumeOptions.labels) {
1103
- volumeOptions ||= {};
1104
- volumeOptions.Labels = mount.volumeOptions.labels;
1105
- }
1106
- }
1107
- let bindOptions;
1108
- if (mount.bindOptions) {
1109
- if (mount.bindOptions.propagation) {
1110
- bindOptions ||= {};
1111
- bindOptions.Propagation = mount.bindOptions.propagation;
1112
- }
1113
- }
1114
- let tmpfsOptions;
1115
- if (mount.tmpfsOptions) {
1116
- if (mount.tmpfsOptions.size !== undefined) {
1117
- tmpfsOptions ||= {};
1118
- tmpfsOptions.SizeBytes = mount.tmpfsOptions.size;
1119
- }
1120
- if (mount.tmpfsOptions.mode !== undefined) {
1121
- tmpfsOptions ||= {};
1122
- tmpfsOptions.Mode = mount.tmpfsOptions.mode;
1123
- }
1124
- }
1125
- if (mount.source === true) {
1126
- throw new Error(`Mount source must be a string, but got boolean true`);
1127
- }
1128
- const m = {
1129
- Target: mount.target,
1130
- Source: mount.source || '',
1131
- Type: mount.type,
1132
- ReadOnly: mount.readOnly,
1133
- Consistency: mount.consistency,
1134
- VolumeOptions: volumeOptions,
1135
- BindOptions: bindOptions,
1136
- TmpfsOptions: tmpfsOptions,
1137
- };
1138
- mounts ||= [];
1139
- mounts.push(m);
1140
- }
1141
- }
1142
- if (!config.name) {
1143
- throw new Error(`Container name must be a string, but got boolean true`);
1144
- }
1145
- return {
1146
- name: config.name,
1147
- Image: config.image,
1148
- Cmd: Array.isArray(config.command)
1149
- ? config.command
1150
- : typeof config.command === 'string'
1151
- ? [config.command]
1152
- : undefined,
1153
- Entrypoint: config.entrypoint,
1154
- Env: config.environment
1155
- ? Object.keys(config.environment).map(key => `${key}=${config.environment[key]}`)
1156
- : undefined,
1157
- // WorkingDir: config.workingDir,
1158
- // { '/data': {} }
1159
- Volumes: config.volumes?.reduce((acc, vol) => (acc[vol] = {}), {}),
1160
- Labels: config.labels,
1161
- ExposedPorts: config.ports
1162
- ? config.ports.reduce((acc, port) => {
1163
- acc[`${port.containerPort}/${port.protocol || 'tcp'}`] = {};
1164
- return acc;
1165
- }, {})
1166
- : undefined,
1167
- HostConfig: {
1168
- // Binds: config.binds,
1169
- PortBindings: config.ports
1170
- ? config.ports.reduce((acc, port) => {
1171
- acc[`${port.containerPort}/${port.protocol || 'tcp'}`] = [
1172
- {
1173
- HostPort: port.hostPort ? port.hostPort.toString() : undefined,
1174
- HostIp: port.hostIP || undefined,
1175
- },
1176
- ];
1177
- return acc;
1178
- }, {})
1179
- : undefined,
1180
- Mounts: mounts,
1181
- NetworkMode: config.networkMode === true ? '' : config.networkMode || undefined,
1182
- // Links: config.links,
1183
- // Dns: config.dns,
1184
- // DnsOptions: config.dnsOptions,
1185
- // DnsSearch: config.dnsSearch,
1186
- ExtraHosts: config.extraHosts,
1187
- // VolumesFrom: config.volumesFrom,
1188
- Privileged: config.security?.privileged,
1189
- CapAdd: config.security?.capAdd,
1190
- CapDrop: config.security?.capDrop,
1191
- UsernsMode: config.security?.usernsMode,
1192
- IpcMode: config.security?.ipc,
1193
- PidMode: config.security?.pid,
1194
- GroupAdd: config.security?.groupAdd?.map(g => g.toString()),
1195
- ReadonlyRootfs: config.readOnly,
1196
- RestartPolicy: {
1197
- Name: config.restart?.policy || 'no',
1198
- MaximumRetryCount: config.restart?.maxRetries || 0,
1199
- },
1200
- CpuShares: config.resources?.cpuShares,
1201
- CpuPeriod: config.resources?.cpuPeriod,
1202
- CpuQuota: config.resources?.cpuQuota,
1203
- CpusetCpus: config.resources?.cpus?.toString(),
1204
- Memory: config.resources?.memory,
1205
- MemorySwap: config.resources?.memorySwap,
1206
- MemoryReservation: config.resources?.memoryReservation,
1207
- // OomKillDisable: config.resources?.oomKillDisable,
1208
- // OomScoreAdj: config.resources?.oomScoreAdj,
1209
- LogConfig: config.logging
1210
- ? {
1211
- Type: config.logging.driver || 'json-file',
1212
- Config: config.logging.options || {},
1213
- }
1214
- : undefined,
1215
- SecurityOpt: [
1216
- ...(config.security?.seccomp ? [`seccomp=${config.security.seccomp}`] : []),
1217
- ...(config.security?.apparmor ? [`apparmor=${config.security.apparmor}`] : []),
1218
- ...(config.security?.noNewPrivileges ? ['no-new-privileges:true'] : []),
1219
- ],
1220
- Sysctls: config.sysctls,
1221
- Init: config.init,
1222
- },
1223
- StopSignal: config.stop?.signal,
1224
- StopTimeout: config.stop?.gracePeriodSec,
1225
- Tty: config.tty,
1226
- OpenStdin: config.openStdin,
1227
- };
1228
- }
1229
- /**
1230
- * Create and start a container with the given configuration. No checks are done.
1231
- */
1232
- async containerRun(config) {
1233
- if (this.#dockerode) {
1234
- const container = await this.#dockerode.createContainer(_a.getDockerodeConfig(config));
1235
- await container.start();
1236
- return { stdout: `Container ${config.name} started`, stderr: '' };
1237
- }
1238
- try {
1239
- return await this.#exec(`run ${_a.toDockerRun(config)}`);
1240
- }
1241
- catch (e) {
1242
- return { stdout: '', stderr: e.message.toString() };
1243
- }
1244
- }
1245
- /**
1246
- * Create a container with the given configuration without starting it. No checks are done.
1247
- */
1248
- async containerCreate(config) {
1249
- if (this.#dockerode) {
1250
- const container = await this.#dockerode.createContainer(_a.getDockerodeConfig(config));
1251
- return { stdout: `Container ${container.id} created`, stderr: '' };
1252
- }
1253
- try {
1254
- return await this.#exec(`create ${_a.toDockerRun(config, true)}`);
1255
- }
1256
- catch (e) {
1257
- return { stdout: '', stderr: e.message.toString() };
1258
- }
1259
- }
1260
- /**
1261
- * Recreate a container
1262
- *
1263
- * This function checks if a container is running, stops it if necessary,
1264
- * removes it and creates a new one with the given configuration.
1265
- * The container is not started after creation.
1266
- *
1267
- * @param config new configuration
1268
- * @returns stdout and stderr of the create command
1269
- */
1270
- async containerReCreate(config) {
1271
- if (this.#dockerode) {
1272
- // Get if the container is running
1273
- let containers = await this.containerList();
1274
- // find ID of container
1275
- const containerInfo = containers.find(it => it.names === config.name);
1276
- if (containerInfo) {
1277
- const container = this.#dockerode.getContainer(containerInfo.id);
1278
- if (containerInfo.status === 'running' || containerInfo.status === 'restarting') {
1279
- await container.stop();
1280
- containers = await this.containerList();
1281
- if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
1282
- this.adapter.log.warn(`Cannot remove container: still running`);
1283
- throw new Error(`Container ${containerInfo.id} still running after stop`);
1284
- }
1285
- }
1286
- // Remove container
1287
- await container.remove();
1288
- containers = await this.containerList();
1289
- if (containers.find(it => it.id === containerInfo.id)) {
1290
- this.adapter.log.warn(`Cannot remove container: still existing`);
1291
- throw new Error(`Container ${containerInfo.id} still found after remove`);
1292
- }
1293
- }
1294
- const newContainer = await this.#dockerode.createContainer(_a.getDockerodeConfig(config));
1295
- return { stdout: `Container ${newContainer.id} created`, stderr: '' };
1296
- }
1297
- try {
1298
- // Get if the container is running
1299
- let containers = await this.containerList();
1300
- // find ID of container
1301
- const containerInfo = containers.find(it => it.names === config.name);
1302
- if (containerInfo) {
1303
- if (containerInfo.status === 'running' || containerInfo.status === 'restarting') {
1304
- const stopResult = await this.#exec(`stop ${containerInfo.id}`);
1305
- containers = await this.containerList();
1306
- if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
1307
- this.adapter.log.warn(`Cannot remove container: ${stopResult.stderr || stopResult.stdout}`);
1308
- throw new Error(`Container ${containerInfo.id} still running after stop`);
1309
- }
1310
- }
1311
- // Remove container
1312
- const rmResult = await this.#exec(`rm ${containerInfo.id}`);
1313
- containers = await this.containerList();
1314
- if (containers.find(it => it.id === containerInfo.id)) {
1315
- this.adapter.log.warn(`Cannot remove container: ${rmResult.stderr || rmResult.stdout}`);
1316
- throw new Error(`Container ${containerInfo.id} still found after remove`);
1317
- }
1318
- }
1319
- return await this.#exec(`create ${_a.toDockerRun(config, true)}`);
1320
- }
1321
- catch (e) {
1322
- return { stdout: '', stderr: e.message.toString() };
1323
- }
1324
- }
1325
- async containerCreateCompose(compose) {
1326
- try {
1327
- return await this.#exec(`compose -f ${compose} create`);
1328
- }
1329
- catch (e) {
1330
- return { stdout: '', stderr: e.message.toString() };
1331
- }
1332
- }
1333
- /** List all images */
1334
- async imageList() {
1335
- if (this.#dockerode) {
1336
- const images = await this.#dockerode.listImages();
1337
- return images.map(img => {
1338
- const repoTag = img.RepoTags && img.RepoTags.length ? img.RepoTags[0] : '<none>:<none>';
1339
- const [repository, tag] = repoTag.split(':');
1340
- return {
1341
- repository,
1342
- tag,
1343
- id: img.Id.startsWith('sha256:') ? img.Id.substring(7, 19) : img.Id.substring(0, 12),
1344
- createdSince: new Date(img.Created * 1000).toISOString(),
1345
- size: img.Size,
1346
- };
1347
- });
1348
- }
1349
- try {
1350
- const { stdout } = await this.#exec('images --format "{{.Repository}}:{{.Tag}};{{.ID}};{{.CreatedAt}};{{.Size}}"');
1351
- return stdout
1352
- .split('\n')
1353
- .filter(line => line.trim() !== '')
1354
- .map(line => {
1355
- const [repositoryTag, id, createdSince, size] = line.split(';');
1356
- const [repository, tag] = repositoryTag.split(':');
1357
- return {
1358
- repository,
1359
- tag,
1360
- id,
1361
- createdSince,
1362
- size: this.#parseSize(size),
1363
- };
1364
- });
1365
- }
1366
- catch (e) {
1367
- this.adapter.log.debug(`Cannot list images: ${e.message}`);
1368
- return [];
1369
- }
1370
- }
1371
- /** Build an image from a Dockerfile */
1372
- async imageBuild(dockerfilePath, tag) {
1373
- if (this.#dockerode) {
1374
- try {
1375
- const stream = await this.#dockerode.buildImage(dockerfilePath, {
1376
- t: tag,
1377
- dockerfile: dockerfilePath,
1378
- });
1379
- return new Promise((resolve, reject) => {
1380
- let stdout = '';
1381
- let stderr = '';
1382
- const onFinished = (err) => {
1383
- if (err) {
1384
- return reject(err);
1385
- }
1386
- resolve({ stdout, stderr });
1387
- };
1388
- const onProgress = (event) => {
1389
- if (event.stream) {
1390
- stdout += event.stream;
1391
- }
1392
- if (event.error) {
1393
- stderr += event.error;
1394
- }
1395
- this.adapter.log.debug(JSON.stringify(event));
1396
- };
1397
- this.#dockerode.modem.followProgress(stream, onFinished, onProgress);
1398
- });
1399
- }
1400
- catch (e) {
1401
- return { stdout: '', stderr: e.message.toString() };
1402
- }
1403
- }
1404
- try {
1405
- return await this.#exec(`build -t ${tag} -f ${dockerfilePath} .`);
1406
- }
1407
- catch (e) {
1408
- return { stdout: '', stderr: e.message.toString() };
1409
- }
1410
- }
1411
- /** Tag an image with a new tag */
1412
- async imageTag(imageId, newTag) {
1413
- try {
1414
- return await this.#exec(`tag ${imageId} ${newTag}`);
1415
- }
1416
- catch (e) {
1417
- return { stdout: '', stderr: e.message.toString() };
1418
- }
1419
- }
1420
- /** Remove an image */
1421
- async imageRemove(imageId) {
1422
- try {
1423
- const result = await this.#exec(`rmi ${imageId}`);
1424
- const images = await this.imageList();
1425
- if (images.find(it => `${it.repository}:${it.tag}` === imageId)) {
1426
- return { stdout: '', stderr: `Image ${imageId} still found after deletion`, images };
1427
- }
1428
- return { ...result, images };
1429
- }
1430
- catch (e) {
1431
- return { stdout: '', stderr: e.message.toString() };
1432
- }
1433
- }
1434
- static rodeInspect2DockerImageInspect(data) {
1435
- return {
1436
- Id: data.Id.startsWith('sha256:') ? data.Id.substring(7, 19) : data.Id.substring(0, 12),
1437
- RepoTags: data.RepoTags,
1438
- RepoDigests: data.RepoDigests,
1439
- Parent: data.Parent,
1440
- Comment: data.Comment,
1441
- Created: data.Created,
1442
- DockerVersion: data.DockerVersion,
1443
- Author: data.Author,
1444
- Architecture: data.Architecture,
1445
- Os: data.Os,
1446
- Size: data.Size,
1447
- GraphDriver: {
1448
- Data: data.GraphDriver.Data,
1449
- Name: data.GraphDriver.Name,
1450
- },
1451
- RootFS: data.RootFS,
1452
- Config: {
1453
- ...data.Config,
1454
- Entrypoint: Array.isArray(data.Config.Entrypoint)
1455
- ? data.Config.Entrypoint
1456
- : typeof data.Config.Entrypoint === 'string'
1457
- ? [data.Config.Entrypoint]
1458
- : [],
1459
- },
1460
- };
1461
- }
1462
- /** Inspect an image */
1463
- async imageInspect(imageId) {
1464
- if (this.#dockerode) {
1465
- const image = this.#dockerode.getImage(imageId);
1466
- const data = await image.inspect();
1467
- return _a.rodeInspect2DockerImageInspect(data);
1468
- }
1469
- try {
1470
- const { stdout } = await this.#exec(`inspect ${imageId}`);
1471
- return JSON.parse(stdout)[0];
1472
- }
1473
- catch (e) {
1474
- this.adapter.log.debug(`Cannot inspect image: ${e.message.toString()}`);
1475
- return null;
1476
- }
1477
- }
1478
- async imagePrune() {
1479
- if (this.#dockerode) {
1480
- try {
1481
- await this.#dockerode.pruneImages();
1482
- return { stdout: 'Unused images pruned', stderr: '' };
1483
- }
1484
- catch (e) {
1485
- return { stdout: '', stderr: e.message.toString() };
1486
- }
1487
- }
1488
- try {
1489
- return await this.#exec(`image prune -f`);
1490
- }
1491
- catch (e) {
1492
- return { stdout: '', stderr: e.message.toString() };
1493
- }
1494
- }
1495
- #parseSize(sizeStr) {
1496
- const units = {
1497
- B: 1,
1498
- KB: 1024,
1499
- MB: 1024 * 1024,
1500
- GB: 1024 * 1024 * 1024,
1501
- TB: 1024 * 1024 * 1024 * 1024,
1502
- };
1503
- const match = sizeStr.match(/^([\d.]+)([KMGTP]?B)$/);
1504
- if (match) {
1505
- const value = parseFloat(match[1]);
1506
- const unit = match[2];
1507
- return value * (units[unit] || 1);
1508
- }
1509
- return 0;
1510
- }
1511
- /**
1512
- * Stop a container
1513
- *
1514
- * @param container Container name or ID
1515
- */
1516
- async containerStop(container) {
1517
- if (this.#dockerode) {
1518
- let containers = await this.containerList();
1519
- // find ID of container
1520
- const containerInfo = containers.find(it => it.names === container || it.id === container);
1521
- if (!containerInfo) {
1522
- throw new Error(`Container ${container} not found`);
1523
- }
1524
- const dockerContainer = this.#dockerode.getContainer(containerInfo.id);
1525
- await dockerContainer.stop();
1526
- containers = await this.containerList();
1527
- if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
1528
- throw new Error(`Container ${container} still running after stop`);
1529
- }
1530
- return { stdout: `Contained ${containerInfo.id} stopped`, stderr: '', containers };
1531
- }
1532
- try {
1533
- let containers = await this.containerList();
1534
- // find ID of container
1535
- const containerInfo = containers.find(it => it.names === container || it.id === container);
1536
- if (!containerInfo) {
1537
- throw new Error(`Container ${container} not found`);
1538
- }
1539
- const result = await this.#exec(`stop ${containerInfo.id}`);
1540
- containers = await this.containerList();
1541
- if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
1542
- throw new Error(`Container ${container} still running after stop`);
1543
- }
1544
- return { ...result, containers };
1545
- }
1546
- catch (e) {
1547
- return { stdout: '', stderr: e.message.toString() };
1548
- }
1549
- }
1550
- /**
1551
- * Start a container
1552
- *
1553
- * @param container Container name or ID
1554
- */
1555
- async containerStart(container) {
1556
- if (this.#dockerode) {
1557
- let containers = await this.containerList();
1558
- // find ID of container
1559
- const containerInfo = containers.find(it => it.names === container || it.id === container);
1560
- if (!containerInfo) {
1561
- throw new Error(`Container ${container} not found`);
1562
- }
1563
- const dockerContainer = this.#dockerode.getContainer(containerInfo.id);
1564
- await dockerContainer.start();
1565
- containers = await this.containerList();
1566
- if (containers.find(it => it.id === containerInfo.id && it.status !== 'running' && it.status !== 'restarting')) {
1567
- throw new Error(`Container ${container} still running after stop`);
1568
- }
1569
- return { stdout: `Container ${containerInfo.id} started`, stderr: '', containers };
1570
- }
1571
- try {
1572
- let containers = await this.containerList();
1573
- // find ID of container
1574
- const containerInfo = containers.find(it => it.names === container || it.id === container);
1575
- if (!containerInfo) {
1576
- throw new Error(`Container ${container} not found`);
1577
- }
1578
- const result = await this.#exec(`start ${containerInfo.id}`);
1579
- containers = await this.containerList();
1580
- if (containers.find(it => it.id === containerInfo.id && it.status !== 'running' && it.status !== 'restarting')) {
1581
- throw new Error(`Container ${container} still running after stop`);
1582
- }
1583
- return { ...result, containers };
1584
- }
1585
- catch (e) {
1586
- return { stdout: '', stderr: e.message.toString() };
1587
- }
1588
- }
1589
- /**
1590
- * Restart a container
1591
- *
1592
- * This function restarts a container by its name or ID.
1593
- * It accepts an optional timeout in seconds to wait before killing the container (default is 5 seconds).
1594
- *
1595
- * @param container Container name or ID
1596
- * @param timeoutSeconds Timeout in seconds to wait before killing the container (default: 5)
1597
- */
1598
- async containerRestart(container, timeoutSeconds) {
1599
- container ||= this.getDefaultContainerName();
1600
- if (this.#dockerode) {
1601
- const containers = await this.containerList();
1602
- // find ID of container
1603
- const containerInfo = containers.find(it => it.names === container || it.id === container);
1604
- if (!containerInfo) {
1605
- throw new Error(`Container ${container} not found`);
1606
- }
1607
- const dockerContainer = this.#dockerode.getContainer(containerInfo.id);
1608
- await dockerContainer.restart({ t: timeoutSeconds || 5 });
1609
- return { stdout: `Container ${containerInfo.id} restarted`, stderr: '' };
1610
- }
1611
- try {
1612
- const containers = await this.containerList();
1613
- // find ID of container
1614
- const containerInfo = containers.find(it => it.names === container || it.id === container);
1615
- if (!containerInfo) {
1616
- throw new Error(`Container ${container} not found`);
1617
- }
1618
- return await this.#exec(`restart -t ${timeoutSeconds || 5} ${containerInfo.id}`);
1619
- }
1620
- catch (e) {
1621
- return { stdout: '', stderr: e.message.toString() };
1622
- }
1623
- }
1624
- /** Find the IP address of a container, via which it can be reached from the host */
1625
- async getIpOfContainer(containerName) {
1626
- containerName ||= this.getDefaultContainerName();
1627
- const data = await this.containerInspect(containerName);
1628
- if (!data?.NetworkSettings?.Networks) {
1629
- throw new Error(`No network settings found for container ${containerName}`);
1630
- }
1631
- for (const n in data.NetworkSettings.Networks) {
1632
- if (data.NetworkSettings.Networks[n].IPAddress) {
1633
- return data.NetworkSettings.Networks[n].IPAddress;
1634
- }
1635
- }
1636
- throw new Error(`No IP address found for container ${containerName}`);
1637
- }
1638
- /**
1639
- * Remove the container and if necessary, stop it first
1640
- *
1641
- * @param container Container name or ID
1642
- */
1643
- async containerRemove(container) {
1644
- try {
1645
- let containers = await this.containerList();
1646
- // find ID of container
1647
- const containerInfo = containers.find(it => it.names === container || it.id === container);
1648
- if (!containerInfo) {
1649
- throw new Error(`Container ${container} not found`);
1650
- }
1651
- // ensure that container is stopped
1652
- if (containerInfo.status === 'running' || containerInfo.status === 'restarting') {
1653
- // stop container
1654
- const result = await this.#exec(`stop ${containerInfo.id}`);
1655
- if (result.stderr) {
1656
- throw new Error(`Cannot stop container ${container}: ${result.stderr}`);
1657
- }
1658
- }
1659
- const result = await this.#exec(`rm ${container}`);
1660
- containers = await this.containerList();
1661
- if (containers.find(it => it.id === containerInfo.id)) {
1662
- throw new Error(`Container ${container} still found after stop`);
1663
- }
1664
- return { ...result, containers };
1665
- }
1666
- catch (e) {
1667
- return { stdout: '', stderr: e.message.toString() };
1668
- }
1669
- }
1670
- /**
1671
- * List all containers
1672
- *
1673
- * @param all If true, list all containers. If false, list only running containers. Default is true.
1674
- */
1675
- async containerList(all = true) {
1676
- if (this.#dockerode) {
1677
- const containers = await this.#dockerode.listContainers({ all });
1678
- return containers.map(cont => {
1679
- const statusKey = cont.State.toLowerCase();
1680
- let status;
1681
- if (statusKey === 'up') {
1682
- status = 'running';
1683
- }
1684
- else if (statusKey === 'exited') {
1685
- status = 'exited';
1686
- }
1687
- else if (statusKey === 'created') {
1688
- status = 'created';
1689
- }
1690
- else if (statusKey === 'paused') {
1691
- status = 'paused';
1692
- }
1693
- else if (statusKey === 'restarting') {
1694
- status = 'restarting';
1695
- }
1696
- else {
1697
- status = statusKey;
1698
- }
1699
- // Try to convert: Up 6 minutes, Up 6 hours, Up 6 days to minutes
1700
- const uptimeMatch = cont.Status.match(/Up (\d+) (seconds?|minutes?|hours?|days?)/);
1701
- let minutes = 0;
1702
- if (uptimeMatch) {
1703
- const value = parseInt(uptimeMatch[1], 10);
1704
- const unit = uptimeMatch[2];
1705
- if (unit.startsWith('hour')) {
1706
- minutes = value * 60;
1707
- }
1708
- else if (unit.startsWith('day')) {
1709
- minutes = value * 60 * 24;
1710
- }
1711
- else if (unit.startsWith('second')) {
1712
- minutes = Math.ceil(value / 60);
1713
- }
1714
- }
1715
- else if (cont.Status === 'Up About a minute') {
1716
- minutes = 1;
1717
- }
1718
- return {
1719
- id: cont.Id.substring(0, 12),
1720
- image: cont.Image,
1721
- command: cont.Command,
1722
- createdAt: new Date(cont.Created * 1000).toISOString(),
1723
- status,
1724
- uptime: minutes.toString(),
1725
- ports: cont.Ports.map(p => `${p.IP ? `${p.IP}:` : ''}${p.PublicPort ? `${p.PublicPort}->` : ''}${p.PrivatePort}/${p.Type}`).join(', '),
1726
- names: cont.Names.map(n => (n.startsWith('/') ? n.substring(1) : n)).join(', '),
1727
- labels: cont.Labels || {},
1728
- };
1729
- });
1730
- }
1731
- try {
1732
- const { stdout } = await this.#exec(`ps ${all ? '-a' : ''} --format "{{.Names}};{{.Status}};{{.ID}};{{.Image}};{{.Command}};{{.CreatedAt}};{{.Ports}};{{.Labels}}"`);
1733
- return stdout
1734
- .split('\n')
1735
- .filter(line => line.trim() !== '')
1736
- .map(line => {
1737
- const [names, statusInfo, id, image, command, createdAt, ports, labels] = line.split(';');
1738
- const [status, ...uptime] = statusInfo.split(' ');
1739
- let statusKey = status.toLowerCase();
1740
- if (statusKey === 'up') {
1741
- statusKey = 'running';
1742
- }
1743
- return {
1744
- id,
1745
- image,
1746
- command,
1747
- createdAt,
1748
- status: statusKey,
1749
- uptime: uptime.join(' '),
1750
- ports,
1751
- names,
1752
- labels: labels?.split(',').reduce((acc, label) => {
1753
- const [key, value] = label.split('=');
1754
- if (key && value) {
1755
- acc[key] = value;
1756
- }
1757
- return acc;
1758
- }, {}) || {},
1759
- };
1760
- });
1761
- }
1762
- catch (e) {
1763
- this.adapter.log.debug(`Cannot list containers: ${e.message}`);
1764
- return [];
1765
- }
1766
- }
1767
- /**
1768
- * Get the logs of a container
1769
- *
1770
- * @param containerNameOrId Container name or ID
1771
- * @param options Options for logs
1772
- * @param options.tail Number of lines to show from the end of the logs
1773
- * @param options.follow If true, follow the logs (not implemented yet)
1774
- */
1775
- async containerLogs(containerNameOrId, options = {}) {
1776
- if (this.#dockerode) {
1777
- try {
1778
- const container = this.#dockerode.getContainer(containerNameOrId);
1779
- const data = await container.logs({
1780
- stdout: true,
1781
- stderr: true,
1782
- follow: false,
1783
- tail: options.tail || undefined,
1784
- });
1785
- return data
1786
- .toString()
1787
- .split('\n')
1788
- .filter(line => line.trim() !== '');
1789
- }
1790
- catch (e) {
1791
- return e
1792
- .toString()
1793
- .split('\n')
1794
- .map((line) => line.trim());
1795
- }
1796
- }
1797
- try {
1798
- const args = [];
1799
- if (options.tail !== undefined) {
1800
- args.push(`--tail ${options.tail}`);
1801
- }
1802
- if (options.follow) {
1803
- args.push(`--follow`);
1804
- throw new Error('Follow option is not implemented yet');
1805
- }
1806
- const result = await this.#exec(`logs${args.length ? ` ${args.join(' ')}` : ''} ${containerNameOrId}`);
1807
- return (result.stdout || result.stderr).split('\n').filter(line => line.trim() !== '');
1808
- }
1809
- catch (e) {
1810
- return e
1811
- .toString()
1812
- .split('\n')
1813
- .map((line) => line.trim());
1814
- }
1815
- }
1816
- static dockerodeInspect2DockerContainerInspect(data) {
1817
- return {
1818
- ...data,
1819
- };
1820
- }
1821
- /** Inspect a container */
1822
- async containerInspect(containerNameOrId) {
1823
- if (this.#dockerode) {
1824
- try {
1825
- const container = this.#dockerode.getContainer(containerNameOrId);
1826
- const dResult = await container.inspect();
1827
- const result = _a.dockerodeInspect2DockerContainerInspect(dResult);
1828
- if (result.State.Running) {
1829
- result.Stats = (await this.containerGetRamAndCpuUsage(containerNameOrId)) || undefined;
1830
- }
1831
- return result;
1832
- }
1833
- catch (e) {
1834
- this.adapter.log.debug(`Cannot inspect container: ${e.message.toString()}`);
1835
- return null;
1836
- }
1837
- }
1838
- try {
1839
- const { stdout } = await this.#exec(`inspect ${containerNameOrId}`);
1840
- const result = JSON.parse(stdout)[0];
1841
- if (result.State.Running) {
1842
- result.Stats = (await this.containerGetRamAndCpuUsage(containerNameOrId)) || undefined;
1843
- }
1844
- return result;
1845
- }
1846
- catch (e) {
1847
- this.adapter.log.debug(`Cannot inspect container: ${e.message.toString()}`);
1848
- return null;
1849
- }
1850
- }
1851
- async containerPrune() {
1852
- if (this.#dockerode) {
1853
- try {
1854
- const result = await this.#dockerode.pruneContainers();
1855
- return { stdout: `Containers pruned: ${result.ContainersDeleted?.join(', ') || 'none'}`, stderr: '' };
1856
- }
1857
- catch (e) {
1858
- return { stdout: '', stderr: e.message.toString() };
1859
- }
1860
- }
1861
- try {
1862
- return await this.#exec(`container prune -f`);
1863
- }
1864
- catch (e) {
1865
- return { stdout: '', stderr: e.message.toString() };
1866
- }
1867
- }
1868
- /**
1869
- * Build a docker run command string from ContainerConfig
1870
- */
1871
- static toDockerRun(config, create) {
1872
- const args = [];
1873
- // detach / interactive
1874
- if (config.detach !== false && !create) {
1875
- // default is true
1876
- args.push('-d');
1877
- }
1878
- if (config.tty) {
1879
- args.push('-t');
1880
- }
1881
- if (config.stdinOpen) {
1882
- args.push('-i');
1883
- }
1884
- if (config.removeOnExit) {
1885
- args.push('--rm');
1886
- }
1887
- // name
1888
- if (config.name) {
1889
- args.push('--name', config.name);
1890
- }
1891
- // hostname / domain
1892
- if (config.hostname) {
1893
- args.push('--hostname', config.hostname);
1894
- }
1895
- if (config.domainname) {
1896
- args.push('--domainname', config.domainname);
1897
- }
1898
- // environment
1899
- if (config.environment) {
1900
- for (const [key, value] of Object.entries(config.environment)) {
1901
- if (key && value) {
1902
- args.push('-e', `${key}=${value}`);
1903
- }
1904
- }
1905
- }
1906
- if (config.envFile) {
1907
- for (const file of config.envFile) {
1908
- args.push('--env-file', file);
1909
- }
1910
- }
1911
- // labels
1912
- if (config.labels) {
1913
- for (const [key, value] of Object.entries(config.labels)) {
1914
- args.push('--label', `${key}=${value}`);
1915
- }
1916
- }
1917
- // ports
1918
- if (config.publishAllPorts) {
1919
- args.push('-P');
1920
- }
1921
- if (config.ports) {
1922
- for (const p of config.ports) {
1923
- if (!p.containerPort) {
1924
- continue;
1925
- }
1926
- const mapping = (p.hostIP ? `${p.hostIP}:` : '') +
1927
- (p.hostPort ? `${p.hostPort}:` : '') +
1928
- p.containerPort +
1929
- (p.protocol ? `/${p.protocol}` : '');
1930
- args.push('-p', mapping);
1931
- }
1932
- }
1933
- // volumes / mounts
1934
- if (config.volumes) {
1935
- for (const v of config.volumes) {
1936
- args.push('-v', v);
1937
- }
1938
- }
1939
- if (config.mounts) {
1940
- for (const m of config.mounts) {
1941
- let mount = `type=${m.type},target=${m.target}`;
1942
- if (m.source) {
1943
- mount += `,source=${m.source}`;
1944
- }
1945
- if (m.readOnly) {
1946
- mount += `,readonly`;
1947
- }
1948
- args.push('--mount', mount);
1949
- }
1950
- }
1951
- // restart policy
1952
- if (config.restart?.policy) {
1953
- const val = config.restart.policy === 'on-failure' && config.restart.maxRetries
1954
- ? `on-failure:${config.restart.maxRetries}`
1955
- : config.restart.policy;
1956
- args.push('--restart', val);
1957
- }
1958
- // user & workdir
1959
- if (config.user) {
1960
- args.push('--user', String(config.user));
1961
- }
1962
- if (config.workdir) {
1963
- args.push('--workdir', config.workdir);
1964
- }
1965
- // logging
1966
- if (config.logging?.driver) {
1967
- args.push('--log-driver', config.logging.driver);
1968
- if (config.logging.options) {
1969
- for (const [k, v] of Object.entries(config.logging.options)) {
1970
- args.push('--log-opt', `${k}=${v}`);
1971
- }
1972
- }
1973
- }
1974
- // security
1975
- if (config.security?.privileged) {
1976
- args.push('--privileged');
1977
- }
1978
- if (config.security?.capAdd) {
1979
- for (const cap of config.security.capAdd) {
1980
- args.push('--cap-add', cap);
1981
- }
1982
- }
1983
- if (config.security?.capDrop) {
1984
- for (const cap of config.security.capDrop) {
1985
- args.push('--cap-drop', cap);
1986
- }
1987
- }
1988
- if (config.security?.noNewPrivileges) {
1989
- args.push('--security-opt', 'no-new-privileges');
1990
- }
1991
- if (config.security?.apparmor) {
1992
- args.push('--security-opt', `apparmor=${config.security.apparmor}`);
1993
- }
1994
- // network
1995
- if (config.networkMode && typeof config.networkMode === 'string') {
1996
- args.push('--network', config.networkMode);
1997
- }
1998
- // extra hosts
1999
- if (config.extraHosts) {
2000
- for (const host of config.extraHosts) {
2001
- if (typeof host === 'string') {
2002
- args.push('--add-host', host);
2003
- }
2004
- else {
2005
- args.push('--add-host', `${host.host}:${host.ip}`);
2006
- }
2007
- }
2008
- }
2009
- // sysctls
2010
- if (config.sysctls) {
2011
- for (const [k, v] of Object.entries(config.sysctls)) {
2012
- args.push('--sysctl', `${k}=${v}`);
2013
- }
2014
- }
2015
- // stop signal / timeout
2016
- if (config.stop?.signal) {
2017
- args.push('--stop-signal', config.stop.signal);
2018
- }
2019
- if (config.stop?.gracePeriodSec !== undefined) {
2020
- args.push('--stop-timeout', String(config.stop.gracePeriodSec));
2021
- }
2022
- // resources
2023
- if (config.resources?.cpus) {
2024
- args.push('--cpus', String(config.resources.cpus));
2025
- }
2026
- if (config.resources?.memory) {
2027
- args.push('--memory', String(config.resources.memory));
2028
- }
2029
- // image
2030
- if (!config.image) {
2031
- throw new Error('ContainerConfig.image is required for docker run');
2032
- }
2033
- args.push(config.image);
2034
- // command override
2035
- if (config.command) {
2036
- if (Array.isArray(config.command)) {
2037
- args.push(...config.command);
2038
- }
2039
- else {
2040
- args.push(config.command);
2041
- }
2042
- }
2043
- return args.join(' ');
2044
- }
2045
- async networkList() {
2046
- if (this.#dockerode) {
2047
- const networks = await this.#dockerode.listNetworks();
2048
- return networks.map(net => ({
2049
- name: net.Name,
2050
- id: net.Id,
2051
- driver: net.Driver,
2052
- scope: net.Scope,
2053
- }));
2054
- }
2055
- // docker network ls
2056
- try {
2057
- const { stdout } = await this.#exec(`network ls --format "{{.Name}};{{.ID}};{{.Driver}};{{.Scope}}"`);
2058
- return stdout
2059
- .split('\n')
2060
- .filter(line => line.trim() !== '')
2061
- .map(line => {
2062
- const [name, id, driver, scope] = line.split(';');
2063
- return { name, id, driver: driver, scope };
2064
- });
2065
- }
2066
- catch (e) {
2067
- this.adapter.log.debug(`Cannot list networks: ${e.message.toString()}`);
2068
- return [];
2069
- }
2070
- }
2071
- async networkCreate(name, driver) {
2072
- if (this.#dockerode) {
2073
- const net = await this.#dockerode.createNetwork({ Name: name, Driver: driver || 'bridge' });
2074
- const networks = await this.networkList();
2075
- if (!networks.find(it => it.name === name)) {
2076
- throw new Error(`Network ${name} not found after creation`);
2077
- }
2078
- return { stdout: `Network ${net.id} created`, stderr: '', networks };
2079
- }
2080
- const result = await this.#exec(`network create ${driver ? `--driver ${driver}` : ''} ${name}`);
2081
- const networks = await this.networkList();
2082
- if (!networks.find(it => it.name === name)) {
2083
- throw new Error(`Network ${name} not found after creation`);
2084
- }
2085
- return { ...result, networks };
2086
- }
2087
- async networkRemove(networkId) {
2088
- const result = await this.#exec(`network remove ${networkId}`);
2089
- const networks = await this.networkList();
2090
- if (networks.find(it => it.id === networkId)) {
2091
- throw new Error(`Network ${networkId} still found after deletion`);
2092
- }
2093
- return { ...result, networks };
2094
- }
2095
- async networkPrune() {
2096
- if (this.#dockerode) {
2097
- const result = await this.#dockerode.pruneNetworks();
2098
- const networks = await this.networkList();
2099
- return { stdout: `Networks pruned`, stderr: JSON.stringify(result), networks };
2100
- }
2101
- const result = await this.#exec(`network prune -f`);
2102
- const networks = await this.networkList();
2103
- return { ...result, networks };
2104
- }
2105
- /** List all volumes */
2106
- async volumeList() {
2107
- if (this.#dockerode) {
2108
- const volumesData = await this.#dockerode.listVolumes();
2109
- return (volumesData.Volumes || []).map(vol => ({
2110
- name: vol.Name,
2111
- driver: vol.Driver,
2112
- volume: vol.Mountpoint,
2113
- }));
2114
- }
2115
- // docker network ls
2116
- try {
2117
- const { stdout } = await this.#exec(`volume ls --format "{{.Name}};{{.Driver}};{{.Mountpoint}}"`);
2118
- return stdout
2119
- .split('\n')
2120
- .filter(line => line.trim() !== '')
2121
- .map(line => {
2122
- const [name, driver, volume] = line.split(';');
2123
- return { name, driver: driver, volume };
2124
- });
2125
- }
2126
- catch (e) {
2127
- this.adapter.log.debug(`Cannot list networks: ${e.message.toString()}`);
2128
- return [];
2129
- }
2130
- }
2131
- async volumeCopyTo(volumeName, sourcePath) {
2132
- const tempContainerName = `iobroker_temp_copy_${Date.now()}`;
2133
- if (this.#dockerode) {
2134
- // Check if alpine image is there
2135
- const images = await this.imageList();
2136
- if (!images.find(img => img.repository === 'alpine')) {
2137
- const pullResult = await this.imagePull('alpine');
2138
- if (pullResult.stderr) {
2139
- return { stdout: '', stderr: `Cannot pull alpine image: ${pullResult.stderr}` };
2140
- }
2141
- }
2142
- // create a temporary container with volume mounted
2143
- const container = await this.#dockerode.createContainer({
2144
- Image: 'alpine',
2145
- name: tempContainerName,
2146
- Cmd: ['sleep', '30'],
2147
- HostConfig: {
2148
- Binds: [`${volumeName}:/data`],
2149
- },
2150
- });
2151
- try {
2152
- await container.start();
2153
- // Lazy loading tar-fs
2154
- if (!this.#tarPack) {
2155
- await import('tar-fs')
2156
- .then(tarFs => (this.#tarPack = tarFs.default.pack))
2157
- .catch(e => this.adapter.log.error(`Cannot import tar-fs package: ${e.message}`));
2158
- }
2159
- if (!this.#tarPack) {
2160
- throw new Error('Cannot load tar-fs package');
2161
- }
2162
- // use dockerode to copy files
2163
- const pack = this.#tarPack(sourcePath);
2164
- await container.putArchive(pack, { path: '/data' });
2165
- return { stdout: 'Data copied to volume', stderr: '' };
2166
- }
2167
- catch (e) {
2168
- return { stdout: '', stderr: `Cannot copy data to volume: ${e.message}` };
2169
- }
2170
- finally {
2171
- // remove temporary container
2172
- try {
2173
- await container.stop();
2174
- await container.remove({ force: true });
2175
- }
2176
- catch (e) {
2177
- this.adapter.log.warn(`Cannot remove temporary container ${tempContainerName}: ${e.message}`);
2178
- }
2179
- }
2180
- }
2181
- // create a temporary container with volume mounted
2182
- const createResult = await this.#exec(`create -v ${volumeName}:/data --name ${tempContainerName} alpine sleep 60`);
2183
- if (createResult.stderr) {
2184
- return { stdout: '', stderr: `Cannot create temporary container: ${createResult.stderr}` };
2185
- }
2186
- try {
2187
- const copyResult = await this.#exec(`cp ${sourcePath} ${tempContainerName}:/data/`);
2188
- if (copyResult.stderr) {
2189
- return { stdout: '', stderr: `Cannot copy data to volume: ${copyResult.stderr}` };
2190
- }
2191
- return { stdout: 'Data copied to volume', stderr: '' };
2192
- }
2193
- finally {
2194
- // remove temporary container
2195
- await this.#exec(`rm -f ${tempContainerName}`);
2196
- }
2197
- }
2198
- /**
2199
- * Create a volume
2200
- *
2201
- * @param name Volume name
2202
- * @param driver Volume driver
2203
- * @param volume Volume options (depends on driver)
2204
- */
2205
- async volumeCreate(name, driver, volume) {
2206
- if (this.#dockerode) {
2207
- const vol = await this.#dockerode.createVolume({
2208
- Name: name,
2209
- Driver: driver || 'local',
2210
- DriverOpts: volume ? { device: volume, o: 'bind', type: 'none' } : {},
2211
- });
2212
- const volumes = await this.volumeList();
2213
- if (!volumes.find(it => it.name === name)) {
2214
- throw new Error(`Network ${name} not found after creation`);
2215
- }
2216
- return { stdout: `Volume ${vol.Name} created`, stderr: '', volumes };
2217
- }
2218
- let result;
2219
- if (driver === 'local' || !driver) {
2220
- if (volume) {
2221
- result = await this.#exec(`volume create local --opt type=none --opt device=${volume} --opt o=bind ${name}`);
2222
- }
2223
- else {
2224
- result = await this.#exec(`volume create ${name}`);
2225
- }
2226
- }
2227
- else {
2228
- throw new Error('not implemented');
2229
- }
2230
- const volumes = await this.volumeList();
2231
- if (!volumes.find(it => it.name === name)) {
2232
- throw new Error(`Network ${name} not found after creation`);
2233
- }
2234
- return { ...result, volumes };
2235
- }
2236
- /** Remove a volume */
2237
- async volumeRemove(volumeName) {
2238
- const result = await this.#exec(`volume remove ${volumeName}`);
2239
- const volumes = await this.volumeList();
2240
- if (volumes.find(it => it.name === volumeName)) {
2241
- throw new Error(`Volume ${volumeName} still found after deletion`);
2242
- }
2243
- return { ...result, volumes };
2244
- }
2245
- /** Prune unused volumes */
2246
- async volumePrune() {
2247
- if (this.#dockerode) {
2248
- const result = await this.#dockerode.pruneVolumes();
2249
- const volumes = await this.volumeList();
2250
- return { stdout: `Volumes pruned`, stderr: JSON.stringify(result), volumes };
2251
- }
2252
- const result = await this.#exec(`volume prune -f`);
2253
- const volumes = await this.volumeList();
2254
- return { ...result, volumes };
2255
- }
2256
- /** Stop own containers if necessary */
2257
- async destroy() {
2258
- if (this.#monitoringInterval) {
2259
- clearInterval(this.#monitoringInterval);
2260
- this.#monitoringInterval = null;
2261
- }
2262
- for (const container of this.#ownContainers) {
2263
- if (container.iobEnabled !== false && container.iobStopOnUnload && container.name) {
2264
- this.adapter.log.info(`Stopping own container ${container.name} on destroy`);
2265
- try {
2266
- await this.containerStop(container.name);
2267
- }
2268
- catch (e) {
2269
- this.adapter.log.warn(`Cannot stop own container ${container.name} on destroy: ${e.message}`);
2270
- }
2271
- }
2272
- }
2273
- }
2274
- }
2275
- _a = DockerManager;
2276
- exports.default = DockerManager;
2277
- //# sourceMappingURL=DockerManager.js.map