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]