Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 118 additions & 7 deletions cli/src/Vdk/Services/ReverseProxyClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@
private readonly IDockerEngine _docker;
private readonly Func<string, IKubernetesClient> _client;
private readonly IConsole _console;



private static readonly string NginxConf = Path.Combine("vega.conf");

private readonly IKindClient _kind;


// ReverseProxyHostPort is 443 by default, unless REVERSE_PROXY_HOST_PORT is set as an env var
private int ReverseProxyHostPort = GetEnvironmentVariableAsInt("REVERSE_PROXY_HOST_PORT", 443);

Expand Down Expand Up @@ -62,7 +60,7 @@
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);

Check warning on line 63 in cli/src/Vdk/Services/ReverseProxyClient.cs

View workflow job for this annotation

GitHub Actions / build

Nullable value type may be null.
}
});
}
Expand Down Expand Up @@ -105,7 +103,7 @@
return true;
}

return false;

Check warning on line 106 in cli/src/Vdk/Services/ReverseProxyClient.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected
}

private void InitConfFile(FileInfo conf)
Expand Down Expand Up @@ -147,6 +145,15 @@
}

public void UpsertCluster(string clusterName, int targetPortHttps, int targetPortHttp, bool reload = true)
{
PatchNginxConfig(clusterName, targetPortHttps);
if (CreateTlsSecret(clusterName)) return;
PatchCoreDns(clusterName);
if (reload)
ReloadConfigs();
}

private void PatchNginxConfig(string clusterName, int targetPortHttps)
{
// create a new server block in the nginx conf pointing to the target port listening on the https://clusterName.dev-k8s.cloud domain
Comment on lines +149 to 158
Copy link

Copilot AI Jun 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter 'targetPortHttp' is not used within UpsertCluster. Consider removing it or adding logic to use it for HTTP traffic.

Suggested change
PatchNginxConfig(clusterName, targetPortHttps);
if (CreateTlsSecret(clusterName)) return;
PatchCoreDns(clusterName);
if (reload)
ReloadConfigs();
}
private void PatchNginxConfig(string clusterName, int targetPortHttps)
{
// create a new server block in the nginx conf pointing to the target port listening on the https://clusterName.dev-k8s.cloud domain
PatchNginxConfig(clusterName, targetPortHttps, targetPortHttp);
if (CreateTlsSecret(clusterName)) return;
PatchCoreDns(clusterName);
if (reload)
ReloadConfigs();
}
private void PatchNginxConfig(string clusterName, int targetPortHttps, int targetPortHttp)
{
// create a new server block in the nginx conf pointing to the target port listening on the https://clusterName.dev-k8s.cloud domain
using (var writer = File.AppendText("/etc/nginx/conf.d/default.conf"))
{
writer.WriteLine("server {");
writer.WriteLine($" listen {targetPortHttp};");
writer.WriteLine($" server_name {clusterName}.dev-k8s.cloud;");
writer.WriteLine(" location / {");
writer.WriteLine(" proxy_pass http://host.docker.internal:5000;");
writer.WriteLine(" proxy_set_header Host $host;");
writer.WriteLine(" proxy_set_header X-Real-IP $remote_addr;");
writer.WriteLine(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;");
writer.WriteLine(" proxy_set_header X-Forwarded-Proto $scheme;");
writer.WriteLine(" }");
writer.WriteLine("}");
}

Copilot uses AI. Check for mistakes.
// reload the nginx configuration
Expand Down Expand Up @@ -179,7 +186,112 @@
_console.WriteWarning($"Error clearing cluster configuration ({NginxConf}): {e.Message}");
_console.WriteWarning("Please check the configuration and try again.");
}
}

