Skip to content

Commit 082a50e

Browse files
authored
Merge pull request #53 from ArchetypicalSoftware/hotfix/certs
This pull request introduces several improvements to how the system handles Docker volume mounts, particularly addressing the issue where Docker may create directories instead of files when mounting non-existent paths. It adds robust validation and correction logic for mount sources, improves error handling and user guidance for certificate and config file issues, and enhances cross-platform compatibility (including WSL2). Additionally, it expands the set of allowed commands in the local settings. Docker Volume Mount Validation and Correction: Added EnsureVolumeMountSource logic to both LocalDockerClient and FallbackDockerEngine to validate and fix mount sources before container creation, preventing Docker from creating directories where files are expected. If a file is expected but a directory exists, it is removed and appropriate errors are thrown if the file is missing. Parent directories are ensured to exist. [1] [2] [3] [4] Certificate and Config File Handling Improvements: Enhanced logic in ReverseProxyClient and UpdateClustersCommand to handle cases where certificate paths are directories instead of files, including fallback to shell commands (with sudo if needed) for removal on systems like WSL2 or Mac. Improved error messages guide the user to resolve permission issues. [1] [2] [3] [4] [5] In DockerHubClient, added logic to ensure the ConfigMounts directory exists, remove incorrectly created directories for config files, and create or copy a default config if missing. User Guidance and Error Messaging: Improved error messages for missing certificate files, including suggestions for copying certificates from .bin/Certs to the project root and clarifying expected file locations. [1] [2] Cross-Platform and WSL2 Support: Updated comments and logic to explicitly mention and support WSL2 environments, not just Mac, for Docker directory issues. [1] [2] Local Settings Update: Expanded the list of allowed commands in .claude/settings.local.json to include grep, dotnet build, dotnet test, and generic test commands, as well as web fetches from github.com.
2 parents f70c168 + 82c2210 commit 082a50e

7 files changed

Lines changed: 482 additions & 63 deletions

File tree

.claude/settings.local.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
"Bash(done)",
1313
"Bash(for node in idp-control-plane idp-worker idp-worker2)",
1414
"Bash(kubectl --context kind-idp get pods:*)",
15-
"Bash(kubectl --context kind-idp delete pod:*)"
15+
"Bash(kubectl --context kind-idp delete pod:*)",
16+
"Bash(grep:*)",
17+
"WebFetch(domain:github.com)",
18+
"Bash(dotnet build:*)",
19+
"Bash(dotnet test:*)",
20+
"Bash(test:*)"
1621
]
1722
}
1823
}

cli/src/Vdk/Commands/UpdateClustersCommand.cs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ private async Task RolloutRestartDeployment(IKubernetesClient client, V1Deployme
420420

421421
/// <summary>
422422
/// Checks if a certificate path exists as a directory instead of a file
423-
/// and removes it. On some systems (especially Mac), Docker may incorrectly
423+
/// and removes it. On some systems (especially Mac and WSL2), Docker may incorrectly
424424
/// create directories when mounting paths that don't exist.
425425
/// </summary>
426426
private void FixCertificatePathIfDirectory(string path, bool verbose)
@@ -436,10 +436,73 @@ private void FixCertificatePathIfDirectory(string path, bool verbose)
436436
_console.WriteLine($"[DEBUG] Successfully removed directory '{path}'");
437437
}
438438
}
439-
catch (Exception ex)
439+
catch (Exception)
440440
{
441-
_console.WriteError($"Failed to remove directory '{path}': {ex.Message}");
441+
// On WSL2/Linux, directories created by Docker may have root ownership.
442+
// Fall back to shell command with elevated permissions.
443+
if (TryRemoveDirectoryWithShell(path, verbose))
444+
{
445+
if (verbose)
446+
{
447+
_console.WriteLine($"[DEBUG] Successfully removed directory '{path}' using shell command");
448+
}
449+
}
450+
else
451+
{
452+
_console.WriteError($"Failed to remove directory '{path}'. Try running: sudo rm -rf \"{path}\"");
453+
}
442454
}
443455
}
444456
}
457+
458+
/// <summary>
459+
/// Attempts to remove a directory using shell commands, which may succeed
460+
/// when .NET Directory.Delete fails due to permission issues.
461+
/// </summary>
462+
private bool TryRemoveDirectoryWithShell(string path, bool verbose)
463+
{
464+
try
465+
{
466+
var isWindows = System.OperatingSystem.IsWindows();
467+
var startInfo = new System.Diagnostics.ProcessStartInfo
468+
{
469+
FileName = isWindows ? "cmd.exe" : "/bin/sh",
470+
Arguments = isWindows
471+
? $"/c rmdir /s /q \"{path}\""
472+
: $"-c \"rm -rf '{path}'\"",
473+
UseShellExecute = false,
474+
RedirectStandardOutput = true,
475+
RedirectStandardError = true,
476+
CreateNoWindow = true
477+
};
478+
479+
using var process = System.Diagnostics.Process.Start(startInfo);
480+
process?.WaitForExit(5000);
481+
482+
// Check if directory was actually removed
483+
if (!_fileSystem.Directory.Exists(path))
484+
{
485+
return true;
486+
}
487+
488+
// If still exists, try with sudo on Linux/Mac
489+
if (!isWindows)
490+
{
491+
if (verbose)
492+
{
493+
_console.WriteLine($"[DEBUG] Attempting sudo rm -rf for '{path}'");
494+
}
495+
startInfo.Arguments = $"-c \"sudo rm -rf '{path}'\"";
496+
using var sudoProcess = System.Diagnostics.Process.Start(startInfo);
497+
sudoProcess?.WaitForExit(10000);
498+
return !_fileSystem.Directory.Exists(path);
499+
}
500+
501+
return false;
502+
}
503+
catch
504+
{
505+
return false;
506+
}
507+
}
445508
}

