Skip to content

Commit b89941c

Browse files
authored
Merge pull request #39 from ArchetypicalSoftware/feature/coredns-rewrite
Adding CoreDNS rewrite
2 parents 424d87a + 57dcb04 commit b89941c

2 files changed

Lines changed: 264 additions & 9 deletions

File tree

cli/src/Vdk/Services/ReverseProxyClient.cs

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@ internal class ReverseProxyClient : IReverseProxyClient
1313
private readonly IDockerEngine _docker;
1414
private readonly Func<string, IKubernetesClient> _client;
1515
private readonly IConsole _console;
16-
17-
16+
1817
private static readonly string NginxConf = Path.Combine("vega.conf");
19-
18+
2019
private readonly IKindClient _kind;
2120

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

@@ -147,6 +145,15 @@ public void Delete()
147145
}
148146

149147
public void UpsertCluster(string clusterName, int targetPortHttps, int targetPortHttp, bool reload = true)
148+
{
149+
PatchNginxConfig(clusterName, targetPortHttps);
150+
if (CreateTlsSecret(clusterName)) return;
151+
PatchCoreDns(clusterName);
152+
if (reload)
153+
ReloadConfigs();
154+
}
155+
156+
private void PatchNginxConfig(string clusterName, int targetPortHttps)
150157
{
151158
// create a new server block in the nginx conf pointing to the target port listening on the https://clusterName.dev-k8s.cloud domain
152159
// reload the nginx configuration
@@ -179,7 +186,112 @@ public void UpsertCluster(string clusterName, int targetPortHttps, int targetPor
179186
_console.WriteWarning($"Error clearing cluster configuration ({NginxConf}): {e.Message}");
180187
_console.WriteWarning("Please check the configuration and try again.");
181188
}
189+
}
182190

