1.0.35
This commit is contained in:
17 files changed
+284
-203
No files matched your search
@@ -33,47 +33,64 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
try {
|
||||
const containerId = params.id;
|
||||
const availableShells: string[] = [];
|
||||
const shellNames = SHELLS_TO_CHECK.map(s => s.path.split('/').pop()!);
|
||||
const namedPaths = new Map<string, string>(); // name → resolved absolute path
|
||||
|
||||
// Check each shell by testing if the file exists and is executable
|
||||
// Use a single command to check all shells at once for efficiency
|
||||
const checkCommand = SHELLS_TO_CHECK.map(s =>
|
||||
`test -x ${s.path} && echo "${s.path}"`
|
||||
).join('; ');
|
||||
// Resolve each shell through the container's own PATH using `command -v`,
|
||||
// so that images with shells at non-standard locations (e.g. /usr/bin/sh
|
||||
// where /bin/sh isn't a symlink, or PATH ordering that shadows the
|
||||
// shell-builtin `test`) still report correctly. `exit 0` at the end
|
||||
// keeps the exec exit code clean even when some shells are absent —
|
||||
// otherwise execInContainer treats non-zero as a thrown error and the
|
||||
// valid output is discarded (issue #1189).
|
||||
const probe =
|
||||
`for s in ${shellNames.join(' ')}; do ` +
|
||||
`p=$(command -v $s 2>/dev/null) && [ -n "$p" ] && echo "$s:$p"; ` +
|
||||
`done; exit 0`;
|
||||
|
||||
try {
|
||||
const output = await execInContainer(
|
||||
containerId,
|
||||
['sh', '-c', checkCommand],
|
||||
['sh', '-c', probe],
|
||||
envIdNum
|
||||
);
|
||||
|
||||
// Parse output - each line is an available shell path
|
||||
const lines = output.trim().split('\n').filter(Boolean);
|
||||
availableShells.push(...lines);
|
||||
for (const line of output.trim().split('\n').filter(Boolean)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const name = line.slice(0, colon);
|
||||
const resolved = line.slice(colon + 1);
|
||||
if (resolved.startsWith('/')) namedPaths.set(name, resolved);
|
||||
}
|
||||
} catch {
|
||||
// If even sh fails, try checking with test commands individually
|
||||
// This handles edge cases where sh might not be available
|
||||
// `sh` itself was not invocable. Fall back to probing the canonical
|
||||
// paths directly with a non-shell-resolved test — exec each one
|
||||
// against its absolute path and rely on docker exec's own
|
||||
// "executable not found" failure to indicate absence. This handles
|
||||
// the rare image where there's no `sh` at all but bash exists.
|
||||
for (const shell of SHELLS_TO_CHECK) {
|
||||
try {
|
||||
await execInContainer(
|
||||
containerId,
|
||||
['test', '-x', shell.path],
|
||||
[shell.path, '-c', 'exit 0'],
|
||||
envIdNum
|
||||
);
|
||||
availableShells.push(shell.path);
|
||||
const name = shell.path.split('/').pop()!;
|
||||
namedPaths.set(name, shell.path);
|
||||
} catch {
|
||||
// Shell not available, continue to next
|
||||
// Shell not available at this path; try the next.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const availableShells = Array.from(namedPaths.values());
|
||||
|
||||
// Determine default shell - prefer bash, then sh, then first available
|
||||
let defaultShell: string | null = null;
|
||||
if (availableShells.includes('/bin/bash')) {
|
||||
defaultShell = '/bin/bash';
|
||||
} else if (availableShells.includes('/bin/sh')) {
|
||||
defaultShell = '/bin/sh';
|
||||
if (namedPaths.has('bash')) {
|
||||
defaultShell = namedPaths.get('bash')!;
|
||||
} else if (namedPaths.has('sh')) {
|
||||
defaultShell = namedPaths.get('sh')!;
|
||||
} else if (availableShells.length > 0) {
|
||||
defaultShell = availableShells[0];
|
||||
}
|
||||
@@ -81,11 +98,15 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
return json({
|
||||
shells: availableShells,
|
||||
defaultShell,
|
||||
allShells: SHELLS_TO_CHECK.map(s => ({
|
||||
path: s.path,
|
||||
label: s.label,
|
||||
available: availableShells.includes(s.path)
|
||||
}))
|
||||
allShells: SHELLS_TO_CHECK.map(s => {
|
||||
const name = s.path.split('/').pop()!;
|
||||
const resolved = namedPaths.get(name);
|
||||
return {
|
||||
path: resolved ?? s.path,
|
||||
label: s.label,
|
||||
available: namedPaths.has(name)
|
||||
};
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error detecting shells:', error);
|
||||
|
||||
@@ -38,7 +38,7 @@ import { DEFAULT_GRYPE_IMAGE, DEFAULT_TRIVY_IMAGE } from '$lib/server/scanner';
|
||||
|
||||
export type TimeFormat = '12h' | '24h';
|
||||
export type DateFormat = 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'DD.MM.YYYY';
|
||||
export type DownloadFormat = 'tar' | 'tar.gz';
|
||||
export type DownloadFormat = 'tar' | 'tar.gz' | 'raw';
|
||||
export type EventCollectionMode = 'stream' | 'poll';
|
||||
|
||||
export interface GeneralSettings {
|
||||
@@ -91,7 +91,8 @@ export interface GeneralSettings {
|
||||
// Label filter mode
|
||||
labelFilterMode: 'any' | 'all';
|
||||
// Whether to surface URLs inferred from reverse-proxy labels — currently
|
||||
// Traefik (traefik.http.routers.*) and Pangolin (pangolin.proxy-resources.*).
|
||||
// Traefik (traefik.http.routers.*) and Pangolin
|
||||
// (pangolin.{public,private}-resources.*).
|
||||
// When false both parsers are bypassed and no proxy-derived pills are rendered.
|
||||
honorProxyLabels: boolean;
|
||||
// Whether to surface a "view changelog" link next to the update badge.
|
||||
@@ -99,6 +100,8 @@ export interface GeneralSettings {
|
||||
showImageChangelogLinks: boolean;
|
||||
// Whether spinning icons (animate-spin etc.) are animated (#1169)
|
||||
animateIcons: boolean;
|
||||
// Skip Dockhand's scanner images (grype, trivy) during 'prune all unused' (#625)
|
||||
protectScannerImages: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRetentionDays' | 'scheduleCleanupCron' | 'eventCleanupCron' | 'scheduleCleanupEnabled' | 'eventCleanupEnabled' | 'scannerCleanupCron' | 'scannerCleanupEnabled'> = {
|
||||
@@ -134,6 +137,7 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
|
||||
honorProxyLabels: true,
|
||||
showImageChangelogLinks: true,
|
||||
animateIcons: true,
|
||||
protectScannerImages: true,
|
||||
defaultComposeTemplate: `version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -214,7 +218,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
labelFilterMode,
|
||||
honorProxyLabels,
|
||||
showImageChangelogLinks,
|
||||
animateIcons
|
||||
animateIcons,
|
||||
protectScannerImages
|
||||
] = await Promise.all([
|
||||
getSetting('confirm_destructive'),
|
||||
getSetting('show_stopped_containers'),
|
||||
@@ -256,7 +261,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
getSetting('label_filter_mode'),
|
||||
getSetting('honor_proxy_labels'),
|
||||
getSetting('show_image_changelog_links'),
|
||||
getSetting('animate_icons')
|
||||
getSetting('animate_icons'),
|
||||
getSetting('protect_scanner_images')
|
||||
]);
|
||||
|
||||
const settings: GeneralSettings = {
|
||||
@@ -302,7 +308,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
labelFilterMode: labelFilterMode ?? DEFAULT_SETTINGS.labelFilterMode,
|
||||
honorProxyLabels: honorProxyLabels ?? DEFAULT_SETTINGS.honorProxyLabels,
|
||||
showImageChangelogLinks: showImageChangelogLinks ?? DEFAULT_SETTINGS.showImageChangelogLinks,
|
||||
animateIcons: animateIcons ?? DEFAULT_SETTINGS.animateIcons
|
||||
animateIcons: animateIcons ?? DEFAULT_SETTINGS.animateIcons,
|
||||
protectScannerImages: protectScannerImages ?? DEFAULT_SETTINGS.protectScannerImages
|
||||
};
|
||||
|
||||
return json(settings);
|
||||
@@ -320,7 +327,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, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, honorProxyLabels, showImageChangelogLinks, animateIcons } = 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, honorProxyLabels, showImageChangelogLinks, animateIcons, protectScannerImages } = body;
|
||||
|
||||
if (confirmDestructive !== undefined) {
|
||||
await setSetting('confirm_destructive', confirmDestructive);
|
||||
@@ -337,7 +344,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
if (dateFormat !== undefined && VALID_DATE_FORMATS.includes(dateFormat)) {
|
||||
await setSetting('date_format', dateFormat);
|
||||
}
|
||||
if (downloadFormat !== undefined && (downloadFormat === 'tar' || downloadFormat === 'tar.gz')) {
|
||||
if (downloadFormat !== undefined && (downloadFormat === 'tar' || downloadFormat === 'tar.gz' || downloadFormat === 'raw')) {
|
||||
await setSetting('download_format', downloadFormat);
|
||||
}
|
||||
if (defaultGrypeArgs !== undefined && typeof defaultGrypeArgs === 'string') {
|
||||
@@ -469,6 +476,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
if (animateIcons !== undefined && typeof animateIcons === 'boolean') {
|
||||
await setSetting('animate_icons', animateIcons);
|
||||
}
|
||||
if (protectScannerImages !== undefined && typeof protectScannerImages === 'boolean') {
|
||||
await setSetting('protect_scanner_images', protectScannerImages);
|
||||
}
|
||||
|
||||
// Fetch all settings in parallel for the response
|
||||
const [
|
||||
@@ -512,7 +522,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
labelFilterModeVal,
|
||||
honorProxyLabelsVal,
|
||||
showImageChangelogLinksVal,
|
||||
animateIconsVal
|
||||
animateIconsVal,
|
||||
protectScannerImagesVal
|
||||
] = await Promise.all([
|
||||
getSetting('confirm_destructive'),
|
||||
getSetting('show_stopped_containers'),
|
||||
@@ -554,7 +565,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
getSetting('label_filter_mode'),
|
||||
getSetting('honor_proxy_labels'),
|
||||
getSetting('show_image_changelog_links'),
|
||||
getSetting('animate_icons')
|
||||
getSetting('animate_icons'),
|
||||
getSetting('protect_scanner_images')
|
||||
]);
|
||||
|
||||
const settings: GeneralSettings = {
|
||||
@@ -599,6 +611,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
defaultComposeTemplate: defaultComposeTemplateVal ?? DEFAULT_SETTINGS.defaultComposeTemplate,
|
||||
labelFilterMode: labelFilterModeVal ?? DEFAULT_SETTINGS.labelFilterMode,
|
||||
honorProxyLabels: honorProxyLabelsVal ?? DEFAULT_SETTINGS.honorProxyLabels,
|
||||
protectScannerImages: protectScannerImagesVal ?? DEFAULT_SETTINGS.protectScannerImages,
|
||||
showImageChangelogLinks: showImageChangelogLinksVal ?? DEFAULT_SETTINGS.showImageChangelogLinks,
|
||||
animateIcons: animateIconsVal ?? DEFAULT_SETTINGS.animateIcons
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user