This commit is contained in:
jarek committed 2026-01-22 16:23:26 +01:00
1 parent a3cc26d958
commit 86a06d9de0
32 files changed
+799 -522

No files matched your search

+52 -19
View File
@@ -2,19 +2,21 @@
* Host Path Resolution Module
*
* Dockhand runs inside a Docker container where paths differ from the host.
* This module detects the host path for the DATA_DIR mount, enabling proper
* volume path resolution for compose stacks.
* This module detects the host paths for ALL container mounts, enabling proper
* volume path resolution for compose stacks (both internal and adopted/external).
*
* Problem:
* - Dockhand container has /app/data mounted from host (e.g., -v dockhand_data:/app/data)
* - User may also mount external directories (e.g., -v /host/stacks:/external-stacks)
* - Compose file says: ./ca.pem:/ca.pem (relative path)
* - docker-compose resolves this to /app/data/stacks/.../ca.pem
* - Docker daemon on HOST receives this path, but /app/data doesn't exist on host!
* - docker-compose resolves this to container path (e.g., /external-stacks/.../ca.pem)
* - Docker daemon on HOST receives this path, but /external-stacks doesn't exist on host!
* - Docker creates a directory instead of mounting the file
*
* Solution:
* - Query Docker API to find the host source path for our /app/data mount
* - Rewrite relative paths in compose files to use the host path
* - Query Docker API to find ALL host source paths for our container mounts
* - Rewrite relative paths in compose files to use the correct host path
* - Works for both internal stacks (DATA_DIR) and adopted stacks (external mounts)
*/
import { readFileSync } from 'node:fs';
@@ -24,6 +26,9 @@ import { resolve } from 'node:path';
let cachedHostDataDir: string | null = null;
let detectionAttempted = false;
// Cache ALL mounts for path translation (not just DATA_DIR)
let cachedMounts: Array<{ source: string; destination: string }> | null = null;
/**
* Get our own container ID
*/
@@ -111,6 +116,13 @@ export async function detectHostDataDir(): Promise<string | null> {
}>;
};
// Cache ALL mounts for later path translation (used by rewriteComposeVolumePaths)
cachedMounts = (containerInfo.Mounts || []).map(m => ({
source: m.Source,
destination: m.Destination
}));
console.log(`[HostPath] Cached ${cachedMounts.length} mount(s)`);
// Find the mount for our DATA_DIR
const dataMount = containerInfo.Mounts?.find(m => m.Destination === dataDir);
@@ -168,6 +180,34 @@ export function translateToHostPath(containerPath: string): string {
return containerPath;
}
/**
* Translate any container path to host path using ALL cached mounts.
* This is more general than translateToHostPath() which only handles DATA_DIR.
*
* @param containerPath - Path inside the container (e.g., /external-stacks/mystack)
* @returns Host path if a matching mount is found, or null if no translation possible
*/
export function translateContainerPathViaMount(containerPath: string): string | null {
if (!cachedMounts || cachedMounts.length === 0) {
return null;
}
// Sort mounts by destination length (longest first) to match most specific mount
const sortedMounts = [...cachedMounts].sort(
(a, b) => b.destination.length - a.destination.length
);
for (const mount of sortedMounts) {
if (containerPath.startsWith(mount.destination + '/') ||
containerPath === mount.destination) {
const relativePath = containerPath.substring(mount.destination.length);
return mount.source + relativePath;
}
}
return null;
}
/**
* Rewrite relative volume paths in a compose file to use absolute host paths.
* This is necessary when Dockhand runs inside Docker with a mounted data volume.
@@ -180,24 +220,17 @@ export function translateToHostPath(containerPath: string): string {
* @returns Modified compose content with absolute host paths, or original if no translation needed
*/
export function rewriteComposeVolumePaths(composeContent: string, workingDir: string): { content: string; modified: boolean; changes: string[] } {
const hostDataDir = getHostDataDir();
const changes: string[] = [];
if (!hostDataDir) {
// Try to translate workingDir to host path using ANY cached mount
// This handles both DATA_DIR mounts and external mounts (e.g., /external-stacks)
const hostWorkingDir = translateContainerPathViaMount(workingDir);
if (!hostWorkingDir) {
// Can't translate - workingDir is not under any known mount
return { content: composeContent, modified: false, changes };
}
const dataDir = resolve(process.env.DATA_DIR || '/app/data');
// Check if workingDir is under DATA_DIR
if (!workingDir.startsWith(dataDir + '/') && workingDir !== dataDir) {
return { content: composeContent, modified: false, changes };
}
// Calculate the host working directory
const relativePath = workingDir.substring(dataDir.length);
const hostWorkingDir = hostDataDir + relativePath;
// Parse compose content line by line to find and rewrite volume mounts
// We look for patterns like:
// - ./something:/container/path