iobroker.admin 7.7.1 → 7.7.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 +44 -11
- package/admin/admin.png +0 -0
- package/admin/i18n/de.json +1 -1
- package/admin/i18n/es.json +1 -1
- package/admin/i18n/fr.json +1 -1
- package/admin/i18n/it.json +1 -1
- package/admin/i18n/nl.json +1 -1
- package/admin/i18n/pl.json +1 -1
- package/admin/i18n/pt.json +1 -1
- package/admin/i18n/ru.json +1 -1
- package/admin/i18n/uk.json +1 -1
- package/admin/i18n/zh-cn.json +1 -1
- package/admin/jsonConfig.json5 +1 -2
- package/adminWww/assets/{AdapterUpdateDialog-Bgswiqs9.js → AdapterUpdateDialog-BQUTMe7v.js} +1 -1
- package/adminWww/assets/Adapters-KuMqwfzU.js +9 -0
- package/adminWww/assets/Config-LH40Fzyp.js +1 -0
- package/adminWww/assets/CustomModal-UQ6ICKdg.js +1 -0
- package/adminWww/assets/CustomTab-CE2qvngn.js +1 -0
- package/adminWww/assets/DefaultPropsProvider-BxjOYH1h.js +30 -0
- package/adminWww/assets/EasyMode-XXQKZmWs.js +1 -0
- package/adminWww/assets/{Enums-8m252lzC.js → Enums-Ddmi-fYo.js} +3 -3
- package/adminWww/assets/{Fields-lI3YkIzQ.js → Fields-CQNdUYkd.js} +1 -1
- package/adminWww/assets/{Files-fQpSPYWI.js → Files-DgOiCRiN.js} +2 -2
- package/adminWww/assets/FilledInput-B9JSi0Ta.js +2 -0
- package/adminWww/assets/Hosts-BgDpLhdl.js +121 -0
- package/adminWww/assets/Instances-DikJwBl0.js +2 -0
- package/adminWww/assets/Intro-vxIHtYzr.js +2 -0
- package/adminWww/assets/Logs-DZUWTbJn.js +1 -0
- package/adminWww/assets/Objects-Dm1_4aGR.js +60 -0
- package/adminWww/assets/{State-nZ6sjU9_.js → State-CrcWiBdx.js} +1 -1
- package/adminWww/assets/TextField-j-uewRwI.js +138 -0
- package/adminWww/assets/ThemeProvider-DHntAcOp.js +29 -0
- package/adminWww/assets/Users-GcWjwa7A.js +1 -0
- package/adminWww/assets/ace-1DqSbvTN.js +985 -0
- package/adminWww/assets/bootstrap-B3ZI_-0g.js +2024 -0
- package/adminWww/assets/{createSvgIcon-DuKI6CvW.js → createSvgIcon-Ds12z7B3.js} +1 -1
- package/adminWww/assets/emotion-cache.browser.esm-BgJfNZJN.js +1 -0
- package/adminWww/assets/emotion-react.browser.esm-CBtpmfsG.js +8 -0
- package/adminWww/assets/emotion-serialize.esm-Q4o_CgeF.js +1 -0
- package/adminWww/assets/emotion-styled.browser.esm-BeD8nBzN.js +1 -0
- package/adminWww/assets/emotion-use-insertion-effect-with-fallbacks.browser.esm-BIy6Leuu.js +1 -0
- package/adminWww/assets/geosearch.module-B6h9BuIR.js +1 -0
- package/adminWww/assets/hostInit-fzxL8JAx.js +1 -1
- package/adminWww/assets/iconBase-BAOnvyiQ.js +1 -0
- package/adminWww/assets/{index-BP5kWU3L.js → index-BOnh7es7.js} +1 -1
- package/adminWww/assets/{index-BmWdDCX_.js → index-BSfwmoAE.js} +4 -4
- package/adminWww/assets/{index-gHyOgbGJ.js → index-BVgYaw7t.js} +1 -1
- package/adminWww/assets/{index-D7Rlw-5R.js → index-C9i3HTsG.js} +1 -1
- package/adminWww/assets/{index-DH1kmwK1.js → index-CELb-gCK.js} +2 -2
- package/adminWww/assets/{index-HOidq_rM.js → index-CU6eZCem.js} +1 -1
- package/adminWww/assets/{index-Bsv_DXxB.js → index-DZ0MncEz.js} +2 -2
- package/adminWww/assets/index-Dg-s3k0-.js +10 -0
- package/adminWww/assets/index-tS5iCAF7.js +55 -0
- package/adminWww/assets/iobroker_admin__loadShare___mf_0_emotion_mf_1_react__loadShare__-CXogaIXO.js +1 -0
- package/adminWww/assets/{iobroker_admin__loadShare__leaflet__loadShare__-B1OZ7tj-.js → iobroker_admin__loadShare__leaflet__loadShare__-DIpcqFbm.js} +1 -1
- package/adminWww/assets/iobroker_admin__loadShare__prop_mf_2_types__loadShare__-pDONiJUe.js +1 -0
- package/adminWww/assets/{iobroker_admin__loadShare__react__loadShare__-DtlEM_52.js → iobroker_admin__loadShare__react__loadShare__-CuzHmAOj.js} +1 -1
- package/adminWww/assets/iobroker_admin__mf_v__runtimeInit__mf_v__-CHE4rLsT.js +5 -0
- package/adminWww/assets/leaflet-src-BRBM-1HK.js +4 -0
- package/adminWww/assets/{sentry-CNy_SQq1.js → sentry-B6yHtsbd.js} +1 -1
- package/adminWww/assets/{zh-cn-DZTx1vAh.js → zh-cn-heTcsLnU.js} +15 -15
- package/adminWww/img/admin.png +0 -0
- package/adminWww/index.html +2 -2
- package/adminWww/mf-manifest.json +1 -1
- package/adminWww/remoteEntry.js +2 -2
- package/adminWww/static/js/worker-javascript.js +1 -1
- package/adminWww/static/js/worker-yaml.js +1 -1
- package/build/i18n/de.json +18 -0
- package/build/i18n/es.json +18 -0
- package/build/i18n/fr.json +18 -0
- package/build/i18n/it.json +18 -0
- package/build/i18n/nl.json +18 -0
- package/build/i18n/pl.json +18 -0
- package/build/i18n/pt.json +18 -0
- package/build/i18n/ru.json +18 -0
- package/build/i18n/uk.json +18 -0
- package/build/i18n/zh-cn.json +18 -0
- package/build/lib/DockerManager.js +1315 -0
- package/build/lib/DockerManager.js.map +1 -0
- package/build/lib/dockerManager.types.js +3 -0
- package/build/lib/dockerManager.types.js.map +1 -0
- package/{build-backend → build}/lib/testPassword.js +1 -1
- package/{build-backend → build}/lib/testPassword.js.map +1 -1
- package/{build-backend/src → build}/lib/translations.js +16 -1
- package/build/lib/translations.js.map +1 -0
- package/{build-backend → build}/main.js +58 -5
- package/build/main.js.map +1 -0
- package/io-package.json +27 -27
- package/package.json +13 -12
- package/adminWww/assets/Adapters-yQiEeoh8.js +0 -7
- package/adminWww/assets/Config-C1Gjt8DC.js +0 -1
- package/adminWww/assets/CustomModal-CgEtkfCG.js +0 -1
- package/adminWww/assets/CustomTab-CwioFUor.js +0 -1
- package/adminWww/assets/DefaultPropsProvider-DxsXuIyE.js +0 -30
- package/adminWww/assets/EasyMode-CN-reJVI.js +0 -1
- package/adminWww/assets/FilledInput-CKswrNd5.js +0 -2
- package/adminWww/assets/Hosts-BPdUlXV5.js +0 -121
- package/adminWww/assets/Instances-BStmfz4r.js +0 -2
- package/adminWww/assets/Intro-Dr1-aqO9.js +0 -2
- package/adminWww/assets/Logs-xCCrWV30.js +0 -1
- package/adminWww/assets/Objects-BF8brXaf.js +0 -60
- package/adminWww/assets/Tabs-BJ1rL35Z.js +0 -138
- package/adminWww/assets/Users-B0iasrSG.js +0 -1
- package/adminWww/assets/ace-CFIUQz8j.js +0 -970
- package/adminWww/assets/blueGrey-D-KQaGde.js +0 -29
- package/adminWww/assets/bootstrap-DDrI3lVl.js +0 -2036
- package/adminWww/assets/emotion-cache.browser.esm-B8BFze5o.js +0 -1
- package/adminWww/assets/emotion-react.browser.esm-DApPKwaj.js +0 -8
- package/adminWww/assets/emotion-serialize.esm-BUa21YfQ.js +0 -1
- package/adminWww/assets/emotion-styled.browser.esm-Dc05hmeu.js +0 -1
- package/adminWww/assets/emotion-utils.browser.esm-DOjH6Olm.js +0 -1
- package/adminWww/assets/geosearch.module-CWfRtxrN.js +0 -1
- package/adminWww/assets/iconBase-CgxZ5MMC.js +0 -1
- package/adminWww/assets/index-BjVqnwqt.js +0 -10
- package/adminWww/assets/index-RvHSqeMD.js +0 -55
- package/adminWww/assets/iobroker_admin__loadShare___mf_0_emotion_mf_1_react__loadShare__-gw2iqPBS.js +0 -1
- package/adminWww/assets/iobroker_admin__loadShare__prop_mf_2_types__loadShare__-BGyJCibC.js +0 -1
- package/adminWww/assets/iobroker_admin__mf_v__runtimeInit__mf_v__-VDoBF19b.js +0 -10
- package/adminWww/assets/leaflet-src-A2ZHl6nF.js +0 -4
- package/build-backend/i18n/de.json +0 -18
- package/build-backend/i18n/es.json +0 -18
- package/build-backend/i18n/fr.json +0 -18
- package/build-backend/i18n/it.json +0 -18
- package/build-backend/i18n/nl.json +0 -18
- package/build-backend/i18n/pl.json +0 -18
- package/build-backend/i18n/pt.json +0 -18
- package/build-backend/i18n/ru.json +0 -18
- package/build-backend/i18n/uk.json +0 -18
- package/build-backend/i18n/zh-cn.json +0 -18
- package/build-backend/lib/translations.js +0 -27
- package/build-backend/lib/translations.js.map +0 -1
- package/build-backend/main.js.map +0 -1
- package/build-backend/src/lib/checkLinuxPass.js +0 -280
- package/build-backend/src/lib/checkLinuxPass.js.map +0 -1
- package/build-backend/src/lib/testPassword.js +0 -58
- package/build-backend/src/lib/testPassword.js.map +0 -1
- package/build-backend/src/lib/translations.js.map +0 -1
- package/build-backend/src/lib/utils.js +0 -344
- package/build-backend/src/lib/utils.js.map +0 -1
- package/build-backend/src/lib/web.js +0 -1165
- package/build-backend/src/lib/web.js.map +0 -1
- package/build-backend/src/main.js +0 -1704
- package/build-backend/src/main.js.map +0 -1
- package/build-backend/src-admin/src/components/Adapters/Utils.js +0 -39
- package/build-backend/src-admin/src/components/Adapters/Utils.js.map +0 -1
- /package/{build-backend → build}/i18n/en.json +0 -0
- /package/{build-backend → build}/lib/checkLinuxPass.js +0 -0
- /package/{build-backend → build}/lib/checkLinuxPass.js.map +0 -0
- /package/{build-backend → build}/lib/utils.js +0 -0
- /package/{build-backend → build}/lib/utils.js.map +0 -0
- /package/{build-backend → build}/lib/web.js +0 -0
- /package/{build-backend → build}/lib/web.js.map +0 -0
|
@@ -0,0 +1,1315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// This class implements docker commands using CLI and
|
|
3
|
+
// it monitors periodically the docker daemon status.
|
|
4
|
+
// It manages containers defined in adapter.config.containers and monitors other containers
|
|
5
|
+
var _a;
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_util_1 = require("node:util");
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const execPromise = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
10
|
+
const dockerDefaults = {
|
|
11
|
+
tty: false,
|
|
12
|
+
stdinOpen: false,
|
|
13
|
+
attachStdin: false,
|
|
14
|
+
attachStdout: false,
|
|
15
|
+
attachStderr: false,
|
|
16
|
+
openStdin: false,
|
|
17
|
+
publishAllPorts: false,
|
|
18
|
+
readOnly: false,
|
|
19
|
+
user: '',
|
|
20
|
+
workdir: '',
|
|
21
|
+
domainname: '',
|
|
22
|
+
macAddress: '',
|
|
23
|
+
networkMode: 'bridge',
|
|
24
|
+
};
|
|
25
|
+
function isDefault(value, def) {
|
|
26
|
+
return JSON.stringify(value) === JSON.stringify(def);
|
|
27
|
+
}
|
|
28
|
+
function deepCompare(object1, object2) {
|
|
29
|
+
if (typeof object1 === 'number') {
|
|
30
|
+
object1 = object1.toString();
|
|
31
|
+
}
|
|
32
|
+
if (typeof object2 === 'number') {
|
|
33
|
+
object2 = object2.toString();
|
|
34
|
+
}
|
|
35
|
+
if (typeof object1 !== typeof object2) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (typeof object1 !== 'object' || object1 === null || object2 === null) {
|
|
39
|
+
return object1 === object2;
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(object1)) {
|
|
42
|
+
if (!Array.isArray(object2) || object1.length !== object2.length) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i < object1.length; i++) {
|
|
46
|
+
if (!deepCompare(object1[i], object2[i])) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const keys1 = Object.keys(object1);
|
|
53
|
+
const keys2 = Object.keys(object2);
|
|
54
|
+
if (keys1.length !== keys2.length) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
for (const key of keys1) {
|
|
58
|
+
if (!deepCompare(object1[key], object2[key])) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function compareConfigs(desired, existing) {
|
|
65
|
+
const diffs = [];
|
|
66
|
+
const keys = Object.keys(desired);
|
|
67
|
+
// We only compare keys that are in the desired config
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
// ignore iob* properties as they belong to ioBroker configuration
|
|
70
|
+
if (key.startsWith('iob')) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// ignore hostname
|
|
74
|
+
if (key === 'hostname') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (typeof desired[key] === 'object' && desired[key] !== null) {
|
|
78
|
+
if (Array.isArray(desired[key])) {
|
|
79
|
+
if (!Array.isArray(existing[key]) || desired[key].length !== existing[key].length) {
|
|
80
|
+
diffs.push(key);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
for (let i = 0; i < desired[key].length; i++) {
|
|
84
|
+
if (!deepCompare(desired[key][i], existing[key][i])) {
|
|
85
|
+
diffs.push(`${key}[${i}]`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
Object.keys(desired[key]).forEach((subKey) => {
|
|
92
|
+
if (!deepCompare(desired[key][subKey], existing[key][subKey])) {
|
|
93
|
+
diffs.push(`${key}.${subKey}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (desired[key] !== existing[key]) {
|
|
99
|
+
diffs.push(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return diffs;
|
|
103
|
+
}
|
|
104
|
+
// remove undefined entries recursively
|
|
105
|
+
function removeUndefined(obj) {
|
|
106
|
+
if (Array.isArray(obj)) {
|
|
107
|
+
const arr = obj.map(v => (v && typeof v === 'object' ? removeUndefined(v) : v)).filter(v => v !== undefined);
|
|
108
|
+
if (!arr.length) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return arr;
|
|
112
|
+
}
|
|
113
|
+
if (obj && typeof obj === 'object') {
|
|
114
|
+
const _obj = Object.fromEntries(Object.entries(obj)
|
|
115
|
+
.map(([k, v]) => [k, v && typeof v === 'object' ? removeUndefined(v) : v])
|
|
116
|
+
.filter(([_, v]) => v !== undefined &&
|
|
117
|
+
v !== null &&
|
|
118
|
+
v !== '' &&
|
|
119
|
+
!(Array.isArray(v) && v.length === 0) &&
|
|
120
|
+
!(typeof v === 'object' && Object.keys(v).length === 0)));
|
|
121
|
+
if (Object.keys(_obj).length === 0) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return _obj;
|
|
125
|
+
}
|
|
126
|
+
if (obj === '') {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
function cleanContainerConfig(obj) {
|
|
132
|
+
obj = removeUndefined(obj);
|
|
133
|
+
Object.keys(obj).forEach(name => {
|
|
134
|
+
if (isDefault(obj[name], dockerDefaults[name])) {
|
|
135
|
+
delete obj[name];
|
|
136
|
+
}
|
|
137
|
+
if (name === 'mounts') {
|
|
138
|
+
if (!obj.mounts) {
|
|
139
|
+
delete obj.mounts;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
obj.mounts = obj.mounts.map((mount) => {
|
|
143
|
+
const m = { ...mount };
|
|
144
|
+
delete m.readOnly;
|
|
145
|
+
return m;
|
|
146
|
+
});
|
|
147
|
+
if (!obj.mounts.length) {
|
|
148
|
+
delete obj.mounts;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
obj.mounts?.sort((a, b) => a.target.localeCompare(b.target));
|
|
152
|
+
}
|
|
153
|
+
if (name === 'ports') {
|
|
154
|
+
if (!obj.ports) {
|
|
155
|
+
delete obj.ports;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
obj.ports = obj.ports.map((port) => {
|
|
159
|
+
const p = { ...port };
|
|
160
|
+
if (p.protocol === 'tcp') {
|
|
161
|
+
delete p.protocol;
|
|
162
|
+
}
|
|
163
|
+
return p;
|
|
164
|
+
});
|
|
165
|
+
if (!obj.ports.length) {
|
|
166
|
+
delete obj.ports;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
obj.ports?.sort((a, b) => {
|
|
170
|
+
if (a.hostPort !== b.hostPort) {
|
|
171
|
+
return parseInt(a.containerPort, 10) - parseInt(b.containerPort, 10);
|
|
172
|
+
}
|
|
173
|
+
if (a.hostIP !== b.hostIP && a.hostIP && b.hostIP) {
|
|
174
|
+
return a.hostIP?.localeCompare(b.hostIP);
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (name === 'environment') {
|
|
180
|
+
if (!obj.environment) {
|
|
181
|
+
delete obj.environment;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const env = obj.environment;
|
|
185
|
+
if (Object.keys(env).length) {
|
|
186
|
+
obj.environment = {};
|
|
187
|
+
Object.keys(env)
|
|
188
|
+
.sort()
|
|
189
|
+
.forEach(key => {
|
|
190
|
+
if (key && env[key]) {
|
|
191
|
+
obj.environment[key] = env[key];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
delete obj.environment;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
obj.volumes?.sort();
|
|
201
|
+
return obj;
|
|
202
|
+
}
|
|
203
|
+
class DockerManager {
|
|
204
|
+
installed = false;
|
|
205
|
+
dockerVersion = '';
|
|
206
|
+
needSudo = false;
|
|
207
|
+
#waitReady;
|
|
208
|
+
adapter;
|
|
209
|
+
#ownContainers = [];
|
|
210
|
+
#monitoringInterval = null;
|
|
211
|
+
#ownContainersStats = {};
|
|
212
|
+
constructor(adapter, containers) {
|
|
213
|
+
this.adapter = adapter;
|
|
214
|
+
this.#ownContainers = containers || [];
|
|
215
|
+
this.#waitReady = new Promise(resolve => this.#init().then(() => resolve()));
|
|
216
|
+
}
|
|
217
|
+
/** Wait till the check if docker is installed and the daemon is running is ready */
|
|
218
|
+
isReady() {
|
|
219
|
+
return this.#waitReady;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Convert information from inspect to docker configuration to start it
|
|
223
|
+
*
|
|
224
|
+
* @param inspect Inspect information
|
|
225
|
+
*/
|
|
226
|
+
static mapInspectToConfig(inspect) {
|
|
227
|
+
const obj = {
|
|
228
|
+
image: inspect.Config.Image,
|
|
229
|
+
name: inspect.Name.replace(/^\//, ''),
|
|
230
|
+
command: inspect.Config.Cmd ?? undefined,
|
|
231
|
+
entrypoint: inspect.Config.Entrypoint ?? undefined,
|
|
232
|
+
user: inspect.Config.User ?? undefined,
|
|
233
|
+
workdir: inspect.Config.WorkingDir ?? undefined,
|
|
234
|
+
hostname: inspect.Config.Hostname ?? undefined,
|
|
235
|
+
domainname: inspect.Config.Domainname ?? undefined,
|
|
236
|
+
macAddress: inspect.NetworkSettings.MacAddress ?? undefined,
|
|
237
|
+
environment: inspect.Config.Env
|
|
238
|
+
? Object.fromEntries(inspect.Config.Env.map(e => {
|
|
239
|
+
const [key, ...rest] = e.split('=');
|
|
240
|
+
return [key, rest.join('=')];
|
|
241
|
+
}))
|
|
242
|
+
: undefined,
|
|
243
|
+
labels: inspect.Config.Labels ?? undefined,
|
|
244
|
+
tty: inspect.Config.Tty,
|
|
245
|
+
stdinOpen: inspect.Config.OpenStdin,
|
|
246
|
+
attachStdin: inspect.Config.AttachStdin,
|
|
247
|
+
attachStdout: inspect.Config.AttachStdout,
|
|
248
|
+
attachStderr: inspect.Config.AttachStderr,
|
|
249
|
+
openStdin: inspect.Config.OpenStdin,
|
|
250
|
+
publishAllPorts: inspect.HostConfig.PublishAllPorts,
|
|
251
|
+
ports: inspect.HostConfig.PortBindings
|
|
252
|
+
? Object.entries(inspect.HostConfig.PortBindings).flatMap(([containerPort, bindings]) => bindings.map(binding => ({
|
|
253
|
+
containerPort: containerPort.split('/')[0],
|
|
254
|
+
protocol: containerPort.split('/')[1] || 'tcp',
|
|
255
|
+
hostPort: binding.HostPort,
|
|
256
|
+
hostIP: binding.HostIp,
|
|
257
|
+
})))
|
|
258
|
+
: undefined,
|
|
259
|
+
mounts: inspect.Mounts?.map(mount => ({
|
|
260
|
+
type: mount.Type,
|
|
261
|
+
source: mount.Source,
|
|
262
|
+
target: mount.Destination,
|
|
263
|
+
readOnly: mount.RW,
|
|
264
|
+
})),
|
|
265
|
+
volumes: inspect.Config.Volumes ? Object.keys(inspect.Config.Volumes) : undefined,
|
|
266
|
+
extraHosts: inspect.HostConfig.ExtraHosts ?? undefined,
|
|
267
|
+
dns: {
|
|
268
|
+
servers: inspect.HostConfig.Dns,
|
|
269
|
+
search: inspect.HostConfig.DnsSearch,
|
|
270
|
+
options: inspect.HostConfig.DnsOptions,
|
|
271
|
+
},
|
|
272
|
+
networkMode: inspect.HostConfig.NetworkMode,
|
|
273
|
+
networks: inspect.NetworkSettings.Networks
|
|
274
|
+
? Object.entries(inspect.NetworkSettings.Networks).map(([name, net]) => ({
|
|
275
|
+
name,
|
|
276
|
+
aliases: net.Aliases ?? undefined,
|
|
277
|
+
ipv4Address: net.IPAddress,
|
|
278
|
+
ipv6Address: net.GlobalIPv6Address,
|
|
279
|
+
driverOpts: net.DriverOpts ?? undefined,
|
|
280
|
+
}))
|
|
281
|
+
: undefined,
|
|
282
|
+
restart: {
|
|
283
|
+
policy: inspect.HostConfig.RestartPolicy.Name,
|
|
284
|
+
maxRetries: inspect.HostConfig.RestartPolicy.MaximumRetryCount,
|
|
285
|
+
},
|
|
286
|
+
resources: {
|
|
287
|
+
cpuShares: inspect.HostConfig.CpuShares,
|
|
288
|
+
cpuQuota: inspect.HostConfig.CpuQuota,
|
|
289
|
+
cpuPeriod: inspect.HostConfig.CpuPeriod,
|
|
290
|
+
cpusetCpus: inspect.HostConfig.CpusetCpus,
|
|
291
|
+
memory: inspect.HostConfig.Memory,
|
|
292
|
+
memorySwap: inspect.HostConfig.MemorySwap,
|
|
293
|
+
memoryReservation: inspect.HostConfig.MemoryReservation,
|
|
294
|
+
pidsLimit: inspect.HostConfig.PidsLimit ?? undefined,
|
|
295
|
+
shmSize: inspect.HostConfig.ShmSize,
|
|
296
|
+
readOnlyRootFilesystem: inspect.HostConfig.ReadonlyRootfs,
|
|
297
|
+
},
|
|
298
|
+
logging: {
|
|
299
|
+
driver: inspect.HostConfig.LogConfig.Type,
|
|
300
|
+
options: inspect.HostConfig.LogConfig.Config,
|
|
301
|
+
},
|
|
302
|
+
security: {
|
|
303
|
+
privileged: inspect.HostConfig.Privileged,
|
|
304
|
+
capAdd: inspect.HostConfig.CapAdd ?? undefined,
|
|
305
|
+
capDrop: inspect.HostConfig.CapDrop ?? undefined,
|
|
306
|
+
usernsMode: inspect.HostConfig.UsernsMode ?? undefined,
|
|
307
|
+
ipc: inspect.HostConfig.IpcMode,
|
|
308
|
+
pid: inspect.HostConfig.PidMode,
|
|
309
|
+
seccomp: inspect.HostConfig.SecurityOpt?.find(opt => opt.startsWith('seccomp='))?.split('=')[1] ?? undefined,
|
|
310
|
+
apparmor: inspect.AppArmorProfile,
|
|
311
|
+
groupAdd: inspect.HostConfig.GroupAdd ?? undefined,
|
|
312
|
+
noNewPrivileges: undefined, // Nicht direkt verfügbar
|
|
313
|
+
},
|
|
314
|
+
sysctls: inspect.HostConfig.Sysctls ?? undefined,
|
|
315
|
+
init: inspect.HostConfig.Init ?? undefined,
|
|
316
|
+
stop: {
|
|
317
|
+
signal: inspect.Config.StopSignal ?? undefined,
|
|
318
|
+
gracePeriodSec: inspect.Config.StopTimeout ?? undefined,
|
|
319
|
+
},
|
|
320
|
+
readOnly: inspect.HostConfig.ReadonlyRootfs,
|
|
321
|
+
timezone: undefined, // Nicht direkt verfügbar
|
|
322
|
+
__meta: undefined, // Eigene Metadaten
|
|
323
|
+
};
|
|
324
|
+
return cleanContainerConfig(obj);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get information about the Docker daemon: is it running and which version
|
|
328
|
+
*
|
|
329
|
+
* @returns Object with version and daemonRunning
|
|
330
|
+
*/
|
|
331
|
+
async getDockerDaemonInfo() {
|
|
332
|
+
await this.isReady();
|
|
333
|
+
const daemonRunning = await this.#isDockerDaemonRunning();
|
|
334
|
+
return {
|
|
335
|
+
version: this.dockerVersion,
|
|
336
|
+
daemonRunning,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async #init() {
|
|
340
|
+
const version = await this.#isDockerInstalled();
|
|
341
|
+
this.installed = !!version;
|
|
342
|
+
if (version) {
|
|
343
|
+
this.dockerVersion = version;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const daemonRunning = await this.#isDockerDaemonRunning();
|
|
347
|
+
if (daemonRunning) {
|
|
348
|
+
// Docker daemon is running, but docker command not found
|
|
349
|
+
this.adapter.log.warn('Docker daemon is running, but docker command not found. May be "iobroker" user has no access to Docker. Run "iob fix" command to fix it.');
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
this.adapter.log.warn('Docker is not installed. Please install Docker.');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (this.installed) {
|
|
356
|
+
this.needSudo = await this.#isNeedSudo();
|
|
357
|
+
await this.#checkOwnContainers();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async #isDockerDaemonRunning() {
|
|
361
|
+
try {
|
|
362
|
+
const { stdout, stderr } = await execPromise('systemctl status docker');
|
|
363
|
+
// ● docker.service - Docker Application Container Engine
|
|
364
|
+
// Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
|
|
365
|
+
// Active: active (running) since Fri 2025-08-15 08:37:22 CEST; 3 weeks 2 days ago
|
|
366
|
+
// TriggeredBy: ● docker.socket
|
|
367
|
+
// Docs: https://docs.docker.com
|
|
368
|
+
// Main PID: 785 (dockerd)
|
|
369
|
+
// Tasks: 30
|
|
370
|
+
// CPU: 4min 17.003s
|
|
371
|
+
// CGroup: /system.slice/docker.service
|
|
372
|
+
// ├─ 785 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
|
|
373
|
+
// ├─97032 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5000 -container-ip 172.17.0.2 -container-port 5000 -use-listen-fd
|
|
374
|
+
// └─97039 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 5000 -container-ip 172.17.0.2 -container-port 5000 -use-listen-fd
|
|
375
|
+
if (stderr?.includes('could not be found') || stderr.includes('not-found')) {
|
|
376
|
+
this.adapter.log.error(`Docker is not installed: ${stderr}`);
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
return stdout.includes('(running)');
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Ensure that the given container is running with the actual configuration
|
|
387
|
+
*
|
|
388
|
+
* @param container Container configuration
|
|
389
|
+
*/
|
|
390
|
+
async #ensureActualConfiguration(container) {
|
|
391
|
+
// Check the configuration of the container
|
|
392
|
+
const inspect = await this.containerInspect(container.name);
|
|
393
|
+
if (inspect) {
|
|
394
|
+
const existingConfig = _a.mapInspectToConfig(inspect);
|
|
395
|
+
console.log('Compare existing config', existingConfig, ' and', container);
|
|
396
|
+
container = cleanContainerConfig(container);
|
|
397
|
+
const diffs = compareConfigs(container, existingConfig);
|
|
398
|
+
if (diffs.length) {
|
|
399
|
+
this.adapter.log.info(`Configuration of own container ${container.name} has changed: ${diffs.join(', ')}. Restarting container...`);
|
|
400
|
+
const result = await this.containerReCreate(container);
|
|
401
|
+
if (result.stderr) {
|
|
402
|
+
this.adapter.log.warn(`Cannot recreate own container ${container.name}: ${result.stderr}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Check if container is running
|
|
406
|
+
this.adapter.log.debug(`Configuration of own container ${container.name} is up to date`);
|
|
407
|
+
const status = await this.containerList(true);
|
|
408
|
+
const containerInfo = status.find(it => it.names === container.name);
|
|
409
|
+
if (containerInfo) {
|
|
410
|
+
if (containerInfo.status !== 'running' && containerInfo.status !== 'restarting') {
|
|
411
|
+
// Start the container
|
|
412
|
+
this.adapter.log.info(`Starting own container ${container.name}`);
|
|
413
|
+
try {
|
|
414
|
+
const result = await this.containerStart(containerInfo.id);
|
|
415
|
+
if (result.stderr) {
|
|
416
|
+
this.adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
this.adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
this.adapter.log.debug(`Own container ${container.name} is already running`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
this.adapter.log.warn(`Own container ${container.name} not found in container list after recreation`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async #checkOwnContainers() {
|
|
433
|
+
if (!this.#ownContainers.length) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const status = await this.containerList(true);
|
|
437
|
+
let images = await this.imageList();
|
|
438
|
+
let anyStartedOrRunning = false;
|
|
439
|
+
const networkChecked = [];
|
|
440
|
+
for (let c = 0; c < this.#ownContainers.length; c++) {
|
|
441
|
+
const container = this.#ownContainers[c];
|
|
442
|
+
if (container.iobEnabled !== false) {
|
|
443
|
+
if (!container.image.includes(':')) {
|
|
444
|
+
container.image += ':latest';
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
// create iobroker network if necessary
|
|
448
|
+
if (container.networkMode &&
|
|
449
|
+
container.networkMode !== 'container' &&
|
|
450
|
+
container.networkMode !== 'host' &&
|
|
451
|
+
container.networkMode !== 'bridge' &&
|
|
452
|
+
container.networkMode !== 'none') {
|
|
453
|
+
if (!networkChecked.includes(container.networkMode)) {
|
|
454
|
+
// check if the network exists
|
|
455
|
+
const networks = await this.networkList();
|
|
456
|
+
if (!networks.find(it => it.name === container.networkMode)) {
|
|
457
|
+
this.adapter.log.info(`Creating docker network ${container.networkMode}`);
|
|
458
|
+
await this.networkCreate(container.networkMode);
|
|
459
|
+
}
|
|
460
|
+
networkChecked.push(container.networkMode);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
let containerInfo = status.find(it => it.names === container.name);
|
|
464
|
+
let image = images.find(it => `${it.repository}:${it.tag}` === container.image);
|
|
465
|
+
if (container.iobAutoImageUpdate) {
|
|
466
|
+
// ensure that the image is actual
|
|
467
|
+
const newImage = await this.imageUpdate(container.image, true);
|
|
468
|
+
if (newImage) {
|
|
469
|
+
this.adapter.log.info(`Image ${container.image} for own container ${container.name} was updated`);
|
|
470
|
+
if (containerInfo) {
|
|
471
|
+
// destroy current container
|
|
472
|
+
await this.containerRemove(containerInfo.id);
|
|
473
|
+
containerInfo = undefined;
|
|
474
|
+
}
|
|
475
|
+
image = newImage;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!image) {
|
|
479
|
+
this.adapter.log.info(`Pulling image ${container.image} for own container ${container.name}`);
|
|
480
|
+
try {
|
|
481
|
+
const result = await this.imagePull(container.image);
|
|
482
|
+
if (result.stderr) {
|
|
483
|
+
this.adapter.log.warn(`Cannot pull image ${container.image}: ${result.stderr}`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (e) {
|
|
488
|
+
this.adapter.log.warn(`Cannot pull image ${container.image}: ${e.message}`);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
// Check that image is available now
|
|
492
|
+
images = await this.imageList();
|
|
493
|
+
image = images.find(it => `${it.repository}:${it.tag}` === container.image);
|
|
494
|
+
if (!image) {
|
|
495
|
+
this.adapter.log.warn(`Image ${container.image} for own container ${container.name} not found after pull`);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (containerInfo) {
|
|
500
|
+
await this.#ensureActualConfiguration(container);
|
|
501
|
+
anyStartedOrRunning ||= !!container.iobMonitoringEnabled;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
// Create and start the container, as the container was not found
|
|
505
|
+
this.adapter.log.info(`Creating and starting own container ${container.name}`);
|
|
506
|
+
try {
|
|
507
|
+
const result = await this.containerRun(container);
|
|
508
|
+
if (result.stderr) {
|
|
509
|
+
this.adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
anyStartedOrRunning ||= !!container.iobMonitoringEnabled;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch (e) {
|
|
516
|
+
this.adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
this.adapter.log.warn(`Cannot check own container ${container.name}: ${e.message}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (anyStartedOrRunning) {
|
|
526
|
+
this.#monitoringInterval ||= setInterval(() => this.#monitorOwnContainers(), 60000);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async #monitorOwnContainers() {
|
|
530
|
+
// get the status of containers
|
|
531
|
+
const containers = await this.containerList();
|
|
532
|
+
// Check the status of own containers
|
|
533
|
+
for (let c = 0; c < this.#ownContainers.length; c++) {
|
|
534
|
+
const container = this.#ownContainers[c];
|
|
535
|
+
if (container.iobEnabled !== false && container.iobMonitoringEnabled) {
|
|
536
|
+
// Check if container is running
|
|
537
|
+
const running = containers.find(it => it.names === container.name);
|
|
538
|
+
if (!running || (running.status !== 'running' && running.status !== 'restarting')) {
|
|
539
|
+
this.adapter.log.warn(`Own container ${container.name} is not running. Restarting...`);
|
|
540
|
+
try {
|
|
541
|
+
const result = await this.containerStart(container.name);
|
|
542
|
+
if (result.stderr) {
|
|
543
|
+
this.adapter.log.warn(`Cannot start own container ${container.name}: ${result.stderr}`);
|
|
544
|
+
this.#ownContainersStats[container.name] = {
|
|
545
|
+
...this.#ownContainersStats[container.name],
|
|
546
|
+
status: running?.status || 'unknown',
|
|
547
|
+
statusTs: Date.now(),
|
|
548
|
+
};
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
this.adapter.log.warn(`Cannot start own container ${container.name}: ${e.message}`);
|
|
554
|
+
this.#ownContainersStats[container.name] = {
|
|
555
|
+
...this.#ownContainersStats[container.name],
|
|
556
|
+
status: running?.status || 'unknown',
|
|
557
|
+
statusTs: Date.now(),
|
|
558
|
+
};
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// check the stats
|
|
563
|
+
this.#ownContainersStats[container.name] = {
|
|
564
|
+
...((await this.containerGetRamAndCpuUsage(container.name)) || {}),
|
|
565
|
+
status: running?.status || 'unknown',
|
|
566
|
+
statusTs: Date.now(),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/** Read own container stats */
|
|
572
|
+
getOwnContainerStats() {
|
|
573
|
+
return this.#ownContainersStats;
|
|
574
|
+
}
|
|
575
|
+
async containerGetRamAndCpuUsage(containerNameOrId) {
|
|
576
|
+
try {
|
|
577
|
+
const { stdout } = await this.#exec(`stats ${containerNameOrId} --no-stream --format "{{.CPUPerc}};{{.MemUsage}};{{.NetIO}};{{.BlockIO}};{{.PIDs}}"`);
|
|
578
|
+
// Example: "0.15%;12.34MiB / 512MiB;1.2kB / 2.3kB;0B / 0B;5"
|
|
579
|
+
const [cpuStr, memStr, netStr, blockIoStr, pid] = stdout.trim().split(';');
|
|
580
|
+
const [memUsed, memMax] = memStr.split('/').map(it => it.trim());
|
|
581
|
+
const [netRead, netWrite] = netStr.split('/').map(it => it.trim());
|
|
582
|
+
const [blockIoRead, blockIoWrite] = blockIoStr.split('/').map(it => it.trim());
|
|
583
|
+
return {
|
|
584
|
+
ts: Date.now(),
|
|
585
|
+
cpu: parseFloat(cpuStr.replace('%', '').replace(',', '.')),
|
|
586
|
+
memUsed: this.#parseSize(memUsed.replace('iB', 'B')),
|
|
587
|
+
memMax: this.#parseSize(memMax.replace('iB', 'B')),
|
|
588
|
+
netRead: this.#parseSize(netRead.replace('iB', 'B')),
|
|
589
|
+
netWrite: this.#parseSize(netWrite.replace('iB', 'B')),
|
|
590
|
+
processes: parseInt(pid, 10),
|
|
591
|
+
blockIoRead: this.#parseSize(blockIoRead.replace('iB', 'B')),
|
|
592
|
+
blockIoWrite: this.#parseSize(blockIoWrite.replace('iB', 'B')),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
this.adapter.log.debug(`Cannot get stats: ${e.message}`);
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Update the image if a newer version is available
|
|
602
|
+
*
|
|
603
|
+
* @param image Image name with tag
|
|
604
|
+
* @param ignoreIfNotExist If true, do not throw error if image does not exist
|
|
605
|
+
* @returns New image info if image was updated, null if no update was necessary
|
|
606
|
+
*/
|
|
607
|
+
async imageUpdate(image, ignoreIfNotExist) {
|
|
608
|
+
const list = await this.imageList();
|
|
609
|
+
if (!image.includes(':')) {
|
|
610
|
+
image += ':latest';
|
|
611
|
+
}
|
|
612
|
+
const existingImage = list.find(it => `${it.repository}:${it.tag}` === image);
|
|
613
|
+
if (!existingImage && !ignoreIfNotExist) {
|
|
614
|
+
throw new Error(`Image ${image} not found`);
|
|
615
|
+
}
|
|
616
|
+
// Pull the image
|
|
617
|
+
const result = await this.imagePull(image);
|
|
618
|
+
if (result.stderr) {
|
|
619
|
+
throw new Error(`Cannot pull image ${image}: ${result.stderr}`);
|
|
620
|
+
}
|
|
621
|
+
const newList = await this.imageList();
|
|
622
|
+
const newImage = newList.find(it => `${it.repository}:${it.tag}` === image);
|
|
623
|
+
if (!newImage) {
|
|
624
|
+
throw new Error(`Image ${image} not found after pull`);
|
|
625
|
+
}
|
|
626
|
+
// If image ID has changed, image was updated
|
|
627
|
+
return !existingImage || existingImage.id !== newImage.id ? newImage : null;
|
|
628
|
+
}
|
|
629
|
+
#exec(command) {
|
|
630
|
+
if (!this.installed) {
|
|
631
|
+
return Promise.reject(new Error('Docker is not installed'));
|
|
632
|
+
}
|
|
633
|
+
const finalCommand = this.needSudo ? `sudo docker ${command}` : `docker ${command}`;
|
|
634
|
+
return execPromise(finalCommand);
|
|
635
|
+
}
|
|
636
|
+
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();
|
|
642
|
+
}
|
|
643
|
+
this.adapter.log.debug(`Docker not installed: ${result.stderr}`);
|
|
644
|
+
}
|
|
645
|
+
catch (e) {
|
|
646
|
+
this.adapter.log.debug(`Docker not installed: ${e.message}`);
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
async #isNeedSudo() {
|
|
651
|
+
try {
|
|
652
|
+
await execPromise('docker ps');
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/** Get disk usage information */
|
|
660
|
+
async discUsage() {
|
|
661
|
+
const { stdout } = await this.#exec(`system df`);
|
|
662
|
+
const result = { total: { size: 0, reclaimable: 0 } };
|
|
663
|
+
// parse the output
|
|
664
|
+
// TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
|
665
|
+
// Images 2 1 2.715GB 2.715GB (99%)
|
|
666
|
+
// Containers 1 1 26.22MB 0B (0%)
|
|
667
|
+
// Local Volumes 0 0 0B 0B
|
|
668
|
+
// Build Cache 0 0 0B 0B
|
|
669
|
+
const lines = stdout.split('\n');
|
|
670
|
+
for (const line of lines) {
|
|
671
|
+
const parts = line.trim().split(/\s+/);
|
|
672
|
+
if (parts.length >= 5 && parts[0] !== 'TYPE') {
|
|
673
|
+
let size;
|
|
674
|
+
let reclaimable;
|
|
675
|
+
if (parts[0] === 'Images') {
|
|
676
|
+
const sizeStr = parts[3];
|
|
677
|
+
const reclaimableStr = parts[4].split(' ')[0];
|
|
678
|
+
size = this.#parseSize(sizeStr);
|
|
679
|
+
reclaimable = this.#parseSize(reclaimableStr);
|
|
680
|
+
result.images = {
|
|
681
|
+
total: parseInt(parts[1], 10),
|
|
682
|
+
active: parseInt(parts[2], 10),
|
|
683
|
+
size,
|
|
684
|
+
reclaimable: reclaimable,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
else if (parts[0] === 'Containers') {
|
|
688
|
+
const sizeStr = parts[3];
|
|
689
|
+
const reclaimableStr = parts[4].split(' ')[0];
|
|
690
|
+
size = this.#parseSize(sizeStr);
|
|
691
|
+
reclaimable = this.#parseSize(reclaimableStr);
|
|
692
|
+
result.containers = {
|
|
693
|
+
total: parseInt(parts[1], 10),
|
|
694
|
+
active: parseInt(parts[2], 10),
|
|
695
|
+
size,
|
|
696
|
+
reclaimable: reclaimable,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
else if (parts[0] === 'Local' && parts[1] === 'Volumes') {
|
|
700
|
+
const sizeStr = parts[4];
|
|
701
|
+
const reclaimableStr = parts[5].split(' ')[0];
|
|
702
|
+
size = this.#parseSize(sizeStr);
|
|
703
|
+
reclaimable = this.#parseSize(reclaimableStr);
|
|
704
|
+
result.volumes = {
|
|
705
|
+
total: parseInt(parts[2], 10),
|
|
706
|
+
active: parseInt(parts[3], 10),
|
|
707
|
+
size,
|
|
708
|
+
reclaimable: reclaimable,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
else if (parts[0] === 'Build' && parts[1] === 'Cache') {
|
|
712
|
+
const sizeStr = parts[4];
|
|
713
|
+
const reclaimableStr = parts[5].split(' ')[0];
|
|
714
|
+
size = this.#parseSize(sizeStr);
|
|
715
|
+
reclaimable = this.#parseSize(reclaimableStr);
|
|
716
|
+
result.buildCache = {
|
|
717
|
+
total: parseInt(parts[2], 10),
|
|
718
|
+
active: parseInt(parts[3], 10),
|
|
719
|
+
size,
|
|
720
|
+
reclaimable: reclaimable,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
result.total.size += size || 0;
|
|
724
|
+
result.total.reclaimable += reclaimable || 0;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
/** Pull an image from the registry */
|
|
730
|
+
async imagePull(image) {
|
|
731
|
+
try {
|
|
732
|
+
if (!image.includes(':')) {
|
|
733
|
+
image += ':latest';
|
|
734
|
+
}
|
|
735
|
+
const result = await this.#exec(`pull ${image}`);
|
|
736
|
+
const images = await this.imageList();
|
|
737
|
+
if (!images.find(it => `${it.repository}:${it.tag}` === image)) {
|
|
738
|
+
throw new Error(`Image ${image} not found after pull`);
|
|
739
|
+
}
|
|
740
|
+
return { ...result, images };
|
|
741
|
+
}
|
|
742
|
+
catch (e) {
|
|
743
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/** Autocomplete image names from Docker Hub */
|
|
747
|
+
async imageNameAutocomplete(partialName) {
|
|
748
|
+
try {
|
|
749
|
+
// Read stars and descriptions
|
|
750
|
+
const { stdout } = await this.#exec(`search ${partialName} --format "{{.Name}};{{.Description}};{{.IsOfficial}};{{.StarCount}}" --limit 50`);
|
|
751
|
+
return stdout
|
|
752
|
+
.split('\n')
|
|
753
|
+
.filter(line => line.trim() !== '')
|
|
754
|
+
.map(line => {
|
|
755
|
+
const [name, description, isOfficial, starCount] = line.split(';');
|
|
756
|
+
return {
|
|
757
|
+
name,
|
|
758
|
+
description,
|
|
759
|
+
isOfficial: isOfficial === 'true',
|
|
760
|
+
starCount: parseInt(starCount, 10) || 0,
|
|
761
|
+
};
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch (e) {
|
|
765
|
+
this.adapter.log.debug(`Cannot search images: ${e.message}`);
|
|
766
|
+
return [];
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Create and start a container with the given configuration. No checks are done.
|
|
771
|
+
*/
|
|
772
|
+
async containerRun(config) {
|
|
773
|
+
try {
|
|
774
|
+
return await this.#exec(`run ${this.#toDockerRun(config)}`);
|
|
775
|
+
}
|
|
776
|
+
catch (e) {
|
|
777
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Create a container with the given configuration without starting it. No checks are done.
|
|
782
|
+
*/
|
|
783
|
+
async containerCreate(config) {
|
|
784
|
+
try {
|
|
785
|
+
return await this.#exec(`create ${this.#toDockerRun(config, true)}`);
|
|
786
|
+
}
|
|
787
|
+
catch (e) {
|
|
788
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Recreate a container
|
|
793
|
+
*
|
|
794
|
+
* This function checks if a container is running, stops it if necessary,
|
|
795
|
+
* removes it and creates a new one with the given configuration.
|
|
796
|
+
*
|
|
797
|
+
* @param config new configuration
|
|
798
|
+
* @returns stdout and stderr of the create command
|
|
799
|
+
*/
|
|
800
|
+
async containerReCreate(config) {
|
|
801
|
+
try {
|
|
802
|
+
// Get if the container is running
|
|
803
|
+
let containers = await this.containerList();
|
|
804
|
+
// find ID of container
|
|
805
|
+
const containerInfo = containers.find(it => it.names === config.name);
|
|
806
|
+
if (containerInfo) {
|
|
807
|
+
if (containerInfo.status === 'running' || containerInfo.status === 'restarting') {
|
|
808
|
+
const stopResult = await this.#exec(`stop ${containerInfo.id}`);
|
|
809
|
+
containers = await this.containerList();
|
|
810
|
+
if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
|
|
811
|
+
this.adapter.log.warn(`Cannot remove container: ${stopResult.stderr || stopResult.stdout}`);
|
|
812
|
+
throw new Error(`Container ${containerInfo.id} still running after stop`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// Remove container
|
|
816
|
+
const rmResult = await this.#exec(`rm ${containerInfo.id}`);
|
|
817
|
+
containers = await this.containerList();
|
|
818
|
+
if (containers.find(it => it.id === containerInfo.id)) {
|
|
819
|
+
this.adapter.log.warn(`Cannot remove container: ${rmResult.stderr || rmResult.stdout}`);
|
|
820
|
+
throw new Error(`Container ${containerInfo.id} still found after stop`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return await this.#exec(`create ${this.#toDockerRun(config, true)}`);
|
|
824
|
+
}
|
|
825
|
+
catch (e) {
|
|
826
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/** List all images */
|
|
830
|
+
async imageList() {
|
|
831
|
+
try {
|
|
832
|
+
const { stdout } = await this.#exec('images --format "{{.Repository}}:{{.Tag}};{{.ID}};{{.CreatedAt}};{{.Size}}"');
|
|
833
|
+
return stdout
|
|
834
|
+
.split('\n')
|
|
835
|
+
.filter(line => line.trim() !== '')
|
|
836
|
+
.map(line => {
|
|
837
|
+
const [repositoryTag, id, createdSince, size] = line.split(';');
|
|
838
|
+
const [repository, tag] = repositoryTag.split(':');
|
|
839
|
+
return {
|
|
840
|
+
repository,
|
|
841
|
+
tag,
|
|
842
|
+
id,
|
|
843
|
+
createdSince,
|
|
844
|
+
size: this.#parseSize(size),
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
catch (e) {
|
|
849
|
+
this.adapter.log.debug(`Cannot list images: ${e.message}`);
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/** Build an image from a Dockerfile */
|
|
854
|
+
async imageBuild(dockerfilePath, tag) {
|
|
855
|
+
try {
|
|
856
|
+
return await this.#exec(`build -t ${tag} -f ${dockerfilePath} .`);
|
|
857
|
+
}
|
|
858
|
+
catch (e) {
|
|
859
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/** Tag an image with a new tag */
|
|
863
|
+
async imageTag(imageId, newTag) {
|
|
864
|
+
try {
|
|
865
|
+
return await this.#exec(`tag ${imageId} ${newTag}`);
|
|
866
|
+
}
|
|
867
|
+
catch (e) {
|
|
868
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/** Remove an image */
|
|
872
|
+
async imageRemove(imageId) {
|
|
873
|
+
try {
|
|
874
|
+
const result = await this.#exec(`rmi ${imageId}`);
|
|
875
|
+
const images = await this.imageList();
|
|
876
|
+
if (images.find(it => `${it.repository}:${it.tag}` === imageId)) {
|
|
877
|
+
return { stdout: '', stderr: `Image ${imageId} still found after deletion`, images };
|
|
878
|
+
}
|
|
879
|
+
return { ...result, images };
|
|
880
|
+
}
|
|
881
|
+
catch (e) {
|
|
882
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Inspect an image */
|
|
886
|
+
async imageInspect(imageId) {
|
|
887
|
+
try {
|
|
888
|
+
const { stdout } = await this.#exec(`inspect ${imageId}`);
|
|
889
|
+
return JSON.parse(stdout)[0];
|
|
890
|
+
}
|
|
891
|
+
catch (e) {
|
|
892
|
+
this.adapter.log.debug(`Cannot inspect image: ${e.message.toString()}`);
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
#parseSize(sizeStr) {
|
|
897
|
+
const units = {
|
|
898
|
+
B: 1,
|
|
899
|
+
KB: 1024,
|
|
900
|
+
MB: 1024 * 1024,
|
|
901
|
+
GB: 1024 * 1024 * 1024,
|
|
902
|
+
TB: 1024 * 1024 * 1024 * 1024,
|
|
903
|
+
};
|
|
904
|
+
const match = sizeStr.match(/^([\d.]+)([KMGTP]?B)$/);
|
|
905
|
+
if (match) {
|
|
906
|
+
const value = parseFloat(match[1]);
|
|
907
|
+
const unit = match[2];
|
|
908
|
+
return value * (units[unit] || 1);
|
|
909
|
+
}
|
|
910
|
+
return 0;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Stop a container
|
|
914
|
+
*
|
|
915
|
+
* @param container Container name or ID
|
|
916
|
+
*/
|
|
917
|
+
async containerStop(container) {
|
|
918
|
+
try {
|
|
919
|
+
let containers = await this.containerList();
|
|
920
|
+
// find ID of container
|
|
921
|
+
const containerInfo = containers.find(it => it.names === container || it.id === container);
|
|
922
|
+
if (!containerInfo) {
|
|
923
|
+
throw new Error(`Container ${container} not found`);
|
|
924
|
+
}
|
|
925
|
+
const result = await this.#exec(`stop ${containerInfo.id}`);
|
|
926
|
+
containers = await this.containerList();
|
|
927
|
+
if (containers.find(it => it.id === containerInfo.id && it.status === 'running')) {
|
|
928
|
+
throw new Error(`Container ${container} still running after stop`);
|
|
929
|
+
}
|
|
930
|
+
return { ...result, containers };
|
|
931
|
+
}
|
|
932
|
+
catch (e) {
|
|
933
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Start a container
|
|
938
|
+
*
|
|
939
|
+
* @param container Container name or ID
|
|
940
|
+
*/
|
|
941
|
+
async containerStart(container) {
|
|
942
|
+
try {
|
|
943
|
+
let containers = await this.containerList();
|
|
944
|
+
// find ID of container
|
|
945
|
+
const containerInfo = containers.find(it => it.names === container || it.id === container);
|
|
946
|
+
if (!containerInfo) {
|
|
947
|
+
throw new Error(`Container ${container} not found`);
|
|
948
|
+
}
|
|
949
|
+
const result = await this.#exec(`start ${containerInfo.id}`);
|
|
950
|
+
containers = await this.containerList();
|
|
951
|
+
if (containers.find(it => it.id === containerInfo.id && it.status !== 'running' && it.status !== 'restarting')) {
|
|
952
|
+
throw new Error(`Container ${container} still running after stop`);
|
|
953
|
+
}
|
|
954
|
+
return { ...result, containers };
|
|
955
|
+
}
|
|
956
|
+
catch (e) {
|
|
957
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Restart a container
|
|
962
|
+
*
|
|
963
|
+
* This function restarts a container by its name or ID.
|
|
964
|
+
* It accepts an optional timeout in seconds to wait before killing the container (default is 5 seconds).
|
|
965
|
+
*
|
|
966
|
+
* @param container Container name or ID
|
|
967
|
+
* @param timeoutSeconds Timeout in seconds to wait before killing the container (default: 5)
|
|
968
|
+
*/
|
|
969
|
+
async containerRestart(container, timeoutSeconds) {
|
|
970
|
+
try {
|
|
971
|
+
const containers = await this.containerList();
|
|
972
|
+
// find ID of container
|
|
973
|
+
const containerInfo = containers.find(it => it.names === container || it.id === container);
|
|
974
|
+
if (!containerInfo) {
|
|
975
|
+
throw new Error(`Container ${container} not found`);
|
|
976
|
+
}
|
|
977
|
+
return await this.#exec(`restart -t ${timeoutSeconds || 5} ${containerInfo.id}`);
|
|
978
|
+
}
|
|
979
|
+
catch (e) {
|
|
980
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Remove the container and if necessary, stop it first
|
|
985
|
+
*
|
|
986
|
+
* @param container Container name or ID
|
|
987
|
+
*/
|
|
988
|
+
async containerRemove(container) {
|
|
989
|
+
try {
|
|
990
|
+
let containers = await this.containerList();
|
|
991
|
+
// find ID of container
|
|
992
|
+
const containerInfo = containers.find(it => it.names === container || it.id === container);
|
|
993
|
+
if (!containerInfo) {
|
|
994
|
+
throw new Error(`Container ${container} not found`);
|
|
995
|
+
}
|
|
996
|
+
// ensure that container is stopped
|
|
997
|
+
if (containerInfo.status === 'running' || containerInfo.status === 'restarting') {
|
|
998
|
+
// stop container
|
|
999
|
+
const result = await this.#exec(`stop ${containerInfo.id}`);
|
|
1000
|
+
if (result.stderr) {
|
|
1001
|
+
throw new Error(`Cannot stop container ${container}: ${result.stderr}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const result = await this.#exec(`rm ${container}`);
|
|
1005
|
+
containers = await this.containerList();
|
|
1006
|
+
if (containers.find(it => it.id === containerInfo.id)) {
|
|
1007
|
+
throw new Error(`Container ${container} still found after stop`);
|
|
1008
|
+
}
|
|
1009
|
+
return { ...result, containers };
|
|
1010
|
+
}
|
|
1011
|
+
catch (e) {
|
|
1012
|
+
return { stdout: '', stderr: e.message.toString() };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* List all containers
|
|
1017
|
+
*
|
|
1018
|
+
* @param all If true, list all containers. If false, list only running containers. Default is true.
|
|
1019
|
+
*/
|
|
1020
|
+
async containerList(all = true) {
|
|
1021
|
+
try {
|
|
1022
|
+
const { stdout } = await this.#exec(`ps ${all ? '-a' : ''} --format "{{.Names}};{{.Status}};{{.ID}};{{.Image}};{{.Command}};{{.CreatedAt}};{{.Ports}}"`);
|
|
1023
|
+
return stdout
|
|
1024
|
+
.split('\n')
|
|
1025
|
+
.filter(line => line.trim() !== '')
|
|
1026
|
+
.map(line => {
|
|
1027
|
+
const [names, statusInfo, id, image, command, createdAt, ports] = line.split(';');
|
|
1028
|
+
const [status, ...uptime] = statusInfo.split(' ');
|
|
1029
|
+
let statusKey = status.toLowerCase();
|
|
1030
|
+
if (statusKey === 'up') {
|
|
1031
|
+
statusKey = 'running';
|
|
1032
|
+
}
|
|
1033
|
+
return { id, image, command, createdAt, status: statusKey, uptime: uptime.join(' '), ports, names };
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
catch (e) {
|
|
1037
|
+
this.adapter.log.debug(`Cannot list containers: ${e.message}`);
|
|
1038
|
+
return [];
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Get the logs of a container
|
|
1043
|
+
*
|
|
1044
|
+
* @param containerNameOrId Container name or ID
|
|
1045
|
+
* @param options Options for logs
|
|
1046
|
+
* @param options.tail Number of lines to show from the end of the logs
|
|
1047
|
+
* @param options.follow If true, follow the logs (not implemented yet)
|
|
1048
|
+
*/
|
|
1049
|
+
async containerLogs(containerNameOrId, options = {}) {
|
|
1050
|
+
try {
|
|
1051
|
+
const args = [];
|
|
1052
|
+
if (options.tail !== undefined) {
|
|
1053
|
+
args.push(`--tail ${options.tail}`);
|
|
1054
|
+
}
|
|
1055
|
+
if (options.follow) {
|
|
1056
|
+
args.push(`--follow`);
|
|
1057
|
+
throw new Error('Follow option is not implemented yet');
|
|
1058
|
+
}
|
|
1059
|
+
const result = await this.#exec(`logs${args.length ? ` ${args.join(' ')}` : ''} ${containerNameOrId}`);
|
|
1060
|
+
return (result.stdout || result.stderr).split('\n').filter(line => line.trim() !== '');
|
|
1061
|
+
}
|
|
1062
|
+
catch (e) {
|
|
1063
|
+
return e
|
|
1064
|
+
.toString()
|
|
1065
|
+
.split('\n')
|
|
1066
|
+
.map((line) => line.trim());
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/** Inspect a container */
|
|
1070
|
+
async containerInspect(containerNameOrId) {
|
|
1071
|
+
try {
|
|
1072
|
+
const { stdout } = await this.#exec(`inspect ${containerNameOrId}`);
|
|
1073
|
+
const result = JSON.parse(stdout)[0];
|
|
1074
|
+
if (result.State.Running) {
|
|
1075
|
+
result.Stats = (await this.containerGetRamAndCpuUsage(containerNameOrId)) || undefined;
|
|
1076
|
+
}
|
|
1077
|
+
return result;
|
|
1078
|
+
}
|
|
1079
|
+
catch (e) {
|
|
1080
|
+
this.adapter.log.debug(`Cannot inspect container: ${e.message.toString()}`);
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Build a docker run command string from ContainerConfig
|
|
1086
|
+
*/
|
|
1087
|
+
#toDockerRun(config, create) {
|
|
1088
|
+
const args = [];
|
|
1089
|
+
// detach / interactive
|
|
1090
|
+
if (config.detach !== false && !create) {
|
|
1091
|
+
// default is true
|
|
1092
|
+
args.push('-d');
|
|
1093
|
+
}
|
|
1094
|
+
if (config.tty) {
|
|
1095
|
+
args.push('-t');
|
|
1096
|
+
}
|
|
1097
|
+
if (config.stdinOpen) {
|
|
1098
|
+
args.push('-i');
|
|
1099
|
+
}
|
|
1100
|
+
if (config.removeOnExit) {
|
|
1101
|
+
args.push('--rm');
|
|
1102
|
+
}
|
|
1103
|
+
// name
|
|
1104
|
+
if (config.name) {
|
|
1105
|
+
args.push('--name', config.name);
|
|
1106
|
+
}
|
|
1107
|
+
// hostname / domain
|
|
1108
|
+
if (config.hostname) {
|
|
1109
|
+
args.push('--hostname', config.hostname);
|
|
1110
|
+
}
|
|
1111
|
+
if (config.domainname) {
|
|
1112
|
+
args.push('--domainname', config.domainname);
|
|
1113
|
+
}
|
|
1114
|
+
// environment
|
|
1115
|
+
if (config.environment) {
|
|
1116
|
+
for (const [key, value] of Object.entries(config.environment)) {
|
|
1117
|
+
if (key && value) {
|
|
1118
|
+
args.push('-e', `${key}=${value}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (config.envFile) {
|
|
1123
|
+
for (const file of config.envFile) {
|
|
1124
|
+
args.push('--env-file', file);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// labels
|
|
1128
|
+
if (config.labels) {
|
|
1129
|
+
for (const [key, value] of Object.entries(config.labels)) {
|
|
1130
|
+
args.push('--label', `${key}=${value}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
// ports
|
|
1134
|
+
if (config.publishAllPorts) {
|
|
1135
|
+
args.push('-P');
|
|
1136
|
+
}
|
|
1137
|
+
if (config.ports) {
|
|
1138
|
+
for (const p of config.ports) {
|
|
1139
|
+
if (!p.containerPort) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const mapping = (p.hostIP ? `${p.hostIP}:` : '') +
|
|
1143
|
+
(p.hostPort ? `${p.hostPort}:` : '') +
|
|
1144
|
+
p.containerPort +
|
|
1145
|
+
(p.protocol ? `/${p.protocol}` : '');
|
|
1146
|
+
args.push('-p', mapping);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
// volumes / mounts
|
|
1150
|
+
if (config.volumes) {
|
|
1151
|
+
for (const v of config.volumes) {
|
|
1152
|
+
args.push('-v', v);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (config.mounts) {
|
|
1156
|
+
for (const m of config.mounts) {
|
|
1157
|
+
let mount = `type=${m.type},target=${m.target}`;
|
|
1158
|
+
if (m.source) {
|
|
1159
|
+
mount += `,source=${m.source}`;
|
|
1160
|
+
}
|
|
1161
|
+
if (m.readOnly) {
|
|
1162
|
+
mount += `,readonly`;
|
|
1163
|
+
}
|
|
1164
|
+
args.push('--mount', mount);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// restart policy
|
|
1168
|
+
if (config.restart?.policy) {
|
|
1169
|
+
const val = config.restart.policy === 'on-failure' && config.restart.maxRetries
|
|
1170
|
+
? `on-failure:${config.restart.maxRetries}`
|
|
1171
|
+
: config.restart.policy;
|
|
1172
|
+
args.push('--restart', val);
|
|
1173
|
+
}
|
|
1174
|
+
// user & workdir
|
|
1175
|
+
if (config.user) {
|
|
1176
|
+
args.push('--user', String(config.user));
|
|
1177
|
+
}
|
|
1178
|
+
if (config.workdir) {
|
|
1179
|
+
args.push('--workdir', config.workdir);
|
|
1180
|
+
}
|
|
1181
|
+
// logging
|
|
1182
|
+
if (config.logging?.driver) {
|
|
1183
|
+
args.push('--log-driver', config.logging.driver);
|
|
1184
|
+
if (config.logging.options) {
|
|
1185
|
+
for (const [k, v] of Object.entries(config.logging.options)) {
|
|
1186
|
+
args.push('--log-opt', `${k}=${v}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// security
|
|
1191
|
+
if (config.security?.privileged) {
|
|
1192
|
+
args.push('--privileged');
|
|
1193
|
+
}
|
|
1194
|
+
if (config.security?.capAdd) {
|
|
1195
|
+
for (const cap of config.security.capAdd) {
|
|
1196
|
+
args.push('--cap-add', cap);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (config.security?.capDrop) {
|
|
1200
|
+
for (const cap of config.security.capDrop) {
|
|
1201
|
+
args.push('--cap-drop', cap);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (config.security?.noNewPrivileges) {
|
|
1205
|
+
args.push('--security-opt', 'no-new-privileges');
|
|
1206
|
+
}
|
|
1207
|
+
if (config.security?.apparmor) {
|
|
1208
|
+
args.push('--security-opt', `apparmor=${config.security.apparmor}`);
|
|
1209
|
+
}
|
|
1210
|
+
// network
|
|
1211
|
+
if (config.networkMode) {
|
|
1212
|
+
args.push('--network', config.networkMode);
|
|
1213
|
+
}
|
|
1214
|
+
// extra hosts
|
|
1215
|
+
if (config.extraHosts) {
|
|
1216
|
+
for (const host of config.extraHosts) {
|
|
1217
|
+
if (typeof host === 'string') {
|
|
1218
|
+
args.push('--add-host', host);
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
args.push('--add-host', `${host.host}:${host.ip}`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// sysctls
|
|
1226
|
+
if (config.sysctls) {
|
|
1227
|
+
for (const [k, v] of Object.entries(config.sysctls)) {
|
|
1228
|
+
args.push('--sysctl', `${k}=${v}`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
// stop signal / timeout
|
|
1232
|
+
if (config.stop?.signal) {
|
|
1233
|
+
args.push('--stop-signal', config.stop.signal);
|
|
1234
|
+
}
|
|
1235
|
+
if (config.stop?.gracePeriodSec !== undefined) {
|
|
1236
|
+
args.push('--stop-timeout', String(config.stop.gracePeriodSec));
|
|
1237
|
+
}
|
|
1238
|
+
// resources
|
|
1239
|
+
if (config.resources?.cpus) {
|
|
1240
|
+
args.push('--cpus', String(config.resources.cpus));
|
|
1241
|
+
}
|
|
1242
|
+
if (config.resources?.memory) {
|
|
1243
|
+
args.push('--memory', String(config.resources.memory));
|
|
1244
|
+
}
|
|
1245
|
+
// image
|
|
1246
|
+
if (!config.image) {
|
|
1247
|
+
throw new Error('ContainerConfig.image is required for docker run');
|
|
1248
|
+
}
|
|
1249
|
+
args.push(config.image);
|
|
1250
|
+
// command override
|
|
1251
|
+
if (config.command) {
|
|
1252
|
+
if (Array.isArray(config.command)) {
|
|
1253
|
+
args.push(...config.command);
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
args.push(config.command);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return args.join(' ');
|
|
1260
|
+
}
|
|
1261
|
+
async networkList() {
|
|
1262
|
+
// docker network ls
|
|
1263
|
+
try {
|
|
1264
|
+
const { stdout } = await this.#exec(`network ls --format "{{.Name}};{{.ID}};{{.Driver}};{{.Scope}}"`);
|
|
1265
|
+
return stdout
|
|
1266
|
+
.split('\n')
|
|
1267
|
+
.filter(line => line.trim() !== '')
|
|
1268
|
+
.map(line => {
|
|
1269
|
+
const [name, id, driver, scope] = line.split(';');
|
|
1270
|
+
return { name, id, driver: driver, scope };
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
this.adapter.log.debug(`Cannot list networks: ${e.message.toString()}`);
|
|
1275
|
+
return [];
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async networkCreate(name, driver) {
|
|
1279
|
+
const result = await this.#exec(`network create ${driver ? `--driver ${driver}` : ''} ${name}`);
|
|
1280
|
+
const networks = await this.networkList();
|
|
1281
|
+
if (!networks.find(it => it.name === name)) {
|
|
1282
|
+
throw new Error(`Network ${name} not found after creation`);
|
|
1283
|
+
}
|
|
1284
|
+
return { ...result, networks };
|
|
1285
|
+
}
|
|
1286
|
+
async networkRemove(networkId) {
|
|
1287
|
+
const result = await this.#exec(`network remove ${networkId}`);
|
|
1288
|
+
const networks = await this.networkList();
|
|
1289
|
+
if (networks.find(it => it.id === networkId)) {
|
|
1290
|
+
throw new Error(`Network ${networkId} still found after deletion`);
|
|
1291
|
+
}
|
|
1292
|
+
return { ...result, networks };
|
|
1293
|
+
}
|
|
1294
|
+
/** Stop own containers if necessary */
|
|
1295
|
+
async destroy() {
|
|
1296
|
+
if (this.#monitoringInterval) {
|
|
1297
|
+
clearInterval(this.#monitoringInterval);
|
|
1298
|
+
this.#monitoringInterval = null;
|
|
1299
|
+
}
|
|
1300
|
+
for (const container of this.#ownContainers) {
|
|
1301
|
+
if (container.iobEnabled !== false && container.iobStopOnUnload) {
|
|
1302
|
+
this.adapter.log.info(`Stopping own container ${container.name} on destroy`);
|
|
1303
|
+
try {
|
|
1304
|
+
await this.containerStop(container.name);
|
|
1305
|
+
}
|
|
1306
|
+
catch (e) {
|
|
1307
|
+
this.adapter.log.warn(`Cannot stop own container ${container.name} on destroy: ${e.message}`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
_a = DockerManager;
|
|
1314
|
+
exports.default = DockerManager;
|
|
1315
|
+
//# sourceMappingURL=DockerManager.js.map
|