mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-30 12:09:15 +00:00
fix: suppress ERROR logging for missing .env files in image update scan (#936)
* fix: suppress ERROR logging for missing .env files in image update scan The ImageUpdateService logged a full ERROR stack trace for every stack without a .env file, which is a normal and expected configuration. Also added a 5-minute check timeout, developer_mode diagnostic logging, and proper startup timeout cleanup. * fix: add missing Node fields in test mock to satisfy tsc strict checking * fix: remove unused variables to satisfy ESLint no-unused-vars
This commit is contained in:
@@ -11,7 +11,7 @@ const {
|
|||||||
mockGetStackUpdateStatus, mockUpsertStackUpdateStatus, mockClearStackUpdateStatus,
|
mockGetStackUpdateStatus, mockUpsertStackUpdateStatus, mockClearStackUpdateStatus,
|
||||||
mockGetSystemState, mockSetSystemState, mockAddNotificationHistory,
|
mockGetSystemState, mockSetSystemState, mockAddNotificationHistory,
|
||||||
mockDispatchAlert,
|
mockDispatchAlert,
|
||||||
mockGetStacks, mockGetStackContent, mockGetEnvContent,
|
mockGetStacks, mockGetStackContent, mockGetEnvContent, mockEnvExists,
|
||||||
mockGetAllContainers,
|
mockGetAllContainers,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
mockGetAuthForRegistry: vi.fn().mockResolvedValue(null),
|
mockGetAuthForRegistry: vi.fn().mockResolvedValue(null),
|
||||||
@@ -25,6 +25,7 @@ const {
|
|||||||
mockGetStacks: vi.fn().mockResolvedValue([]),
|
mockGetStacks: vi.fn().mockResolvedValue([]),
|
||||||
mockGetStackContent: vi.fn().mockResolvedValue(''),
|
mockGetStackContent: vi.fn().mockResolvedValue(''),
|
||||||
mockGetEnvContent: vi.fn().mockRejectedValue(new Error('no env')),
|
mockGetEnvContent: vi.fn().mockRejectedValue(new Error('no env')),
|
||||||
|
mockEnvExists: vi.fn().mockResolvedValue(false),
|
||||||
mockGetAllContainers: vi.fn().mockResolvedValue([]),
|
mockGetAllContainers: vi.fn().mockResolvedValue([]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ vi.mock('../services/FileSystemService', () => ({
|
|||||||
getStacks: mockGetStacks,
|
getStacks: mockGetStacks,
|
||||||
getStackContent: mockGetStackContent,
|
getStackContent: mockGetStackContent,
|
||||||
getEnvContent: mockGetEnvContent,
|
getEnvContent: mockGetEnvContent,
|
||||||
|
envExists: mockEnvExists,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -444,3 +446,264 @@ services:
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── .env file handling ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ImageUpdateService - .env file handling in checkNode', () => {
|
||||||
|
const COMPOSE = `
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx:latest
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fakeDb = () => ({
|
||||||
|
getStackUpdateStatus: mockGetStackUpdateStatus,
|
||||||
|
upsertStackUpdateStatus: mockUpsertStackUpdateStatus,
|
||||||
|
clearStackUpdateStatus: mockClearStackUpdateStatus,
|
||||||
|
getSystemState: mockGetSystemState,
|
||||||
|
setSystemState: mockSetSystemState,
|
||||||
|
addNotificationHistory: mockAddNotificationHistory,
|
||||||
|
});
|
||||||
|
|
||||||
|
function stubCheckImage(service: ImageUpdateService, hasUpdate: boolean) {
|
||||||
|
(service as any).checkImage = vi.fn().mockResolvedValue({ hasUpdate });
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
(ImageUpdateService as any).instance = undefined;
|
||||||
|
mockGetSystemState.mockReturnValue('1');
|
||||||
|
mockGetStacks.mockResolvedValue(['stackA']);
|
||||||
|
mockGetStackContent.mockResolvedValue(COMPOSE);
|
||||||
|
mockGetAllContainers.mockResolvedValue([]);
|
||||||
|
mockGetEnvContent.mockRejectedValue(new Error('no env'));
|
||||||
|
mockEnvExists.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips getEnvContent when envExists returns false', async () => {
|
||||||
|
mockEnvExists.mockResolvedValue(false);
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
stubCheckImage(service, false);
|
||||||
|
|
||||||
|
await (service as any).checkNode(1, 'local', fakeDb());
|
||||||
|
|
||||||
|
expect(mockEnvExists).toHaveBeenCalledWith('stackA');
|
||||||
|
expect(mockGetEnvContent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads .env when envExists returns true', async () => {
|
||||||
|
mockEnvExists.mockResolvedValue(true);
|
||||||
|
mockGetEnvContent.mockResolvedValue('IMAGE_TAG=1.0');
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
stubCheckImage(service, false);
|
||||||
|
|
||||||
|
await (service as any).checkNode(1, 'local', fakeDb());
|
||||||
|
|
||||||
|
expect(mockEnvExists).toHaveBeenCalledWith('stackA');
|
||||||
|
expect(mockGetEnvContent).toHaveBeenCalledWith('stackA');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('continues gracefully when .env exists but is unreadable', async () => {
|
||||||
|
mockEnvExists.mockResolvedValue(true);
|
||||||
|
mockGetEnvContent.mockRejectedValue(new Error('EACCES: permission denied'));
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
stubCheckImage(service, false);
|
||||||
|
|
||||||
|
await (service as any).checkNode(1, 'local', fakeDb());
|
||||||
|
|
||||||
|
// Should not throw; should still complete and write status
|
||||||
|
expect(mockUpsertStackUpdateStatus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── check() timeout ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ImageUpdateService - check() timeout', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
(ImageUpdateService as any).instance = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('releases isRunning lock after CHECK_TIMEOUT_MS', async () => {
|
||||||
|
// Override the module-level DatabaseService mock to return a node
|
||||||
|
const dbModule = await import('../services/DatabaseService');
|
||||||
|
const origGetInstance = dbModule.DatabaseService.getInstance;
|
||||||
|
dbModule.DatabaseService.getInstance = (() => ({
|
||||||
|
getGlobalSettings: () => ({ developer_mode: '0' }),
|
||||||
|
getNodes: () => [{ type: 'local', id: 1, name: 'local', mode: 'proxy', compose_dir: '/tmp/compose', is_default: true, status: 'online', created_at: 1 }],
|
||||||
|
upsertStackUpdateStatus: mockUpsertStackUpdateStatus,
|
||||||
|
getStackUpdateStatus: mockGetStackUpdateStatus,
|
||||||
|
clearStackUpdateStatus: mockClearStackUpdateStatus,
|
||||||
|
getSystemState: mockGetSystemState,
|
||||||
|
setSystemState: mockSetSystemState,
|
||||||
|
addNotificationHistory: mockAddNotificationHistory,
|
||||||
|
})) as unknown as typeof dbModule.DatabaseService.getInstance;
|
||||||
|
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
// Make checkNode hang indefinitely so the timeout fires
|
||||||
|
(service as any).checkNode = vi.fn().mockImplementation(() =>
|
||||||
|
new Promise(() => { /* never resolves */ })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Override timeout to 100ms for fast test
|
||||||
|
const orig = (ImageUpdateService as any).CHECK_TIMEOUT_MS;
|
||||||
|
(ImageUpdateService as any).CHECK_TIMEOUT_MS = 100;
|
||||||
|
|
||||||
|
// Don't await; this will hang intentionally
|
||||||
|
const checkPromise = (service as any).check();
|
||||||
|
|
||||||
|
// Let microtasks flush so check() enters its body
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
expect(service.isChecking()).toBe(true);
|
||||||
|
|
||||||
|
// Wait for timeout to fire
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
|
||||||
|
expect(service.isChecking()).toBe(false);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
(ImageUpdateService as any).CHECK_TIMEOUT_MS = orig;
|
||||||
|
dbModule.DatabaseService.getInstance = origGetInstance;
|
||||||
|
checkPromise.catch(() => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── stop() cancels startup timeout ──────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ImageUpdateService - stop() cancels startup timeout', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
(ImageUpdateService as any).instance = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents check from firing after stop() is called during startup delay', async () => {
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
const checkSpy = vi.spyOn(service as any, 'check');
|
||||||
|
|
||||||
|
service.start();
|
||||||
|
service.stop();
|
||||||
|
|
||||||
|
// Wait past the startup delay to see if check fires
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect(checkSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stale stack pruning ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ImageUpdateService - stale stack pruning', () => {
|
||||||
|
const COMPOSE = `
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx:latest
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fakeDb = () => ({
|
||||||
|
getStackUpdateStatus: mockGetStackUpdateStatus,
|
||||||
|
upsertStackUpdateStatus: mockUpsertStackUpdateStatus,
|
||||||
|
clearStackUpdateStatus: mockClearStackUpdateStatus,
|
||||||
|
getSystemState: mockGetSystemState,
|
||||||
|
setSystemState: mockSetSystemState,
|
||||||
|
addNotificationHistory: mockAddNotificationHistory,
|
||||||
|
});
|
||||||
|
|
||||||
|
function stubCheckImage(service: ImageUpdateService, hasUpdate: boolean) {
|
||||||
|
(service as any).checkImage = vi.fn().mockResolvedValue({ hasUpdate });
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
(ImageUpdateService as any).instance = undefined;
|
||||||
|
mockGetSystemState.mockReturnValue('1');
|
||||||
|
mockGetStacks.mockResolvedValue(['stackA']);
|
||||||
|
mockGetStackContent.mockResolvedValue(COMPOSE);
|
||||||
|
mockGetAllContainers.mockResolvedValue([]);
|
||||||
|
mockEnvExists.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prunes stale stacks no longer on disk', async () => {
|
||||||
|
// previousState has stackB which no longer exists on disk
|
||||||
|
mockGetStackUpdateStatus.mockReturnValue({ stackA: false, stackB: true });
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
stubCheckImage(service, false);
|
||||||
|
|
||||||
|
await (service as any).checkNode(1, 'local', fakeDb());
|
||||||
|
|
||||||
|
expect(mockClearStackUpdateStatus).toHaveBeenCalledWith(1, 'stackB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not prune stacks still on disk', async () => {
|
||||||
|
mockGetStackUpdateStatus.mockReturnValue({ stackA: false });
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
stubCheckImage(service, false);
|
||||||
|
|
||||||
|
await (service as any).checkNode(1, 'local', fakeDb());
|
||||||
|
|
||||||
|
expect(mockClearStackUpdateStatus).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Container augmentation filtering ────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ImageUpdateService - container augmentation filtering', () => {
|
||||||
|
const COMPOSE = `
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx:latest
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fakeDb = () => ({
|
||||||
|
getStackUpdateStatus: mockGetStackUpdateStatus,
|
||||||
|
upsertStackUpdateStatus: mockUpsertStackUpdateStatus,
|
||||||
|
clearStackUpdateStatus: mockClearStackUpdateStatus,
|
||||||
|
getSystemState: mockGetSystemState,
|
||||||
|
setSystemState: mockSetSystemState,
|
||||||
|
addNotificationHistory: mockAddNotificationHistory,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
(ImageUpdateService as any).instance = undefined;
|
||||||
|
mockGetSystemState.mockReturnValue('1');
|
||||||
|
mockGetStacks.mockResolvedValue(['stackA']);
|
||||||
|
mockGetStackContent.mockResolvedValue(COMPOSE);
|
||||||
|
mockEnvExists.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes containers whose working_dir matches compose dir', async () => {
|
||||||
|
mockGetAllContainers.mockResolvedValue([
|
||||||
|
{
|
||||||
|
Labels: { 'com.docker.compose.project.working_dir': '/tmp/compose/stackA' },
|
||||||
|
Image: 'nginx:1.25',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
const checkImageSpy = vi.fn().mockResolvedValue({ hasUpdate: false });
|
||||||
|
(service as any).checkImage = checkImageSpy;
|
||||||
|
|
||||||
|
await (service as any).checkNode(1, 'local', fakeDb());
|
||||||
|
|
||||||
|
// Should check both the compose image and the container image
|
||||||
|
const checkedImages = checkImageSpy.mock.calls.map((c: any[]) => c[1]);
|
||||||
|
expect(checkedImages).toContain('nginx:1.25');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes containers outside compose dir', async () => {
|
||||||
|
mockGetAllContainers.mockResolvedValue([
|
||||||
|
{
|
||||||
|
Labels: { 'com.docker.compose.project.working_dir': '/other/place/app' },
|
||||||
|
Image: 'someapp:v2',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const service = ImageUpdateService.getInstance();
|
||||||
|
const checkImageSpy = vi.fn().mockResolvedValue({ hasUpdate: false });
|
||||||
|
(service as any).checkImage = checkImageSpy;
|
||||||
|
|
||||||
|
await (service as any).checkNode(1, 'local', fakeDb());
|
||||||
|
|
||||||
|
const checkedImages = checkImageSpy.mock.calls.map((c: any[]) => c[1]);
|
||||||
|
expect(checkedImages).not.toContain('someapp:v2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -185,8 +185,10 @@ export class FileSystemService {
|
|||||||
try {
|
try {
|
||||||
return await fsPromises.readFile(envPath, 'utf-8');
|
return await fsPromises.readFile(envPath, 'utf-8');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading env file:', error);
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||||
throw new Error(`Failed to read env file for stack: ${stackName}`);
|
console.error('Error reading env file:', error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export function extractImagesFromCompose(
|
|||||||
export class ImageUpdateService {
|
export class ImageUpdateService {
|
||||||
private static instance: ImageUpdateService;
|
private static instance: ImageUpdateService;
|
||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
|
private startupTimeoutId: NodeJS.Timeout | null = null;
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
private lastManualRefreshAt = 0;
|
private lastManualRefreshAt = 0;
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ export class ImageUpdateService {
|
|||||||
private static readonly STARTUP_DELAY_MS = 2 * 60 * 1000; // 2 min after boot
|
private static readonly STARTUP_DELAY_MS = 2 * 60 * 1000; // 2 min after boot
|
||||||
private static readonly MANUAL_COOLDOWN_MS = 2 * 60 * 1000; // 2 min between manual triggers
|
private static readonly MANUAL_COOLDOWN_MS = 2 * 60 * 1000; // 2 min between manual triggers
|
||||||
private static readonly INTER_IMAGE_DELAY_MS = 300; // be polite to registries
|
private static readonly INTER_IMAGE_DELAY_MS = 300; // be polite to registries
|
||||||
|
private static readonly CHECK_TIMEOUT_MS = 5 * 60 * 1000; // 5 min overall cap per scan
|
||||||
|
|
||||||
public static get manualCooldownMinutes(): number {
|
public static get manualCooldownMinutes(): number {
|
||||||
return ImageUpdateService.MANUAL_COOLDOWN_MS / (60 * 1000);
|
return ImageUpdateService.MANUAL_COOLDOWN_MS / (60 * 1000);
|
||||||
@@ -114,11 +116,15 @@ export class ImageUpdateService {
|
|||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
if (this.intervalId) return;
|
if (this.intervalId) return;
|
||||||
setTimeout(() => this.check(), ImageUpdateService.STARTUP_DELAY_MS);
|
this.startupTimeoutId = setTimeout(() => this.check(), ImageUpdateService.STARTUP_DELAY_MS);
|
||||||
this.intervalId = setInterval(() => this.check(), ImageUpdateService.INTERVAL_MS);
|
this.intervalId = setInterval(() => this.check(), ImageUpdateService.INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
|
if (this.startupTimeoutId) {
|
||||||
|
clearTimeout(this.startupTimeoutId);
|
||||||
|
this.startupTimeoutId = null;
|
||||||
|
}
|
||||||
if (this.intervalId) {
|
if (this.intervalId) {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
this.intervalId = null;
|
this.intervalId = null;
|
||||||
@@ -151,6 +157,12 @@ export class ImageUpdateService {
|
|||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
console.log('[ImageUpdateService] Starting image update check...');
|
console.log('[ImageUpdateService] Starting image update check...');
|
||||||
|
|
||||||
|
const checkTimeout = setTimeout(() => {
|
||||||
|
console.warn('[ImageUpdateService] Check timed out after ' +
|
||||||
|
`${ImageUpdateService.CHECK_TIMEOUT_MS / 60_000} minutes; releasing lock`);
|
||||||
|
this.isRunning = false;
|
||||||
|
}, ImageUpdateService.CHECK_TIMEOUT_MS);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = DatabaseService.getInstance();
|
const db = DatabaseService.getInstance();
|
||||||
// Only check local nodes - remote nodes run their own instance
|
// Only check local nodes - remote nodes run their own instance
|
||||||
@@ -166,6 +178,7 @@ export class ImageUpdateService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ImageUpdateService] Check failed:', e);
|
console.error('[ImageUpdateService] Check failed:', e);
|
||||||
} finally {
|
} finally {
|
||||||
|
clearTimeout(checkTimeout);
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,6 +193,10 @@ export class ImageUpdateService {
|
|||||||
const stackImages = new Map<string, Set<string>>();
|
const stackImages = new Map<string, Set<string>>();
|
||||||
for (const name of stacks) stackImages.set(name, new Set());
|
for (const name of stacks) stackImages.set(name, new Set());
|
||||||
|
|
||||||
|
if (isDebugEnabled()) {
|
||||||
|
console.log(`[ImageUpdateService:debug] Node ${nodeId}: Phase 1 complete - ${stacks.length} stack(s) found`);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 2: Parse compose files for image refs
|
// Phase 2: Parse compose files for image refs
|
||||||
for (const stackName of stacks) {
|
for (const stackName of stacks) {
|
||||||
try {
|
try {
|
||||||
@@ -188,10 +205,13 @@ export class ImageUpdateService {
|
|||||||
// Load .env for variable resolution (best-effort)
|
// Load .env for variable resolution (best-effort)
|
||||||
let envVars: Record<string, string> = {};
|
let envVars: Record<string, string> = {};
|
||||||
try {
|
try {
|
||||||
const envContent = await fs.getEnvContent(stackName);
|
const hasEnv = await fs.envExists(stackName);
|
||||||
envVars = loadDotEnv(envContent);
|
if (hasEnv) {
|
||||||
|
const envContent = await fs.getEnvContent(stackName);
|
||||||
|
envVars = loadDotEnv(envContent);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// No .env file or unreadable; continue with process.env only
|
// .env file exists but unreadable; continue with process.env only
|
||||||
}
|
}
|
||||||
// Docker Compose precedence: host env overrides .env
|
// Docker Compose precedence: host env overrides .env
|
||||||
const merged: Record<string, string> = { ...envVars };
|
const merged: Record<string, string> = { ...envVars };
|
||||||
@@ -207,6 +227,11 @@ export class ImageUpdateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDebugEnabled()) {
|
||||||
|
const composeImageCount = [...stackImages.values()].reduce((sum, s) => sum + s.size, 0);
|
||||||
|
console.log(`[ImageUpdateService:debug] Node ${nodeId}: Phase 2 complete - ${composeImageCount} image(s) extracted from compose files`);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: Container augmentation (captures actual deployed image tags)
|
// Phase 3: Container augmentation (captures actual deployed image tags)
|
||||||
try {
|
try {
|
||||||
const containers = await docker.getAllContainers();
|
const containers = await docker.getAllContainers();
|
||||||
@@ -230,6 +255,11 @@ export class ImageUpdateService {
|
|||||||
console.warn('[ImageUpdateService] Container augmentation failed:', e);
|
console.warn('[ImageUpdateService] Container augmentation failed:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDebugEnabled()) {
|
||||||
|
const totalBeforeDedup = [...stackImages.values()].reduce((sum, s) => sum + s.size, 0);
|
||||||
|
console.log(`[ImageUpdateService:debug] Node ${nodeId}: Phase 3 complete - ${totalBeforeDedup} image(s) across all stacks (pre-dedup)`);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 4: Deduplicate and check all unique images
|
// Phase 4: Deduplicate and check all unique images
|
||||||
const allImages = new Set<string>();
|
const allImages = new Set<string>();
|
||||||
for (const imgs of stackImages.values()) for (const img of imgs) allImages.add(img);
|
for (const imgs of stackImages.values()) for (const img of imgs) allImages.add(img);
|
||||||
|
|||||||
Reference in New Issue
Block a user