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.
This commit is contained in:
YewFence
2026-03-24 15:33:33 +08:00
committed by jarek
parent 74741d2a01
commit 94657735fb
4 changed files with 79 additions and 5 deletions
+10
View File
@@ -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;
+27 -2
View File
@@ -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<string | null> {
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<string | null> {
Config?: {
Env?: string[];
};
HostConfig?: {
ExtraHosts?: string[];
};
NetworkSettings?: {
Networks?: Record<string, unknown>;
};
@@ -176,6 +179,19 @@ export async function detectHostDataDir(): Promise<string | null> {
}
}
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
*
+25 -2
View File
@@ -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<string> {
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,
+17 -1
View File
@@ -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<string, unknown> = { 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