From 86bbda53446bfda1b1cc063d4afe6d9b397e1ff6 Mon Sep 17 00:00:00 2001 From: Eddie Wassef Date: Wed, 28 Jan 2026 15:16:55 -0600 Subject: [PATCH] Handle cert dir mounts; add Docker restart Detect and remove incorrectly-created certificate directories (common on Mac when Docker mounts missing paths) and validate certificate files before use. Add Validate/Fix helpers in UpdateClustersCommand and ReverseProxyClient, update reverse proxy creation to restart a stopped proxy or create it with proper file mappings, and restart the proxy when configs change. Expose Restart on IDockerEngine and implement it in FallbackDockerEngine and LocalDockerClient. Also switch Docker container lookups to use name filters/NAMES instead of the vega-component label, fix async removal/start calls, adjust CoreDNS rewrite insertion to before the kubernetes block, and update tests. Minor change: update registry image reference to ghcr.io/project-zot/zot:v2.1.0. --- cli/src/Vdk/Commands/UpdateClustersCommand.cs | 30 ++++ cli/src/Vdk/Constants/Containers.cs | 2 +- cli/src/Vdk/Services/FallbackDockerEngine.cs | 5 + cli/src/Vdk/Services/IDockerEngine.cs | 2 + cli/src/Vdk/Services/LocalDockerClient.cs | 38 +++-- cli/src/Vdk/Services/ReverseProxyClient.cs | 157 +++++++++++------- .../Vdk.Tests/ReverseProxyClientTests.cs | 21 ++- 7 files changed, 178 insertions(+), 77 deletions(-) diff --git a/cli/src/Vdk/Commands/UpdateClustersCommand.cs b/cli/src/Vdk/Commands/UpdateClustersCommand.cs index c9a10af..4a7ec9d 100644 --- a/cli/src/Vdk/Commands/UpdateClustersCommand.cs +++ b/cli/src/Vdk/Commands/UpdateClustersCommand.cs @@ -42,6 +42,11 @@ public async Task InvokeAsync(bool verbose = false) var fullChainPath = _fileSystem.Path.Combine("Certs", "fullchain.pem"); var privKeyPath = _fileSystem.Path.Combine("Certs", "privkey.pem"); + // Check for and fix certificate paths that were incorrectly created as directories + // This can happen on Mac when Docker creates directories for missing mount paths + FixCertificatePathIfDirectory(fullChainPath, verbose); + FixCertificatePathIfDirectory(privKeyPath, verbose); + if (!_fileSystem.File.Exists(fullChainPath) || !_fileSystem.File.Exists(privKeyPath)) { _console.WriteError("Certificate files not found. Expected: Certs/fullchain.pem and Certs/privkey.pem"); @@ -412,4 +417,29 @@ private async Task RolloutRestartDeployment(IKubernetesClient client, V1Deployme await Task.CompletedTask; } + + /// + /// Checks if a certificate path exists as a directory instead of a file + /// and removes it. On some systems (especially Mac), Docker may incorrectly + /// create directories when mounting paths that don't exist. + /// + private void FixCertificatePathIfDirectory(string path, bool verbose) + { + if (_fileSystem.Directory.Exists(path)) + { + _console.WriteWarning($"Certificate path '{path}' exists as a directory instead of a file. Removing..."); + try + { + _fileSystem.Directory.Delete(path, recursive: true); + if (verbose) + { + _console.WriteLine($"[DEBUG] Successfully removed directory '{path}'"); + } + } + catch (Exception ex) + { + _console.WriteError($"Failed to remove directory '{path}': {ex.Message}"); + } + } + } } diff --git a/cli/src/Vdk/Constants/Containers.cs b/cli/src/Vdk/Constants/Containers.cs index c75b251..aec704d 100644 --- a/cli/src/Vdk/Constants/Containers.cs +++ b/cli/src/Vdk/Constants/Containers.cs @@ -3,7 +3,7 @@ namespace Vdk.Constants; public static class Containers { public const string RegistryName = "vega-registry"; - public const string RegistryImage = "ghcr.io/project-zot/zot-linux-amd64:v2.1.0"; + public const string RegistryImage = "ghcr.io/project-zot/zot:v2.1.0"; public const int RegistryContainerPort = 5000; public const int RegistryHostPort = 5000; public const string ProxyName = "vega-proxy"; diff --git a/cli/src/Vdk/Services/FallbackDockerEngine.cs b/cli/src/Vdk/Services/FallbackDockerEngine.cs index f9795a5..a93c7fa 100644 --- a/cli/src/Vdk/Services/FallbackDockerEngine.cs +++ b/cli/src/Vdk/Services/FallbackDockerEngine.cs @@ -82,6 +82,11 @@ public bool Exec(string name, string[] commands) return RunProcess("docker", args, out _, out _); } + public bool Restart(string name) + { + return RunProcess("docker", $"restart {name}", out _, out _); + } + public bool CanConnect() { var args = $"ps"; diff --git a/cli/src/Vdk/Services/IDockerEngine.cs b/cli/src/Vdk/Services/IDockerEngine.cs index f2c90c6..b449dc0 100644 --- a/cli/src/Vdk/Services/IDockerEngine.cs +++ b/cli/src/Vdk/Services/IDockerEngine.cs @@ -14,5 +14,7 @@ public interface IDockerEngine bool Exec(string name, string[] commands); + bool Restart(string name); + bool CanConnect(); } \ No newline at end of file diff --git a/cli/src/Vdk/Services/LocalDockerClient.cs b/cli/src/Vdk/Services/LocalDockerClient.cs index 695264f..48a92c5 100644 --- a/cli/src/Vdk/Services/LocalDockerClient.cs +++ b/cli/src/Vdk/Services/LocalDockerClient.cs @@ -81,14 +81,14 @@ public bool Exists(string name, bool checkRunning = true) IList containers = _dockerClient.Containers.ListContainersAsync( new ContainersListParameters() { - Filters = new Dictionary> { { "label", new Dictionary { { "vega-component", true } } } }, - Limit = 10, + Filters = new Dictionary> { { "name", new Dictionary { { name, true } } } }, + All = true, }).GetAwaiter().GetResult(); - var container = containers.FirstOrDefault(x => x.Labels["vega-component"].Contains(name)); - + var container = containers.FirstOrDefault(x => x.Names.Any(n => n.TrimStart('/') == name)); + if (container == null) return false; if (checkRunning == false) return true; - + return container.State == "running" || // this should be started, lets do it _dockerClient.Containers.StartContainerAsync(container.ID, new ContainerStartParameters() { }).GetAwaiter().GetResult(); @@ -99,13 +99,13 @@ public bool Delete(string name) IList containers = _dockerClient.Containers.ListContainersAsync( new ContainersListParameters() { - Filters = new Dictionary> { { "label", new Dictionary { { "vega-component", true } } } }, - Limit = 10, + Filters = new Dictionary> { { "name", new Dictionary { { name, true } } } }, + All = true, }).GetAwaiter().GetResult(); - var container = containers.FirstOrDefault(x => x.Labels["vega-component"].Contains(name)); + var container = containers.FirstOrDefault(x => x.Names.Any(n => n.TrimStart('/') == name)); if (container != null) { - _dockerClient.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters(){Force = true}); + _dockerClient.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters(){Force = true}).GetAwaiter().GetResult(); } return true; } @@ -117,13 +117,11 @@ public bool Stop(string name) public bool Exec(string name, string[] commands) { - // Get container id from name and labels var container = _dockerClient.Containers.ListContainersAsync( new ContainersListParameters() { - Filters = new Dictionary> { { "label", new Dictionary { { "vega-component", true } } } }, - Limit = 10, - }).GetAwaiter().GetResult().FirstOrDefault(x => x.Labels["vega-component"].Contains(name)); + Filters = new Dictionary> { { "name", new Dictionary { { name, true } } } }, + }).GetAwaiter().GetResult().FirstOrDefault(x => x.Names.Any(n => n.TrimStart('/') == name)); if (container == null) return false; _dockerClient.Exec.ExecCreateContainerAsync(container.ID, new ContainerExecCreateParameters @@ -135,6 +133,20 @@ public bool Exec(string name, string[] commands) return true; } + public bool Restart(string name) + { + var container = _dockerClient.Containers.ListContainersAsync( + new ContainersListParameters() + { + Filters = new Dictionary> { { "name", new Dictionary { { name, true } } } }, + All = true, + }).GetAwaiter().GetResult().FirstOrDefault(x => x.Names.Any(n => n.TrimStart('/') == name)); + + if (container == null) return false; + _dockerClient.Containers.RestartContainerAsync(container.ID, new ContainerRestartParameters()).GetAwaiter().GetResult(); + return true; + } + public bool CanConnect() { try diff --git a/cli/src/Vdk/Services/ReverseProxyClient.cs b/cli/src/Vdk/Services/ReverseProxyClient.cs index 22d7629..fb3431c 100644 --- a/cli/src/Vdk/Services/ReverseProxyClient.cs +++ b/cli/src/Vdk/Services/ReverseProxyClient.cs @@ -34,60 +34,103 @@ public ReverseProxyClient(IDockerEngine docker, Func public void Create() { - var proxyExists = Exists(); - if (!proxyExists) + // Check if the container exists (running or stopped) + if (_docker.Exists(Containers.ProxyName)) { - _console.WriteLine("Creating Vega VDK Proxy"); - _console.WriteLine(" - This may take a few minutes..."); - var conf = new FileInfo(NginxConf); - if (!conf.Exists) + _console.WriteLine("Vega VDK Proxy is running."); + return; + } + + // Container exists but is stopped - restart it + if (_docker.Exists(Containers.ProxyName, false)) + { + _console.WriteLine("Vega VDK Proxy exists but is not running. Restarting..."); + _docker.Restart(Containers.ProxyName); + return; + } + + // Container does not exist at all - create it + _console.WriteLine("Creating Vega VDK Proxy"); + _console.WriteLine(" - This may take a few minutes..."); + var conf = new FileInfo(NginxConf); + if (!conf.Exists) + { + if (!conf.Directory!.Exists) { - if (!conf.Directory!.Exists) + conf.Directory.Create(); + } + + InitConfFile(conf); + } + else + { + _console.WriteLine($" - Reverse proxy configuration for {conf.FullName} exists, running a quick validation..."); + conf.Delete(); + InitConfFile(conf); + // iterate the clusters and create the endpoints for each + _kind.ListClusters().ForEach(tuple => + { + if (tuple is { isVdk: true, master: not null } && tuple.master.HttpsHostPort.HasValue) { - conf.Directory.Create(); + _console.WriteLine($" - Adding cluster {tuple.name} to reverse proxy configuration"); + UpsertCluster(tuple.name, tuple.master.HttpsHostPort.Value, tuple.master.HttpHostPort.Value, false); } + }); + } + var fullChain = new FileInfo(Path.Combine("Certs", "fullchain.pem")); + var privKey = new FileInfo(Path.Combine("Certs", "privkey.pem")); - InitConfFile(conf); - } - else - { - _console.WriteLine($" - Reverse proxy configuration for {conf.FullName} exists, running a quick validation..."); - conf.Delete(); - InitConfFile(conf); - // iterate the clusters and create the endpoints for each - _kind.ListClusters().ForEach(tuple => + // Check for and fix certificate paths that were incorrectly created as directories + if (!ValidateAndFixCertificatePath(fullChain.FullName) || + !ValidateAndFixCertificatePath(privKey.FullName)) + { + _console.WriteError("Certificate files are missing. Please ensure Certs/fullchain.pem and Certs/privkey.pem exist."); + return; + } + + try + { + _docker.Run(Containers.ProxyImage, Containers.ProxyName, + new[] { new PortMapping() { HostPort = ReverseProxyHostPort, ContainerPort = 443 } }, + null, + new[] { - if (tuple is { isVdk: true, master: not null } && tuple.master.HttpsHostPort.HasValue) - { - _console.WriteLine($" - Adding cluster {tuple.name} to reverse proxy configuration"); - UpsertCluster(tuple.name, tuple.master.HttpsHostPort.Value, tuple.master.HttpHostPort.Value, false); - } - }); - } - var fullChain = new FileInfo(Path.Combine("Certs", "fullchain.pem")); - var privKey = new FileInfo(Path.Combine("Certs", "privkey.pem")); + new FileMapping() { Destination = "/etc/nginx/conf.d/vega.conf", Source = conf.FullName }, + new FileMapping() { Destination = "/etc/certs/fullchain.pem", Source = fullChain.FullName }, + new FileMapping() { Destination = "/etc/certs/privkey.pem", Source = privKey.FullName }, + }, + null); + } + catch (Exception e) + { + Console.WriteLine($"Error creating reverse proxy: {e}"); + } + } + + /// + /// Validates a certificate path exists as a file, not a directory. + /// On some systems (especially Mac), Docker may incorrectly create directories + /// when mounting paths that don't exist. This method detects and removes such directories. + /// + private bool ValidateAndFixCertificatePath(string path) + { + // Check if path exists as a directory (incorrect state) + if (Directory.Exists(path)) + { + _console.WriteWarning($"Certificate path '{path}' exists as a directory instead of a file. Removing..."); try { - _docker.Run(Containers.ProxyImage, Containers.ProxyName, - new[] { new PortMapping() { HostPort = ReverseProxyHostPort, ContainerPort = 443 } }, - null, - new[] - { - new FileMapping() { Destination = "/etc/nginx/conf.d/vega.conf", Source = conf.FullName }, - new FileMapping() { Destination = "/etc/certs/fullchain.pem", Source = fullChain.FullName }, - new FileMapping() { Destination = "/etc/certs/privkey.pem", Source = privKey.FullName }, - }, - null); + Directory.Delete(path, recursive: true); } - catch (Exception e) + catch (Exception ex) { - Console.WriteLine($"Error creating reverse proxy: {e}"); + _console.WriteError($"Failed to remove directory '{path}': {ex.Message}"); + return false; } } - else - { - _console.WriteLine("Vega VDK Proxy already created"); - } + + // Now check if the file exists + return File.Exists(path); } public bool Exists() @@ -102,8 +145,6 @@ public bool Exists() _console.WriteError(e); return true; } - - return false; } private void InitConfFile(FileInfo conf) @@ -257,15 +298,8 @@ private bool PatchCoreDns(string clusterName) return false; } - // insert the new entry after the kubernetes block by searching for the closing brace then inserting it - - var closingBraceIndex = lines.FindIndex(kubernetesBlockIndex, line => line.Trim() == "}"); - if (closingBraceIndex == -1) - { - _console.WriteError("CoreDNS Corefile does not contain a closing brace for the kubernetes block. Please check the configuration and try again."); - return false; - } - lines.Insert(closingBraceIndex, rewriteString); + // insert the rewrite BEFORE the kubernetes block so it is evaluated first by CoreDNS + lines.Insert(kubernetesBlockIndex, rewriteString); // join the lines back into a single string var updatedCorefile = string.Join(Environment.NewLine, lines); @@ -324,6 +358,16 @@ private bool CreateTlsSecret(string clusterName) return true; } + // Validate certificate files before reading + var fullChainPath = Path.Combine("Certs", "fullchain.pem"); + var privKeyPath = Path.Combine("Certs", "privkey.pem"); + if (!ValidateAndFixCertificatePath(fullChainPath) || + !ValidateAndFixCertificatePath(privKeyPath)) + { + _console.WriteError("Certificate files are missing. Cannot create TLS secret."); + return true; + } + // write the cert secret to the cluster var tls = new V1Secret() { @@ -335,8 +379,8 @@ private bool CreateTlsSecret(string clusterName) Type = "kubernetes.io/tls", Data = new Dictionary { - { "tls.crt", File.ReadAllBytes("Certs/fullchain.pem") }, - { "tls.key", File.ReadAllBytes("Certs/privkey.pem") } + { "tls.crt", File.ReadAllBytes(fullChainPath) }, + { "tls.key", File.ReadAllBytes(privKeyPath) } } }; var secret = _client(clusterName).Get("dev-tls", "vega-system"); @@ -351,7 +395,8 @@ private bool CreateTlsSecret(string clusterName) private void ReloadConfigs() { - _docker.Exec("nginx", new[] { "nginx", "-s", "reload" }); + _console.WriteLine("Restarting reverse proxy to apply configuration changes..."); + _docker.Restart(Containers.ProxyName); } private static StreamWriter ClearCluster(string clusterName) diff --git a/cli/tests/Vdk.Tests/ReverseProxyClientTests.cs b/cli/tests/Vdk.Tests/ReverseProxyClientTests.cs index 30e059f..1a93ba7 100644 --- a/cli/tests/Vdk.Tests/ReverseProxyClientTests.cs +++ b/cli/tests/Vdk.Tests/ReverseProxyClientTests.cs @@ -120,22 +120,29 @@ public void PatchCoreDns_ReturnsFalse_WhenIngressServiceNotFound() } [Fact] - public void PatchCoreDns_ReturnsFalse_WhenNoClosingBrace() + public void PatchCoreDns_InsertsRewriteBeforeKubernetesBlock() { - // Arrange + // Arrange: Corefile with kubernetes block but no closing brace — rewrite should still insert before the kubernetes line + var clusterName = "test-cluster"; var corefile = "kubernetes cluster.local in-addr.arpa ip6.arpa {"; + var configMap = new V1ConfigMap { Data = new Dictionary { { "Corefile", corefile } } }; + var pod = new V1Pod { Metadata = new V1ObjectMeta { Name = "coredns-1" } }; _kubeClientMock.Setup(x => x.Get("kgateway-system-kgateway", "kgateway-system")) .Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } }); _kubeClientMock.Setup(x => x.Get("coredns", "kube-system")) - .Returns(new V1ConfigMap { Data = new Dictionary { { "Corefile", corefile } } }); + .Returns(configMap); + _kubeClientMock.Setup(x => x.List("kube-system", It.IsAny())) + .Returns(new List { pod }); var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object); // Act - var result = InvokePatchCoreDns(client, "test-cluster"); + var result = InvokePatchCoreDns(client, clusterName); - // Assert - Assert.False(result); - _consoleMock.Verify(x => x.WriteError(It.Is(s => s.Contains("does not contain a closing brace"))), Times.Once); + // Assert — rewrite is inserted before the kubernetes block + Assert.True(result); + _kubeClientMock.Verify(x => x.Update(It.Is(cm => + cm.Data["Corefile"].Contains($"rewrite name {clusterName}.dev-k8s.cloud svc.ns.svc.cluster.local"))), Times.Once); + _kubeClientMock.Verify(x => x.Delete(pod), Times.Once); } [Fact]