cli/src/Vdk/Services/DockerHubClient.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,67 @@ public void CreateRegistry()
1515
var configFile = new FileInfo(Path.Combine("ConfigMounts", "zot-config.json"));
1616
var imagesDir = new DirectoryInfo("images");
1717

18+
// Ensure ConfigMounts directory exists
19+
var configMountsDir = configFile.Directory;
20+
if (configMountsDir != null && !configMountsDir.Exists)
21+
{
22+
configMountsDir.Create();
23+
}
24+
25+
// Fix: Check if config file was incorrectly created as a directory by Docker
26+
if (Directory.Exists(configFile.FullName))
27+
{
28+
console.WriteLine($"Config path '{configFile.FullName}' exists as a directory instead of a file. Removing...");
29+
Directory.Delete(configFile.FullName, recursive: true);
30+
}
31+
32+
// Ensure config file exists - try to copy from app directory or create default
33+
if (!configFile.Exists)
34+
{
35+
// Try to find config in application base directory
36+
var appBaseConfig = Path.Combine(AppContext.BaseDirectory, "ConfigMounts", "zot-config.json");
37+
if (File.Exists(appBaseConfig))
38+
{
39+
console.WriteLine($"Copying zot config from {appBaseConfig}");
40+
File.Copy(appBaseConfig, configFile.FullName);
41+
}
42+
else
43+
{
44+
// Create default config
45+
console.WriteLine("Creating default zot-config.json");
46+
var defaultConfig = """
47+
{
48+
"distSpecVersion": "1.1.0",
49+
"storage": {
50+
"rootDirectory": "/var/lib/registry",
51+
"gc": true,
52+
"gcDelay": "1h",
53+
"gcInterval": "24h"
54+
},
55+
"http": {
56+
"address": "0.0.0.0",
57+
"port": "5000"
58+
},
59+
"log": {
60+
"level": "info"
61+
},
62+
"extensions": {
63+
"ui": {
64+
"enable": true
65+
},
66+
"search": {
67+
"enable": true,
68+
"cve": {
69+
"updateInterval": "24h"
70+
}
71+
}
72+
}
73+
}
74+
""";
75+
File.WriteAllText(configFile.FullName, defaultConfig);
76+
}
77+
}
78+
1879
// Ensure images directory exists
1980
if (!imagesDir.Exists)
2081
{

cli/src/Vdk/Services/FallbackDockerEngine.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ internal static bool RunProcess(string fileName, string arguments, out string st
2525

2626
public bool Run(string image, string name, PortMapping[]? ports, Dictionary<string, string>? envs, FileMapping[]? volumes, string[]? commands, string? network = null)
2727
{
28+
// Validate and fix volume mount sources before creating container
29+
// This prevents Docker from creating directories when files are expected
30+
if (volumes != null)
31+
{
32+
foreach (var volume in volumes)
33+
{
34+
EnsureVolumeMountSource(volume.Source, volume.Destination);
35+
}
36+
}
37+
2838
var args = $"run -d --name {name}";
2939
if (network != null)
3040
args += $" --network {network}";
@@ -53,6 +63,54 @@ public bool Run(string image, string name, PortMapping[]? ports, Dictionary<stri
5363
return RunProcess("docker", args, out _, out _);
5464
}
5565

66+
/// <summary>
67+
/// Ensures that a volume mount source path exists correctly.
68+
/// Fixes the common Docker issue where mounting a non-existent file creates a directory instead.
69+
/// </summary>
70+
private void EnsureVolumeMountSource(string sourcePath, string destinationPath)
71+
{
72+
// Determine if destination looks like a file (has extension) or directory
73+
bool isFilePath = Path.HasExtension(destinationPath) && !destinationPath.EndsWith('/') && !destinationPath.EndsWith('\\');
74+
75+
// Check if path was incorrectly created as a directory when it should be a file
76+
if (isFilePath && Directory.Exists(sourcePath))
77+
{
78+
Console.WriteLine($"Mount path '{sourcePath}' exists as a directory instead of a file. Removing...");
79+
try
80+
{
81+
Directory.Delete(sourcePath, recursive: true);
82+
}
83+
catch (Exception ex)
84+
{
85+
throw new InvalidOperationException(
86+
$"Failed to remove directory '{sourcePath}': {ex.Message}",
87+
ex);
88+
}
89+
}
90+
91+
// Ensure parent directory exists
92+
var parentDir = Path.GetDirectoryName(sourcePath);
93+
if (!string.IsNullOrEmpty(parentDir) && !Directory.Exists(parentDir))
94+
{
95+
Directory.CreateDirectory(parentDir);
96+
}
97+
98+
// For file paths, ensure the file exists
99+
if (isFilePath && !File.Exists(sourcePath))
100+
{
101+
throw new FileNotFoundException(
102+
$"Mount source file does not exist: '{sourcePath}'. " +
103+
$"Please ensure the required config file exists before running this command.",
104+
sourcePath);
105+
}
106+
107+
// For directory paths, ensure directory exists
108+
if (!isFilePath && !Directory.Exists(sourcePath))
109+
{
110+
Directory.CreateDirectory(sourcePath);
111+
}
112+
}
113+
56114
public bool Exists(string name, bool checkRunning = true)
57115
{
58116
var filter = checkRunning ? "--filter \"status=running\"" : "";

cli/src/Vdk/Services/LocalDockerClient.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ public LocalDockerClient(Docker.DotNet.IDockerClient dockerClient)
1414

1515
public bool Run(string image, string name, PortMapping[]? ports, Dictionary<string, string>? envs, FileMapping[]? volumes, string[]? commands, string? network = null)
1616
{
17+
// Validate and fix volume mount sources before creating container
18+
// This prevents Docker from creating directories when files are expected
19+
if (volumes != null)
20+
{
21+
foreach (var volume in volumes)
22+
{
23+
EnsureVolumeMountSource(volume.Source, volume.Destination);
24+
}
25+
}
26+
1727
_dockerClient.Images.CreateImageAsync(
1828
new ImagesCreateParameters
1929
{
@@ -160,4 +170,52 @@ public bool CanConnect()
160170
return false;
161171
}
162172
}
173+
174+
/// <summary>
175+
/// Ensures that a volume mount source path exists correctly.
176+
/// Fixes the common Docker issue where mounting a non-existent file creates a directory instead.
177+
/// </summary>
178+
private void EnsureVolumeMountSource(string sourcePath, string destinationPath)
179+
{
180+
// Determine if destination looks like a file (has extension) or directory
181+
bool isFilePath = Path.HasExtension(destinationPath) && !destinationPath.EndsWith('/') && !destinationPath.EndsWith('\\');
182+
183+
// Check if path was incorrectly created as a directory when it should be a file
184+
if (isFilePath && Directory.Exists(sourcePath))
185+
{
186+
Console.WriteLine($"Certificate path '{sourcePath}' exists as a directory instead of a file. Removing...");
187+
try
188+
{
189+
Directory.Delete(sourcePath, recursive: true);
190+
}
191+
catch (Exception ex)
192+
{
193+
throw new InvalidOperationException(
194+
$"Failed to remove directory '{sourcePath}': {ex.Message}",
195+
ex);
196+
}
197+
}
198+
199+
// Ensure parent directory exists
200+
var parentDir = Path.GetDirectoryName(sourcePath);
201+
if (!string.IsNullOrEmpty(parentDir) && !Directory.Exists(parentDir))
202+
{
203+
Directory.CreateDirectory(parentDir);
204+
}
205+
206+
// For file paths, ensure the file exists
207+
if (isFilePath && !File.Exists(sourcePath))
208+
{
209+
throw new FileNotFoundException(
210+
$"Mount source file does not exist: '{sourcePath}'. " +
211+
$"Please ensure the required config file exists before running this command.",
212+
sourcePath);
213+
}
214+
215+
// For directory paths, ensure directory exists
216+
if (!isFilePath && !Directory.Exists(sourcePath))
217+
{
218+
Directory.CreateDirectory(sourcePath);
219+
}
220+
}
163221
}

0 commit comments

Comments
 (0)