@phystack/device-phyos 4.5.18-dev → 4.5.20-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/dist/edge-hub.js +44 -5
- package/dist/edge-hub.js.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/methods/configure-phystack-sudoers.js +82 -0
- package/dist/methods/configure-phystack-sudoers.js.map +1 -0
- package/dist/methods/setup-phystack-password.js +93 -0
- package/dist/methods/setup-phystack-password.js.map +1 -0
- package/dist/types/twin.types.js +8 -1
- package/dist/types/twin.types.js.map +1 -1
- package/dist/utilities/audio-control.js +2 -2
- package/dist/utilities/audio-control.js.map +1 -1
- package/dist/utilities/docker-progress-tracker.js +5 -0
- package/dist/utilities/docker-progress-tracker.js.map +1 -1
- package/dist/utilities/docker.js +291 -74
- package/dist/utilities/docker.js.map +1 -1
- package/dist/utilities/encrypt-password.js +19 -0
- package/dist/utilities/encrypt-password.js.map +1 -0
- package/dist/utilities/generate-phystack-password.js +23 -0
- package/dist/utilities/generate-phystack-password.js.map +1 -0
- package/dist/utilities/instances.js +8 -1
- package/dist/utilities/instances.js.map +1 -1
- package/package.json +6 -6
package/dist/utilities/docker.js
CHANGED
|
@@ -13,6 +13,8 @@ const docker_progress_tracker_1 = require("./docker-progress-tracker");
|
|
|
13
13
|
const uuid_1 = require("uuid");
|
|
14
14
|
const hub_client_1 = require("@phystack/hub-client");
|
|
15
15
|
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const IMAGE_PULL_TIMES_FILE = '/data/settings/phyhub/docker-image-pull-times.json';
|
|
16
18
|
class DockerManager {
|
|
17
19
|
docker;
|
|
18
20
|
logger;
|
|
@@ -26,6 +28,7 @@ class DockerManager {
|
|
|
26
28
|
sessionId = null;
|
|
27
29
|
operationLock = new async_mutex_1.Mutex();
|
|
28
30
|
imagePullsInProgress = new Map();
|
|
31
|
+
imagePullTimes = new Map();
|
|
29
32
|
constructor() {
|
|
30
33
|
this.docker = new dockerode_1.default();
|
|
31
34
|
this.logger = new phy_logger_1.PhyLogger({
|
|
@@ -37,6 +40,10 @@ class DockerManager {
|
|
|
37
40
|
this.twinInstancesRunning = new Map();
|
|
38
41
|
this.imagesInProgress = new Map();
|
|
39
42
|
this.instances = new Map();
|
|
43
|
+
this.loadImagePullTimes();
|
|
44
|
+
this.cleanupStaleImagePullTimes().catch((error) => {
|
|
45
|
+
this.logger.warn('cleanupStaleImagePullTimes(): Failed to cleanup stale entries', error);
|
|
46
|
+
});
|
|
40
47
|
this.listenToDockerEvents();
|
|
41
48
|
}
|
|
42
49
|
async initializeHubSignals(initSignalsParams, dataResidency) {
|
|
@@ -59,24 +66,39 @@ class DockerManager {
|
|
|
59
66
|
eventStream.on('data', async (buffer) => {
|
|
60
67
|
try {
|
|
61
68
|
const event = JSON.parse(buffer.toString());
|
|
62
|
-
if (event.status === 'die'
|
|
63
|
-
|
|
69
|
+
if (event.status === 'die') {
|
|
70
|
+
const exitCode = parseInt(event.Actor?.Attributes?.exitCode || '0', 10);
|
|
71
|
+
if (exitCode !== 0) {
|
|
72
|
+
await this.onContainerCrash(event.id);
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
catch (error) {
|
|
77
|
+
this.logger.error('listenToDockerEvents(): Error processing Docker event:', error);
|
|
67
78
|
}
|
|
68
79
|
});
|
|
69
80
|
eventStream.on('error', (error) => {
|
|
81
|
+
this.logger.error('listenToDockerEvents(): Docker event stream error:', error);
|
|
70
82
|
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
71
83
|
});
|
|
72
84
|
eventStream.on('end', () => {
|
|
85
|
+
this.logger.warn('listenToDockerEvents(): Docker event stream ended, attempting to reconnect...');
|
|
73
86
|
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
74
87
|
});
|
|
75
88
|
}
|
|
76
89
|
catch (error) {
|
|
90
|
+
this.logger.error('listenToDockerEvents(): Failed to set up Docker event listener:', error);
|
|
77
91
|
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
78
92
|
}
|
|
79
93
|
}
|
|
94
|
+
isCredentialError(error) {
|
|
95
|
+
const msg = error?.message || '';
|
|
96
|
+
return (msg.includes('unauthorized') ||
|
|
97
|
+
msg.includes('authentication required') ||
|
|
98
|
+
msg.includes('access denied') ||
|
|
99
|
+
msg.includes('credential') ||
|
|
100
|
+
error?.statusCode === 401);
|
|
101
|
+
}
|
|
80
102
|
getDefaultPlatform() {
|
|
81
103
|
const arch = process.arch;
|
|
82
104
|
if (arch === 'x64') {
|
|
@@ -130,12 +152,12 @@ class DockerManager {
|
|
|
130
152
|
: 0;
|
|
131
153
|
const files = fs_1.default.readdirSync(coreDumpDir);
|
|
132
154
|
const coreFiles = files
|
|
133
|
-
.filter(
|
|
134
|
-
.map(
|
|
155
|
+
.filter(fileName => fileName.startsWith('core.'))
|
|
156
|
+
.map(fileName => ({
|
|
135
157
|
name: fileName,
|
|
136
158
|
mtime: fs_1.default.statSync(`${coreDumpDir}/${fileName}`).mtime.getTime(),
|
|
137
159
|
}))
|
|
138
|
-
.filter(
|
|
160
|
+
.filter(coreFile => crashTime === 0 || coreFile.mtime >= crashTime)
|
|
139
161
|
.sort((firstFile, secondFile) => secondFile.mtime - firstFile.mtime);
|
|
140
162
|
if (coreFiles.length > 0) {
|
|
141
163
|
coreDumpInfo = {
|
|
@@ -190,7 +212,7 @@ class DockerManager {
|
|
|
190
212
|
};
|
|
191
213
|
await this.initializeHubSignals(initSignalsParams, deviceStatus.dataResidency.toUpperCase());
|
|
192
214
|
}
|
|
193
|
-
if (this.phyHubClient) {
|
|
215
|
+
if (this.phyHubClient && this.signalsClient) {
|
|
194
216
|
if (!this.sessionId) {
|
|
195
217
|
const instance = this.signalsClient.getInstanceProps();
|
|
196
218
|
this.sessionId = instance.sessionId;
|
|
@@ -283,6 +305,7 @@ class DockerManager {
|
|
|
283
305
|
return resolve();
|
|
284
306
|
}
|
|
285
307
|
let isCompleted = false;
|
|
308
|
+
const streamErrors = [];
|
|
286
309
|
const abortHandler = () => {
|
|
287
310
|
if (isCompleted)
|
|
288
311
|
return;
|
|
@@ -317,13 +340,7 @@ class DockerManager {
|
|
|
317
340
|
if (err) {
|
|
318
341
|
clearTimeout(timeoutId);
|
|
319
342
|
this.logger.error(`pullImage(): Error initiating pull for image ${image}`, err);
|
|
320
|
-
|
|
321
|
-
(err.message.includes('unauthorized') ||
|
|
322
|
-
err.message.includes('authentication required') ||
|
|
323
|
-
err.message.includes('access denied') ||
|
|
324
|
-
err.message.includes('credential') ||
|
|
325
|
-
err.statusCode === 401);
|
|
326
|
-
if (isCredentialError) {
|
|
343
|
+
if (this.isCredentialError(err)) {
|
|
327
344
|
this.logger.error(`pullImage(): Credential error detected for image ${image}, releasing lock immediately`);
|
|
328
345
|
const imageProgress = this.imagesInProgress.get(image);
|
|
329
346
|
if (imageProgress) {
|
|
@@ -344,25 +361,82 @@ class DockerManager {
|
|
|
344
361
|
clearTimeout(timeoutId);
|
|
345
362
|
if (isCompleted)
|
|
346
363
|
return;
|
|
347
|
-
|
|
348
|
-
|
|
364
|
+
const actualError = err ||
|
|
365
|
+
(streamErrors.length > 0
|
|
366
|
+
? new Error(`Pull failed with ${streamErrors.length} error(s): ${streamErrors.map(streamError => streamError.message).join('; ')}`)
|
|
367
|
+
: null);
|
|
368
|
+
if (actualError) {
|
|
369
|
+
this.logger.error(`pullImage(): Error pulling image ${image}`, actualError);
|
|
370
|
+
if (streamErrors.length > 0) {
|
|
371
|
+
this.logger.error(`pullImage(): Stream errors captured during pull:`, streamErrors.map(streamError => streamError.message));
|
|
372
|
+
}
|
|
349
373
|
imageProgress.inProgress = false;
|
|
350
374
|
this.imagesInProgress.delete(image);
|
|
351
375
|
isCompleted = true;
|
|
352
|
-
progressTracker.markFailed(
|
|
353
|
-
reject(
|
|
376
|
+
progressTracker.markFailed(actualError);
|
|
377
|
+
reject(actualError);
|
|
354
378
|
return;
|
|
355
379
|
}
|
|
356
380
|
this.logger.info(`pullImage(): Image ${image} download complete`);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
381
|
+
(async () => {
|
|
382
|
+
try {
|
|
383
|
+
const imageExists = await this.imageExists(image);
|
|
384
|
+
if (!imageExists) {
|
|
385
|
+
const errorMsg = `Image ${image} pull reported success but image is not available locally. ` +
|
|
386
|
+
`This may indicate a network issue, proxy problem, or the image does not exist.`;
|
|
387
|
+
this.logger.error(`pullImage(): ${errorMsg}`);
|
|
388
|
+
imageProgress.inProgress = false;
|
|
389
|
+
this.imagesInProgress.delete(image);
|
|
390
|
+
isCompleted = true;
|
|
391
|
+
progressTracker.markFailed(new Error(errorMsg));
|
|
392
|
+
reject(new Error(errorMsg));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
imageProgress.inProgress = false;
|
|
396
|
+
this.imagesInProgress.delete(image);
|
|
397
|
+
isCompleted = true;
|
|
398
|
+
progressTracker.markComplete();
|
|
399
|
+
(async () => {
|
|
400
|
+
try {
|
|
401
|
+
const imageObject = this.docker.getImage(image);
|
|
402
|
+
const imageInspection = await imageObject.inspect();
|
|
403
|
+
const imageId = imageInspection.Id;
|
|
404
|
+
const pullTimestamp = Math.floor(Date.now() / 1000);
|
|
405
|
+
this.imagePullTimes.set(imageId, pullTimestamp);
|
|
406
|
+
const pullTimeFormatted = new Date(pullTimestamp * 1000).toISOString();
|
|
407
|
+
this.logger.info(`pullImage(): Recorded pull time for ${image} ` +
|
|
408
|
+
`(ID: ${imageId.substring(0, 12)}): ${pullTimeFormatted}`);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
this.logger.warn(`pullImage(): Failed to record pull time for ${image}`, error);
|
|
412
|
+
}
|
|
413
|
+
})();
|
|
414
|
+
this.logger.info(`pullImage(): Starting image management for ${image}`);
|
|
415
|
+
this.manageDockerImages(image).catch((error) => {
|
|
416
|
+
this.logger.error(`pullImage(): Error managing images after pull for ${image}`, error);
|
|
417
|
+
});
|
|
418
|
+
resolve();
|
|
419
|
+
}
|
|
420
|
+
catch (verifyError) {
|
|
421
|
+
const errorMsg = `Failed to verify image ${image} exists after pull: ` +
|
|
422
|
+
`${verifyError.message || verifyError}`;
|
|
423
|
+
this.logger.error(`pullImage(): ${errorMsg}`, verifyError);
|
|
424
|
+
imageProgress.inProgress = false;
|
|
425
|
+
this.imagesInProgress.delete(image);
|
|
426
|
+
isCompleted = true;
|
|
427
|
+
progressTracker.markFailed(verifyError);
|
|
428
|
+
reject(new Error(errorMsg));
|
|
429
|
+
}
|
|
430
|
+
})();
|
|
362
431
|
}, (event) => {
|
|
363
432
|
if (abortController.signal.aborted || isCompleted) {
|
|
364
433
|
return;
|
|
365
434
|
}
|
|
435
|
+
if (event.error || event.errorDetail) {
|
|
436
|
+
const errorMsg = event.errorDetail?.message || event.error || 'Unknown pull error';
|
|
437
|
+
this.logger.error(`pullImage(): Error event during pull for ${image}: ${errorMsg}`, { event });
|
|
438
|
+
streamErrors.push(new Error(errorMsg));
|
|
439
|
+
}
|
|
366
440
|
progressTracker.processEvent(event);
|
|
367
441
|
if (event.id && event.progressDetail) {
|
|
368
442
|
imageProgress.layersProgress.set(event.id, event.progressDetail);
|
|
@@ -379,6 +453,24 @@ class DockerManager {
|
|
|
379
453
|
this.logger.info(`createContainer(): Device requests for ${twin.id}:`, JSON.stringify(parsedOptions?.HostConfig?.DeviceRequests, null, 2));
|
|
380
454
|
const isHostNetwork = parsedOptions?.HostConfig?.NetworkMode === 'host';
|
|
381
455
|
const existingBinds = parsedOptions?.HostConfig?.Binds ?? [];
|
|
456
|
+
const coreDumpHostDir = '/var/coredumps';
|
|
457
|
+
try {
|
|
458
|
+
if (!fs_1.default.existsSync(coreDumpHostDir)) {
|
|
459
|
+
fs_1.default.mkdirSync(coreDumpHostDir, { recursive: true, mode: 0o777 });
|
|
460
|
+
this.logger.info(`createContainer(): Created core dump directory ${coreDumpHostDir}`);
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
fs_1.default.chmodSync(coreDumpHostDir, 0o777);
|
|
464
|
+
this.logger.info(`createContainer(): Set permissions on ${coreDumpHostDir} to 777`);
|
|
465
|
+
}
|
|
466
|
+
catch (chmodError) {
|
|
467
|
+
this.logger.error(`createContainer(): CRITICAL: Could not set permissions on ${coreDumpHostDir}. ` +
|
|
468
|
+
`Container may not be able to write coredumps. Error:`, chmodError);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
this.logger.error(`createContainer(): Failed to create or verify ${coreDumpHostDir}, mount may fail`, error);
|
|
473
|
+
}
|
|
382
474
|
const coreDumpBind = '/var/coredumps:/var/coredumps:z';
|
|
383
475
|
const hasCoreDumpBind = existingBinds.some((bind) => bind.includes('/var/coredumps'));
|
|
384
476
|
const allBinds = hasCoreDumpBind ? existingBinds : [...existingBinds, coreDumpBind];
|
|
@@ -392,7 +484,11 @@ class DockerManager {
|
|
|
392
484
|
];
|
|
393
485
|
const containerConfig = {
|
|
394
486
|
Image: image,
|
|
395
|
-
Env: [
|
|
487
|
+
Env: [
|
|
488
|
+
...(parsedOptions?.Env ?? []),
|
|
489
|
+
`TWIN_ID=${twin.id}`,
|
|
490
|
+
...(process.env.TZ ? [`TZ=${process.env.TZ}`] : []),
|
|
491
|
+
],
|
|
396
492
|
Entrypoint: parsedOptions?.Entrypoint,
|
|
397
493
|
Cmd: parsedOptions?.Cmd,
|
|
398
494
|
HostConfig: {
|
|
@@ -509,22 +605,17 @@ class DockerManager {
|
|
|
509
605
|
error.message.includes('gpu'));
|
|
510
606
|
if (isGpuError) {
|
|
511
607
|
this.logger.warn(`startContainer(): GPU support not available for twin ${twin.id}, retrying without GPU capabilities. Error: ${error.message}`, { error: error.message });
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
catch (removeError) {
|
|
521
|
-
this.logger.info(`startContainer(): Could not remove container ${containerId}, may not exist`);
|
|
522
|
-
}
|
|
608
|
+
const existingInstance = this.instances.get(twin.id);
|
|
609
|
+
if (existingInstance?.container) {
|
|
610
|
+
try {
|
|
611
|
+
await existingInstance.container.remove({ force: true });
|
|
612
|
+
this.logger.info(`startContainer(): Removed failed GPU container for twin ${twin.id}`);
|
|
613
|
+
}
|
|
614
|
+
catch (removeError) {
|
|
615
|
+
this.logger.info(`startContainer(): Could not remove container for twin ${twin.id}, may not exist`);
|
|
523
616
|
}
|
|
524
617
|
}
|
|
525
|
-
|
|
526
|
-
this.logger.warn(`startContainer(): Error during cleanup: ${cleanupError.message}`);
|
|
527
|
-
}
|
|
618
|
+
this.instances.delete(twin.id);
|
|
528
619
|
const container = await this.createContainer(twin, true);
|
|
529
620
|
this.instances.set(twin.id, { container, twin });
|
|
530
621
|
await container.start();
|
|
@@ -577,18 +668,20 @@ class DockerManager {
|
|
|
577
668
|
this.logger.info(`ensureImageAvailable(): Pull already in progress for ${image}, waiting for it to complete`);
|
|
578
669
|
try {
|
|
579
670
|
await existingPull.promise;
|
|
580
|
-
|
|
671
|
+
const imageExists = await this.imageExists(image);
|
|
672
|
+
if (!imageExists) {
|
|
673
|
+
const errorMsg = `Existing pull for ${image} reported success but image is not available locally`;
|
|
674
|
+
this.logger.error(`ensureImageAvailable(): ${errorMsg}`);
|
|
675
|
+
this.imagePullsInProgress.delete(image);
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
this.logger.info(`ensureImageAvailable(): Existing pull for ${image} completed successfully ` +
|
|
679
|
+
`and image verified locally`);
|
|
581
680
|
return true;
|
|
582
681
|
}
|
|
583
682
|
catch (error) {
|
|
584
683
|
this.logger.error(`ensureImageAvailable(): Existing pull for ${image} failed`, error);
|
|
585
|
-
|
|
586
|
-
(error.message.includes('unauthorized') ||
|
|
587
|
-
error.message.includes('authentication required') ||
|
|
588
|
-
error.message.includes('access denied') ||
|
|
589
|
-
error.message.includes('credential') ||
|
|
590
|
-
error.statusCode === 401);
|
|
591
|
-
if (isCredentialError) {
|
|
684
|
+
if (this.isCredentialError(error)) {
|
|
592
685
|
this.logger.error(`ensureImageAvailable(): Credential error detected for existing pull of ${image}`);
|
|
593
686
|
}
|
|
594
687
|
this.imagePullsInProgress.delete(image);
|
|
@@ -606,21 +699,21 @@ class DockerManager {
|
|
|
606
699
|
});
|
|
607
700
|
try {
|
|
608
701
|
await pullPromise;
|
|
609
|
-
this.
|
|
702
|
+
const imageExists = await this.imageExists(image);
|
|
703
|
+
if (!imageExists) {
|
|
704
|
+
const errorMsg = `Pull for ${image} reported success but image is not available locally`;
|
|
705
|
+
this.logger.error(`ensureImageAvailable(): ${errorMsg}`);
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
this.logger.info(`ensureImageAvailable(): Pull for ${image} completed successfully ` +
|
|
709
|
+
`and image verified locally`);
|
|
610
710
|
return true;
|
|
611
711
|
}
|
|
612
712
|
catch (error) {
|
|
613
713
|
this.logger.error(`ensureImageAvailable(): Pull failed for ${image}`, error);
|
|
614
|
-
|
|
615
|
-
(error.message.includes('unauthorized') ||
|
|
616
|
-
error.message.includes('authentication required') ||
|
|
617
|
-
error.message.includes('access denied') ||
|
|
618
|
-
error.message.includes('credential') ||
|
|
619
|
-
error.statusCode === 401);
|
|
620
|
-
if (isCredentialError) {
|
|
714
|
+
if (this.isCredentialError(error)) {
|
|
621
715
|
this.logger.error(`ensureImageAvailable(): Credential error detected for pull of ${image}`);
|
|
622
716
|
}
|
|
623
|
-
this.imagePullsInProgress.delete(image);
|
|
624
717
|
return false;
|
|
625
718
|
}
|
|
626
719
|
}
|
|
@@ -671,15 +764,10 @@ class DockerManager {
|
|
|
671
764
|
catch (error) {
|
|
672
765
|
this.logger.error(`startEdgeApp(): Error pulling image ${image}`, error);
|
|
673
766
|
const errorMsg = error.message || '';
|
|
674
|
-
const isCredentialError = errorMsg.includes('unauthorized') ||
|
|
675
|
-
errorMsg.includes('authentication required') ||
|
|
676
|
-
errorMsg.includes('access denied') ||
|
|
677
|
-
errorMsg.includes('credential') ||
|
|
678
|
-
error.statusCode === 401;
|
|
679
767
|
return {
|
|
680
768
|
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
681
769
|
error: {
|
|
682
|
-
message: isCredentialError
|
|
770
|
+
message: this.isCredentialError(error)
|
|
683
771
|
? `Authentication failed for image ${image}. Please check your credentials.`
|
|
684
772
|
: `Failed to pull image ${image}: ${errorMsg}`,
|
|
685
773
|
timestamp: new Date(),
|
|
@@ -834,30 +922,159 @@ class DockerManager {
|
|
|
834
922
|
getInstances() {
|
|
835
923
|
return this.instances;
|
|
836
924
|
}
|
|
925
|
+
extractRepositoryName(image) {
|
|
926
|
+
this.logger.info(`extractRepositoryName(): Processing image: ${image}`);
|
|
927
|
+
let normalizedImage = image;
|
|
928
|
+
if (image.startsWith('docker.io/')) {
|
|
929
|
+
normalizedImage = image.substring('docker.io/'.length);
|
|
930
|
+
this.logger.info(`extractRepositoryName(): Removed docker.io/ prefix: ${normalizedImage}`);
|
|
931
|
+
}
|
|
932
|
+
const tagSeparatorIndex = normalizedImage.lastIndexOf(':');
|
|
933
|
+
const pathSeparatorIndex = normalizedImage.lastIndexOf('/');
|
|
934
|
+
if (tagSeparatorIndex === -1) {
|
|
935
|
+
this.logger.info(`extractRepositoryName(): No tag found, repository: ${normalizedImage}`);
|
|
936
|
+
return normalizedImage;
|
|
937
|
+
}
|
|
938
|
+
if (pathSeparatorIndex !== -1 && tagSeparatorIndex < pathSeparatorIndex) {
|
|
939
|
+
this.logger.info(`extractRepositoryName(): Colon is port separator, repository: ${normalizedImage}`);
|
|
940
|
+
return normalizedImage;
|
|
941
|
+
}
|
|
942
|
+
const repositoryName = normalizedImage.substring(0, tagSeparatorIndex);
|
|
943
|
+
this.logger.info(`extractRepositoryName(): Extracted repository: ${repositoryName}`);
|
|
944
|
+
return repositoryName;
|
|
945
|
+
}
|
|
837
946
|
async manageDockerImages(image) {
|
|
838
947
|
try {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
948
|
+
this.logger.info(`manageDockerImages(): Starting management for ${image}`);
|
|
949
|
+
const repositoryName = this.extractRepositoryName(image);
|
|
950
|
+
const referenceFilters = [repositoryName];
|
|
951
|
+
if (!repositoryName.startsWith('docker.io/')) {
|
|
952
|
+
referenceFilters.push(`docker.io/${repositoryName}`);
|
|
953
|
+
}
|
|
954
|
+
this.logger.info(`manageDockerImages(): Querying images for repository: ${repositoryName}`);
|
|
955
|
+
let repositoryImages = await this.docker.listImages({
|
|
956
|
+
filters: { reference: referenceFilters },
|
|
843
957
|
});
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
958
|
+
if (repositoryImages.length === 0) {
|
|
959
|
+
this.logger.info(`manageDockerImages(): Reference filter returned no results, trying manual filter`);
|
|
960
|
+
const allImages = await this.docker.listImages();
|
|
961
|
+
repositoryImages = allImages.filter(imageInfo => {
|
|
962
|
+
const imageTags = imageInfo.RepoTags || [];
|
|
963
|
+
return imageTags.some((imageTag) => {
|
|
964
|
+
let normalizedTag = imageTag;
|
|
965
|
+
if (imageTag.startsWith('docker.io/')) {
|
|
966
|
+
normalizedTag = imageTag.substring('docker.io/'.length);
|
|
967
|
+
}
|
|
968
|
+
const tagRepositoryName = this.extractRepositoryName(normalizedTag);
|
|
969
|
+
return (tagRepositoryName === repositoryName || normalizedTag.startsWith(`${repositoryName}:`));
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
this.logger.info(`manageDockerImages(): Manual filter found ${repositoryImages.length} image(s)`);
|
|
973
|
+
}
|
|
974
|
+
if (repositoryImages.length === 0) {
|
|
975
|
+
this.logger.warn(`manageDockerImages(): No images found for repository ${repositoryName}`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (repositoryImages.length <= 2) {
|
|
979
|
+
this.logger.info(`manageDockerImages(): Repository has ${repositoryImages.length} image(s), no cleanup needed`);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
this.logger.info(`manageDockerImages(): Determining pull times for ${repositoryImages.length} images`);
|
|
983
|
+
const imagesWithPullTimes = repositoryImages.map(imageInfo => {
|
|
984
|
+
const imageTags = imageInfo.RepoTags || imageInfo.RepoDigests || ['<untagged>'];
|
|
985
|
+
const trackedPullTime = this.imagePullTimes.get(imageInfo.Id);
|
|
986
|
+
const pullTime = trackedPullTime || imageInfo.Created;
|
|
987
|
+
const pullTimeFormatted = new Date(pullTime * 1000).toISOString();
|
|
988
|
+
const imageTagsString = imageTags.join(', ');
|
|
989
|
+
if (trackedPullTime) {
|
|
990
|
+
this.logger.info(`manageDockerImages(): Using tracked pull time for ${imageTagsString}: ` +
|
|
991
|
+
`${pullTimeFormatted}`);
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
this.logger.info(`manageDockerImages(): Using Created time for ${imageTagsString}: ` +
|
|
995
|
+
`${pullTimeFormatted}`);
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
imageInfo,
|
|
999
|
+
pullTime,
|
|
1000
|
+
imageTags,
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
imagesWithPullTimes.sort((firstImage, secondImage) => secondImage.pullTime - firstImage.pullTime);
|
|
1004
|
+
this.logger.info(`manageDockerImages(): Sorted ${imagesWithPullTimes.length} images by pull time`);
|
|
1005
|
+
const imagesToRemove = imagesWithPullTimes.slice(2);
|
|
1006
|
+
this.logger.info(`manageDockerImages(): Removing ${imagesToRemove.length} oldest image(s), keeping newest 2`);
|
|
1007
|
+
for (const imageToRemove of imagesToRemove) {
|
|
1008
|
+
try {
|
|
1009
|
+
const imageTagsString = imageToRemove.imageTags.join(', ');
|
|
1010
|
+
const pullTimeFormatted = new Date(imageToRemove.pullTime * 1000).toISOString();
|
|
1011
|
+
this.logger.info(`manageDockerImages(): Removing image: ${imageTagsString} ` +
|
|
1012
|
+
`(Pulled: ${pullTimeFormatted})`);
|
|
1013
|
+
await this.docker.getImage(imageToRemove.imageInfo.Id).remove({ force: true });
|
|
1014
|
+
this.imagePullTimes.delete(imageToRemove.imageInfo.Id);
|
|
1015
|
+
this.persistImagePullTimes();
|
|
1016
|
+
this.logger.info(`manageDockerImages(): Successfully removed: ${imageToRemove.imageTags.join(', ')}`);
|
|
1017
|
+
}
|
|
1018
|
+
catch (error) {
|
|
1019
|
+
this.logger.error(`manageDockerImages(): Failed to remove image ${imageToRemove.imageInfo.Id}`, error);
|
|
854
1020
|
}
|
|
855
1021
|
}
|
|
1022
|
+
this.logger.info(`manageDockerImages(): Completed management for ${image}`);
|
|
856
1023
|
}
|
|
857
1024
|
catch (error) {
|
|
858
1025
|
this.logger.error(`manageDockerImages(): Error managing images for ${image}`, error);
|
|
859
1026
|
}
|
|
860
1027
|
}
|
|
1028
|
+
loadImagePullTimes() {
|
|
1029
|
+
try {
|
|
1030
|
+
if (fs_1.default.existsSync(IMAGE_PULL_TIMES_FILE)) {
|
|
1031
|
+
const data = JSON.parse(fs_1.default.readFileSync(IMAGE_PULL_TIMES_FILE, 'utf-8'));
|
|
1032
|
+
const validEntries = Object.entries(data)
|
|
1033
|
+
.filter(([_, value]) => typeof value === 'number' && value > 0)
|
|
1034
|
+
.map(([key, value]) => [key, value]);
|
|
1035
|
+
this.imagePullTimes = new Map(validEntries);
|
|
1036
|
+
this.logger.info(`loadImagePullTimes(): Loaded ${this.imagePullTimes.size} image pull times from disk`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
catch (error) {
|
|
1040
|
+
this.logger.warn('loadImagePullTimes(): Failed to load image pull times, starting fresh', error);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async cleanupStaleImagePullTimes() {
|
|
1044
|
+
try {
|
|
1045
|
+
const allImages = await this.docker.listImages();
|
|
1046
|
+
const existingImageIds = new Set(allImages.map(img => img.Id));
|
|
1047
|
+
let cleanedCount = 0;
|
|
1048
|
+
for (const [imageId] of this.imagePullTimes.entries()) {
|
|
1049
|
+
if (!existingImageIds.has(imageId)) {
|
|
1050
|
+
this.imagePullTimes.delete(imageId);
|
|
1051
|
+
cleanedCount++;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (cleanedCount > 0) {
|
|
1055
|
+
this.logger.info(`cleanupStaleImagePullTimes(): Removed ${cleanedCount} stale image pull time entries`);
|
|
1056
|
+
this.persistImagePullTimes();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
this.logger.warn('cleanupStaleImagePullTimes(): Error during cleanup', error);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
persistImagePullTimes() {
|
|
1064
|
+
try {
|
|
1065
|
+
const dir = path_1.default.dirname(IMAGE_PULL_TIMES_FILE);
|
|
1066
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
1067
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
1068
|
+
}
|
|
1069
|
+
const data = Object.fromEntries(this.imagePullTimes);
|
|
1070
|
+
const tempFile = `${IMAGE_PULL_TIMES_FILE}.tmp`;
|
|
1071
|
+
fs_1.default.writeFileSync(tempFile, JSON.stringify(data, null, 2));
|
|
1072
|
+
fs_1.default.renameSync(tempFile, IMAGE_PULL_TIMES_FILE);
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
this.logger.warn('persistImagePullTimes(): Failed to persist image pull times', error);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
861
1078
|
async isContainerRunning(twinId) {
|
|
862
1079
|
try {
|
|
863
1080
|
const containers = await this.docker.listContainers();
|