@phystack/device-phyos 4.3.40-dev
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 +6 -0
- package/bin/index.js +2 -0
- package/dist/edge-hub.js +1089 -0
- package/dist/edge-hub.js.map +1 -0
- package/dist/http-proxy.js +54 -0
- package/dist/http-proxy.js.map +1 -0
- package/dist/index.js +489 -0
- package/dist/index.js.map +1 -0
- package/dist/methods/devdevice.js +123 -0
- package/dist/methods/devdevice.js.map +1 -0
- package/dist/methods/environment.js +356 -0
- package/dist/methods/environment.js.map +1 -0
- package/dist/methods/index.js +24 -0
- package/dist/methods/index.js.map +1 -0
- package/dist/methods/network/ca.js +48 -0
- package/dist/methods/network/ca.js.map +1 -0
- package/dist/methods/network/hostname.js +56 -0
- package/dist/methods/network/hostname.js.map +1 -0
- package/dist/methods/network/lan.js +193 -0
- package/dist/methods/network/lan.js.map +1 -0
- package/dist/methods/network/wifi.js +299 -0
- package/dist/methods/network/wifi.js.map +1 -0
- package/dist/methods/reboot.js +22 -0
- package/dist/methods/reboot.js.map +1 -0
- package/dist/methods/ssh.js +148 -0
- package/dist/methods/ssh.js.map +1 -0
- package/dist/services/scheduler.service.js +335 -0
- package/dist/services/scheduler.service.js.map +1 -0
- package/dist/store/types/network-interface.type.js +3 -0
- package/dist/store/types/network-interface.type.js.map +1 -0
- package/dist/time-sync.js +119 -0
- package/dist/time-sync.js.map +1 -0
- package/dist/types/command.types.js +8 -0
- package/dist/types/command.types.js.map +1 -0
- package/dist/types/container.types.js +3 -0
- package/dist/types/container.types.js.map +1 -0
- package/dist/types/event.types.js +9 -0
- package/dist/types/event.types.js.map +1 -0
- package/dist/types/job.types.js +15 -0
- package/dist/types/job.types.js.map +1 -0
- package/dist/types/twin.types.js +21 -0
- package/dist/types/twin.types.js.map +1 -0
- package/dist/utilities/docker-progress-tracker.js +28 -0
- package/dist/utilities/docker-progress-tracker.js.map +1 -0
- package/dist/utilities/docker.js +819 -0
- package/dist/utilities/docker.js.map +1 -0
- package/dist/utilities/instances.js +169 -0
- package/dist/utilities/instances.js.map +1 -0
- package/dist/utilities/jobs.js +103 -0
- package/dist/utilities/jobs.js.map +1 -0
- package/dist/utilities/local-twins.js +204 -0
- package/dist/utilities/local-twins.js.map +1 -0
- package/dist/utilities/network-settings.js +147 -0
- package/dist/utilities/network-settings.js.map +1 -0
- package/dist/utilities/symlink.js +47 -0
- package/dist/utilities/symlink.js.map +1 -0
- package/dist/utilities/sysinfo.js +128 -0
- package/dist/utilities/sysinfo.js.map +1 -0
- package/dist/utilities/twin-manager.js +108 -0
- package/dist/utilities/twin-manager.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const dockerode_1 = __importDefault(require("dockerode"));
|
|
7
|
+
const lodash_1 = require("lodash");
|
|
8
|
+
const phy_logger_1 = require("@phystack/phy-logger");
|
|
9
|
+
const twin_types_1 = require("../types/twin.types");
|
|
10
|
+
const async_mutex_1 = require("async-mutex");
|
|
11
|
+
const twin_manager_1 = require("./twin-manager");
|
|
12
|
+
const docker_progress_tracker_1 = require("./docker-progress-tracker");
|
|
13
|
+
const uuid_1 = require("uuid");
|
|
14
|
+
const hub_client_1 = require("@phystack/hub-client");
|
|
15
|
+
class DockerManager {
|
|
16
|
+
docker;
|
|
17
|
+
logger;
|
|
18
|
+
twinInstancesRunning;
|
|
19
|
+
imagesInProgress;
|
|
20
|
+
instances;
|
|
21
|
+
firstRunCompleted = false;
|
|
22
|
+
hubDevice = false;
|
|
23
|
+
phyHubClient = null;
|
|
24
|
+
signalsClient;
|
|
25
|
+
sessionId = null;
|
|
26
|
+
operationLock = new async_mutex_1.Mutex();
|
|
27
|
+
imagePullsInProgress = new Map();
|
|
28
|
+
constructor() {
|
|
29
|
+
this.docker = new dockerode_1.default();
|
|
30
|
+
this.logger = new phy_logger_1.PhyLogger({
|
|
31
|
+
logToFile: false,
|
|
32
|
+
logToConsole: true,
|
|
33
|
+
includeTrace: true,
|
|
34
|
+
namespace: 'DockerManager',
|
|
35
|
+
});
|
|
36
|
+
this.twinInstancesRunning = new Map();
|
|
37
|
+
this.imagesInProgress = new Map();
|
|
38
|
+
this.instances = new Map();
|
|
39
|
+
this.listenToDockerEvents();
|
|
40
|
+
}
|
|
41
|
+
async initializeHubSignals(initSignalsParams, dataResidency) {
|
|
42
|
+
try {
|
|
43
|
+
this.phyHubClient = await (0, hub_client_1.connectPhyClient)({ moduleName: 'docker-crash', dataResidency });
|
|
44
|
+
this.signalsClient = await this.phyHubClient.initializeSignals(initSignalsParams);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
this.logger.error('Failed to initialize signals client for docker-crash signals:', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async listenToDockerEvents() {
|
|
51
|
+
try {
|
|
52
|
+
const eventStream = await this.docker.getEvents({
|
|
53
|
+
filters: {
|
|
54
|
+
event: ['die', 'stop', 'kill'],
|
|
55
|
+
type: ['container'],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
eventStream.on('data', async (buffer) => {
|
|
59
|
+
try {
|
|
60
|
+
const event = JSON.parse(buffer.toString());
|
|
61
|
+
if (event.status === 'die' && event.exitCode !== 0) {
|
|
62
|
+
await this.onContainerCrash(event.id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
eventStream.on('error', (error) => {
|
|
69
|
+
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
70
|
+
});
|
|
71
|
+
eventStream.on('end', () => {
|
|
72
|
+
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
getDefaultPlatform() {
|
|
80
|
+
const arch = process.arch;
|
|
81
|
+
if (arch === 'x64') {
|
|
82
|
+
return 'linux/amd64';
|
|
83
|
+
}
|
|
84
|
+
else if (arch === 'arm64') {
|
|
85
|
+
return 'linux/arm64';
|
|
86
|
+
}
|
|
87
|
+
else if (arch === 'arm') {
|
|
88
|
+
return 'linux/arm/v7';
|
|
89
|
+
}
|
|
90
|
+
else if (arch === 'ia32') {
|
|
91
|
+
return 'linux/386';
|
|
92
|
+
}
|
|
93
|
+
return 'linux/amd64';
|
|
94
|
+
}
|
|
95
|
+
extractNameFromImage = (image) => {
|
|
96
|
+
try {
|
|
97
|
+
const regex = /[^\/]+\.([^\/:]+:[^\/]+)$/;
|
|
98
|
+
const match = image.match(regex);
|
|
99
|
+
return match && match.length > 0 ? match[1].split(':')[0] : null;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
async onContainerCrash(containerId) {
|
|
106
|
+
try {
|
|
107
|
+
const twinManager = await (0, twin_manager_1.getTwinManagerInstance)();
|
|
108
|
+
if (!twinManager) {
|
|
109
|
+
throw new Error('Error getting twin manager instance');
|
|
110
|
+
}
|
|
111
|
+
this.hubDevice = await twinManager.getHubDevice();
|
|
112
|
+
let twinId;
|
|
113
|
+
for (const [id, instance] of this.instances.entries()) {
|
|
114
|
+
if (instance.container.id === containerId) {
|
|
115
|
+
twinId = id;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const container = this.docker.getContainer(containerId);
|
|
120
|
+
const logs = await container.logs({ stdout: true, stderr: true, tail: 50 });
|
|
121
|
+
const logString = logs.toString();
|
|
122
|
+
if (twinId) {
|
|
123
|
+
const reportedProperties = {
|
|
124
|
+
lastError: {
|
|
125
|
+
message: `container crashed`,
|
|
126
|
+
timestamp: new Date(),
|
|
127
|
+
errorObject: {
|
|
128
|
+
containerLogs: logString,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
this.hubDevice.reportEdgeTwinProperties(twinId, reportedProperties);
|
|
133
|
+
}
|
|
134
|
+
const deviceStatus = await this.hubDevice.getDeviceStatus();
|
|
135
|
+
const twinResponse = await this.hubDevice.getEdgeInstance({ twinId });
|
|
136
|
+
let twin = undefined;
|
|
137
|
+
if (twinResponse && twinResponse.twin) {
|
|
138
|
+
twin = twinResponse.twin;
|
|
139
|
+
}
|
|
140
|
+
let ip = '127.0.0.1';
|
|
141
|
+
if (deviceStatus.ip && deviceStatus.ip.length > 0) {
|
|
142
|
+
ip = deviceStatus.ip[0].ipv4;
|
|
143
|
+
}
|
|
144
|
+
if (!this.phyHubClient) {
|
|
145
|
+
const initSignalsParams = {
|
|
146
|
+
deviceId: deviceStatus.deviceId,
|
|
147
|
+
installationId: twin?.properties.desired.installationId ?? 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
148
|
+
spaceId: deviceStatus.spaceId,
|
|
149
|
+
tenantId: deviceStatus.tenantId,
|
|
150
|
+
appVersion: 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
151
|
+
appId: 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
152
|
+
installationVersion: 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
153
|
+
environment: deviceStatus.gridEnv,
|
|
154
|
+
dataResidency: deviceStatus.dataResidency.toUpperCase(),
|
|
155
|
+
country: 'SE',
|
|
156
|
+
accessToken: deviceStatus.accessKey,
|
|
157
|
+
clientUserAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
|
158
|
+
ip,
|
|
159
|
+
};
|
|
160
|
+
await this.initializeHubSignals(initSignalsParams, deviceStatus.dataResidency.toUpperCase());
|
|
161
|
+
}
|
|
162
|
+
if (this.phyHubClient) {
|
|
163
|
+
if (!this.sessionId) {
|
|
164
|
+
const instance = this.signalsClient.getInstanceProps();
|
|
165
|
+
this.sessionId = instance.sessionId;
|
|
166
|
+
const clientId = instance.clientId ?? (0, uuid_1.v4)();
|
|
167
|
+
await this.hubDevice.sendSessionSignal({
|
|
168
|
+
deviceId: this.hubDevice.deviceId,
|
|
169
|
+
data: {
|
|
170
|
+
c: instance.tenantId,
|
|
171
|
+
b: this.sessionId,
|
|
172
|
+
d: instance.sessionCreated,
|
|
173
|
+
f: instance.environment,
|
|
174
|
+
g: instance.dataResidency,
|
|
175
|
+
m: instance.country,
|
|
176
|
+
n: instance.locationAccuracy,
|
|
177
|
+
o: instance.latitude,
|
|
178
|
+
p: instance.longitude,
|
|
179
|
+
h: instance.spaceId,
|
|
180
|
+
i: instance.appId,
|
|
181
|
+
j: instance.appVersion,
|
|
182
|
+
k: instance.installationId,
|
|
183
|
+
l: instance.installationVersion,
|
|
184
|
+
q: instance.deviceId,
|
|
185
|
+
r: clientId,
|
|
186
|
+
e: instance.clientIp ?? instance.ip,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
await this.hubDevice.sendClientSignal({
|
|
190
|
+
deviceId: this.hubDevice.deviceId,
|
|
191
|
+
data: {
|
|
192
|
+
c: instance.tenantId,
|
|
193
|
+
r: clientId,
|
|
194
|
+
ah: instance.clientCreated,
|
|
195
|
+
aj: instance.clientUserAgent,
|
|
196
|
+
g: instance.dataResidency,
|
|
197
|
+
m: instance.country,
|
|
198
|
+
o: instance.latitude,
|
|
199
|
+
p: instance.longitude,
|
|
200
|
+
n: instance.locationAccuracy,
|
|
201
|
+
e: instance.ip ?? instance.clientIp ?? undefined,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const timeWithoutZ = new Date().toISOString().replace(/Z$/, '');
|
|
206
|
+
const eventPayload = {
|
|
207
|
+
g: deviceStatus.dataResidency.toUpperCase(),
|
|
208
|
+
b: this.sessionId,
|
|
209
|
+
t: timeWithoutZ,
|
|
210
|
+
s: 'MONITOR_CONTAINER_CRASH',
|
|
211
|
+
u: false,
|
|
212
|
+
h: deviceStatus.spaceId,
|
|
213
|
+
ac: twinId,
|
|
214
|
+
ad: deviceStatus.deviceId,
|
|
215
|
+
c: deviceStatus.tenantId,
|
|
216
|
+
};
|
|
217
|
+
await this.hubDevice.sendEventSignal({
|
|
218
|
+
deviceId: this.hubDevice.deviceId,
|
|
219
|
+
data: eventPayload,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this.logger.error(`onContainerCrash(): Error in reporting the container crash`, error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async imageExists(image) {
|
|
228
|
+
this.logger.info(`imageExists(): Checking if docker image ${image} exists`);
|
|
229
|
+
try {
|
|
230
|
+
await this.docker.getImage(image).inspect();
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async pullImage(image, authConfig, abortController) {
|
|
238
|
+
this.logger.info(`pullImage(): Pulling docker image ${image}`);
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const platform = this.getDefaultPlatform();
|
|
241
|
+
this.logger.info(`pullImage(): Using platform ${platform} for image ${image}`);
|
|
242
|
+
if (abortController.signal.aborted) {
|
|
243
|
+
this.logger.info(`pullImage(): Pull for image ${image} was aborted before starting`);
|
|
244
|
+
return resolve();
|
|
245
|
+
}
|
|
246
|
+
let isCompleted = false;
|
|
247
|
+
const abortHandler = () => {
|
|
248
|
+
if (isCompleted)
|
|
249
|
+
return;
|
|
250
|
+
this.logger.info(`pullImage(): Pull for image ${image} was aborted`);
|
|
251
|
+
isCompleted = true;
|
|
252
|
+
const imageProgress = this.imagesInProgress.get(image);
|
|
253
|
+
if (imageProgress) {
|
|
254
|
+
imageProgress.inProgress = false;
|
|
255
|
+
this.imagesInProgress.delete(image);
|
|
256
|
+
}
|
|
257
|
+
resolve();
|
|
258
|
+
};
|
|
259
|
+
abortController.signal.addEventListener('abort', abortHandler);
|
|
260
|
+
const timeoutId = setTimeout(() => {
|
|
261
|
+
if (!isCompleted) {
|
|
262
|
+
this.logger.error(`pullImage(): Timeout waiting for image ${image} to pull`);
|
|
263
|
+
abortController.abort();
|
|
264
|
+
}
|
|
265
|
+
}, 7200000);
|
|
266
|
+
const progressTracker = new docker_progress_tracker_1.DockerProgressTracker(image, 'DockerManager');
|
|
267
|
+
const imageProgress = {
|
|
268
|
+
inProgress: true,
|
|
269
|
+
layersProgress: new Map(),
|
|
270
|
+
};
|
|
271
|
+
this.imagesInProgress.set(image, imageProgress);
|
|
272
|
+
this.docker.pull(image, {
|
|
273
|
+
authconfig: authConfig,
|
|
274
|
+
platform,
|
|
275
|
+
architecture: process.arch,
|
|
276
|
+
os: 'linux',
|
|
277
|
+
}, (err, stream) => {
|
|
278
|
+
if (err) {
|
|
279
|
+
clearTimeout(timeoutId);
|
|
280
|
+
this.logger.error(`pullImage(): Error initiating pull for image ${image}`, err);
|
|
281
|
+
const isCredentialError = err.message &&
|
|
282
|
+
(err.message.includes('unauthorized') ||
|
|
283
|
+
err.message.includes('authentication required') ||
|
|
284
|
+
err.message.includes('access denied') ||
|
|
285
|
+
err.message.includes('credential') ||
|
|
286
|
+
err.statusCode === 401);
|
|
287
|
+
if (isCredentialError) {
|
|
288
|
+
this.logger.error(`pullImage(): Credential error detected for image ${image}, releasing lock immediately`);
|
|
289
|
+
const imageProgress = this.imagesInProgress.get(image);
|
|
290
|
+
if (imageProgress) {
|
|
291
|
+
imageProgress.inProgress = false;
|
|
292
|
+
this.imagesInProgress.delete(image);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
isCompleted = true;
|
|
296
|
+
progressTracker.markFailed(err);
|
|
297
|
+
return reject(err);
|
|
298
|
+
}
|
|
299
|
+
if (abortController.signal.aborted) {
|
|
300
|
+
clearTimeout(timeoutId);
|
|
301
|
+
abortHandler();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.docker.modem.followProgress(stream, (err, output) => {
|
|
305
|
+
clearTimeout(timeoutId);
|
|
306
|
+
if (isCompleted)
|
|
307
|
+
return;
|
|
308
|
+
if (err) {
|
|
309
|
+
this.logger.error(`pullImage(): Error pulling image ${image}`, err);
|
|
310
|
+
imageProgress.inProgress = false;
|
|
311
|
+
this.imagesInProgress.delete(image);
|
|
312
|
+
isCompleted = true;
|
|
313
|
+
progressTracker.markFailed(err);
|
|
314
|
+
reject(err);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.logger.info(`pullImage(): Image ${image} download complete`);
|
|
318
|
+
imageProgress.inProgress = false;
|
|
319
|
+
this.imagesInProgress.delete(image);
|
|
320
|
+
isCompleted = true;
|
|
321
|
+
progressTracker.markComplete();
|
|
322
|
+
resolve();
|
|
323
|
+
}, (event) => {
|
|
324
|
+
if (abortController.signal.aborted || isCompleted) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
progressTracker.processEvent(event);
|
|
328
|
+
if (event.id && event.progressDetail) {
|
|
329
|
+
imageProgress.layersProgress.set(event.id, event.progressDetail);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async createContainer(twin, skipGpu = false) {
|
|
336
|
+
const { image, createOptions } = twin.properties.desired;
|
|
337
|
+
const parsedOptions = createOptions;
|
|
338
|
+
this.logger.info(`createContainer(): Twin object for ${twin.id}:`, JSON.stringify(twin, null, 2));
|
|
339
|
+
this.logger.info(`createContainer(): Parsed options for ${twin.id}:`, JSON.stringify(parsedOptions, null, 2));
|
|
340
|
+
this.logger.info(`createContainer(): Device requests for ${twin.id}:`, JSON.stringify(parsedOptions?.HostConfig?.DeviceRequests, null, 2));
|
|
341
|
+
const isHostNetwork = parsedOptions?.HostConfig?.NetworkMode === 'host';
|
|
342
|
+
const containerConfig = {
|
|
343
|
+
Image: image,
|
|
344
|
+
Env: [...(parsedOptions?.Env ?? []), `TWIN_ID=${twin.id}`],
|
|
345
|
+
HostConfig: {
|
|
346
|
+
...parsedOptions?.HostConfig,
|
|
347
|
+
NetworkMode: isHostNetwork ? 'host' : 'default',
|
|
348
|
+
ExtraHosts: [isHostNetwork ? `phyos:127.0.0.1` : `phyos:172.26.128.1`],
|
|
349
|
+
RestartPolicy: parsedOptions?.HostConfig?.RestartPolicy ?? { Name: 'always' },
|
|
350
|
+
Privileged: parsedOptions?.HostConfig?.Privileged ?? false,
|
|
351
|
+
Memory: parsedOptions?.HostConfig?.Memory ?? 0,
|
|
352
|
+
CpuShares: parsedOptions?.HostConfig?.CpuShares ?? 0,
|
|
353
|
+
StorageOpt: parsedOptions?.HostConfig?.StorageOpt ?? {},
|
|
354
|
+
DiskQuota: parsedOptions?.HostConfig?.DiskQuota ?? 0,
|
|
355
|
+
ReadonlyRootfs: parsedOptions?.HostConfig?.ReadonlyRootfs ?? false,
|
|
356
|
+
BlkioWeight: parsedOptions?.HostConfig?.BlkioWeight ?? 0,
|
|
357
|
+
BlkioDeviceReadBps: parsedOptions?.HostConfig?.BlkioDeviceReadBps ?? [],
|
|
358
|
+
BlkioDeviceWriteBps: parsedOptions?.HostConfig?.BlkioDeviceWriteBps ?? [],
|
|
359
|
+
Devices: parsedOptions?.HostConfig?.Devices ?? [],
|
|
360
|
+
DeviceRequests: parsedOptions?.HostConfig?.DeviceRequests ?? [],
|
|
361
|
+
Binds: parsedOptions?.HostConfig?.Binds ?? [],
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
if (skipGpu && parsedOptions?.HostConfig?.DeviceRequests) {
|
|
365
|
+
this.logger.info(`createContainer(): Original DeviceRequests before filtering:`, JSON.stringify(parsedOptions.HostConfig.DeviceRequests, null, 2));
|
|
366
|
+
const filteredDeviceRequests = parsedOptions.HostConfig.DeviceRequests.filter((request) => {
|
|
367
|
+
const isGpuRequest = request.Capabilities &&
|
|
368
|
+
request.Capabilities.some((cap) => cap.includes('gpu'));
|
|
369
|
+
return !isGpuRequest;
|
|
370
|
+
});
|
|
371
|
+
if (containerConfig.HostConfig) {
|
|
372
|
+
containerConfig.HostConfig.DeviceRequests = filteredDeviceRequests;
|
|
373
|
+
this.logger.info(`createContainer(): Creating container for twin ${twin.id} without GPU support. Filtered DeviceRequests:`, JSON.stringify(containerConfig.HostConfig.DeviceRequests, null, 2));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (parsedOptions?.HostConfig?.DeviceRequests) {
|
|
377
|
+
this.logger.info(`createContainer(): Original DeviceRequests:`, JSON.stringify(parsedOptions.HostConfig.DeviceRequests, null, 2));
|
|
378
|
+
if (containerConfig.HostConfig) {
|
|
379
|
+
containerConfig.HostConfig.DeviceRequests = parsedOptions.HostConfig.DeviceRequests;
|
|
380
|
+
const hasGpuRequest = parsedOptions.HostConfig.DeviceRequests.some((req) => req.Capabilities && req.Capabilities.some((cap) => cap.includes('gpu')));
|
|
381
|
+
if (hasGpuRequest) {
|
|
382
|
+
try {
|
|
383
|
+
const info = await this.docker.info();
|
|
384
|
+
const availableRuntimes = info.Runtimes ? Object.keys(info.Runtimes) : [];
|
|
385
|
+
this.logger.info(`Available Docker runtimes:`, JSON.stringify(availableRuntimes, null, 2));
|
|
386
|
+
if (availableRuntimes.includes('nvidia')) {
|
|
387
|
+
containerConfig.HostConfig.Runtime = 'nvidia';
|
|
388
|
+
this.logger.info(`createContainer(): Setting nvidia runtime for GPU support`);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
this.logger.warn(`createContainer(): NVIDIA runtime not available, GPU may not work`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
this.logger.warn(`createContainer(): Error checking for available runtimes:`, error);
|
|
396
|
+
containerConfig.HostConfig.Runtime = 'nvidia';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.logger.info(`createContainer(): Applied DeviceRequests to container config:`, JSON.stringify(containerConfig.HostConfig.DeviceRequests, null, 2));
|
|
400
|
+
}
|
|
401
|
+
if (parsedOptions.HostConfig.DeviceRequests.some((req) => req.Capabilities && req.Capabilities.some((cap) => cap.includes('gpu')))) {
|
|
402
|
+
this.logger.info(`createContainer(): Creating container for twin ${twin.id} with GPU support`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!isHostNetwork) {
|
|
406
|
+
containerConfig.NetworkingConfig = {
|
|
407
|
+
EndpointsConfig: {
|
|
408
|
+
'phyos-edge-net': {
|
|
409
|
+
Aliases: [twin.id],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
this.logger.info(`createContainer(): Creating container for twin ${twin.id}`, {
|
|
415
|
+
containerConfig,
|
|
416
|
+
});
|
|
417
|
+
this.logger.info(`createContainer(): Final container config for twin ${twin.id}:`, JSON.stringify(containerConfig, null, 2));
|
|
418
|
+
const container = await this.docker.createContainer(containerConfig);
|
|
419
|
+
try {
|
|
420
|
+
const containerDetails = await container.inspect();
|
|
421
|
+
this.logger.info(`createContainer(): Created container details for twin ${twin.id}:`, JSON.stringify(containerDetails, null, 2));
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
this.logger.error(`createContainer(): Failed to inspect container for twin ${twin.id}:`, error);
|
|
425
|
+
}
|
|
426
|
+
return container;
|
|
427
|
+
}
|
|
428
|
+
async _startContainer(twin) {
|
|
429
|
+
if (this.twinInstancesRunning.get(twin.id)) {
|
|
430
|
+
this.logger.info(`startContainer(): Container for twin ${twin.id} is already running or starting`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.twinInstancesRunning.set(twin.id, true);
|
|
434
|
+
try {
|
|
435
|
+
this.logger.info(`startContainer(): Attempting to create container for twin ${twin.id} with GPU support`);
|
|
436
|
+
try {
|
|
437
|
+
const container = await this.createContainer(twin, false);
|
|
438
|
+
this.logger.info(`startContainer(): Container created for twin ${twin.id}, preparing to start it`);
|
|
439
|
+
this.instances.set(twin.id, { container, twin });
|
|
440
|
+
await container.start();
|
|
441
|
+
this.logger.info(`startContainer(): Started container for twin ${twin.id}`);
|
|
442
|
+
const logs = await container.logs({ stdout: true, stderr: true });
|
|
443
|
+
this.logger.info(`Container logs for twin ${twin.id}:`, logs.toString());
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
this.logger.error(`startContainer(): Error starting container with GPU for twin ${twin.id}:`, error.message);
|
|
447
|
+
const isGpuError = error.message &&
|
|
448
|
+
((error.message.includes('could not select device driver') &&
|
|
449
|
+
error.message.includes('capabilities: [[gpu]]')) ||
|
|
450
|
+
error.message.includes('nvidia-container-cli: initialization error: nvml error: driver not loaded') ||
|
|
451
|
+
error.message.includes('nvidia-container-cli') ||
|
|
452
|
+
error.message.includes('unknown or invalid runtime name: nvidia') ||
|
|
453
|
+
error.message.includes('nvidia runtime') ||
|
|
454
|
+
error.message.includes('GPU') ||
|
|
455
|
+
error.message.includes('gpu'));
|
|
456
|
+
if (isGpuError) {
|
|
457
|
+
this.logger.warn(`startContainer(): GPU support not available for twin ${twin.id}, retrying without GPU capabilities. Error: ${error.message}`, { error: error.message });
|
|
458
|
+
try {
|
|
459
|
+
this.instances.delete(twin.id);
|
|
460
|
+
const containerId = error.json?.message?.match(/^.*container: (.*?)(?:\s|$)/)?.[1];
|
|
461
|
+
if (containerId) {
|
|
462
|
+
try {
|
|
463
|
+
await this.docker.getContainer(containerId).remove({ force: true });
|
|
464
|
+
this.logger.info(`startContainer(): Removed failed GPU container ${containerId}`);
|
|
465
|
+
}
|
|
466
|
+
catch (removeError) {
|
|
467
|
+
this.logger.info(`startContainer(): Could not remove container ${containerId}, may not exist`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (cleanupError) {
|
|
472
|
+
this.logger.warn(`startContainer(): Error during cleanup: ${cleanupError.message}`);
|
|
473
|
+
}
|
|
474
|
+
const container = await this.createContainer(twin, true);
|
|
475
|
+
this.instances.set(twin.id, { container, twin });
|
|
476
|
+
await container.start();
|
|
477
|
+
this.logger.info(`startContainer(): Started container for twin ${twin.id} without GPU support`);
|
|
478
|
+
const logs = await container.logs({ stdout: true, stderr: true });
|
|
479
|
+
this.logger.info(`Container logs for twin ${twin.id}:`, logs.toString());
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
this.logger.error(`startContainer(): Failed to start container for twin ${twin.id}`, error);
|
|
488
|
+
if (error.message.includes('port is already allocated')) {
|
|
489
|
+
this.logger.error(`startContainer(): Port conflict detected for twin ${twin.id}`);
|
|
490
|
+
}
|
|
491
|
+
this.instances.delete(twin.id);
|
|
492
|
+
this.twinInstancesRunning.set(twin.id, false);
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
abortImagePull(image) {
|
|
497
|
+
const pullInfo = this.imagePullsInProgress.get(image);
|
|
498
|
+
if (pullInfo) {
|
|
499
|
+
this.logger.info(`abortImagePull(): Aborting pull for image ${image}`);
|
|
500
|
+
pullInfo.abortController.abort();
|
|
501
|
+
const imageProgress = this.imagesInProgress.get(image);
|
|
502
|
+
if (imageProgress) {
|
|
503
|
+
imageProgress.inProgress = false;
|
|
504
|
+
this.imagesInProgress.delete(image);
|
|
505
|
+
}
|
|
506
|
+
setTimeout(() => {
|
|
507
|
+
if (this.imagePullsInProgress.has(image)) {
|
|
508
|
+
this.logger.warn(`abortImagePull(): Cleaning up stale pull info for ${image}`);
|
|
509
|
+
this.imagePullsInProgress.delete(image);
|
|
510
|
+
}
|
|
511
|
+
}, 5000);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
isImagePullInProgress(image) {
|
|
515
|
+
return this.imagePullsInProgress.has(image);
|
|
516
|
+
}
|
|
517
|
+
async ensureImageAvailable(image, authConfig) {
|
|
518
|
+
const imageExists = await this.imageExists(image);
|
|
519
|
+
if (imageExists)
|
|
520
|
+
return true;
|
|
521
|
+
const existingPull = this.imagePullsInProgress.get(image);
|
|
522
|
+
if (existingPull) {
|
|
523
|
+
this.logger.info(`ensureImageAvailable(): Pull already in progress for ${image}, waiting for it to complete`);
|
|
524
|
+
try {
|
|
525
|
+
await existingPull.promise;
|
|
526
|
+
this.logger.info(`ensureImageAvailable(): Existing pull for ${image} completed successfully`);
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
this.logger.error(`ensureImageAvailable(): Existing pull for ${image} failed`, error);
|
|
531
|
+
const isCredentialError = error.message &&
|
|
532
|
+
(error.message.includes('unauthorized') ||
|
|
533
|
+
error.message.includes('authentication required') ||
|
|
534
|
+
error.message.includes('access denied') ||
|
|
535
|
+
error.message.includes('credential') ||
|
|
536
|
+
error.statusCode === 401);
|
|
537
|
+
if (isCredentialError) {
|
|
538
|
+
this.logger.error(`ensureImageAvailable(): Credential error detected for existing pull of ${image}`);
|
|
539
|
+
}
|
|
540
|
+
this.imagePullsInProgress.delete(image);
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
this.logger.info(`ensureImageAvailable(): Starting new pull for ${image}`);
|
|
545
|
+
const abortController = new AbortController();
|
|
546
|
+
const pullPromise = this.pullImage(image, authConfig, abortController).finally(() => {
|
|
547
|
+
this.imagePullsInProgress.delete(image);
|
|
548
|
+
});
|
|
549
|
+
this.imagePullsInProgress.set(image, {
|
|
550
|
+
promise: pullPromise,
|
|
551
|
+
abortController,
|
|
552
|
+
});
|
|
553
|
+
try {
|
|
554
|
+
await pullPromise;
|
|
555
|
+
this.logger.info(`ensureImageAvailable(): Pull for ${image} completed successfully`);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
this.logger.error(`ensureImageAvailable(): Pull failed for ${image}`, error);
|
|
560
|
+
const isCredentialError = error.message &&
|
|
561
|
+
(error.message.includes('unauthorized') ||
|
|
562
|
+
error.message.includes('authentication required') ||
|
|
563
|
+
error.message.includes('access denied') ||
|
|
564
|
+
error.message.includes('credential') ||
|
|
565
|
+
error.statusCode === 401);
|
|
566
|
+
if (isCredentialError) {
|
|
567
|
+
this.logger.error(`ensureImageAvailable(): Credential error detected for pull of ${image}`);
|
|
568
|
+
}
|
|
569
|
+
this.imagePullsInProgress.delete(image);
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async startEdgeApp(twin) {
|
|
574
|
+
const { image } = twin.properties.desired;
|
|
575
|
+
if (!image) {
|
|
576
|
+
return {
|
|
577
|
+
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
578
|
+
error: {
|
|
579
|
+
message: 'No image found in desired properties',
|
|
580
|
+
timestamp: new Date(),
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const hasRegistry = image.includes('/');
|
|
585
|
+
let authConfig;
|
|
586
|
+
let pullError = null;
|
|
587
|
+
if (hasRegistry) {
|
|
588
|
+
const [hostname] = image.split('/');
|
|
589
|
+
authConfig = twin.properties.desired.credentials;
|
|
590
|
+
if (!authConfig) {
|
|
591
|
+
this.logger.error(`startEdgeApp(): No credentials found for registry: ${hostname}`);
|
|
592
|
+
return {
|
|
593
|
+
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
594
|
+
error: {
|
|
595
|
+
message: `No credentials found for registry: ${hostname}`,
|
|
596
|
+
timestamp: new Date(),
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const pullSuccess = await this.ensureImageAvailable(image, {
|
|
602
|
+
username: authConfig.username,
|
|
603
|
+
password: authConfig.password,
|
|
604
|
+
serveraddress: authConfig.address,
|
|
605
|
+
});
|
|
606
|
+
if (!pullSuccess) {
|
|
607
|
+
this.logger.error(`startEdgeApp(): Failed to pull image ${image}`);
|
|
608
|
+
return {
|
|
609
|
+
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
610
|
+
error: {
|
|
611
|
+
message: `Failed to pull image ${image}`,
|
|
612
|
+
timestamp: new Date(),
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
this.logger.error(`startEdgeApp(): Error pulling image ${image}`, error);
|
|
619
|
+
const errorMsg = error.message || '';
|
|
620
|
+
const isCredentialError = errorMsg.includes('unauthorized') ||
|
|
621
|
+
errorMsg.includes('authentication required') ||
|
|
622
|
+
errorMsg.includes('access denied') ||
|
|
623
|
+
errorMsg.includes('credential') ||
|
|
624
|
+
error.statusCode === 401;
|
|
625
|
+
return {
|
|
626
|
+
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
627
|
+
error: {
|
|
628
|
+
message: isCredentialError
|
|
629
|
+
? `Authentication failed for image ${image}. Please check your credentials.`
|
|
630
|
+
: `Failed to pull image ${image}: ${errorMsg}`,
|
|
631
|
+
timestamp: new Date(),
|
|
632
|
+
errorObject: error,
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
try {
|
|
639
|
+
const pullSuccess = await this.ensureImageAvailable(image, {});
|
|
640
|
+
if (!pullSuccess) {
|
|
641
|
+
this.logger.error(`startEdgeApp(): Failed to pull local image ${image}`);
|
|
642
|
+
return {
|
|
643
|
+
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
644
|
+
error: {
|
|
645
|
+
message: `Failed to pull local image ${image}`,
|
|
646
|
+
timestamp: new Date(),
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
this.logger.error(`startEdgeApp(): Error pulling local image ${image}`, error);
|
|
653
|
+
return {
|
|
654
|
+
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
655
|
+
error: {
|
|
656
|
+
message: `Failed to pull local image ${image}: ${error.message || ''}`,
|
|
657
|
+
timestamp: new Date(),
|
|
658
|
+
errorObject: error,
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return this.operationLock.runExclusive(() => this._startEdgeApp(twin));
|
|
664
|
+
}
|
|
665
|
+
async _startEdgeApp(twin) {
|
|
666
|
+
const { image } = twin.properties.desired;
|
|
667
|
+
try {
|
|
668
|
+
const existingTwin = this.instances.get(twin.id);
|
|
669
|
+
const hasImageChanged = existingTwin && existingTwin.twin.properties.desired.image !== image;
|
|
670
|
+
if (existingTwin && hasImageChanged) {
|
|
671
|
+
const oldImage = existingTwin.twin.properties.desired.image;
|
|
672
|
+
this.logger.info(`startEdgeApp(): Twin ${twin.id} image has changed from ${oldImage} to ${image}, aborting old pull if in progress`);
|
|
673
|
+
this.abortImagePull(oldImage);
|
|
674
|
+
}
|
|
675
|
+
const hasConfigChanged = existingTwin &&
|
|
676
|
+
!(0, lodash_1.isEqual)(existingTwin.twin.properties.desired.containerCreateOptions, twin.properties.desired.containerCreateOptions);
|
|
677
|
+
if (existingTwin && (hasConfigChanged || hasImageChanged)) {
|
|
678
|
+
this.logger.info(`startEdgeApp(): Twin ${twin.id} configuration or image has changed, recreating container...`);
|
|
679
|
+
await this._stopAndRemoveContainer(twin.id);
|
|
680
|
+
}
|
|
681
|
+
const finalImageExists = await this.imageExists(image);
|
|
682
|
+
if (!finalImageExists) {
|
|
683
|
+
this.logger.error(`startEdgeApp(): Image ${image} not available locally`);
|
|
684
|
+
return {
|
|
685
|
+
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
686
|
+
error: {
|
|
687
|
+
message: `Image ${image} not available locally`,
|
|
688
|
+
timestamp: new Date(),
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (!this.instances.get(twin.id)) {
|
|
693
|
+
await this._startContainer(twin);
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
status: twin_types_1.TwinStatusEnum.Online,
|
|
697
|
+
error: undefined,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
this.logger.error(`startEdgeApp(): Failed to start edge app for twin ${twin.id}`, error);
|
|
702
|
+
return {
|
|
703
|
+
status: twin_types_1.TwinStatusEnum.Exited,
|
|
704
|
+
error: {
|
|
705
|
+
message: `Failed to start edge app`,
|
|
706
|
+
timestamp: new Date(),
|
|
707
|
+
errorObject: error,
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async _stopAndRemoveContainer(twinId) {
|
|
713
|
+
const containerInfo = this.instances.get(twinId);
|
|
714
|
+
if (!containerInfo) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const containerDetails = await containerInfo.container.inspect();
|
|
719
|
+
if (containerDetails.State.Running) {
|
|
720
|
+
await containerInfo.container.stop();
|
|
721
|
+
this.logger.info(`stopAndRemoveContainer(): Stopped container ${containerDetails.Id}`);
|
|
722
|
+
}
|
|
723
|
+
await containerInfo.container.remove({ force: true });
|
|
724
|
+
this.logger.info(`stopAndRemoveContainer(): Removed container ${containerDetails.Id} for twin ${twinId}`);
|
|
725
|
+
this.instances.delete(twinId);
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
const isNoSuchContainerError = error.statusCode === 404 &&
|
|
729
|
+
(error.reason === 'no such container' ||
|
|
730
|
+
(error.json && error.json.message && error.json.message.includes('No such container')));
|
|
731
|
+
if (isNoSuchContainerError) {
|
|
732
|
+
this.logger.warn(`stopAndRemoveContainer(): Container for twin ${twinId} no longer exists, cleaning up references`, { error: error.message });
|
|
733
|
+
this.instances.delete(twinId);
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
this.logger.error(`stopAndRemoveContainer(): Failed to stop/remove container for twin ${twinId}`, error);
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
this.twinInstancesRunning.set(twinId, false);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async _stopAndRemoveAllContainers() {
|
|
745
|
+
if (this.firstRunCompleted) {
|
|
746
|
+
this.logger.info('stopAndRemoveAllContainers(): First run completed, skipping');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.firstRunCompleted = true;
|
|
750
|
+
this.logger.info('stopAndRemoveAllContainers(): Stopping and removing all running containers');
|
|
751
|
+
try {
|
|
752
|
+
const containers = await this.docker.listContainers({ all: true });
|
|
753
|
+
await Promise.allSettled(containers.map(async (containerInfo) => {
|
|
754
|
+
const container = this.docker.getContainer(containerInfo.Id);
|
|
755
|
+
try {
|
|
756
|
+
const containerDetails = await container.inspect();
|
|
757
|
+
if (containerDetails.State.Running) {
|
|
758
|
+
await container.stop();
|
|
759
|
+
this.logger.info(`stopAndRemoveAllContainers(): Stopped container ${containerInfo.Id}`);
|
|
760
|
+
}
|
|
761
|
+
await container.remove();
|
|
762
|
+
this.logger.info(`stopAndRemoveAllContainers(): Removed container ${containerInfo.Id}`);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
this.logger.error(`stopAndRemoveAllContainers(): Failed to stop/remove container ${containerInfo.Id}`, error);
|
|
766
|
+
}
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
this.logger.error('stopAndRemoveAllContainers(): Failed to list running containers', error);
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async stopAndRemoveContainer(twinId) {
|
|
775
|
+
return this.operationLock.runExclusive(() => this._stopAndRemoveContainer(twinId));
|
|
776
|
+
}
|
|
777
|
+
async stopAndRemoveAllContainers() {
|
|
778
|
+
return this.operationLock.runExclusive(() => this._stopAndRemoveAllContainers());
|
|
779
|
+
}
|
|
780
|
+
getInstances() {
|
|
781
|
+
return this.instances;
|
|
782
|
+
}
|
|
783
|
+
async manageDockerImages(image) {
|
|
784
|
+
try {
|
|
785
|
+
const images = await this.docker.listImages({
|
|
786
|
+
filters: {
|
|
787
|
+
reference: [image.split(':')[0]],
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
images.sort((a, b) => b.Created - a.Created);
|
|
791
|
+
if (images.length > 2) {
|
|
792
|
+
for (const img of images.slice(2)) {
|
|
793
|
+
try {
|
|
794
|
+
await this.docker.getImage(img.Id).remove({ force: true });
|
|
795
|
+
this.logger.info(`manageDockerImages(): Removed old image ${img.Id}`);
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
this.logger.warn(`manageDockerImages(): Failed to remove image ${img.Id}`, error);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
this.logger.error(`manageDockerImages(): Error managing images for ${image}`, error);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async isContainerRunning(twinId) {
|
|
808
|
+
try {
|
|
809
|
+
const containers = await this.docker.listContainers();
|
|
810
|
+
return containers.some(container => container.Names.some(name => name.includes(twinId)) && container.State === 'running');
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
this.logger.error(`isContainerRunning(): Error checking container status for ${twinId}:`, error);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
exports.default = DockerManager;
|
|
819
|
+
//# sourceMappingURL=docker.js.map
|