mirror of
https://github.com/Finsys/dockhand.git
synced 2026-06-17 19:09:33 +03:00
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:
@@ -3964,6 +3964,7 @@ export async function runContainer(options: {
|
|||||||
cmd: string[];
|
cmd: string[];
|
||||||
binds?: string[];
|
binds?: string[];
|
||||||
env?: string[];
|
env?: string[];
|
||||||
|
extraHosts?: string[];
|
||||||
name?: string;
|
name?: string;
|
||||||
envId?: number | null;
|
envId?: number | null;
|
||||||
}): Promise<{ stdout: string; stderr: string }> {
|
}): 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 }>(
|
const createResult = await dockerJsonRequest<{ Id: string }>(
|
||||||
`/containers/create?name=${encodeURIComponent(containerName)}`,
|
`/containers/create?name=${encodeURIComponent(containerName)}`,
|
||||||
{
|
{
|
||||||
@@ -4044,6 +4049,7 @@ export async function runContainerWithStreaming(options: {
|
|||||||
cmd: string[];
|
cmd: string[];
|
||||||
binds?: string[];
|
binds?: string[];
|
||||||
env?: string[];
|
env?: string[];
|
||||||
|
extraHosts?: string[];
|
||||||
name?: string;
|
name?: string;
|
||||||
user?: string;
|
user?: string;
|
||||||
envId?: number | null;
|
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)
|
// Set user if specified (needed for rootless Docker socket access)
|
||||||
if (options.user) {
|
if (options.user) {
|
||||||
containerConfig.User = options.user;
|
containerConfig.User = options.user;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ let cachedMounts: Array<{ source: string; destination: string }> | null = null;
|
|||||||
// Used by scanner to replicate how Dockhand connects to Docker
|
// Used by scanner to replicate how Dockhand connects to Docker
|
||||||
let cachedOwnDockerHost: string | null = null;
|
let cachedOwnDockerHost: string | null = null;
|
||||||
let cachedOwnNetworkMode: string | null = null;
|
let cachedOwnNetworkMode: string | null = null;
|
||||||
|
let cachedOwnExtraHosts: string[] | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get our own container ID
|
* Get our own container ID
|
||||||
@@ -85,12 +86,11 @@ export async function detectHostDataDir(): Promise<string | null> {
|
|||||||
if (process.env.HOST_DATA_DIR) {
|
if (process.env.HOST_DATA_DIR) {
|
||||||
cachedHostDataDir = process.env.HOST_DATA_DIR;
|
cachedHostDataDir = process.env.HOST_DATA_DIR;
|
||||||
console.log(`[HostPath] Using HOST_DATA_DIR from environment: ${cachedHostDataDir}`);
|
console.log(`[HostPath] Using HOST_DATA_DIR from environment: ${cachedHostDataDir}`);
|
||||||
return cachedHostDataDir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerId = getOwnContainerId();
|
const containerId = getOwnContainerId();
|
||||||
if (!containerId) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +140,9 @@ export async function detectHostDataDir(): Promise<string | null> {
|
|||||||
Config?: {
|
Config?: {
|
||||||
Env?: string[];
|
Env?: string[];
|
||||||
};
|
};
|
||||||
|
HostConfig?: {
|
||||||
|
ExtraHosts?: string[];
|
||||||
|
};
|
||||||
NetworkSettings?: {
|
NetworkSettings?: {
|
||||||
Networks?: Record<string, unknown>;
|
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
|
// Find the mount for our DATA_DIR
|
||||||
const dataMount = containerInfo.Mounts?.find(m => m.Destination === dataDir);
|
const dataMount = containerInfo.Mounts?.find(m => m.Destination === dataDir);
|
||||||
|
|
||||||
@@ -229,6 +245,15 @@ export function getOwnNetworkMode(): string | null {
|
|||||||
return cachedOwnNetworkMode;
|
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
|
* Translate a container path to host path
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,7 +16,15 @@ import {
|
|||||||
} from './docker';
|
} from './docker';
|
||||||
import { getEnvironment, getEnvSetting, getSetting } from './db';
|
import { getEnvironment, getEnvSetting, getSetting } from './db';
|
||||||
import { sendEventNotification } from './notifications';
|
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 { resolve } from 'node:path';
|
||||||
import { mkdir, chown, rm } from 'node:fs/promises';
|
import { mkdir, chown, rm } from 'node:fs/promises';
|
||||||
|
|
||||||
@@ -610,6 +618,10 @@ async function runScannerContainerCore(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
console.log(`[Scanner] Starting ${scannerType} scan for image: ${imageName}, envId: ${envId ?? 'local'}`);
|
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
|
// Always use the base cache path — serial lock prevents concurrent conflicts
|
||||||
const basePath = scannerType === 'grype' ? '/cache/grype' : '/cache/trivy';
|
const basePath = scannerType === 'grype' ? '/cache/grype' : '/cache/trivy';
|
||||||
const dbPath = basePath;
|
const dbPath = basePath;
|
||||||
@@ -625,6 +637,7 @@ async function runScannerContainerCore(
|
|||||||
let rootlessUid: string | undefined;
|
let rootlessUid: string | undefined;
|
||||||
let scannerNetworkMode: string | undefined;
|
let scannerNetworkMode: string | undefined;
|
||||||
let scannerDockerHost: 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).
|
// Check if Dockhand itself uses TCP to reach Docker (e.g., socket proxy).
|
||||||
// Detected at startup from Dockhand's own container inspect data.
|
// 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
|
// TCP mode: scanner uses the same DOCKER_HOST + network as Dockhand
|
||||||
scannerDockerHost = ownDockerHost;
|
scannerDockerHost = ownDockerHost;
|
||||||
scannerNetworkMode = getOwnNetworkMode() ?? undefined;
|
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) {
|
} else if (isHawser) {
|
||||||
// Hawser: scanner runs on remote host, uses remote host's standard Docker socket
|
// Hawser: scanner runs on remote host, uses remote host's standard Docker socket
|
||||||
hostSocketPath = '/var/run/docker.sock';
|
hostSocketPath = '/var/run/docker.sock';
|
||||||
@@ -653,6 +671,10 @@ async function runScannerContainerCore(
|
|||||||
console.log(`[Scanner] Rootless Docker detected (UID ${rootlessUid})`);
|
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)`);
|
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
|
// Determine cache storage strategy based on environment
|
||||||
@@ -722,6 +744,7 @@ async function runScannerContainerCore(
|
|||||||
cmd,
|
cmd,
|
||||||
binds,
|
binds,
|
||||||
env: envVars,
|
env: envVars,
|
||||||
|
extraHosts: scannerExtraHosts,
|
||||||
name: `dockhand-${scannerType}-${Date.now()}`,
|
name: `dockhand-${scannerType}-${Date.now()}`,
|
||||||
envId,
|
envId,
|
||||||
networkMode: scannerNetworkMode,
|
networkMode: scannerNetworkMode,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { authorize } from '$lib/server/authorize';
|
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 { buildRegistryAuthHeader, unixSocketRequest, unixSocketStreamRequest } from '$lib/server/docker';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { prefersJSON, sseToJSON } from '$lib/server/sse';
|
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 });
|
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);
|
const writable = await isDockerWritable(containerId);
|
||||||
if (!writable) {
|
if (!writable) {
|
||||||
return json({
|
return json({
|
||||||
@@ -395,6 +405,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
// Configure updater's Docker access based on connection type
|
// Configure updater's Docker access based on connection type
|
||||||
const tcpHost = getDockerTcpHost();
|
const tcpHost = getDockerTcpHost();
|
||||||
const updaterHostConfig: Record<string, unknown> = { AutoRemove: true };
|
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) {
|
if (tcpHost) {
|
||||||
// TCP: pass DOCKER_HOST so docker CLI in sidecar uses TCP
|
// TCP: pass DOCKER_HOST so docker CLI in sidecar uses TCP
|
||||||
|
|||||||
Reference in New Issue
Block a user