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]