iobroker.docker-manager 0.1.3 → 0.1.5

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,1042 +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
- Object.defineProperty(exports, "__esModule", { value: true });
9
- const node_util_1 = require("node:util");
10
- const node_child_process_1 = require("node:child_process");
11
- const node_os_1 = require("node:os");
12
- const promises_1 = require("node:dns/promises");
13
- const node_http_1 = __importDefault(require("node:http"));
14
- const range_check_1 = require("range_check");
15
- const execPromise = (0, node_util_1.promisify)(node_child_process_1.exec);
16
- function isHttpResponse(url, timeout = 1000) {
17
- return new Promise(resolve => {
18
- try {
19
- const req = node_http_1.default.request(url, { method: 'HEAD', timeout }, res => {
20
- resolve(typeof res.statusCode === 'number');
21
- req.destroy();
22
- });
23
- req.on('error', error => {
24
- if (error.toString().toUpperCase().includes('TIMEDOUT') ||
25
- error.toString().toUpperCase().includes('TIMEOUT')) {
26
- resolve('timeout');
27
- }
28
- else {
29
- resolve(false);
30
- }
31
- });
32
- req.end();
33
- }
34
- catch {
35
- resolve(false);
36
- }
37
- });
38
- }
39
- function findOwnIpFor(ipToAccess) {
40
- if (!(0, range_check_1.isIP)(ipToAccess)) {
41
- return null;
42
- }
43
- const interfaces = (0, node_os_1.networkInterfaces)();
44
- for (const iface of Object.values(interfaces)) {
45
- if (iface) {
46
- for (const alias of iface) {
47
- if (alias.family === 'IPv4' && !alias.internal && alias.cidr) {
48
- if ((0, range_check_1.inRange)(ipToAccess, alias.cidr)) {
49
- return alias.address;
50
- }
51
- }
52
- }
53
- }
54
- }
55
- return null;
56
- }
57
- async function getIpForDomain(domain) {
58
- try {
59
- const result = await (0, promises_1.lookup)(domain);
60
- return result.address;
61
- }
62
- catch {
63
- return false;
64
- }
65
- }
66
- class DockerCommands {
67
- #installed = false;
68
- #dockerVersion = '';
69
- #needSudo = false;
70
- #waitReady;
71
- #adapter;
72
- #ownContainers = [];
73
- #timers = {
74
- container: {},
75
- };
76
- #runningCommands = {};
77
- checkedURLs = {};
78
- #ownIps = [];
79
- #domain2ip = {};
80
- constructor(adapter) {
81
- this.#adapter = adapter;
82
- this.#ownContainers = adapter.config.containers || [];
83
- this.#waitReady = new Promise(resolve => this.init().then(() => resolve()));
84
- }
85
- isReady() {
86
- return this.#waitReady;
87
- }
88
- async init() {
89
- const version = await this.#isDockerInstalled();
90
- this.#installed = !!version;
91
- if (version) {
92
- this.#dockerVersion = version;
93
- }
94
- else {
95
- const daemonRunning = await this.#isDockerDaemonRunning();
96
- if (daemonRunning) {
97
- // Docker daemon is running, but docker command not found
98
- 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.');
99
- }
100
- else {
101
- this.#adapter.log.warn('Docker is not installed. Please install Docker.');
102
- }
103
- }
104
- if (this.#installed) {
105
- this.#needSudo = await this.#isNeedSudo();
106
- await this.#checkOwnContainers();
107
- }
108
- }
109
- async #isDockerDaemonRunning() {
110
- try {
111
- const { stdout, stderr } = await execPromise('systemctl status docker');
112
- // ● docker.service - Docker Application Container Engine
113
- // Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
114
- // Active: active (running) since Fri 2025-08-15 08:37:22 CEST; 3 weeks 2 days ago
115
- // TriggeredBy: ● docker.socket
116
- // Docs: https://docs.docker.com
117
- // Main PID: 785 (dockerd)
118
- // Tasks: 30
119
- // CPU: 4min 17.003s
120
- // CGroup: /system.slice/docker.service
121
- // ├─ 785 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
122
- // ├─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
123
- // └─97039 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 5000 -container-ip 172.17.0.2 -container-port 5000 -use-listen-fd
124
- if (stderr?.includes('could not be found') || stderr.includes('not-found')) {
125
- this.#adapter.log.error(`Docker is not installed: ${stderr}`);
126
- return false;
127
- }
128
- this.#adapter.log.error(`Docker daemon is not running`);
129
- return stdout.includes('(running)');
130
- }
131
- catch {
132
- return false;
133
- }
134
- }
135
- async #checkOwnContainers() {
136
- if (!this.#ownContainers.length) {
137
- return;
138
- }
139
- const status = await this.containerList(true);
140
- let images = await this.imageList();
141
- for (let c = 0; c < this.#ownContainers.length; c++) {
142
- const container = this.#ownContainers[c];
143
- if (container.enabled !== false) {
144
- // Check if container is running
145
- const containerInfo = status.find(it => it.names === container.name);
146
- if (containerInfo) {
147
- if (containerInfo.status !== 'running' && containerInfo.status !== 'restarting') {
148
- // Start the container
149
- this.#adapter.log.info(`Starting own container ${container.name}`);
150
- try {
151
- const result = await this.containerStart(containerInfo.id);
152
- if (result.stderr) {
153
- this.#adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
154
- }
155
- }
156
- catch (e) {
157
- this.#adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
158
- }
159
- }
160
- else {
161
- this.#adapter.log.debug(`Own container ${container.name} is already running`);
162
- }
163
- }
164
- else {
165
- // Create and start the container
166
- this.#adapter.log.info(`Creating and starting own container ${container.name}`);
167
- if (!images.find(it => `${it.repository}:${it.tag}` === container.image)) {
168
- this.#adapter.log.info(`Pulling image ${container.image} for own container ${container.name}`);
169
- try {
170
- const result = await this.imagePull(container.image);
171
- if (result.stderr) {
172
- this.#adapter.log.warn(`Cannot pull image ${container.image}: ${result.stderr}`);
173
- continue;
174
- }
175
- }
176
- catch (e) {
177
- this.#adapter.log.warn(`Cannot pull image ${container.image}: ${e.message}`);
178
- continue;
179
- }
180
- // Check that image is available now
181
- images = await this.imageList();
182
- if (!images.find(it => `${it.repository}:${it.tag}` === container.image)) {
183
- this.#adapter.log.warn(`Image ${container.image} for own container ${container.name} not found after pull`);
184
- continue;
185
- }
186
- }
187
- try {
188
- const result = await this.containerRun(container.image, container);
189
- if (result.stderr) {
190
- this.#adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
191
- }
192
- }
193
- catch (e) {
194
- this.#adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
195
- }
196
- }
197
- }
198
- }
199
- }
200
- #exec(command) {
201
- if (!this.#installed) {
202
- return Promise.reject(new Error('Docker is not installed'));
203
- }
204
- const finalCommand = this.#needSudo ? `sudo docker ${command}` : `docker ${command}`;
205
- return execPromise(finalCommand);
206
- }
207
- async #isDockerInstalled() {
208
- try {
209
- const result = await execPromise('docker --version');
210
- if (!result.stderr && result.stdout) {
211
- // "Docker version 28.3.2, build 578ccf6\n"
212
- return result.stdout.split('\n')[0].trim();
213
- }
214
- this.#adapter.log.debug(`Docker not installed: ${result.stderr}`);
215
- }
216
- catch (e) {
217
- this.#adapter.log.debug(`Docker not installed: ${e.message}`);
218
- }
219
- return false;
220
- }
221
- async #isNeedSudo() {
222
- try {
223
- await execPromise('docker ps');
224
- return false;
225
- }
226
- catch {
227
- return true;
228
- }
229
- }
230
- async discUsage() {
231
- const { stdout } = await this.#exec(`system df`);
232
- const result = { total: { size: 0, reclaimable: 0 } };
233
- // parse the output
234
- // TYPE TOTAL ACTIVE SIZE RECLAIMABLE
235
- // Images 2 1 2.715GB 2.715GB (99%)
236
- // Containers 1 1 26.22MB 0B (0%)
237
- // Local Volumes 0 0 0B 0B
238
- // Build Cache 0 0 0B 0B
239
- const lines = stdout.split('\n');
240
- for (const line of lines) {
241
- const parts = line.trim().split(/\s+/);
242
- if (parts.length >= 5 && parts[0] !== 'TYPE') {
243
- let size;
244
- let reclaimable;
245
- if (parts[0] === 'Images') {
246
- const sizeStr = parts[3];
247
- const reclaimableStr = parts[4].split(' ')[0];
248
- size = this.#parseSize(sizeStr);
249
- reclaimable = this.#parseSize(reclaimableStr);
250
- result.images = {
251
- total: parseInt(parts[1], 10),
252
- active: parseInt(parts[2], 10),
253
- size,
254
- reclaimable: reclaimable,
255
- };
256
- }
257
- else if (parts[0] === 'Containers') {
258
- const sizeStr = parts[3];
259
- const reclaimableStr = parts[4].split(' ')[0];
260
- size = this.#parseSize(sizeStr);
261
- reclaimable = this.#parseSize(reclaimableStr);
262
- result.containers = {
263
- total: parseInt(parts[1], 10),
264
- active: parseInt(parts[2], 10),
265
- size,
266
- reclaimable: reclaimable,
267
- };
268
- }
269
- else if (parts[0] === 'Local' && parts[1] === 'Volumes') {
270
- const sizeStr = parts[4];
271
- const reclaimableStr = parts[5].split(' ')[0];
272
- size = this.#parseSize(sizeStr);
273
- reclaimable = this.#parseSize(reclaimableStr);
274
- result.volumes = {
275
- total: parseInt(parts[2], 10),
276
- active: parseInt(parts[3], 10),
277
- size,
278
- reclaimable: reclaimable,
279
- };
280
- }
281
- else if (parts[0] === 'Build' && parts[1] === 'Cache') {
282
- const sizeStr = parts[4];
283
- const reclaimableStr = parts[5].split(' ')[0];
284
- size = this.#parseSize(sizeStr);
285
- reclaimable = this.#parseSize(reclaimableStr);
286
- result.buildCache = {
287
- total: parseInt(parts[2], 10),
288
- active: parseInt(parts[3], 10),
289
- size,
290
- reclaimable: reclaimable,
291
- };
292
- }
293
- result.total.size += size;
294
- result.total.reclaimable += reclaimable;
295
- }
296
- }
297
- return result;
298
- }
299
- async imagePull(image) {
300
- try {
301
- const result = await this.#exec(`pull ${image}`);
302
- const images = await this.imageList();
303
- if (!images.find(it => `${it.repository}:${it.tag}` === image)) {
304
- throw new Error(`Image ${image} not found after pull`);
305
- }
306
- await this.#adapter.sendToGui({
307
- command: 'images',
308
- data: images,
309
- });
310
- return result;
311
- }
312
- catch (e) {
313
- return { stdout: '', stderr: e.message.toString() };
314
- }
315
- }
316
- async imageNameAutocomplete(partialName) {
317
- try {
318
- // Read stars and descriptions
319
- const { stdout } = await this.#exec(`search ${partialName} --format "{{.Name}};{{.Description}};{{.IsOfficial}};{{.StarCount}}" --limit 50`);
320
- return stdout
321
- .split('\n')
322
- .filter(line => line.trim() !== '')
323
- .map(line => {
324
- const [name, description, isOfficial, starCount] = line.split(';');
325
- return {
326
- name,
327
- description,
328
- isOfficial: isOfficial === 'true',
329
- starCount: parseInt(starCount, 10) || 0,
330
- };
331
- });
332
- }
333
- catch (e) {
334
- this.#adapter.log.debug(`Cannot search images: ${e.message}`);
335
- return [];
336
- }
337
- }
338
- async containerRun(image, config) {
339
- try {
340
- const result = await this.#exec(`run ${this.#toDockerRun({ ...config, image })}`);
341
- const containers = await this.containerList();
342
- await this.#adapter.sendToGui({
343
- command: 'containers',
344
- data: containers,
345
- });
346
- return result;
347
- }
348
- catch (e) {
349
- return { stdout: '', stderr: e.message.toString() };
350
- }
351
- }
352
- async containerCreate(image, config) {
353
- try {
354
- const result = await this.#exec(`create ${this.#toDockerRun({ ...config, image })}`);
355
- const containers = await this.containerList();
356
- await this.#adapter.sendToGui({
357
- command: 'containers',
358
- data: containers,
359
- });
360
- return result;
361
- }
362
- catch (e) {
363
- return { stdout: '', stderr: e.message.toString() };
364
- }
365
- }
366
- async imageList() {
367
- try {
368
- const { stdout } = await this.#exec('images --format "{{.Repository}}:{{.Tag}};{{.ID}};{{.CreatedAt}};{{.Size}}"');
369
- return stdout
370
- .split('\n')
371
- .filter(line => line.trim() !== '')
372
- .map(line => {
373
- const [repositoryTag, id, createdSince, size] = line.split(';');
374
- const [repository, tag] = repositoryTag.split(':');
375
- return {
376
- repository,
377
- tag,
378
- id,
379
- createdSince,
380
- size: this.#parseSize(size),
381
- };
382
- });
383
- }
384
- catch (e) {
385
- this.#adapter.log.debug(`Cannot list images: ${e.message}`);
386
- return [];
387
- }
388
- }
389
- async imageBuild(dockerfilePath, tag) {
390
- try {
391
- const result = await this.#exec(`build -t ${tag} -f ${dockerfilePath} .`);
392
- const images = await this.imageList();
393
- await this.#adapter.sendToGui({
394
- command: 'images',
395
- data: images,
396
- });
397
- return result;
398
- }
399
- catch (e) {
400
- return { stdout: '', stderr: e.message.toString() };
401
- }
402
- }
403
- async imageTag(imageId, newTag) {
404
- try {
405
- const result = await this.#exec(`tag ${imageId} ${newTag}`);
406
- const images = await this.imageList();
407
- await this.#adapter.sendToGui({
408
- command: 'images',
409
- data: images,
410
- });
411
- return result;
412
- }
413
- catch (e) {
414
- return { stdout: '', stderr: e.message.toString() };
415
- }
416
- }
417
- async imageRemove(imageId) {
418
- try {
419
- const result = await this.#exec(`rmi ${imageId}`);
420
- const images = await this.imageList();
421
- if (images.find(it => `${it.repository}:${it.tag}` === imageId)) {
422
- return { stdout: '', stderr: `Image ${imageId} still found after deletion` };
423
- }
424
- await this.#adapter.sendToGui({
425
- command: 'images',
426
- data: images,
427
- });
428
- return result;
429
- }
430
- catch (e) {
431
- return { stdout: '', stderr: e.message.toString() };
432
- }
433
- }
434
- async imageInspect(imageId) {
435
- try {
436
- const { stdout } = await this.#exec(`inspect ${imageId}`);
437
- return JSON.parse(stdout)[0];
438
- }
439
- catch (e) {
440
- this.#adapter.log.debug(`Cannot inspect image: ${e.message.toString()}`);
441
- return null;
442
- }
443
- }
444
- #parseSize(sizeStr) {
445
- const units = {
446
- B: 1,
447
- KB: 1024,
448
- MB: 1024 * 1024,
449
- GB: 1024 * 1024 * 1024,
450
- TB: 1024 * 1024 * 1024 * 1024,
451
- };
452
- const match = sizeStr.match(/^([\d.]+)([KMGTP]?B)$/);
453
- if (match) {
454
- const value = parseFloat(match[1]);
455
- const unit = match[2];
456
- return value * (units[unit] || 1);
457
- }
458
- return 0;
459
- }
460
- async containerStop(container) {
461
- try {
462
- let containers = await this.containerList();
463
- // find ID of container
464
- const containerInfo = containers.find(it => it.names === container || it.id === container);
465
- if (!containerInfo) {
466
- throw new Error(`Container ${container} not found`);
467
- }
468
- const result = await this.#exec(`stop ${containerInfo.id}`);
469
- containers = await this.containerList();
470
- if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
471
- throw new Error(`Container ${container} still running after stop`);
472
- }
473
- await this.#adapter.sendToGui({
474
- command: 'containers',
475
- data: containers,
476
- });
477
- return result;
478
- }
479
- catch (e) {
480
- return { stdout: '', stderr: e.message.toString() };
481
- }
482
- }
483
- async containerStart(container) {
484
- try {
485
- let containers = await this.containerList();
486
- // find ID of container
487
- const containerInfo = containers.find(it => it.names === container || it.id === container);
488
- if (!containerInfo) {
489
- throw new Error(`Container ${container} not found`);
490
- }
491
- const result = await this.#exec(`start ${containerInfo.id}`);
492
- containers = await this.containerList();
493
- if (containers.find(it => it.id === containerInfo.id && it.status !== 'running' && it.status !== 'restarting')) {
494
- throw new Error(`Container ${container} still running after stop`);
495
- }
496
- await this.#adapter.sendToGui({
497
- command: 'containers',
498
- data: containers,
499
- });
500
- return result;
501
- }
502
- catch (e) {
503
- return { stdout: '', stderr: e.message.toString() };
504
- }
505
- }
506
- async containerRestart(container, timeoutSeconds) {
507
- try {
508
- let containers = await this.containerList();
509
- // find ID of container
510
- const containerInfo = containers.find(it => it.names === container || it.id === container);
511
- if (!containerInfo) {
512
- throw new Error(`Container ${container} not found`);
513
- }
514
- const result = await this.#exec(`restart -t ${timeoutSeconds || 5} ${containerInfo.id}`);
515
- containers = await this.containerList();
516
- await this.#adapter.sendToGui({
517
- command: 'containers',
518
- data: containers,
519
- });
520
- return result;
521
- }
522
- catch (e) {
523
- return { stdout: '', stderr: e.message.toString() };
524
- }
525
- }
526
- async containerRemove(container) {
527
- try {
528
- let containers = await this.containerList();
529
- // find ID of container
530
- const containerInfo = containers.find(it => it.names === container || it.id === container);
531
- if (!containerInfo) {
532
- throw new Error(`Container ${container} not found`);
533
- }
534
- const result = await this.#exec(`rm ${container}`);
535
- containers = await this.containerList();
536
- if (containers.find(it => it.id === containerInfo.id)) {
537
- throw new Error(`Container ${container} still found after stop`);
538
- }
539
- await this.#adapter.sendToGui({
540
- command: 'containers',
541
- data: containers,
542
- });
543
- return result;
544
- }
545
- catch (e) {
546
- return { stdout: '', stderr: e.message.toString() };
547
- }
548
- }
549
- async containerList(all = true, ownIps) {
550
- try {
551
- const { stdout } = await this.#exec(`ps ${all ? '-a' : ''} --format "{{.Names}};{{.Status}};{{.ID}};{{.Image}};{{.Command}};{{.CreatedAt}};{{.Ports}}"`);
552
- const containers = stdout
553
- .split('\n')
554
- .filter(line => line.trim() !== '')
555
- .map(line => {
556
- const [names, statusInfo, id, image, command, createdAt, ports] = line.split(';');
557
- const [status, ...uptime] = statusInfo.split(' ');
558
- let statusKey = status.toLowerCase();
559
- if (statusKey === 'up') {
560
- statusKey = 'running';
561
- }
562
- return { id, image, command, createdAt, status: statusKey, uptime: uptime.join(' '), ports, names };
563
- });
564
- if (ownIps) {
565
- // Try to define if the provided ports are HTTP server
566
- for (const container of containers) {
567
- if (!container.ports || container.status !== 'running') {
568
- continue;
569
- }
570
- const ports = container.ports?.split(',').map(p => p.trim()) || [];
571
- // We have something like '127.0.0.1:8086->8086/tcp'
572
- for (const port of ports) {
573
- const parts = port.split('->');
574
- if (parts.length === 2) {
575
- const [hostIp, hostPort] = parts[0].split(':');
576
- const [, protocol] = parts[1].split('/');
577
- if (protocol === 'tcp') {
578
- // request to the port and check if we get HTTP response
579
- // create url
580
- const url = `http://${hostIp === '127.0.0.1' ? 'localhost' : hostIp}:${hostPort}`;
581
- if (this.checkedURLs[url] === undefined || this.checkedURLs[url] === 'timeout') {
582
- this.checkedURLs[url] = await isHttpResponse(url);
583
- }
584
- if (this.checkedURLs[url] === true) {
585
- if (hostIp === '0.0.0.0') {
586
- // find the local network interface IP that suits to ownIp
587
- for (const ownIp of ownIps) {
588
- const realIp = this.#domain2ip[ownIp] || ownIp;
589
- if ((0, range_check_1.isIP)(realIp)) {
590
- const hostIp = findOwnIpFor(this.#domain2ip[ownIp] || ownIp);
591
- container.httpLinks ||= {};
592
- container.httpLinks[ownIp] ||= [];
593
- if (hostIp) {
594
- container.httpLinks[ownIp].push(url.replace('0.0.0.0', ownIp));
595
- }
596
- else {
597
- container.httpLinks[ownIp].push(url);
598
- }
599
- }
600
- }
601
- }
602
- else {
603
- for (const ownIp of ownIps) {
604
- container.httpLinks ||= {};
605
- container.httpLinks[ownIp] ||= [];
606
- container.httpLinks[ownIp].push(url);
607
- }
608
- }
609
- }
610
- }
611
- }
612
- }
613
- }
614
- }
615
- return containers;
616
- }
617
- catch (e) {
618
- this.#adapter.log.debug(`Cannot list containers: ${e.message}`);
619
- return [];
620
- }
621
- }
622
- async containerLogs(containerNameOrId, options = {}) {
623
- try {
624
- const args = [];
625
- if (options.tail !== undefined) {
626
- args.push(`--tail ${options.tail}`);
627
- }
628
- if (options.follow) {
629
- args.push(`--follow`);
630
- }
631
- const result = await this.#exec(`logs${args.length ? ` ${args.join(' ')}` : ''} ${containerNameOrId}`);
632
- return (result.stdout || result.stderr).split('\n').filter(line => line.trim() !== '');
633
- }
634
- catch (e) {
635
- return e
636
- .toString()
637
- .split('\n')
638
- .map((line) => line.trim());
639
- }
640
- }
641
- containerExecTerminate(sid, force, onUnsubscribe) {
642
- if (this.#runningCommands[sid]) {
643
- this.#runningCommands[sid].onUnsubscribe = onUnsubscribe;
644
- if (force) {
645
- this.#runningCommands[sid].p.kill('SIGKILL');
646
- }
647
- else {
648
- this.#runningCommands[sid].p.kill('SIGTERM');
649
- this.#runningCommands[sid].killTimeout ||= setTimeout(() => {
650
- if (this.#runningCommands[sid]) {
651
- this.#runningCommands[sid].killTimeout = null;
652
- this.#runningCommands[sid].p.kill('SIGKILL');
653
- }
654
- }, 2000);
655
- }
656
- return true;
657
- }
658
- return false;
659
- }
660
- containerExec(container, command, sid) {
661
- const hasTTY = process.stdin.isTTY && process.stdout.isTTY;
662
- const args = ['exec'];
663
- if (!command) {
664
- this.#adapter.log.error('No command specified for exec');
665
- return;
666
- }
667
- if (hasTTY) {
668
- args.push('-t');
669
- } // add TTY only when available
670
- args.push(container);
671
- command.split(' ').forEach(part => args.push(part));
672
- let p;
673
- try {
674
- if (this.#needSudo) {
675
- this.#adapter.log.debug(`Executing: sudo docker ${args.join(' ')}`);
676
- p = (0, node_child_process_1.spawn)('sudo', ['docker', ...args], { stdio: ['pipe', 'pipe', 'pipe'] });
677
- }
678
- else {
679
- this.#adapter.log.debug(`Executing: docker ${args.join(' ')}`);
680
- p = (0, node_child_process_1.spawn)('docker', args, { stdio: ['pipe', 'pipe', 'pipe'] });
681
- }
682
- }
683
- catch (e) {
684
- void this.#adapter.sendToGui({
685
- command: 'exec',
686
- data: { stdout: '', stderr: e.message.toString(), containerId: container, code: 1 },
687
- }, sid);
688
- return;
689
- }
690
- this.#runningCommands[sid] = { p, killTimeout: null };
691
- let stdout = '';
692
- let stderr = '';
693
- p.stdout.on('data', d => {
694
- if (this.#runningCommands[sid] && !this.#runningCommands[sid].onUnsubscribe) {
695
- stdout += d;
696
- void this.#adapter.sendToGui({ command: 'exec', data: { stdout, stderr, containerId: container } }, sid);
697
- }
698
- });
699
- p.stderr.on('data', d => {
700
- if (this.#runningCommands[sid] && !this.#runningCommands[sid].onUnsubscribe) {
701
- stderr += d;
702
- void this.#adapter.sendToGui({ command: 'exec', data: { stdout, stderr, containerId: container } }, sid);
703
- }
704
- });
705
- p.on('close', code => {
706
- this.#adapter.log.debug(`Command finished with code ${code}`);
707
- const killTimeout = this.#runningCommands[sid]?.killTimeout;
708
- if (killTimeout) {
709
- clearTimeout(killTimeout);
710
- }
711
- if (this.#runningCommands[sid] && !this.#runningCommands[sid].onUnsubscribe) {
712
- void this.#adapter.sendToGui({ command: 'exec', data: { stdout, stderr, containerId: container, code } }, sid);
713
- }
714
- delete this.#runningCommands[sid];
715
- });
716
- }
717
- async containerInspect(containerNameOrId) {
718
- try {
719
- const { stdout } = await this.#exec(`inspect ${containerNameOrId}`);
720
- return JSON.parse(stdout)[0];
721
- }
722
- catch (e) {
723
- this.#adapter.log.debug(`Cannot inspect container: ${e.message.toString()}`);
724
- return null;
725
- }
726
- }
727
- /**
728
- * Build a docker run command string from ContainerConfig
729
- */
730
- #toDockerRun(config) {
731
- const args = [];
732
- // detach / interactive
733
- if (config.detach !== false) {
734
- // default is true
735
- args.push('-d');
736
- }
737
- if (config.tty) {
738
- args.push('-t');
739
- }
740
- if (config.stdinOpen) {
741
- args.push('-i');
742
- }
743
- if (config.removeOnExit) {
744
- args.push('--rm');
745
- }
746
- // name
747
- if (config.name) {
748
- args.push('--name', config.name);
749
- }
750
- // hostname / domain
751
- if (config.hostname) {
752
- args.push('--hostname', config.hostname);
753
- }
754
- if (config.domainname) {
755
- args.push('--domainname', config.domainname);
756
- }
757
- // environment
758
- if (config.environment) {
759
- for (const [key, value] of Object.entries(config.environment)) {
760
- args.push('-e', `${key}=${value}`);
761
- }
762
- }
763
- if (config.envFile) {
764
- for (const file of config.envFile) {
765
- args.push('--env-file', file);
766
- }
767
- }
768
- // labels
769
- if (config.labels) {
770
- for (const [key, value] of Object.entries(config.labels)) {
771
- args.push('--label', `${key}=${value}`);
772
- }
773
- }
774
- // ports
775
- if (config.publishAllPorts) {
776
- args.push('-P');
777
- }
778
- if (config.ports) {
779
- for (const p of config.ports) {
780
- if (!p.containerPort) {
781
- continue;
782
- }
783
- const mapping = (p.hostIP ? `${p.hostIP}:` : '') +
784
- (p.hostPort ? `${p.hostPort}:` : '') +
785
- p.containerPort +
786
- (p.protocol ? `/${p.protocol}` : '');
787
- args.push('-p', mapping);
788
- }
789
- }
790
- // volumes / mounts
791
- if (config.volumes) {
792
- for (const v of config.volumes) {
793
- args.push('-v', v);
794
- }
795
- }
796
- if (config.mounts) {
797
- for (const m of config.mounts) {
798
- let mount = `type=${m.type},target=${m.target}`;
799
- if (m.source) {
800
- mount += `,source=${m.source}`;
801
- }
802
- if (m.readOnly) {
803
- mount += `,readonly`;
804
- }
805
- args.push('--mount', mount);
806
- }
807
- }
808
- // restart policy
809
- if (config.restart?.policy) {
810
- const val = config.restart.policy === 'on-failure' && config.restart.maxRetries
811
- ? `on-failure:${config.restart.maxRetries}`
812
- : config.restart.policy;
813
- args.push('--restart', val);
814
- }
815
- // user & workdir
816
- if (config.user) {
817
- args.push('--user', String(config.user));
818
- }
819
- if (config.workdir) {
820
- args.push('--workdir', config.workdir);
821
- }
822
- // logging
823
- if (config.logging?.driver) {
824
- args.push('--log-driver', config.logging.driver);
825
- if (config.logging.options) {
826
- for (const [k, v] of Object.entries(config.logging.options)) {
827
- args.push('--log-opt', `${k}=${v}`);
828
- }
829
- }
830
- }
831
- // security
832
- if (config.security?.privileged) {
833
- args.push('--privileged');
834
- }
835
- if (config.security?.capAdd) {
836
- for (const cap of config.security.capAdd) {
837
- args.push('--cap-add', cap);
838
- }
839
- }
840
- if (config.security?.capDrop) {
841
- for (const cap of config.security.capDrop) {
842
- args.push('--cap-drop', cap);
843
- }
844
- }
845
- if (config.security?.noNewPrivileges) {
846
- args.push('--security-opt', 'no-new-privileges');
847
- }
848
- if (config.security?.apparmor) {
849
- args.push('--security-opt', `apparmor=${config.security.apparmor}`);
850
- }
851
- // network
852
- if (config.networkMode) {
853
- args.push('--network', config.networkMode);
854
- }
855
- // extra hosts
856
- if (config.extraHosts) {
857
- for (const host of config.extraHosts) {
858
- if (typeof host === 'string') {
859
- args.push('--add-host', host);
860
- }
861
- else {
862
- args.push('--add-host', `${host.host}:${host.ip}`);
863
- }
864
- }
865
- }
866
- // sysctls
867
- if (config.sysctls) {
868
- for (const [k, v] of Object.entries(config.sysctls)) {
869
- args.push('--sysctl', `${k}=${v}`);
870
- }
871
- }
872
- // stop signal / timeout
873
- if (config.stop?.signal) {
874
- args.push('--stop-signal', config.stop.signal);
875
- }
876
- if (config.stop?.gracePeriodSec !== undefined) {
877
- args.push('--stop-timeout', String(config.stop.gracePeriodSec));
878
- }
879
- // resources
880
- if (config.resources?.cpus) {
881
- args.push('--cpus', String(config.resources.cpus));
882
- }
883
- if (config.resources?.memory) {
884
- args.push('--memory', String(config.resources.memory));
885
- }
886
- // image
887
- if (!config.image) {
888
- throw new Error('ContainerConfig.image is required for docker run');
889
- }
890
- args.push(config.image);
891
- // command override
892
- if (config.command) {
893
- if (Array.isArray(config.command)) {
894
- args.push(...config.command);
895
- }
896
- else {
897
- args.push(config.command);
898
- }
899
- }
900
- return args.join(' ');
901
- }
902
- async #pollingInfo() {
903
- await this.isReady();
904
- if (!this.#installed) {
905
- await this.#adapter.sendToGui({
906
- command: 'info',
907
- error: 'not installed',
908
- });
909
- return;
910
- }
911
- const data = await this.discUsage();
912
- await this.#adapter.sendToGui({
913
- command: 'info',
914
- data,
915
- version: this.#dockerVersion,
916
- });
917
- }
918
- async #pollingImages() {
919
- await this.isReady();
920
- if (!this.#installed) {
921
- await this.#adapter.sendToGui({
922
- command: 'info',
923
- error: 'not installed',
924
- });
925
- return;
926
- }
927
- const data = await this.imageList();
928
- await this.#adapter.sendToGui({
929
- command: 'images',
930
- data,
931
- });
932
- }
933
- async #pollingContainers() {
934
- await this.isReady();
935
- if (!this.#installed) {
936
- await this.#adapter.sendToGui({
937
- command: 'info',
938
- error: 'not installed',
939
- });
940
- return;
941
- }
942
- const data = await this.containerList(true, this.#ownIps?.length ? this.#ownIps : undefined);
943
- await this.#adapter.sendToGui({
944
- command: 'containers',
945
- data,
946
- });
947
- }
948
- async #pollingContainer(container) {
949
- await this.isReady();
950
- if (!this.#installed) {
951
- await this.#adapter.sendToGui({
952
- command: 'info',
953
- error: 'not installed',
954
- });
955
- return;
956
- }
957
- const data = await this.containerInspect(container);
958
- await this.#adapter.sendToGui({
959
- command: 'container',
960
- container,
961
- data,
962
- });
963
- }
964
- async pollingUpdate(scan) {
965
- if (scan.info) {
966
- this.#timers.info ||= setInterval(async () => this.#pollingInfo(), 10_000);
967
- setTimeout(() => void this.#pollingInfo(), 50); // do it immediately, too
968
- }
969
- else if (this.#timers.info) {
970
- clearInterval(this.#timers.info);
971
- this.#timers.info = undefined;
972
- }
973
- if (scan.images) {
974
- this.#timers.images ||= setInterval(async () => {
975
- await this.#pollingImages();
976
- }, 10_000);
977
- setTimeout(() => void this.#pollingImages(), 50); // do it immediately, too
978
- }
979
- else if (this.#timers.images) {
980
- clearInterval(this.#timers.images);
981
- this.#timers.images = undefined;
982
- }
983
- if (scan.containers) {
984
- this.#ownIps = scan.containers;
985
- // try to find for all domain names the own IP
986
- for (const ipOrDomain of scan.containers) {
987
- if (this.#domain2ip[ipOrDomain] === undefined && !(0, range_check_1.isIP)(ipOrDomain)) {
988
- const ip = await getIpForDomain(ipOrDomain);
989
- if (ip) {
990
- this.#domain2ip[ipOrDomain] = ip;
991
- }
992
- else {
993
- this.#domain2ip[ipOrDomain] = false;
994
- }
995
- }
996
- }
997
- this.#timers.containers ||= setInterval(async () => this.#pollingContainers(), 10_000);
998
- setTimeout(() => void this.#pollingContainers(), 50); // do it immediately, too
999
- }
1000
- else if (this.#timers.containers) {
1001
- clearInterval(this.#timers.containers);
1002
- this.#timers.containers = undefined;
1003
- }
1004
- scan.container.forEach(container => {
1005
- if (container) {
1006
- this.#timers.container[container] ||= setInterval(async (_container) => this.#pollingContainer(_container), 10_000, container);
1007
- setTimeout(_container => void this.#pollingContainer(_container), 50, container); // do it immediately, too
1008
- }
1009
- });
1010
- // Check deleted containers
1011
- Object.keys(this.#timers.container).forEach(container => {
1012
- if (container && !scan.container.includes(container)) {
1013
- clearInterval(this.#timers.container[container]);
1014
- delete this.#timers.container[container];
1015
- }
1016
- });
1017
- }
1018
- destroy() {
1019
- // Destroy all running commands
1020
- Object.keys(this.#runningCommands).forEach(sid => {
1021
- this.containerExecTerminate(sid, true);
1022
- });
1023
- // destroy all timers
1024
- Object.keys(this.#timers.container).forEach((id) => {
1025
- clearTimeout(this.#timers.container[id]);
1026
- });
1027
- if (this.#timers.images) {
1028
- clearInterval(this.#timers.images);
1029
- delete this.#timers.images;
1030
- }
1031
- if (this.#timers.containers) {
1032
- clearInterval(this.#timers.containers);
1033
- delete this.#timers.containers;
1034
- }
1035
- if (this.#timers.info) {
1036
- clearInterval(this.#timers.info);
1037
- delete this.#timers.info;
1038
- }
1039
- }
1040
- }
1041
- exports.default = DockerCommands;
1042
- //# sourceMappingURL=DockerCommands.js.map