From 94657735fb82aded67442a0847c61ed5c67e2ddb Mon Sep 17 00:00:00 2001 From: YewFence Date: Tue, 24 Mar 2026 15:33:33 +0800 Subject: [PATCH] feat: mirror Dockhand's ExtraHosts into scanner and self-update sidecar containers Add `extraHosts` option to `runContainer` and `runContainerWithStreaming` so arbitrary `HostConfig.ExtraHosts` entries can be passed when spawning containers. Expose `getOwnExtraHosts()` from `host-path.ts` and forward the cached entries into scanner and self-updater containers, ensuring custom host aliases (e.g. internal registry hostnames) are available inside those sidecars without additional user configuration. --- src/lib/server/docker.ts | 10 +++++++++ src/lib/server/host-path.ts | 29 +++++++++++++++++++++++++-- src/lib/server/scanner.ts | 27 +++++++++++++++++++++++-- src/routes/api/self-update/+server.ts | 18 ++++++++++++++++- 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/lib/server/docker.ts b/src/lib/server/docker.ts index 6dfe10a..101963c 100644 --- a/src/lib/server/docker.ts +++ b/src/lib/server/docker.ts @@ -3964,6 +3964,7 @@ export async function runContainer(options: { cmd: string[]; binds?: string[]; env?: string[]; + extraHosts?: string[]; name?: string; envId?: number | null; }): Promise<{ stdout: string; stderr: string }> { @@ -3985,6 +3986,10 @@ export async function runContainer(options: { } }; + if (options.extraHosts && options.extraHosts.length > 0) { + containerConfig.HostConfig.ExtraHosts = options.extraHosts; + } + const createResult = await dockerJsonRequest<{ Id: string }>( `/containers/create?name=${encodeURIComponent(containerName)}`, { @@ -4044,6 +4049,7 @@ export async function runContainerWithStreaming(options: { cmd: string[]; binds?: string[]; env?: string[]; + extraHosts?: string[]; name?: string; user?: string; envId?: number | null; @@ -4071,6 +4077,10 @@ export async function runContainerWithStreaming(options: { } }; + if (options.extraHosts && options.extraHosts.length > 0) { + containerConfig.HostConfig.ExtraHosts = options.extraHosts; + } + // Set user if specified (needed for rootless Docker socket access) if (options.user) { containerConfig.User = options.user; diff --git a/src/lib/server/host-path.ts b/src/lib/server/host-path.ts index f66300c..69107d6 100644 --- a/src/lib/server/host-path.ts +++ b/src/lib/server/host-path.ts @@ -34,6 +34,7 @@ let cachedMounts: Array<{ source: string; destination: string }> | null = null; // Used by scanner to replicate how Dockhand connects to Docker let cachedOwnDockerHost: string | null = null; let cachedOwnNetworkMode: string | null = null; +let cachedOwnExtraHosts: string[] | null = null; /** * Get our own container ID @@ -85,12 +86,11 @@ export async function detectHostDataDir(): Promise { if (process.env.HOST_DATA_DIR) { cachedHostDataDir = process.env.HOST_DATA_DIR; console.log(`[HostPath] Using HOST_DATA_DIR from environment: ${cachedHostDataDir}`); - return cachedHostDataDir; } const containerId = getOwnContainerId(); if (!containerId) { - console.warn('[HostPath] Running in Docker but could not detect container ID'); + console.warn('[HostPath] Running in Docker but could not detect container ID; ExtraHosts will not be mirrored to sidecars'); return null; } @@ -140,6 +140,9 @@ export async function detectHostDataDir(): Promise { Config?: { Env?: string[]; }; + HostConfig?: { + ExtraHosts?: string[]; + }; NetworkSettings?: { Networks?: Record; }; @@ -176,6 +179,19 @@ export async function detectHostDataDir(): Promise { } } + cachedOwnExtraHosts = containerInfo.HostConfig?.ExtraHosts?.length + ? [...containerInfo.HostConfig.ExtraHosts] + : null; + if (cachedOwnExtraHosts) { + console.log(`[HostPath] Detected own ExtraHosts: ${cachedOwnExtraHosts.join(', ')}`); + } + + // Explicit override wins for DATA_DIR path, but we still inspect to populate + // mounts/network/DOCKER_HOST/ExtraHosts caches for sibling sidecars. + if (cachedHostDataDir) { + return cachedHostDataDir; + } + // Find the mount for our DATA_DIR const dataMount = containerInfo.Mounts?.find(m => m.Destination === dataDir); @@ -229,6 +245,15 @@ export function getOwnNetworkMode(): string | null { return cachedOwnNetworkMode; } +/** + * Get the ExtraHosts entries configured on Dockhand itself. + * Used to mirror host aliases into sibling sidecar containers. + * Populated by detectHostDataDir() at startup. + */ +export function getOwnExtraHosts(): string[] | null { + return cachedOwnExtraHosts ? [...cachedOwnExtraHosts] : null; +} + /** * Translate a container path to host path * diff --git a/src/lib/server/scanner.ts b/src/lib/server/scanner.ts index 0978243..f6c4af6 100644 --- a/src/lib/server/scanner.ts +++ b/src/lib/server/scanner.ts @@ -16,7 +16,15 @@ import { } from './docker'; import { getEnvironment, getEnvSetting, getSetting } from './db'; import { sendEventNotification } from './notifications'; -import { getHostDockerSocket, getHostDataDir, extractUidFromSocketPath, getOwnDockerHost, getOwnNetworkMode } from './host-path'; +import { + detectHostDataDir, + getHostDockerSocket, + getHostDataDir, + extractUidFromSocketPath, + getOwnDockerHost, + getOwnExtraHosts, + getOwnNetworkMode +} from './host-path'; import { resolve } from 'node:path'; import { mkdir, chown, rm } from 'node:fs/promises'; @@ -610,6 +618,10 @@ async function runScannerContainerCore( ): Promise { console.log(`[Scanner] Starting ${scannerType} scan for image: ${imageName}, envId: ${envId ?? 'local'}`); + // Ensure startup inspect caches are populated before we mirror Dockhand's own + // Docker access settings into sibling sidecars. + await detectHostDataDir().catch(() => null); + // Always use the base cache path — serial lock prevents concurrent conflicts const basePath = scannerType === 'grype' ? '/cache/grype' : '/cache/trivy'; const dbPath = basePath; @@ -625,6 +637,7 @@ async function runScannerContainerCore( let rootlessUid: string | undefined; let scannerNetworkMode: string | undefined; let scannerDockerHost: string | undefined; + const scannerExtraHosts = !isHawser ? getOwnExtraHosts() ?? undefined : undefined; // Check if Dockhand itself uses TCP to reach Docker (e.g., socket proxy). // Detected at startup from Dockhand's own container inspect data. @@ -636,7 +649,12 @@ async function runScannerContainerCore( // TCP mode: scanner uses the same DOCKER_HOST + network as Dockhand scannerDockerHost = ownDockerHost; scannerNetworkMode = getOwnNetworkMode() ?? undefined; - console.log(`[Scanner] TCP mode (from container inspect) - DOCKER_HOST=${scannerDockerHost}, network=${scannerNetworkMode ?? 'default'}`); + console.log( + `[Scanner] TCP mode (from container inspect) - DOCKER_HOST=${scannerDockerHost}, network=${scannerNetworkMode ?? 'default'}` + ); + if (scannerExtraHosts?.length) { + console.log(`[Scanner] Reusing ExtraHosts from Dockhand: ${scannerExtraHosts.join(', ')}`); + } } else if (isHawser) { // Hawser: scanner runs on remote host, uses remote host's standard Docker socket hostSocketPath = '/var/run/docker.sock'; @@ -653,6 +671,10 @@ async function runScannerContainerCore( console.log(`[Scanner] Rootless Docker detected (UID ${rootlessUid})`); console.log(`[Scanner] Scanner will run as root inside container (maps to UID ${rootlessUid} on host via user namespace)`); } + + if (scannerExtraHosts?.length) { + console.log(`[Scanner] Reusing ExtraHosts from Dockhand: ${scannerExtraHosts.join(', ')}`); + } } // Determine cache storage strategy based on environment @@ -722,6 +744,7 @@ async function runScannerContainerCore( cmd, binds, env: envVars, + extraHosts: scannerExtraHosts, name: `dockhand-${scannerType}-${Date.now()}`, envId, networkMode: scannerNetworkMode, diff --git a/src/routes/api/self-update/+server.ts b/src/routes/api/self-update/+server.ts index 4c520b9..0b098c5 100644 --- a/src/routes/api/self-update/+server.ts +++ b/src/routes/api/self-update/+server.ts @@ -1,6 +1,13 @@ import { json } from '@sveltejs/kit'; import { authorize } from '$lib/server/authorize'; -import { getOwnContainerId, getHostDockerSocket, getOwnDockerHost, getOwnNetworkMode } from '$lib/server/host-path'; +import { + detectHostDataDir, + getOwnContainerId, + getHostDockerSocket, + getOwnDockerHost, + getOwnExtraHosts, + getOwnNetworkMode +} from '$lib/server/host-path'; import { buildRegistryAuthHeader, unixSocketRequest, unixSocketStreamRequest } from '$lib/server/docker'; import type { RequestHandler } from './$types'; import { prefersJSON, sseToJSON } from '$lib/server/sse'; @@ -255,6 +262,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => { return json({ error: 'Not running in Docker' }, { status: 400 }); } + // Populate cached inspect data used to mirror Dockhand's own sidecar settings. + await detectHostDataDir().catch(() => null); + const writable = await isDockerWritable(containerId); if (!writable) { return json({ @@ -395,6 +405,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => { // Configure updater's Docker access based on connection type const tcpHost = getDockerTcpHost(); const updaterHostConfig: Record = { AutoRemove: true }; + const updaterExtraHosts = getOwnExtraHosts() ?? undefined; + + if (updaterExtraHosts?.length) { + updaterHostConfig.ExtraHosts = updaterExtraHosts; + console.log(`[SelfUpdate] Reusing ExtraHosts for updater: ${updaterExtraHosts.join(', ')}`); + } if (tcpHost) { // TCP: pass DOCKER_HOST so docker CLI in sidecar uses TCP