private bool PatchCoreDns(string clusterName)
{
// let's wait for and find the ingress controller service.
V1Service? ingressService = null;
var attempts = 0;
do
{
// check up to 10 times , waiting 5 seconds each time
ingressService = _client(clusterName).Get<V1Service>("ingress-nginx-controller", "ingress-nginx");
if (ingressService == null)
{
_console.WriteLine("Waiting for ingress-nginx-controller service to be available...");
Thread.Sleep(5000);
Copy link

Copilot AI Jun 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Thread.Sleep will block the thread and can significantly slow down both production execution and unit tests. Consider injecting a delay provider or reducing/removing the sleep for better performance and testability.

Suggested change
Thread.Sleep(5000);
await Task.Delay(5000);

Copilot uses AI. Check for mistakes.
attempts++;
}
else
{
_console.WriteLine("Ingress-nginx-controller service found.");
break;
}
}
while (ingressService == null && attempts < 6);
if (ingressService == null)
{
_console.WriteError("Ingress-nginx-controller service not found. Please check the configuration and try again.");
return false;
}
var rewriteString = $" rewrite name {clusterName}.dev-k8s.cloud {ingressService.Name()}.{ingressService.Namespace()}.svc.cluster.local";
// now read the CoreDNS configmap and add the clusterName.dev-k8s.cloud entry to it
var corednsConfigMap = _client(clusterName).Get<V1ConfigMap>("coredns", "kube-system");
if (corednsConfigMap == null)
{
_console.WriteError("CoreDNS configmap not found. Please check the configuration and try again.");
return false;
}
_console.WriteLine("Patching CoreDNS configmap with new cluster entry.");
var corednsData = corednsConfigMap.Data;
if (corednsData == null)
{
corednsData = new Dictionary<string, string>();
}
// extract the corefile from the configmap data
if (!corednsData.TryGetValue("Corefile", out var corefile))
{
_console.WriteError("CoreDNS Corefile not found in configmap. Please check the configuration and try again.");
return false;
}
// add the clusterName.dev-k8s.cloud rewrite entry to the Corefile after the kubernetes block
// if the line already exists, do not add it again
if (corefile.Contains(rewriteString))
{
_console.WriteLine("CoreDNS Corefile already contains the rewrite entry for the cluster. No changes made.");
return true;
}
var lines = corefile.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries).ToList();

var kubernetesBlockIndex = lines.FindIndex(line => line.Trim().StartsWith("kubernetes"));

if (kubernetesBlockIndex == -1)
{
_console.WriteError("CoreDNS Corefile does not contain a kubernetes block. Please check the configuration and try again.");
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);

// join the lines back into a single string
var updatedCorefile = string.Join(Environment.NewLine, lines);

// update the configmap data with the new Corefile
corednsData["Corefile"] = updatedCorefile;

// update the configmap
corednsConfigMap.Data = corednsData;
_client(clusterName).Update(corednsConfigMap);
_console.WriteLine("CoreDNS configmap updated successfully.");

// restart the coredns
_console.WriteLine("Restarting CoreDNS pods to apply changes.");
var corednsPods = _client(clusterName).List<V1Pod>("kube-system", labelSelector: "k8s-app=kube-dns");
if (!corednsPods.Any())
{
_console.WriteError("No CoreDNS pods found. Please check the configuration and try again.");
return false;
}
foreach (var pod in corednsPods)
{
_console.WriteLine($"Deleting CoreDNS pod {pod.Name()} to apply changes.");
_client(clusterName).Delete(pod);
}
_console.WriteLine("CoreDNS pods deleted successfully. They will be recreated automatically.");
return true;
}

private bool CreateTlsSecret(string clusterName)
{
// wait until the namespace vega exists before proceeding with the secrets creation
bool nsVegaExists = false;
int nTimesWaiting = 0;
Expand All @@ -204,7 +316,7 @@
if (nTimesWaiting >= maxTimesWaiting)
{
_console.WriteError("Namespace 'vega-system' does not exist after waiting. Please check the configuration and try again.");
return;
return true;
}

// write the cert secret to the cluster
Expand All @@ -229,8 +341,7 @@
}
_console.WriteLine("Creating vega-system secret");
_client(clusterName).Create(tls);
if (reload)
ReloadConfigs();
return false;
}

private void ReloadConfigs()
Expand Down Expand Up @@ -306,7 +417,7 @@

