iobroker.docker-manager 0.1.0 → 0.1.3
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.
- package/README.md +54 -8
- package/admin/assets/{index-BF04nru-.js → index-DiHGumzD.js} +159 -137
- package/admin/index_m.html +2 -2
- package/admin/tab_m.html +2 -2
- package/build/lib/DockerManager.js +997 -35
- package/build/lib/DockerManager.js.map +1 -1
- package/build/lib/DockerMonitor.js +185 -33
- package/build/lib/DockerMonitor.js.map +1 -1
- package/build/main.js +87 -25
- package/build/main.js.map +1 -1
- package/io-package.json +53 -29
- package/package.json +11 -11
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
// This class implements docker commands using CLI and
|
|
3
3
|
// it monitors periodically the docker daemon status.
|
|
4
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
|
+
};
|
|
5
8
|
var _a;
|
|
6
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
10
|
const node_util_1 = require("node:util");
|
|
8
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"));
|
|
9
14
|
const execPromise = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
10
15
|
const dockerDefaults = {
|
|
11
16
|
tty: false,
|
|
@@ -50,11 +55,12 @@ function deepCompare(object1, object2) {
|
|
|
50
55
|
return true;
|
|
51
56
|
}
|
|
52
57
|
const keys1 = Object.keys(object1);
|
|
53
|
-
const keys2 = Object.keys(object2);
|
|
54
|
-
if (keys1.length !== keys2.length) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
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
|
+
}
|
|
58
64
|
if (!deepCompare(object1[key], object2[key])) {
|
|
59
65
|
return false;
|
|
60
66
|
}
|
|
@@ -67,11 +73,8 @@ function compareConfigs(desired, existing) {
|
|
|
67
73
|
// We only compare keys that are in the desired config
|
|
68
74
|
for (const key of keys) {
|
|
69
75
|
// ignore iob* properties as they belong to ioBroker configuration
|
|
70
|
-
if (key.startsWith('iob')) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
76
|
// ignore hostname
|
|
74
|
-
if (key === 'hostname') {
|
|
77
|
+
if (key.startsWith('iob') || key === 'hostname') {
|
|
75
78
|
continue;
|
|
76
79
|
}
|
|
77
80
|
if (typeof desired[key] === 'object' && desired[key] !== null) {
|
|
@@ -128,7 +131,7 @@ function removeUndefined(obj) {
|
|
|
128
131
|
}
|
|
129
132
|
return obj;
|
|
130
133
|
}
|
|
131
|
-
function cleanContainerConfig(obj) {
|
|
134
|
+
function cleanContainerConfig(obj, mayChange) {
|
|
132
135
|
obj = removeUndefined(obj);
|
|
133
136
|
Object.keys(obj).forEach(name => {
|
|
134
137
|
if (isDefault(obj[name], dockerDefaults[name])) {
|
|
@@ -141,6 +144,11 @@ function cleanContainerConfig(obj) {
|
|
|
141
144
|
}
|
|
142
145
|
obj.mounts = obj.mounts.map((mount) => {
|
|
143
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
|
+
}
|
|
144
152
|
delete m.readOnly;
|
|
145
153
|
return m;
|
|
146
154
|
});
|
|
@@ -195,24 +203,82 @@ function cleanContainerConfig(obj) {
|
|
|
195
203
|
else {
|
|
196
204
|
delete obj.environment;
|
|
197
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
|
+
}
|
|
198
243
|
}
|
|
199
244
|
});
|
|
200
245
|
obj.volumes?.sort();
|
|
201
246
|
return obj;
|
|
202
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
|
+
}
|
|
203
260
|
class DockerManager {
|
|
204
261
|
installed = false;
|
|
205
262
|
dockerVersion = '';
|
|
206
263
|
needSudo = false;
|
|
207
264
|
#waitReady;
|
|
265
|
+
#waitAllChecked;
|
|
266
|
+
#waitAllCheckedResolve;
|
|
208
267
|
adapter;
|
|
209
268
|
#ownContainers = [];
|
|
210
269
|
#monitoringInterval = null;
|
|
211
270
|
#ownContainersStats = {};
|
|
212
|
-
|
|
271
|
+
#driver = 'cli';
|
|
272
|
+
#dockerode = null;
|
|
273
|
+
#cliAvailable = false;
|
|
274
|
+
options;
|
|
275
|
+
#tarPack = null;
|
|
276
|
+
constructor(adapter, options, containers) {
|
|
213
277
|
this.adapter = adapter;
|
|
278
|
+
this.options = options || {};
|
|
214
279
|
this.#ownContainers = containers || [];
|
|
215
280
|
this.#waitReady = new Promise(resolve => this.#init().then(() => resolve()));
|
|
281
|
+
this.#waitAllChecked = new Promise(resolve => (this.#waitAllCheckedResolve = resolve));
|
|
216
282
|
}
|
|
217
283
|
/** Wait till the check if docker is installed and the daemon is running is ready */
|
|
218
284
|
isReady() {
|
|
@@ -307,7 +373,8 @@ class DockerManager {
|
|
|
307
373
|
ipc: inspect.HostConfig.IpcMode,
|
|
308
374
|
pid: inspect.HostConfig.PidMode,
|
|
309
375
|
seccomp: inspect.HostConfig.SecurityOpt?.find(opt => opt.startsWith('seccomp='))?.split('=')[1] ?? undefined,
|
|
310
|
-
apparmor: inspect.
|
|
376
|
+
apparmor: inspect.HostConfig.SecurityOpt?.find(opt => opt.startsWith('apparmor='))?.split('=')[1] ??
|
|
377
|
+
undefined,
|
|
311
378
|
groupAdd: inspect.HostConfig.GroupAdd ?? undefined,
|
|
312
379
|
noNewPrivileges: undefined, // Nicht direkt verfügbar
|
|
313
380
|
},
|
|
@@ -321,7 +388,7 @@ class DockerManager {
|
|
|
321
388
|
timezone: undefined, // Nicht direkt verfügbar
|
|
322
389
|
__meta: undefined, // Eigene Metadaten
|
|
323
390
|
};
|
|
324
|
-
return cleanContainerConfig(obj);
|
|
391
|
+
return cleanContainerConfig(obj, true);
|
|
325
392
|
}
|
|
326
393
|
/**
|
|
327
394
|
* Get information about the Docker daemon: is it running and which version
|
|
@@ -334,9 +401,77 @@ class DockerManager {
|
|
|
334
401
|
return {
|
|
335
402
|
version: this.dockerVersion,
|
|
336
403
|
daemonRunning,
|
|
404
|
+
removeSupported: !this.#dockerode || this.#cliAvailable,
|
|
405
|
+
driver: this.#driver,
|
|
337
406
|
};
|
|
338
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
|
+
}
|
|
339
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
|
+
}
|
|
340
475
|
const version = await this.#isDockerInstalled();
|
|
341
476
|
this.installed = !!version;
|
|
342
477
|
if (version) {
|
|
@@ -353,11 +488,18 @@ class DockerManager {
|
|
|
353
488
|
}
|
|
354
489
|
}
|
|
355
490
|
if (this.installed) {
|
|
491
|
+
// we still must check the sudo as autocompletion works only via CLI
|
|
356
492
|
this.needSudo = await this.#isNeedSudo();
|
|
357
493
|
await this.#checkOwnContainers();
|
|
358
494
|
}
|
|
495
|
+
else {
|
|
496
|
+
this.#waitAllCheckedResolve?.();
|
|
497
|
+
}
|
|
359
498
|
}
|
|
360
499
|
async #isDockerDaemonRunning() {
|
|
500
|
+
if (this.#dockerode) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
361
503
|
try {
|
|
362
504
|
const { stdout, stderr } = await execPromise('systemctl status docker');
|
|
363
505
|
// ● docker.service - Docker Application Container Engine
|
|
@@ -388,6 +530,9 @@ class DockerManager {
|
|
|
388
530
|
* @param container Container configuration
|
|
389
531
|
*/
|
|
390
532
|
async #ensureActualConfiguration(container) {
|
|
533
|
+
if (!container.name) {
|
|
534
|
+
throw new Error(`Container name must be a string, but got boolean true`);
|
|
535
|
+
}
|
|
391
536
|
// Check the configuration of the container
|
|
392
537
|
const inspect = await this.containerInspect(container.name);
|
|
393
538
|
if (inspect) {
|
|
@@ -402,8 +547,10 @@ class DockerManager {
|
|
|
402
547
|
this.adapter.log.warn(`Cannot recreate own container ${container.name}: ${result.stderr}`);
|
|
403
548
|
}
|
|
404
549
|
}
|
|
550
|
+
else {
|
|
551
|
+
this.adapter.log.debug(`Configuration of own container ${container.name} is up to date`);
|
|
552
|
+
}
|
|
405
553
|
// Check if container is running
|
|
406
|
-
this.adapter.log.debug(`Configuration of own container ${container.name} is up to date`);
|
|
407
554
|
const status = await this.containerList(true);
|
|
408
555
|
const containerInfo = status.find(it => it.names === container.name);
|
|
409
556
|
if (containerInfo) {
|
|
@@ -429,20 +576,34 @@ class DockerManager {
|
|
|
429
576
|
}
|
|
430
577
|
}
|
|
431
578
|
}
|
|
579
|
+
getDefaultContainerName() {
|
|
580
|
+
return `iob_${this.adapter.namespace.replace(/[-.]/g, '_')}`;
|
|
581
|
+
}
|
|
432
582
|
async #checkOwnContainers() {
|
|
433
583
|
if (!this.#ownContainers.length) {
|
|
584
|
+
this.#waitAllCheckedResolve?.();
|
|
434
585
|
return;
|
|
435
586
|
}
|
|
436
587
|
const status = await this.containerList(true);
|
|
437
588
|
let images = await this.imageList();
|
|
438
589
|
let anyStartedOrRunning = false;
|
|
439
590
|
const networkChecked = [];
|
|
591
|
+
const prefix = this.getDefaultContainerName();
|
|
440
592
|
for (let c = 0; c < this.#ownContainers.length; c++) {
|
|
441
593
|
const container = this.#ownContainers[c];
|
|
442
594
|
if (container.iobEnabled !== false) {
|
|
443
595
|
if (!container.image.includes(':')) {
|
|
444
596
|
container.image += ':latest';
|
|
445
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
|
+
}
|
|
446
607
|
try {
|
|
447
608
|
// create iobroker network if necessary
|
|
448
609
|
if (container.networkMode &&
|
|
@@ -450,6 +611,13 @@ class DockerManager {
|
|
|
450
611
|
container.networkMode !== 'host' &&
|
|
451
612
|
container.networkMode !== 'bridge' &&
|
|
452
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
|
+
}
|
|
453
621
|
if (!networkChecked.includes(container.networkMode)) {
|
|
454
622
|
// check if the network exists
|
|
455
623
|
const networks = await this.networkList();
|
|
@@ -460,6 +628,54 @@ class DockerManager {
|
|
|
460
628
|
networkChecked.push(container.networkMode);
|
|
461
629
|
}
|
|
462
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
|
+
}
|
|
463
679
|
let containerInfo = status.find(it => it.names === container.name);
|
|
464
680
|
let image = images.find(it => `${it.repository}:${it.tag}` === container.image);
|
|
465
681
|
if (container.iobAutoImageUpdate) {
|
|
@@ -525,6 +741,10 @@ class DockerManager {
|
|
|
525
741
|
if (anyStartedOrRunning) {
|
|
526
742
|
this.#monitoringInterval ||= setInterval(() => this.#monitorOwnContainers(), 60000);
|
|
527
743
|
}
|
|
744
|
+
this.#waitAllCheckedResolve?.();
|
|
745
|
+
}
|
|
746
|
+
allOwnContainersChecked() {
|
|
747
|
+
return this.#waitAllChecked;
|
|
528
748
|
}
|
|
529
749
|
async #monitorOwnContainers() {
|
|
530
750
|
// get the status of containers
|
|
@@ -532,7 +752,7 @@ class DockerManager {
|
|
|
532
752
|
// Check the status of own containers
|
|
533
753
|
for (let c = 0; c < this.#ownContainers.length; c++) {
|
|
534
754
|
const container = this.#ownContainers[c];
|
|
535
|
-
if (container.iobEnabled !== false && container.iobMonitoringEnabled) {
|
|
755
|
+
if (container.iobEnabled !== false && container.iobMonitoringEnabled && container.name) {
|
|
536
756
|
// Check if container is running
|
|
537
757
|
const running = containers.find(it => it.names === container.name);
|
|
538
758
|
if (!running || (running.status !== 'running' && running.status !== 'restarting')) {
|
|
@@ -634,16 +854,29 @@ class DockerManager {
|
|
|
634
854
|
return execPromise(finalCommand);
|
|
635
855
|
}
|
|
636
856
|
async #isDockerInstalled() {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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}`);
|
|
642
868
|
}
|
|
643
|
-
this.adapter.log.debug(`Docker not installed: ${result.stderr}`);
|
|
644
869
|
}
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
}
|
|
647
880
|
}
|
|
648
881
|
return false;
|
|
649
882
|
}
|
|
@@ -658,6 +891,57 @@ class DockerManager {
|
|
|
658
891
|
}
|
|
659
892
|
/** Get disk usage information */
|
|
660
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
|
+
}
|
|
661
945
|
const { stdout } = await this.#exec(`system df`);
|
|
662
946
|
const result = { total: { size: 0, reclaimable: 0 } };
|
|
663
947
|
// parse the output
|
|
@@ -728,10 +1012,49 @@ class DockerManager {
|
|
|
728
1012
|
}
|
|
729
1013
|
/** Pull an image from the registry */
|
|
730
1014
|
async imagePull(image) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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');
|
|
734
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 {
|
|
735
1058
|
const result = await this.#exec(`pull ${image}`);
|
|
736
1059
|
const images = await this.imageList();
|
|
737
1060
|
if (!images.find(it => `${it.repository}:${it.tag}` === image)) {
|
|
@@ -766,12 +1089,154 @@ class DockerManager {
|
|
|
766
1089
|
return [];
|
|
767
1090
|
}
|
|
768
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
|
+
}
|
|
769
1229
|
/**
|
|
770
1230
|
* Create and start a container with the given configuration. No checks are done.
|
|
771
1231
|
*/
|
|
772
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
|
+
}
|
|
773
1238
|
try {
|
|
774
|
-
return await this.#exec(`run ${
|
|
1239
|
+
return await this.#exec(`run ${_a.toDockerRun(config)}`);
|
|
775
1240
|
}
|
|
776
1241
|
catch (e) {
|
|
777
1242
|
return { stdout: '', stderr: e.message.toString() };
|
|
@@ -781,8 +1246,12 @@ class DockerManager {
|
|
|
781
1246
|
* Create a container with the given configuration without starting it. No checks are done.
|
|
782
1247
|
*/
|
|
783
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
|
+
}
|
|
784
1253
|
try {
|
|
785
|
-
return await this.#exec(`create ${
|
|
1254
|
+
return await this.#exec(`create ${_a.toDockerRun(config, true)}`);
|
|
786
1255
|
}
|
|
787
1256
|
catch (e) {
|
|
788
1257
|
return { stdout: '', stderr: e.message.toString() };
|
|
@@ -793,11 +1262,38 @@ class DockerManager {
|
|
|
793
1262
|
*
|
|
794
1263
|
* This function checks if a container is running, stops it if necessary,
|
|
795
1264
|
* removes it and creates a new one with the given configuration.
|
|
1265
|
+
* The container is not started after creation.
|
|
796
1266
|
*
|
|
797
1267
|
* @param config new configuration
|
|
798
1268
|
* @returns stdout and stderr of the create command
|
|
799
1269
|
*/
|
|
800
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
|
+
}
|
|
801
1297
|
try {
|
|
802
1298
|
// Get if the container is running
|
|
803
1299
|
let containers = await this.containerList();
|
|
@@ -817,10 +1313,18 @@ class DockerManager {
|
|
|
817
1313
|
containers = await this.containerList();
|
|
818
1314
|
if (containers.find(it => it.id === containerInfo.id)) {
|
|
819
1315
|
this.adapter.log.warn(`Cannot remove container: ${rmResult.stderr || rmResult.stdout}`);
|
|
820
|
-
throw new Error(`Container ${containerInfo.id} still found after
|
|
1316
|
+
throw new Error(`Container ${containerInfo.id} still found after remove`);
|
|
821
1317
|
}
|
|
822
1318
|
}
|
|
823
|
-
return await this.#exec(`create ${
|
|
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`);
|
|
824
1328
|
}
|
|
825
1329
|
catch (e) {
|
|
826
1330
|
return { stdout: '', stderr: e.message.toString() };
|
|
@@ -828,6 +1332,20 @@ class DockerManager {
|
|
|
828
1332
|
}
|
|
829
1333
|
/** List all images */
|
|
830
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
|
+
}
|
|
831
1349
|
try {
|
|
832
1350
|
const { stdout } = await this.#exec('images --format "{{.Repository}}:{{.Tag}};{{.ID}};{{.CreatedAt}};{{.Size}}"');
|
|
833
1351
|
return stdout
|
|
@@ -852,6 +1370,37 @@ class DockerManager {
|
|
|
852
1370
|
}
|
|
853
1371
|
/** Build an image from a Dockerfile */
|
|
854
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
|
+
}
|
|
855
1404
|
try {
|
|
856
1405
|
return await this.#exec(`build -t ${tag} -f ${dockerfilePath} .`);
|
|
857
1406
|
}
|
|
@@ -882,8 +1431,41 @@ class DockerManager {
|
|
|
882
1431
|
return { stdout: '', stderr: e.message.toString() };
|
|
883
1432
|
}
|
|
884
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
|
+
}
|
|
885
1462
|
/** Inspect an image */
|
|
886
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
|
+
}
|
|
887
1469
|
try {
|
|
888
1470
|
const { stdout } = await this.#exec(`inspect ${imageId}`);
|
|
889
1471
|
return JSON.parse(stdout)[0];
|
|
@@ -893,6 +1475,23 @@ class DockerManager {
|
|
|
893
1475
|
return null;
|
|
894
1476
|
}
|
|
895
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
|
+
}
|
|
896
1495
|
#parseSize(sizeStr) {
|
|
897
1496
|
const units = {
|
|
898
1497
|
B: 1,
|
|
@@ -915,6 +1514,21 @@ class DockerManager {
|
|
|
915
1514
|
* @param container Container name or ID
|
|
916
1515
|
*/
|
|
917
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
|
+
}
|
|
918
1532
|
try {
|
|
919
1533
|
let containers = await this.containerList();
|
|
920
1534
|
// find ID of container
|
|
@@ -939,6 +1553,21 @@ class DockerManager {
|
|
|
939
1553
|
* @param container Container name or ID
|
|
940
1554
|
*/
|
|
941
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
|
+
}
|
|
942
1571
|
try {
|
|
943
1572
|
let containers = await this.containerList();
|
|
944
1573
|
// find ID of container
|
|
@@ -967,6 +1596,18 @@ class DockerManager {
|
|
|
967
1596
|
* @param timeoutSeconds Timeout in seconds to wait before killing the container (default: 5)
|
|
968
1597
|
*/
|
|
969
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
|
+
}
|
|
970
1611
|
try {
|
|
971
1612
|
const containers = await this.containerList();
|
|
972
1613
|
// find ID of container
|
|
@@ -980,6 +1621,20 @@ class DockerManager {
|
|
|
980
1621
|
return { stdout: '', stderr: e.message.toString() };
|
|
981
1622
|
}
|
|
982
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
|
+
}
|
|
983
1638
|
/**
|
|
984
1639
|
* Remove the container and if necessary, stop it first
|
|
985
1640
|
*
|
|
@@ -1018,19 +1673,90 @@ class DockerManager {
|
|
|
1018
1673
|
* @param all If true, list all containers. If false, list only running containers. Default is true.
|
|
1019
1674
|
*/
|
|
1020
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
|
+
}
|
|
1021
1731
|
try {
|
|
1022
|
-
const { stdout } = await this.#exec(`ps ${all ? '-a' : ''} --format "{{.Names}};{{.Status}};{{.ID}};{{.Image}};{{.Command}};{{.CreatedAt}};{{.Ports}}"`);
|
|
1732
|
+
const { stdout } = await this.#exec(`ps ${all ? '-a' : ''} --format "{{.Names}};{{.Status}};{{.ID}};{{.Image}};{{.Command}};{{.CreatedAt}};{{.Ports}};{{.Labels}}"`);
|
|
1023
1733
|
return stdout
|
|
1024
1734
|
.split('\n')
|
|
1025
1735
|
.filter(line => line.trim() !== '')
|
|
1026
1736
|
.map(line => {
|
|
1027
|
-
const [names, statusInfo, id, image, command, createdAt, ports] = line.split(';');
|
|
1737
|
+
const [names, statusInfo, id, image, command, createdAt, ports, labels] = line.split(';');
|
|
1028
1738
|
const [status, ...uptime] = statusInfo.split(' ');
|
|
1029
1739
|
let statusKey = status.toLowerCase();
|
|
1030
1740
|
if (statusKey === 'up') {
|
|
1031
1741
|
statusKey = 'running';
|
|
1032
1742
|
}
|
|
1033
|
-
return {
|
|
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
|
+
};
|
|
1034
1760
|
});
|
|
1035
1761
|
}
|
|
1036
1762
|
catch (e) {
|
|
@@ -1047,6 +1773,27 @@ class DockerManager {
|
|
|
1047
1773
|
* @param options.follow If true, follow the logs (not implemented yet)
|
|
1048
1774
|
*/
|
|
1049
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
|
+
}
|
|
1050
1797
|
try {
|
|
1051
1798
|
const args = [];
|
|
1052
1799
|
if (options.tail !== undefined) {
|
|
@@ -1066,8 +1813,28 @@ class DockerManager {
|
|
|
1066
1813
|
.map((line) => line.trim());
|
|
1067
1814
|
}
|
|
1068
1815
|
}
|
|
1816
|
+
static dockerodeInspect2DockerContainerInspect(data) {
|
|
1817
|
+
return {
|
|
1818
|
+
...data,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1069
1821
|
/** Inspect a container */
|
|
1070
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
|
+
}
|
|
1071
1838
|
try {
|
|
1072
1839
|
const { stdout } = await this.#exec(`inspect ${containerNameOrId}`);
|
|
1073
1840
|
const result = JSON.parse(stdout)[0];
|
|
@@ -1081,10 +1848,27 @@ class DockerManager {
|
|
|
1081
1848
|
return null;
|
|
1082
1849
|
}
|
|
1083
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
|
+
}
|
|
1084
1868
|
/**
|
|
1085
1869
|
* Build a docker run command string from ContainerConfig
|
|
1086
1870
|
*/
|
|
1087
|
-
|
|
1871
|
+
static toDockerRun(config, create) {
|
|
1088
1872
|
const args = [];
|
|
1089
1873
|
// detach / interactive
|
|
1090
1874
|
if (config.detach !== false && !create) {
|
|
@@ -1208,7 +1992,7 @@ class DockerManager {
|
|
|
1208
1992
|
args.push('--security-opt', `apparmor=${config.security.apparmor}`);
|
|
1209
1993
|
}
|
|
1210
1994
|
// network
|
|
1211
|
-
if (config.networkMode) {
|
|
1995
|
+
if (config.networkMode && typeof config.networkMode === 'string') {
|
|
1212
1996
|
args.push('--network', config.networkMode);
|
|
1213
1997
|
}
|
|
1214
1998
|
// extra hosts
|
|
@@ -1259,6 +2043,15 @@ class DockerManager {
|
|
|
1259
2043
|
return args.join(' ');
|
|
1260
2044
|
}
|
|
1261
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
|
+
}
|
|
1262
2055
|
// docker network ls
|
|
1263
2056
|
try {
|
|
1264
2057
|
const { stdout } = await this.#exec(`network ls --format "{{.Name}};{{.ID}};{{.Driver}};{{.Scope}}"`);
|
|
@@ -1276,6 +2069,14 @@ class DockerManager {
|
|
|
1276
2069
|
}
|
|
1277
2070
|
}
|
|
1278
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
|
+
}
|
|
1279
2080
|
const result = await this.#exec(`network create ${driver ? `--driver ${driver}` : ''} ${name}`);
|
|
1280
2081
|
const networks = await this.networkList();
|
|
1281
2082
|
if (!networks.find(it => it.name === name)) {
|
|
@@ -1291,6 +2092,167 @@ class DockerManager {
|
|
|
1291
2092
|
}
|
|
1292
2093
|
return { ...result, networks };
|
|
1293
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
|
+
}
|
|
1294
2256
|
/** Stop own containers if necessary */
|
|
1295
2257
|
async destroy() {
|
|
1296
2258
|
if (this.#monitoringInterval) {
|
|
@@ -1298,7 +2260,7 @@ class DockerManager {
|
|
|
1298
2260
|
this.#monitoringInterval = null;
|
|
1299
2261
|
}
|
|
1300
2262
|
for (const container of this.#ownContainers) {
|
|
1301
|
-
if (container.iobEnabled !== false && container.iobStopOnUnload) {
|
|
2263
|
+
if (container.iobEnabled !== false && container.iobStopOnUnload && container.name) {
|
|
1302
2264
|
this.adapter.log.info(`Stopping own container ${container.name} on destroy`);
|
|
1303
2265
|
try {
|
|
1304
2266
|
await this.containerStop(container.name);
|