191+
private bool PatchCoreDns(string clusterName)
192+
{
193+
// let's wait for and find the ingress controller service.
194+
V1Service? ingressService = null;
195+
var attempts = 0;
196+
do
197+
{
198+
// check up to 10 times , waiting 5 seconds each time
199+
ingressService = _client(clusterName).Get<V1Service>("ingress-nginx-controller", "ingress-nginx");
200+
if (ingressService == null)
201+
{
202+
_console.WriteLine("Waiting for ingress-nginx-controller service to be available...");
203+
Thread.Sleep(5000);
204+
attempts++;
205+
}
206+
else
207+
{
208+
_console.WriteLine("Ingress-nginx-controller service found.");
209+
break;
210+
}
211+
}
212+
while (ingressService == null && attempts < 6);
213+
if (ingressService == null)
214+
{
215+
_console.WriteError("Ingress-nginx-controller service not found. Please check the configuration and try again.");
216+
return false;
217+
}
218+
var rewriteString = $" rewrite name {clusterName}.dev-k8s.cloud {ingressService.Name()}.{ingressService.Namespace()}.svc.cluster.local";
219+
// now read the CoreDNS configmap and add the clusterName.dev-k8s.cloud entry to it
220+
var corednsConfigMap = _client(clusterName).Get<V1ConfigMap>("coredns", "kube-system");
221+
if (corednsConfigMap == null)
222+
{
223+
_console.WriteError("CoreDNS configmap not found. Please check the configuration and try again.");
224+
return false;
225+
}
226+
_console.WriteLine("Patching CoreDNS configmap with new cluster entry.");
227+
var corednsData = corednsConfigMap.Data;
228+
if (corednsData == null)
229+
{
230+
corednsData = new Dictionary<string, string>();
231+
}
232+
// extract the corefile from the configmap data
233+
if (!corednsData.TryGetValue("Corefile", out var corefile))
234+
{
235+
_console.WriteError("CoreDNS Corefile not found in configmap. Please check the configuration and try again.");
236+
return false;
237+
}
238+
// add the clusterName.dev-k8s.cloud rewrite entry to the Corefile after the kubernetes block
239+
// if the line already exists, do not add it again
240+
if (corefile.Contains(rewriteString))
241+
{
242+
_console.WriteLine("CoreDNS Corefile already contains the rewrite entry for the cluster. No changes made.");
243+
return true;
244+
}
245+
var lines = corefile.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries).ToList();
246+
247+
var kubernetesBlockIndex = lines.FindIndex(line => line.Trim().StartsWith("kubernetes"));
248+
249+
if (kubernetesBlockIndex == -1)
250+
{
251+
_console.WriteError("CoreDNS Corefile does not contain a kubernetes block. Please check the configuration and try again.");
252+
return false;
253+
}
254+
255+
// insert the new entry after the kubernetes block by searching for the closing brace then inserting it
256+
257+
var closingBraceIndex = lines.FindIndex(kubernetesBlockIndex, line => line.Trim() == "}");
258+
if (closingBraceIndex == -1)
259+
{
260+
_console.WriteError("CoreDNS Corefile does not contain a closing brace for the kubernetes block. Please check the configuration and try again.");
261+
return false;
262+
}
263+
lines.Insert(closingBraceIndex, rewriteString);
264+
265+
// join the lines back into a single string
266+
var updatedCorefile = string.Join(Environment.NewLine, lines);
267+
268+
// update the configmap data with the new Corefile
269+
corednsData["Corefile"] = updatedCorefile;
270+
271+
// update the configmap
272+
corednsConfigMap.Data = corednsData;
273+
_client(clusterName).Update(corednsConfigMap);
274+
_console.WriteLine("CoreDNS configmap updated successfully.");
275+
276+
// restart the coredns
277+
_console.WriteLine("Restarting CoreDNS pods to apply changes.");
278+
var corednsPods = _client(clusterName).List<V1Pod>("kube-system", labelSelector: "k8s-app=kube-dns");
279+
if (!corednsPods.Any())
280+
{
281+
_console.WriteError("No CoreDNS pods found. Please check the configuration and try again.");
282+
return false;
283+
}
284+
foreach (var pod in corednsPods)
285+
{
286+
_console.WriteLine($"Deleting CoreDNS pod {pod.Name()} to apply changes.");
287+
_client(clusterName).Delete(pod);
288+
}
289+
_console.WriteLine("CoreDNS pods deleted successfully. They will be recreated automatically.");
290+
return true;
291+
}
292+
293+
private bool CreateTlsSecret(string clusterName)
294+
{
183295
// wait until the namespace vega exists before proceeding with the secrets creation
184296
bool nsVegaExists = false;
185297
int nTimesWaiting = 0;
@@ -204,7 +316,7 @@ public void UpsertCluster(string clusterName, int targetPortHttps, int targetPor
204316
if (nTimesWaiting >= maxTimesWaiting)
205317
{
206318
_console.WriteError("Namespace 'vega-system' does not exist after waiting. Please check the configuration and try again.");
207-
return;
319+
return true;
208320
}
209321

210322
// write the cert secret to the cluster
@@ -229,8 +341,7 @@ public void UpsertCluster(string clusterName, int targetPortHttps, int targetPor
229341
}
230342
_console.WriteLine("Creating vega-system secret");
231343
_client(clusterName).Create(tls);
232-
if (reload)
233-
ReloadConfigs();
344+
return false;
234345
}
235346

236347
private void ReloadConfigs()

cli/tests/Vdk.Tests/ReverseProxyClientTests.cs

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ public class ReverseProxyClientTests
1515
private readonly Mock<IDockerEngine> _dockerMock = new();
1616
private readonly Mock<IConsole> _consoleMock = new();
1717
private readonly Mock<IKindClient> _kindMock = new();
18-
private readonly Mock<IKubernetesClient> _k8sMock = new();
18+
private readonly Mock<IKubernetesClient> _kubeClientMock = new();
1919
private readonly Func<string, IKubernetesClient> _clientFunc;
2020

2121
public ReverseProxyClientTests()
2222
{
23-
_clientFunc = _ => _k8sMock.Object;
23+
_clientFunc = _ => _kubeClientMock.Object;
2424
}
2525

2626
[Fact]
@@ -66,5 +66,149 @@ public void InitConfFile_CreatesFileAndWritesConfig()
6666
File.ReadAllText(tempFile).Should().Contain("server {");
6767
File.Delete(tempFile);
6868
}
69+
70+
[Fact]
71+
public void PatchCoreDns_ReturnsFalse_WhenIngressServiceNotFound()
72+
{
73+
// Arrange
74+
_kubeClientMock.SetupSequence(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
75+
.Returns((V1Service?)null);
76+
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);
77+
78+
// Act
79+
var result = InvokePatchCoreDns(client, "test-cluster");
80+
81+
// Assert
82+
Assert.False(result);
83+
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("Ingress-nginx-controller service not found"))), Times.Once);
84+
}
85+
86+
[Fact]
87+
public void PatchCoreDns_ReturnsFalse_WhenCoreDnsConfigMapNotFound()
88+
{
89+
// Arrange
90+
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
91+
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
92+
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
93+
.Returns((V1ConfigMap?)null);
94+
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);
95+
96+
// Act
97+
var result = InvokePatchCoreDns(client, "test-cluster");
98+
99+
// Assert
100+
Assert.False(result);
101+
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("CoreDNS configmap not found"))), Times.Once);
102+
}
103+
104+
[Fact]
105+
public void PatchCoreDns_ReturnsFalse_WhenCorefileMissing()
106+
{
107+
// Arrange
108+
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
109+
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
110+
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
111+
.Returns(new V1ConfigMap { Data = new Dictionary<string, string>() });
112+
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);
113+
114+
// Act
115+
var result = InvokePatchCoreDns(client, "test-cluster");
116+
117+
// Assert
118+
Assert.False(result);
119+
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("CoreDNS Corefile not found"))), Times.Once);
120+
}
121+
122+
[Fact]
123+
public void PatchCoreDns_ReturnsTrue_WhenRewriteAlreadyExists()
124+
{
125+
// Arrange
126+
var clusterName = "test-cluster";
127+
var rewriteString = $" rewrite name {clusterName}.dev-k8s.cloud svc.ns.svc.cluster.local";
128+
var corefile = $"kubernetes cluster.local in-addr.arpa ip6.arpa {{\n}}\n{rewriteString}\n";
129+
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
130+
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
131+
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
132+
.Returns(new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } });
133+
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);
134+
135+
// Act
136+
var result = InvokePatchCoreDns(client, clusterName);
137+
138+
// Assert
139+
Assert.True(result);
140+
_consoleMock.Verify(x => x.WriteLine(It.Is<string>(s => s.Contains("already contains the rewrite entry"))), Times.Once);
141+
}
142+
143+
[Fact]
144+
public void PatchCoreDns_ReturnsFalse_WhenNoKubernetesBlock()
145+
{
146+
// Arrange
147+
var corefile = "some unrelated config";
148+
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
149+
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
150+
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
151+
.Returns(new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } });
152+
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);
153+
154+
// Act
155+
var result = InvokePatchCoreDns(client, "test-cluster");
156+
157+
// Assert
158+
Assert.False(result);
159+
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("does not contain a kubernetes block"))), Times.Once);
160+
}
161+
162+
[Fact]
163+
public void PatchCoreDns_ReturnsFalse_WhenNoClosingBrace()
164+
{
165+
// Arrange
166+
var corefile = "kubernetes cluster.local in-addr.arpa ip6.arpa {";
167+
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
168+
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
169+
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
170+
.Returns(new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } });
171+
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);
172+
173+
// Act
174+
var result = InvokePatchCoreDns(client, "test-cluster");
175+
176+
// Assert
177+
Assert.False(result);
178+
_consoleMock.Verify(x => x.WriteError(It.Is<string>(s => s.Contains("does not contain a closing brace"))), Times.Once);
179+
}
180+
181+
[Fact]
182+
public void PatchCoreDns_UpdatesConfigMapAndRestartsPods()
183+
{
184+
// Arrange
185+
var clusterName = "test-cluster";
186+
var corefile = $"kubernetes cluster.local in-addr.arpa ip6.arpa {{{Environment.NewLine}}}{Environment.NewLine}";
187+
var configMap = new V1ConfigMap { Data = new Dictionary<string, string> { { "Corefile", corefile } } };
188+
var pod = new V1Pod { Metadata = new V1ObjectMeta { Name = "coredns-1" } };
189+
_kubeClientMock.Setup(x => x.Get<V1Service>("ingress-nginx-controller", "ingress-nginx"))
190+
.Returns(new V1Service { Metadata = new V1ObjectMeta { Name = "svc", NamespaceProperty = "ns" } });
191+
_kubeClientMock.Setup(x => x.Get<V1ConfigMap>("coredns", "kube-system"))
192+
.Returns(configMap);
193+
_kubeClientMock.Setup(x => x.List<V1Pod>("kube-system", It.IsAny<string>()))
194+
.Returns(new List<V1Pod> { pod });
195+
var client = new ReverseProxyClient(_dockerMock.Object, _clientFunc, _consoleMock.Object, _kindMock.Object);
196+
197+
// Act
198+
var result = InvokePatchCoreDns(client, clusterName);
199+
200+
// Assert
201+
Assert.True(result);
202+
_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);
203+
_kubeClientMock.Verify(x => x.Delete(pod), Times.Once);
204+
_consoleMock.Verify(x => x.WriteLine(It.Is<string>(s => s.Contains("CoreDNS configmap updated successfully."))), Times.Once);
205+
}
206+
207+
// Helper to invoke private PatchCoreDns via reflection
208+
private static bool InvokePatchCoreDns(ReverseProxyClient client, string clusterName)
209+
{
210+
var method = typeof(ReverseProxyClient).GetMethod("PatchCoreDns", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
211+
return (bool)method.Invoke(client, new object[] { clusterName });
212+
}
69213
}
70214
}

0 commit comments

Comments
 (0)