@phystack/device-phyos 4.5.19-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 +273 -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);
|
|
@@ -410,7 +484,11 @@ class DockerManager {
|
|
|
410
484
|
];
|
|
411
485
|
const containerConfig = {
|
|
412
486
|
Image: image,
|
|
413
|
-
Env: [
|
|
487
|
+
Env: [
|
|
488
|
+
...(parsedOptions?.Env ?? []),
|
|
489
|
+
`TWIN_ID=${twin.id}`,
|
|
490
|
+
...(process.env.TZ ? [`TZ=${process.env.TZ}`] : []),
|
|
491
|
+
],
|
|
414
492
|
Entrypoint: parsedOptions?.Entrypoint,
|
|
415
493
|
Cmd: parsedOptions?.Cmd,
|
|
416
494
|
HostConfig: {
|
|
@@ -527,22 +605,17 @@ class DockerManager {
|
|
|
527
605
|
error.message.includes('gpu'));
|
|
528
606
|
if (isGpuError) {
|
|
529
607
|
this.logger.warn(`startContainer(): GPU support not available for twin ${twin.id}, retrying without GPU capabilities. Error: ${error.message}`, { error: error.message });
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
catch (removeError) {
|
|
539
|
-
this.logger.info(`startContainer(): Could not remove container ${containerId}, may not exist`);
|
|
540
|
-
}
|
|
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`);
|
|
541
616
|
}
|
|
542
617
|
}
|
|
543
|
-
|
|
544
|
-
this.logger.warn(`startContainer(): Error during cleanup: ${cleanupError.message}`);
|
|
545
|
-
}
|
|
618
|
+
this.instances.delete(twin.id);
|
|
546
619
|
const container = await this.createContainer(twin, true);
|
|
547
620
|
this.instances.set(twin.id, { container, twin });
|
|
548
621
|
await container.start();
|
|
@@ -595,18 +668,20 @@ class DockerManager {
|
|
|
595
668
|
this.logger.info(`ensureImageAvailable(): Pull already in progress for ${image}, waiting for it to complete`);
|
|
596
669
|
try {
|
|
597
670
|
await existingPull.promise;
|
|
598
|
-
|
|
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`);
|
|
599
680
|
return true;
|
|
600
681
|
}
|
|
601
682
|
catch (error) {
|
|
602
683
|
this.logger.error(`ensureImageAvailable(): Existing pull for ${image} failed`, error);
|
|
603
|
-
|
|
604
|
-
(error.message.includes('unauthorized') ||
|
|
605
|
-
error.message.includes('authentication required') ||
|
|
606
|
-
error.message.includes('access denied') ||
|
|
607
|
-
error.message.includes('credential') ||
|
|
608
|
-
error.statusCode === 401);
|
|
609
|
-
if (isCredentialError) {
|
|
684
|
+
if (this.isCredentialError(error)) {
|
|
610
685
|
this.logger.error(`ensureImageAvailable(): Credential error detected for existing pull of ${image}`);
|
|
611
686
|
}
|
|
612
687
|
this.imagePullsInProgress.delete(image);
|
|
@@ -624,21 +699,21 @@ class DockerManager {
|
|
|
624
699
|
});
|
|
625
700
|
try {
|
|
626
701
|
await pullPromise;
|
|
627
|
-
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`);
|
|
628
710
|
return true;
|
|
629
711
|
}
|
|
630
712
|
catch (error) {
|
|
631
713
|
this.logger.error(`ensureImageAvailable(): Pull failed for ${image}`, error);
|
|
632
|
-
|
|
633
|
-
(error.message.includes('unauthorized') ||
|
|
634
|
-
error.message.includes('authentication required') ||
|
|
635
|
-
error.message.includes('access denied') ||
|
|
636
|
-
error.message.includes('credential') ||
|
|
637
|
-
error.statusCode === 401);
|
|
638
|
-
if (isCredentialError) {
|
|
714
|
+
if (this.isCredentialError(error)) {
|
|
639
715
|
this.logger.error(`ensureImageAvailable(): Credential error detected for pull of ${image}`);
|
|
640
716
|
}
|
|
641
|
-
this.imagePullsInProgress.delete(image);
|
|
642
717
|
return false;
|
|
643
718
|
}
|
|
644
719
|
}
|
|
@@ -689,15 +764,10 @@ class DockerManager {
|
|
|
689
764
|
catch (error) {
|
|
690
765
|
this.logger.error(`startEdgeApp(): Error pulling image ${image}`, error);
|
|
691
766
|
const errorMsg = error.message || '';
|
|
692
|
-
const isCredentialError = errorMsg.includes('unauthorized') ||
|
|
693
|
-
errorMsg.includes('authentication required') ||
|
|
694
|
-
errorMsg.includes('access denied') ||
|
|
695
|
-
errorMsg.includes('credential') ||
|
|
696
|
-
error.statusCode === 401;
|
|
697
767
|
return {
|
|
698
768
|
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
699
769
|
error: {
|
|
700
|
-
message: isCredentialError
|
|
770
|
+
message: this.isCredentialError(error)
|
|
701
771
|
? `Authentication failed for image ${image}. Please check your credentials.`
|
|
702
772
|
: `Failed to pull image ${image}: ${errorMsg}`,
|
|
703
773
|
timestamp: new Date(),
|
|
@@ -852,30 +922,159 @@ class DockerManager {
|
|
|
852
922
|
getInstances() {
|
|
853
923
|
return this.instances;
|
|
854
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
|
+
}
|
|
855
946
|
async manageDockerImages(image) {
|
|
856
947
|
try {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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 },
|
|
861
957
|
});
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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);
|
|
872
1020
|
}
|
|
873
1021
|
}
|
|
1022
|
+
this.logger.info(`manageDockerImages(): Completed management for ${image}`);
|
|
874
1023
|
}
|
|
875
1024
|
catch (error) {
|
|
876
1025
|
this.logger.error(`manageDockerImages(): Error managing images for ${image}`, error);
|
|
877
1026
|
}
|
|
878
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
|
+
}
|
|
879
1078
|
async isContainerRunning(twinId) {
|
|
880
1079
|
try {
|
|
881
1080
|
const containers = await this.docker.listContainers();
|