This commit is contained in:
jarek committed 2026-06-06 16:18:05 +02:00
1 parent 00bd09df55
commit 3cbcfa3cdb
26 files changed
+1367 -765

No files matched your search

@@ -1,10 +1,14 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { inspectContainer } from '$lib/server/docker';
import { inspectContainer, inspectImage } from '$lib/server/docker';
import { getSecretKeysToMask } from '$lib/server/db';
import { getStackComposeFile } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { validateDockerIdParam } from '$lib/server/docker-validation';
import {
detectImageEnvDivergence,
detectImageLabelDivergence
} from '$lib/server/container-image-divergence';
export const GET: RequestHandler = async ({ params, url, cookies }) => {
const invalid = validateDockerIdParam(params.id, 'container');
@@ -23,6 +27,25 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
try {
const containerData = await inspectContainer(params.id, envIdNum);
// Compute env/label divergence BEFORE masking, so the comparison
// uses real values. Failure to inspect the image is non-fatal —
// the field is omitted in that case.
let divergence: { env: string[]; labels: string[] } | undefined;
try {
const imageRef = containerData.Config?.Image;
if (imageRef) {
const imageData: any = await inspectImage(imageRef, envIdNum);
const imageEnv: string[] = imageData?.Config?.Env || [];
const imageLabels: Record<string, string> = imageData?.Config?.Labels || {};
divergence = {
env: detectImageEnvDivergence(containerData.Config?.Env || [], imageEnv),
labels: detectImageLabelDivergence(containerData.Config?.Labels, imageLabels)
};
}
} catch {
// image not present / not pullable / etc — drop the field
}
// Mask secret env vars for containers belonging to a Compose stack.
// Uses compose file parsing to detect interpolation (e.g., MYSQL_PASSWORD=${db_secret}).
const stackName = containerData.Config?.Labels?.['com.docker.compose.project'];
@@ -42,7 +65,7 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
}
}
return json(containerData);
return json({ ...containerData, divergence });
} catch (error) {
console.error('Failed to inspect container:', error);
return json({ error: 'Failed to inspect container' }, { status: 500 });
@@ -303,7 +303,7 @@ async function getEnvironmentStatsProgressive(
envStats.containers.total = containers.length;
envStats.containers.running = containers.filter((c: any) => c.state === 'running').length;
envStats.containers.stopped = containers.filter((c: any) => c.state === 'exited' && c.exitCode !== 0).length;
envStats.containers.stopped = containers.filter((c: any) => c.state === 'exited').length;
envStats.containers.paused = containers.filter((c: any) => c.state === 'paused').length;
envStats.containers.restarting = containers.filter((c: any) => c.state === 'restarting').length;
envStats.containers.unhealthy = containers.filter((c: any) => c.health === 'unhealthy').length;
+19 -3
View File
@@ -58,7 +58,8 @@ export interface GeneralSettings {
eventCleanupEnabled: boolean;
scannerCleanupCron: string;
scannerCleanupEnabled: boolean;
logBufferSizeKb: number;
logBufferSizeKb: number; // legacy
logMaxLines: number; // line-count cap for log buffer
defaultTimezone: string;
// Background monitoring settings
eventCollectionMode: EventCollectionMode;
@@ -101,6 +102,7 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
defaultGrypeArgs: '-o json -v {image}',
defaultTrivyArgs: 'image --format json {image}',
logBufferSizeKb: 500,
logMaxLines: 2000,
defaultTimezone: 'UTC',
eventCollectionMode: 'stream',
eventPollInterval: 60000,
@@ -177,6 +179,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
scannerCleanupCron,
scannerCleanupEnabled,
logBufferSizeKb,
logMaxLines,
defaultTimezone,
eventCollectionMode,
eventPollInterval,
@@ -215,6 +218,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
getScannerCleanupCron(),
getScannerCleanupEnabled(),
getSetting('log_buffer_size_kb'),
getSetting('log_max_lines'),
getDefaultTimezone(),
getEventCollectionMode(),
getEventPollInterval(),
@@ -255,6 +259,9 @@ export const GET: RequestHandler = async ({ cookies }) => {
scannerCleanupCron,
scannerCleanupEnabled,
logBufferSizeKb: logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb,
logMaxLines: (typeof logMaxLines === 'number' && logMaxLines > 0)
? Math.min(2000, Math.max(100, logMaxLines))
: Math.min(2000, Math.max(100, Math.round((logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb) * 8))),
defaultTimezone: defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone,
eventCollectionMode: (eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode,
eventPollInterval: eventPollInterval ?? DEFAULT_SETTINGS.eventPollInterval,
@@ -292,7 +299,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
try {
const body = await request.json();
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode } = body;
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode } = body;
if (confirmDestructive !== undefined) {
await setSetting('confirm_destructive', confirmDestructive);
@@ -345,9 +352,13 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
await refreshSystemJobs();
}
if (logBufferSizeKb !== undefined && typeof logBufferSizeKb === 'number') {
// Clamp to reasonable range: 100KB - 5000KB (5MB)
// Legacy: clamp to 100KB-5MB range.
await setSetting('log_buffer_size_kb', Math.max(100, Math.min(5000, logBufferSizeKb)));
}
if (logMaxLines !== undefined && typeof logMaxLines === 'number') {
// Clamp to 100 - 50000 lines.
await setSetting('log_max_lines', Math.max(100, Math.min(2000, logMaxLines)));
}
if (defaultTimezone !== undefined && typeof defaultTimezone === 'string') {
await setDefaultTimezone(defaultTimezone);
// Refresh system jobs to use the new timezone
@@ -448,6 +459,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
scannerCleanupCronVal,
scannerCleanupEnabledVal,
logBufferSizeKbVal,
logMaxLinesVal,
defaultTimezoneVal,
eventCollectionModeVal,
eventPollIntervalVal,
@@ -486,6 +498,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
getScannerCleanupCron(),
getScannerCleanupEnabled(),
getSetting('log_buffer_size_kb'),
getSetting('log_max_lines'),
getDefaultTimezone(),
getEventCollectionMode(),
getEventPollInterval(),
@@ -526,6 +539,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
scannerCleanupCron: scannerCleanupCronVal,
scannerCleanupEnabled: scannerCleanupEnabledVal,
logBufferSizeKb: logBufferSizeKbVal ?? DEFAULT_SETTINGS.logBufferSizeKb,
logMaxLines: (typeof logMaxLinesVal === 'number' && logMaxLinesVal > 0)
? Math.min(2000, Math.max(100, logMaxLinesVal))
: Math.min(2000, Math.max(100, Math.round((logBufferSizeKbVal ?? DEFAULT_SETTINGS.logBufferSizeKb) * 8))),
defaultTimezone: defaultTimezoneVal ?? DEFAULT_SETTINGS.defaultTimezone,
eventCollectionMode: (eventCollectionModeVal ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode,
eventPollInterval: eventPollIntervalVal ?? DEFAULT_SETTINGS.eventPollInterval,
+17 -1
View File
@@ -13,6 +13,22 @@ interface ValidationResult {
unused: string[];
}
/** Docker and Compose built-in env vars consumed implicitly at runtime (not via ${} interpolation) */
const DOCKER_COMPOSE_BUILTIN_VARS = new Set([
// Docker Compose
'COMPOSE_PROJECT_NAME', 'COMPOSE_FILE', 'COMPOSE_PROFILES',
'COMPOSE_CONVERT_WINDOWS_PATHS', 'COMPOSE_PATH_SEPARATOR',
'COMPOSE_IGNORE_ORPHANS', 'COMPOSE_REMOVE_ORPHANS',
'COMPOSE_PARALLEL_LIMIT', 'COMPOSE_ANSI', 'COMPOSE_STATUS_STDOUT',
'COMPOSE_ENV_FILES', 'COMPOSE_DISABLE_ENV_FILE', 'COMPOSE_MENU',
'COMPOSE_EXPERIMENTAL', 'COMPOSE_PROGRESS',
// Docker CLI
'DOCKER_API_VERSION', 'DOCKER_CERT_PATH', 'DOCKER_CONFIG',
'DOCKER_CONTEXT', 'DOCKER_CUSTOM_HEADERS', 'DOCKER_DEFAULT_PLATFORM',
'DOCKER_HIDE_LEGACY_COMMANDS', 'DOCKER_HOST', 'DOCKER_TLS',
'DOCKER_TLS_VERIFY', 'BUILDKIT_PROGRESS', 'NO_COLOR',
]);
/**
* Extract environment variables from compose YAML content.
* Matches ${VAR_NAME} and ${VAR_NAME:-default} patterns.
@@ -144,7 +160,7 @@ export const POST: RequestHandler = async ({ params, url, cookies, request }) =>
// Calculate missing and unused
const missing = required.filter(v => !defined.includes(v));
const unused = defined.filter(v => !required.includes(v) && !optional.includes(v));
const unused = defined.filter(v => !required.includes(v) && !optional.includes(v) && !DOCKER_COMPOSE_BUILTIN_VARS.has(v));
const result: ValidationResult = {
valid: missing.length === 0,