From 3596284ba266e083594dd4c894af4a618ba2b551 Mon Sep 17 00:00:00 2001 From: Eddie Wassef Date: Fri, 30 Jan 2026 15:49:48 -0600 Subject: [PATCH 1/2] Improve init script, certs & directory cleanup Updates multiple components to improve installer behavior and handle Docker/WSL2 filesystem edge cases. - .claude/settings.local.json: add more command patterns (grep, WebFetch, dotnet build/test, generic test). - cli/src/Vdk/Commands/UpdateClustersCommand.cs: enhance FixCertificatePathIfDirectory to handle Mac and WSL2-created directories, add TryRemoveDirectoryWithShell fallback that invokes platform shell (and sudo on non-Windows) and improve debug/error messages. - cli/src/Vdk/Services/ReverseProxyClient.cs: add user guidance about where Certs should be located, similar WSL2/Mac handling with TryRemoveDirectoryWithShell, and extra error hints when TLS secrets can't be created. - init.sh: major rewrite to support Linux/macOS detection, ensure docker group/permissions, download appropriate release asset per OS/arch, extract and move vega binary, copy Certs and ConfigMounts into project root (and remove incorrectly created cert directories), update PATH for .bin, and add platform-specific checks/warnings (architecture, Swift runtime, Docker running). Overall the changes aim to reduce failures caused by Docker creating root-owned directories for mounted cert files, provide clearer installer feedback, and make the init process more robust across Linux and macOS. --- .claude/settings.local.json | 7 +- cli/src/Vdk/Commands/UpdateClustersCommand.cs | 69 +++++- cli/src/Vdk/Services/ReverseProxyClient.cs | 63 ++++- init.sh | 229 +++++++++++++----- 4 files changed, 305 insertions(+), 63 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 78beb71..d16f396 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,12 @@ "Bash(done)", "Bash(for node in idp-control-plane idp-worker idp-worker2)", "Bash(kubectl --context kind-idp get pods:*)", - "Bash(kubectl --context kind-idp delete pod:*)" + "Bash(kubectl --context kind-idp delete pod:*)", + "Bash(grep:*)", + "WebFetch(domain:github.com)", + "Bash(dotnet build:*)", + "Bash(dotnet test:*)", + "Bash(test:*)" ] } } diff --git a/cli/src/Vdk/Commands/UpdateClustersCommand.cs b/cli/src/Vdk/Commands/UpdateClustersCommand.cs index 4a7ec9d..9b2225a 100644 --- a/cli/src/Vdk/Commands/UpdateClustersCommand.cs +++ b/cli/src/Vdk/Commands/UpdateClustersCommand.cs @@ -420,7 +420,7 @@ private async Task RolloutRestartDeployment(IKubernetesClient client, V1Deployme /// /// Checks if a certificate path exists as a directory instead of a file - /// and removes it. On some systems (especially Mac), Docker may incorrectly + /// and removes it. On some systems (especially Mac and WSL2), Docker may incorrectly /// create directories when mounting paths that don't exist. /// private void FixCertificatePathIfDirectory(string path, bool verbose) @@ -436,10 +436,73 @@ private void FixCertificatePathIfDirectory(string path, bool verbose) _console.WriteLine($"[DEBUG] Successfully removed directory '{path}'"); } } - catch (Exception ex) + catch (Exception) { - _console.WriteError($"Failed to remove directory '{path}': {ex.Message}"); + // On WSL2/Linux, directories created by Docker may have root ownership. + // Fall back to shell command with elevated permissions. + if (TryRemoveDirectoryWithShell(path, verbose)) + { + if (verbose) + { + _console.WriteLine($"[DEBUG] Successfully removed directory '{path}' using shell command"); + } + } + else + { + _console.WriteError($"Failed to remove directory '{path}'. Try running: sudo rm -rf \"{path}\""); + } } } } + + /// + /// Attempts to remove a directory using shell commands, which may succeed + /// when .NET Directory.Delete fails due to permission issues. + /// + private bool TryRemoveDirectoryWithShell(string path, bool verbose) + { + try + { + var isWindows = System.OperatingSystem.IsWindows(); + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = isWindows ? "cmd.exe" : "/bin/sh", + Arguments = isWindows + ? $"/c rmdir /s /q \"{path}\"" + : $"-c \"rm -rf '{path}'\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = System.Diagnostics.Process.Start(startInfo); + process?.WaitForExit(5000); + + // Check if directory was actually removed + if (!_fileSystem.Directory.Exists(path)) + { + return true; + } + + // If still exists, try with sudo on Linux/Mac + if (!isWindows) + { + if (verbose) + { + _console.WriteLine($"[DEBUG] Attempting sudo rm -rf for '{path}'"); + } + startInfo.Arguments = $"-c \"sudo rm -rf '{path}'\""; + using var sudoProcess = System.Diagnostics.Process.Start(startInfo); + sudoProcess?.WaitForExit(10000); + return !_fileSystem.Directory.Exists(path); + } + + return false; + } + catch + { + return false; + } + } } diff --git a/cli/src/Vdk/Services/ReverseProxyClient.cs b/cli/src/Vdk/Services/ReverseProxyClient.cs index fb3431c..563c3cd 100644 --- a/cli/src/Vdk/Services/ReverseProxyClient.cs +++ b/cli/src/Vdk/Services/ReverseProxyClient.cs @@ -85,6 +85,8 @@ public void Create() !ValidateAndFixCertificatePath(privKey.FullName)) { _console.WriteError("Certificate files are missing. Please ensure Certs/fullchain.pem and Certs/privkey.pem exist."); + _console.WriteError("If using the init.sh installer, certificates should be in the Certs/ folder relative to where you run vega."); + _console.WriteError("You may need to copy them from .bin/ to your project root: cp -r .bin/Certs ./Certs"); return; } @@ -109,7 +111,7 @@ public void Create() /// /// Validates a certificate path exists as a file, not a directory. - /// On some systems (especially Mac), Docker may incorrectly create directories + /// On some systems (especially Mac and WSL2), Docker may incorrectly create directories /// when mounting paths that don't exist. This method detects and removes such directories. /// private bool ValidateAndFixCertificatePath(string path) @@ -122,10 +124,15 @@ private bool ValidateAndFixCertificatePath(string path) { Directory.Delete(path, recursive: true); } - catch (Exception ex) + catch (Exception) { - _console.WriteError($"Failed to remove directory '{path}': {ex.Message}"); - return false; + // On WSL2/Linux, directories created by Docker may have root ownership. + // Fall back to shell command with elevated permissions. + if (!TryRemoveDirectoryWithShell(path)) + { + _console.WriteError($"Failed to remove directory '{path}'. Try running: sudo rm -rf \"{path}\""); + return false; + } } } @@ -133,6 +140,53 @@ private bool ValidateAndFixCertificatePath(string path) return File.Exists(path); } + /// + /// Attempts to remove a directory using shell commands, which may succeed + /// when .NET Directory.Delete fails due to permission issues. + /// + private bool TryRemoveDirectoryWithShell(string path) + { + try + { + var isWindows = System.OperatingSystem.IsWindows(); + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = isWindows ? "cmd.exe" : "/bin/sh", + Arguments = isWindows + ? $"/c rmdir /s /q \"{path}\"" + : $"-c \"rm -rf '{path}'\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = System.Diagnostics.Process.Start(startInfo); + process?.WaitForExit(5000); + + // Check if directory was actually removed + if (!Directory.Exists(path)) + { + return true; + } + + // If still exists, try with sudo on Linux/Mac + if (!isWindows) + { + startInfo.Arguments = $"-c \"sudo rm -rf '{path}'\""; + using var sudoProcess = System.Diagnostics.Process.Start(startInfo); + sudoProcess?.WaitForExit(10000); + return !Directory.Exists(path); + } + + return false; + } + catch + { + return false; + } + } + public bool Exists() { try @@ -365,6 +419,7 @@ private bool CreateTlsSecret(string clusterName) !ValidateAndFixCertificatePath(privKeyPath)) { _console.WriteError("Certificate files are missing. Cannot create TLS secret."); + _console.WriteError("Ensure Certs/fullchain.pem and Certs/privkey.pem exist in your project directory."); return true; } diff --git a/init.sh b/init.sh index 0a716fd..de3c357 100755 --- a/init.sh +++ b/init.sh @@ -1,61 +1,180 @@ -# /bin/bash -if groups $USER | grep -q "\bdocker\b"; then - echo "Welcome to Vega VDK" - # Variables - REPO="ArchetypicalSoftware/VDK" # Replace with your GitHub repository (e.g., username/repo) - TOKEN=$GITHUB_VDK_TOKEN # Replace with your GitHub Personal Access Token - ASSET_NAME="vega-linux-x64.tar.gz" # Replace with the asset name you're looking for - DOWNLOAD_DIR="./.bin" # Specify the download directory - - # Get the latest release information - echo "Fetching the latest release information..." - LATEST_RELEASE=$(curl -s -H "Authorization: token $TOKEN" "https://api.github.com/repos/$REPO/releases/latest") - VERSION=$(echo "$LATEST_RELEASE" | jq -r ".tag_name") - CURRENT=$(cat ./.bin/vdk.version) - if [ "$VERSION" != "$CURRENT" ]; then - # Extract the asset ID for the desired asset - ASSET_ID=$(echo "$LATEST_RELEASE" | jq -r ".assets[] | select(.name == \"$ASSET_NAME\") | .id") - - if [ -z "$ASSET_ID" ]; then - echo "Error: Asset \"$ASSET_NAME\" not found in the latest release." - exit 1 +#!/bin/bash +# Docker permissions hardening for Linux and macOS +OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]') + +if [ "$OS_TYPE" = "linux" ]; then + if ! getent group docker > /dev/null 2>&1; then + echo "[INFO] Creating 'docker' group (requires sudo)" + sudo groupadd docker + fi + if ! groups $USER | grep -q '\bdocker\b'; then + echo "[INFO] Adding user $USER to 'docker' group (requires sudo)" + sudo usermod -aG docker "$USER" + echo "[INFO] Please exit and restart your shell to pick up group membership changes." + exit 0 + fi + if ! command -v docker >/dev/null 2>&1; then + echo "[ERROR] Docker CLI is not installed. Please install Docker before proceeding." + exit 1 + fi + echo "[SUCCESS] Docker permissions are set up for $USER." +elif [ "$OS_TYPE" = "darwin" ]; then + if ! command -v docker >/dev/null 2>&1; then + echo "[ERROR] Docker CLI is not installed. Please install Docker Desktop for Mac." + exit 1 + fi + # Check if Docker Desktop is running + if ! docker info >/dev/null 2>&1; then + echo "[ERROR] Docker Desktop does not appear to be running. Please start Docker Desktop." + exit 1 + fi + echo "[SUCCESS] Docker is available on macOS." +else + echo "[ERROR] Unsupported OS: $OS_TYPE. This script supports only Linux and macOS." + exit 1 +fi + +echo "Welcome to Vega VDK" +# Variables +REPO="ArchetypicalSoftware/VDK" +DOWNLOAD_DIR="./.bin" + +UNAME_OS=$(uname -s | tr '[:upper:]' '[:lower:]') +UNAME_ARCH=$(uname -m) +case "$UNAME_OS" in + linux) + RID_OS="linux" ;; + darwin) + RID_OS="osx" ;; + *) + echo "Unsupported OS: $UNAME_OS" + exit 1 ;; +esac + +case "$UNAME_ARCH" in + x86_64|amd64) + RID_ARCH="x64" ;; + arm64|aarch64) + RID_ARCH="arm64" ;; + *) + echo "Unsupported architecture: $UNAME_ARCH" + exit 1 ;; +esac + +RID="$RID_OS-$RID_ARCH" +ASSET_NAME="vega-$RID.tar.gz" + +echo "Fetching the latest release information..." +LATEST_RELEASE=$(curl -s "https://api.github.com/repos/$REPO/releases/latest") +VERSION=$(echo "$LATEST_RELEASE" | jq -r ".tag_name") +CURRENT=$(cat ./.bin/vdk.version 2>/dev/null) +if [ "$VERSION" != "$CURRENT" ]; then + ASSET_URL=$(echo "$LATEST_RELEASE" | jq -r ".assets[] | select(.name == \"$ASSET_NAME\") | .browser_download_url") + + if [ -z "$ASSET_URL" ] || [ "$ASSET_URL" == "null" ]; then + echo "Error: Asset for OS '$OS' and arch '$ARCH' (expected name: $ASSET_NAME) not found in the latest release." + exit 1 + fi + + mkdir -p "$DOWNLOAD_DIR" + + echo "Downloading asset \"$ASSET_NAME\"..." + curl -L "$ASSET_URL" -o "$DOWNLOAD_DIR/$ASSET_NAME" + + echo "Download complete! File saved to \"$DOWNLOAD_DIR/$ASSET_NAME\"" + echo "Extracting Vega CLI..." + tar --overwrite -xvf "$DOWNLOAD_DIR/$ASSET_NAME" -C "$DOWNLOAD_DIR" + + # Move vega binary to .bin root + FOUND_VEGA=$(find "$DOWNLOAD_DIR" -type f -name vega | head -n 1) + if [ -n "$FOUND_VEGA" ]; then + if [ "$FOUND_VEGA" != "./.bin/vega" ]; then + mv -f "$FOUND_VEGA" ./.bin/vega + echo "[INFO] Vega binary moved to ./.bin/vega" + else + echo "[INFO] Vega binary already in ./.bin/vega" fi + else + echo "[WARNING] Vega binary not found after extraction." + fi - # Create the download directory if it doesn't exist - mkdir -p "$DOWNLOAD_DIR" - - # Download the asset - echo "Downloading asset \"$ASSET_NAME\"..." - curl -L -H "Authorization: token $TOKEN" \ - -H "Accept: application/octet-stream" \ - "https://api.github.com/repos/$REPO/releases/assets/$ASSET_ID" \ - -o "$DOWNLOAD_DIR/$ASSET_NAME" - - echo "Download complete! File saved to \"$DOWNLOAD_DIR/$ASSET_NAME\"" - echo "Extracting Vega CLI..." - tar --overwrite -xvf "$DOWNLOAD_DIR/$ASSET_NAME" -C "$DOWNLOAD_DIR" - mv -f "$DOWNLOAD_DIR/packages/build/linux-x64/vega" ./.bin/vega - rm -rf "$DOWNLOAD_DIR/packages" - rm "$DOWNLOAD_DIR/$ASSET_NAME" - echo "$VERSION" > "$DOWNLOAD_DIR/vdk.version" - echo "Version: $VERSION" + # Copy Certs folder to project root (required for TLS) + # This prevents Docker from creating directories when mounting non-existent cert files + FOUND_CERTS=$(find "$DOWNLOAD_DIR" -type d -name Certs | head -n 1) + if [ -n "$FOUND_CERTS" ] && [ -d "$FOUND_CERTS" ]; then + # Remove any incorrectly created directories from previous Docker runs + if [ -d "./Certs/fullchain.pem" ]; then + echo "[INFO] Removing incorrectly created directory ./Certs/fullchain.pem" + rm -rf "./Certs/fullchain.pem" 2>/dev/null || sudo rm -rf "./Certs/fullchain.pem" + fi + if [ -d "./Certs/privkey.pem" ]; then + echo "[INFO] Removing incorrectly created directory ./Certs/privkey.pem" + rm -rf "./Certs/privkey.pem" 2>/dev/null || sudo rm -rf "./Certs/privkey.pem" + fi + mkdir -p ./Certs + cp -f "$FOUND_CERTS"/* ./Certs/ 2>/dev/null + echo "[INFO] Certificates copied to ./Certs/" else - echo "Version: $CURRENT" - fi - cd ./.bin - BIN_PATH=$(pwd) - cd .. - # echo "$PATH" | grep -q $BIN_PATH - # if [ $? -ne 0 ]; then - # echo "Updating Path" - # echo >> ~/.bashrc && echo "export PATH='$PATH:$BIN_PATH'" >> ~/.bashrc && source ~/.bashrc - # fi + echo "[WARNING] Certs folder not found in extraction. TLS features may not work." + fi + + # Copy ConfigMounts folder to project root (required for container registry) + FOUND_CONFIGMOUNTS=$(find "$DOWNLOAD_DIR" -type d -name ConfigMounts | head -n 1) + if [ -n "$FOUND_CONFIGMOUNTS" ] && [ -d "$FOUND_CONFIGMOUNTS" ]; then + mkdir -p ./ConfigMounts + cp -f "$FOUND_CONFIGMOUNTS"/* ./ConfigMounts/ 2>/dev/null + echo "[INFO] ConfigMounts copied to ./ConfigMounts/" + fi + + rm "$DOWNLOAD_DIR/$ASSET_NAME" + echo "$VERSION" > "$DOWNLOAD_DIR/vdk.version" + echo "Version: $VERSION" else - echo "Adding user $USER to docker group" - echo " (This will require sudo access)" - sudo usermod -aG docker "$USER" - echo "Please exit and restart your shell to pick up group membership changes." + echo "Version: $CURRENT" +fi + +cd ./.bin +BIN_PATH=$(pwd) +cd .. +echo "$PATH" | grep -q $BIN_PATH +if [ $? -ne 0 ]; then + echo "[INFO] Adding $BIN_PATH to PATH for this session." + export PATH="$PATH:$BIN_PATH" fi -# sudo gpasswd -d username groupname -# sudo gpasswd -d $USER docker \ No newline at end of file +if command -v vega >/dev/null 2>&1; then + echo "[INFO] Vega CLI version output:" + VEGA_OUT="$(vega --version 2>&1)" + VEGA_EXIT=$? + echo "$VEGA_OUT" + if [ "$OS_TYPE" = "darwin" ]; then + MAC_ARCH=$(uname -m) + BIN_ARCH=$(file .bin/vega | grep -oE 'arm64|x86_64') + if [ "$MAC_ARCH" = "arm64" ] && [ "$BIN_ARCH" = "x86_64" ]; then + echo "[WARNING] You are running an x64 binary on Apple Silicon. Try installing Rosetta: sudo softwareupdate --install-rosetta, or use the osx-arm64 build." + elif [ "$MAC_ARCH" = "x86_64" ] && [ "$BIN_ARCH" = "arm64" ]; then + echo "[ERROR] You are running an ARM64 binary on an Intel Mac. Use the osx-x64 build." + fi + + MISSING_SWIFT=0 + for LIB in libswiftCore.dylib libswiftFoundation.dylib; do + if ! otool -L .bin/vega | grep -q "$LIB"; then + MISSING_SWIFT=1 + fi + done + if [ $MISSING_SWIFT -eq 1 ]; then + echo "[WARNING] Vega CLI may require the Swift runtime. Install Xcode or Xcode Command Line Tools: xcode-select --install" + fi + + if echo "$VEGA_OUT" | grep -qi 'killed' || [ $VEGA_EXIT -ne 0 ]; then + echo "[ERROR] Vega CLI failed to run. Suggestions:" + echo "- Ensure you are using the correct binary for your architecture." + echo "- Try installing Rosetta (for x64 on Apple Silicon): sudo softwareupdate --install-rosetta" + echo "- Install Xcode or Xcode Command Line Tools for Swift runtime." + echo "- Check for missing libraries: otool -L .bin/vega" + echo "- For more details, check system logs: log show --predicate 'process == \"vega\"' --info --last 1h" + fi + fi +else + echo "[WARNING] Vega CLI not found in PATH or not executable." +fi From 82c2210ee03ba9a6dd243fa3037a14bd9ed86f28 Mon Sep 17 00:00:00 2001 From: Eddie Wassef Date: Sat, 31 Jan 2026 16:28:34 -0600 Subject: [PATCH 2/2] Validate volume mounts and create zot config Add pre-run validation for bind mounts and ensure proper zot config handling. - Introduce EnsureVolumeMountSource in FallbackDockerEngine and LocalDockerClient and call it before creating containers to prevent Docker from creating directories when files are expected. The helper ensures parent directories exist, removes incorrectly-created directories for file mounts, creates missing directories, and throws a FileNotFoundException if a required file mount is missing. - In DockerHubClient, ensure ConfigMounts directory exists, remove an incorrectly-created directory at the config path, and attempt to copy a zot-config.json from the app base directory or create a default zot-config.json if none is found. Console messages were added for visibility. --- cli/src/Vdk/Services/DockerHubClient.cs | 61 ++++++++++++++++++++ cli/src/Vdk/Services/FallbackDockerEngine.cs | 58 +++++++++++++++++++ cli/src/Vdk/Services/LocalDockerClient.cs | 58 +++++++++++++++++++ 3 files changed, 177 insertions(+) diff --git a/cli/src/Vdk/Services/DockerHubClient.cs b/cli/src/Vdk/Services/DockerHubClient.cs index d7703ee..f3966db 100644 --- a/cli/src/Vdk/Services/DockerHubClient.cs +++ b/cli/src/Vdk/Services/DockerHubClient.cs @@ -15,6 +15,67 @@ public void CreateRegistry() var configFile = new FileInfo(Path.Combine("ConfigMounts", "zot-config.json")); var imagesDir = new DirectoryInfo("images"); + // Ensure ConfigMounts directory exists + var configMountsDir = configFile.Directory; + if (configMountsDir != null && !configMountsDir.Exists) + { + configMountsDir.Create(); + } + + // Fix: Check if config file was incorrectly created as a directory by Docker + if (Directory.Exists(configFile.FullName)) + { + console.WriteLine($"Config path '{configFile.FullName}' exists as a directory instead of a file. Removing..."); + Directory.Delete(configFile.FullName, recursive: true); + } + + // Ensure config file exists - try to copy from app directory or create default + if (!configFile.Exists) + { + // Try to find config in application base directory + var appBaseConfig = Path.Combine(AppContext.BaseDirectory, "ConfigMounts", "zot-config.json"); + if (File.Exists(appBaseConfig)) + { + console.WriteLine($"Copying zot config from {appBaseConfig}"); + File.Copy(appBaseConfig, configFile.FullName); + } + else + { + // Create default config + console.WriteLine("Creating default zot-config.json"); + var defaultConfig = """ + { + "distSpecVersion": "1.1.0", + "storage": { + "rootDirectory": "/var/lib/registry", + "gc": true, + "gcDelay": "1h", + "gcInterval": "24h" + }, + "http": { + "address": "0.0.0.0", + "port": "5000" + }, + "log": { + "level": "info" + }, + "extensions": { + "ui": { + "enable": true + }, + "search": { + "enable": true, + "cve": { + "updateInterval": "24h" + } + } + } + } + """; + File.WriteAllText(configFile.FullName, defaultConfig); + } + } + // Ensure images directory exists if (!imagesDir.Exists) { diff --git a/cli/src/Vdk/Services/FallbackDockerEngine.cs b/cli/src/Vdk/Services/FallbackDockerEngine.cs index a93c7fa..c4fd7c3 100644 --- a/cli/src/Vdk/Services/FallbackDockerEngine.cs +++ b/cli/src/Vdk/Services/FallbackDockerEngine.cs @@ -25,6 +25,16 @@ internal static bool RunProcess(string fileName, string arguments, out string st public bool Run(string image, string name, PortMapping[]? ports, Dictionary? envs, FileMapping[]? volumes, string[]? commands, string? network = null) { + // Validate and fix volume mount sources before creating container + // This prevents Docker from creating directories when files are expected + if (volumes != null) + { + foreach (var volume in volumes) + { + EnsureVolumeMountSource(volume.Source, volume.Destination); + } + } + var args = $"run -d --name {name}"; if (network != null) args += $" --network {network}"; @@ -53,6 +63,54 @@ public bool Run(string image, string name, PortMapping[]? ports, Dictionary + /// Ensures that a volume mount source path exists correctly. + /// Fixes the common Docker issue where mounting a non-existent file creates a directory instead. + /// + private void EnsureVolumeMountSource(string sourcePath, string destinationPath) + { + // Determine if destination looks like a file (has extension) or directory + bool isFilePath = Path.HasExtension(destinationPath) && !destinationPath.EndsWith('/') && !destinationPath.EndsWith('\\'); + + // Check if path was incorrectly created as a directory when it should be a file + if (isFilePath && Directory.Exists(sourcePath)) + { + Console.WriteLine($"Mount path '{sourcePath}' exists as a directory instead of a file. Removing..."); + try + { + Directory.Delete(sourcePath, recursive: true); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to remove directory '{sourcePath}': {ex.Message}", + ex); + } + } + + // Ensure parent directory exists + var parentDir = Path.GetDirectoryName(sourcePath); + if (!string.IsNullOrEmpty(parentDir) && !Directory.Exists(parentDir)) + { + Directory.CreateDirectory(parentDir); + } + + // For file paths, ensure the file exists + if (isFilePath && !File.Exists(sourcePath)) + { + throw new FileNotFoundException( + $"Mount source file does not exist: '{sourcePath}'. " + + $"Please ensure the required config file exists before running this command.", + sourcePath); + } + + // For directory paths, ensure directory exists + if (!isFilePath && !Directory.Exists(sourcePath)) + { + Directory.CreateDirectory(sourcePath); + } + } + public bool Exists(string name, bool checkRunning = true) { var filter = checkRunning ? "--filter \"status=running\"" : ""; diff --git a/cli/src/Vdk/Services/LocalDockerClient.cs b/cli/src/Vdk/Services/LocalDockerClient.cs index 48a92c5..da31a35 100644 --- a/cli/src/Vdk/Services/LocalDockerClient.cs +++ b/cli/src/Vdk/Services/LocalDockerClient.cs @@ -14,6 +14,16 @@ public LocalDockerClient(Docker.DotNet.IDockerClient dockerClient) public bool Run(string image, string name, PortMapping[]? ports, Dictionary? envs, FileMapping[]? volumes, string[]? commands, string? network = null) { + // Validate and fix volume mount sources before creating container + // This prevents Docker from creating directories when files are expected + if (volumes != null) + { + foreach (var volume in volumes) + { + EnsureVolumeMountSource(volume.Source, volume.Destination); + } + } + _dockerClient.Images.CreateImageAsync( new ImagesCreateParameters { @@ -160,4 +170,52 @@ public bool CanConnect() return false; } } + + /// + /// Ensures that a volume mount source path exists correctly. + /// Fixes the common Docker issue where mounting a non-existent file creates a directory instead. + /// + private void EnsureVolumeMountSource(string sourcePath, string destinationPath) + { + // Determine if destination looks like a file (has extension) or directory + bool isFilePath = Path.HasExtension(destinationPath) && !destinationPath.EndsWith('/') && !destinationPath.EndsWith('\\'); + + // Check if path was incorrectly created as a directory when it should be a file + if (isFilePath && Directory.Exists(sourcePath)) + { + Console.WriteLine($"Certificate path '{sourcePath}' exists as a directory instead of a file. Removing..."); + try + { + Directory.Delete(sourcePath, recursive: true); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to remove directory '{sourcePath}': {ex.Message}", + ex); + } + } + + // Ensure parent directory exists + var parentDir = Path.GetDirectoryName(sourcePath); + if (!string.IsNullOrEmpty(parentDir) && !Directory.Exists(parentDir)) + { + Directory.CreateDirectory(parentDir); + } + + // For file paths, ensure the file exists + if (isFilePath && !File.Exists(sourcePath)) + { + throw new FileNotFoundException( + $"Mount source file does not exist: '{sourcePath}'. " + + $"Please ensure the required config file exists before running this command.", + sourcePath); + } + + // For directory paths, ensure directory exists + if (!isFilePath && !Directory.Exists(sourcePath)) + { + Directory.CreateDirectory(sourcePath); + } + } } \ No newline at end of file