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.
@@ -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
- constructor(adapter, containers) {
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.AppArmorProfile,
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
- try {
638
- const result = await execPromise('docker --version');
639
- if (!result.stderr && result.stdout) {
640
- // "Docker version 28.3.2, build 578ccf6\n"
641
- return result.stdout.split('\n')[0].trim();
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
- catch (e) {
646
- this.adapter.log.debug(`Docker not installed: ${e.message}`);
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
- try {
732
- if (!image.includes(':')) {
733
- image += ':latest';
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 ${this.#toDockerRun(config)}`);
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 ${this.#toDockerRun(config, true)}`);
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 stop`);
1316
+ throw new Error(`Container ${containerInfo.id} still found after remove`);
821
1317
  }
822
1318
  }
823
- return await this.#exec(`create ${this.#toDockerRun(config, true)}`);
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 { id, image, command, createdAt, status: statusKey, uptime: uptime.join(' '), ports, names };
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
- #toDockerRun(config, create) {
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);