1.0.32
This commit is contained in:
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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user