private static int GetEnvironmentVariableAsInt(string variableName, int defaultValue = 0)
{
string strValue = Environment.GetEnvironmentVariable(variableName);

Check warning on line 420 in cli/src/Vdk/Services/ReverseProxyClient.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
if (strValue != null && int.TryParse(strValue, out int intValue))
{
return intValue;
Expand Down
148 changes: 146 additions & 2 deletions cli/tests/Vdk.Tests/ReverseProxyClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ public class ReverseProxyClientTests
private readonly Mock<IDockerEngine> _dockerMock = new();
private readonly Mock<IConsole> _consoleMock = new();
private readonly Mock<IKindClient> _kindMock = new();
private readonly Mock<IKubernetesClient> _k8sMock = new();
private readonly Mock<IKubernetesClient> _kubeClientMock = new();
private readonly Func<string, IKubernetesClient> _clientFunc;

public ReverseProxyClientTests()
{
_clientFunc = _ => _k8sMock.Object;
_clientFunc = _ => _kubeClientMock.Object;
}

[Fact]
Expand Down Expand Up @@ -66,5 +66,149 @@ public void InitConfFile_CreatesFileAndWritesConfig()
File.ReadAllText(tempFile).Should().Contain("server {");
File.Delete(tempFile);
}

[Fact]
public void PatchCoreDns_ReturnsFalse_WhenIngressServiceNotFound()
{
// Arrange
_kubeClientMock.SetupSequence(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
.Returns((V1Service?)null);
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);

// Act
var result = InvokePatchCoreDns(client, "test-cluster");

// Assert
Assert.False(result);
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("Ingress-nginx-controller service not found"))), Times.Once);
}

[Fact]
public void PatchCoreDns_ReturnsFalse_WhenCoreDnsConfigMapNotFound()
{
// Arrange
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
.Returns((V1ConfigMap?)null);
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);

// Act
var result = InvokePatchCoreDns(client, "test-cluster");

// Assert
Assert.False(result);
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("CoreDNS configmap not found"))), Times.Once);
}

[Fact]
public void PatchCoreDns_ReturnsFalse_WhenCorefileMissing()
{
// Arrange
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
.Returns(new V1ConfigMap { Data = new Dictionary<string, string>() });
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);

// Act
var result = InvokePatchCoreDns(client, "test-cluster");

// Assert
Assert.False(result);
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("CoreDNS Corefile not found"))), Times.Once);
}

[Fact]
public void PatchCoreDns_ReturnsTrue_WhenRewriteAlreadyExists()
{
// Arrange
var clusterName = "test-cluster";
var rewriteString = $" rewrite name {clusterName}.dev-k8s.cloud svc.ns.svc.cluster.local";
var corefile = $"kubernetes cluster.local in-addr.arpa ip6.arpa {{\n}}\n{rewriteString}\n";
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
.Returns(new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } });
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);

// Act
var result = InvokePatchCoreDns(client, clusterName);

// Assert
Assert.True(result);
_consoleMock.Verify(x => x.WriteLine(It.Is<string>(s => s.Contains("already contains the rewrite entry"))), Times.Once);
}

[Fact]
public void PatchCoreDns_ReturnsFalse_WhenNoKubernetesBlock()
{
// Arrange
var corefile = "some unrelated config";
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
.Returns(new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } });
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);

// Act
var result = InvokePatchCoreDns(client, "test-cluster");

// Assert
Assert.False(result);
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("does not contain a kubernetes block"))), Times.Once);
}

[Fact]
public void PatchCoreDns_ReturnsFalse_WhenNoClosingBrace()
{
// Arrange
var corefile = "kubernetes cluster.local in-addr.arpa ip6.arpa {";
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
.Returns(new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } });
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);

// Act
var result = InvokePatchCoreDns(client, "test-cluster");

// Assert
Assert.False(result);
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("does not contain a closing brace"))), Times.Once);
}

[Fact]
public void PatchCoreDns_UpdatesConfigMapAndRestartsPods()
{
// Arrange
var clusterName = "test-cluster";
var corefile = $"kubernetes cluster.local in-addr.arpa ip6.arpa {{{Environment.NewLine}}}{Environment.NewLine}";
var configMap = new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } };
var pod = new V1Pod { Metadata = new V1ObjectMeta { Name = "coredns-1" } };
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
.Returns(configMap);
_kubeClientMock.Setup(x => x.List<V1Pod>("kube-system", It.IsAny<string>()))
.Returns(new List<V1Pod> { pod });
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);

// Act
var result = InvokePatchCoreDns(client, clusterName);

// Assert
Assert.True(result);
_kubeClientMock.Verify(x => x.Update(It.Is<V1ConfigMap>(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);
_consoleMock.Verify(x => x.WriteLine(It.Is<string>(s => s.Contains("CoreDNS configmap updated successfully."))), Times.Once);
}

// Helper to invoke private PatchCoreDns via reflection
private static bool InvokePatchCoreDns(ReverseProxyClient client, string clusterName)
{
var method = typeof(ReverseProxyClient).GetMethod("PatchCoreDns", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
return (bool)method.Invoke(client, new object[] { clusterName });
}
}
}
Loading