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
12 changes: 11 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
"permissions": {
"allow": [
"Bash(docker stop:*)",
"Bash(docker rm:*)"
"Bash(docker rm:*)",
"Bash(docker run:*)",
"Bash(MSYS_NO_PATHCONV=1 docker run:*)",
"Bash(MSYS_NO_PATHCONV=1 docker exec:*)",
"Bash(for node in idp-worker idp-worker2)",
"Bash(do:*)",
"Bash(echo:*)",
"Bash(done)",
"Bash(for node in idp-control-plane idp-worker idp-worker2)",
"Bash(kubectl --context kind-idp get pods:*)",
"Bash(kubectl --context kind-idp delete pod:*)"
]
}
}
20 changes: 17 additions & 3 deletions cli/src/Vdk/Commands/CreateClusterCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public CreateClusterCommand(
controlNodes.Aliases.Add("-c");
var workers = new Option<int>("--Workers") { DefaultValueFactory = _ => Defaults.WorkerNodes, Description = "The number of worker nodes in the cluster." };
workers.Aliases.Add("-w");
var kubeVersion = new Option<string>("--KubeVersion") { DefaultValueFactory = _ => "1.29", Description = "The kubernetes api version." };
var kubeVersion = new Option<string>("--KubeVersion") { DefaultValueFactory = _ => "1.32", Description = "The kubernetes api version." };
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The documentation still references Kubernetes version 1.29 as the default, but the code has been updated to use 1.32. The documentation files docs/usage/command-reference.md (line 39) and docs/usage/creating-clusters.md (line 31) should be updated to reflect the new default version of 1.32.

Copilot uses AI. Check for mistakes.
kubeVersion.Aliases.Add("-k");

Options.Add(nameOption);
Expand Down Expand Up @@ -111,6 +111,16 @@ public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPla
return;
}

// Write hosts.toml to a temp file for containerd registry config
// This ensures the file is accessible to Docker regardless of working directory
var hostsTomlContent = """
server = "http://host.docker.internal:5000"
[host."http://host.docker.internal:5000"]
capabilities = ["pull", "resolve"]
""";
var hostsTomlPath = _fileSystem.Path.Combine(_fileSystem.Path.GetTempPath(), $"hosts-{Guid.NewGuid()}.toml");
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The temporary hosts.toml file created at this location is never cleaned up after the cluster is created. This will lead to accumulation of orphaned files in the temp directory over time. Consider using a try-finally block or ensuring the file is deleted after the Kind cluster creation completes, or use a deterministic naming scheme that allows reuse of the same file.

Suggested change
var hostsTomlPath = _fileSystem.Path.Combine(_fileSystem.Path.GetTempPath(), $"hosts-{Guid.NewGuid()}.toml");
var hostsTomlPath = _fileSystem.Path.Combine(_fileSystem.Path.GetTempPath(), "vdk-hosts-hub.dev-k8s.cloud.toml");

Copilot uses AI. Check for mistakes.
await _fileSystem.File.WriteAllTextAsync(hostsTomlPath, hostsTomlContent);

var cluster = new KindCluster();
for (int index = 0; index < controlPlaneNodes; index++)
{
Expand All @@ -127,7 +137,7 @@ public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPla
{
new()
{
HostPath = _fileSystem.FileInfo.New("ConfigMounts/hosts.toml").FullName,
HostPath = hostsTomlPath,
ContainerPath = "/etc/containerd/certs.d/hub.dev-k8s.cloud/hosts.toml"
}
};
Expand All @@ -144,7 +154,7 @@ public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPla
{
new()
{
HostPath = _fileSystem.FileInfo.New("ConfigMounts/hosts.toml").FullName,
HostPath = hostsTomlPath,
ContainerPath = "/etc/containerd/certs.d/hub.dev-k8s.cloud/hosts.toml"
}
}
Expand Down Expand Up @@ -194,6 +204,10 @@ public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPla
}

_flux.Bootstrap(name.ToLower(), "./clusters/default", branch: "main");

// Wait for all Flux kustomizations to reconcile before configuring the reverse proxy
_flux.WaitForKustomizations(name.ToLower());

try
{
_reverseProxy.UpsertCluster(name.ToLower(), masterNode.ExtraPortMappings.First().HostPort,
Expand Down
88 changes: 87 additions & 1 deletion cli/src/Vdk/Services/FluxClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.Text.Json;
using k8s;
using k8s.Autorest;
using k8s.Models;
Expand Down Expand Up @@ -114,6 +115,91 @@ public void Bootstrap(string clusterName, string path, string branch = DefaultBr
}

_console.WriteLine("Flux bootstrap complete.");


}

public bool WaitForKustomizations(string clusterName, int maxAttempts = 60, int delaySeconds = 5)
{
Comment on lines +121 to +122
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

This synchronous method uses Thread.Sleep which blocks the calling thread. Since it's called from InvokeAsync (an async method at line 209), this creates a blocking call in an async context. Consider making this method async and using Task.Delay instead of Thread.Sleep. This would improve responsiveness and follow async/await best practices. The pattern should be: public async Task<bool> WaitForKustomizations(...) with await Task.Delay(delaySeconds * 1000) instead of Thread.Sleep.

Copilot uses AI. Check for mistakes.
_console.WriteLine("Waiting for Flux kustomizations to reconcile...");

for (int attempt = 0; attempt < maxAttempts; attempt++)
{
try
{
var result = _client(clusterName).ApiClient.CustomObjects
.ListNamespacedCustomObject(
"kustomize.toolkit.fluxcd.io", "v1",
"flux-system", "kustomizations");

var json = JsonSerializer.Serialize(result);
using var doc = JsonDocument.Parse(json);
var items = doc.RootElement.GetProperty("items");

if (items.GetArrayLength() == 0)
{
if (attempt % 5 == 0)
_console.WriteLine(" No kustomizations found yet. Waiting...");
Thread.Sleep(delaySeconds * 1000);
continue;
}

int total = items.GetArrayLength();
int readyCount = 0;

foreach (var item in items.EnumerateArray())
{
var name = item.GetProperty("metadata").GetProperty("name").GetString();
bool isReady = false;

if (item.TryGetProperty("status", out var status) &&
status.TryGetProperty("conditions", out var conditions))
{
foreach (var condition in conditions.EnumerateArray())
{
if (condition.GetProperty("type").GetString() == "Ready")
{
var condStatus = condition.GetProperty("status").GetString();
Comment on lines +151 to +161
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The GetString() calls on lines 151, 159, and 161 could potentially return null if the JSON property values are null. This could lead to NullReferenceException when these values are used. Consider using null-conditional operators or null-coalescing operators to handle potential null values, e.g., GetString() ?? "unknown" for name comparisons.

Copilot uses AI. Check for mistakes.
if (condStatus == "True")
{
isReady = true;
}
else if (attempt % 5 == 0)
{
var reason = condition.TryGetProperty("reason", out var r)
? r.GetString() : "Unknown";
var message = condition.TryGetProperty("message", out var m)
? m.GetString() : "";
_console.WriteLine(
$" Kustomization '{name}' not ready: {reason} - {message}");
}
break;
}
}
Comment on lines +157 to +177
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
}

if (isReady) readyCount++;
}

if (attempt % 5 == 0 || readyCount == total)
_console.WriteLine($" Kustomizations ready: {readyCount}/{total}");

if (readyCount == total)
{
_console.WriteLine("All Flux kustomizations are ready.");
return true;
}
}
catch (Exception ex)
{
if (attempt % 5 == 0)
_console.WriteLine($" Error checking kustomizations: {ex.Message}. Retrying...");
}

Thread.Sleep(delaySeconds * 1000);
}

_console.WriteWarning(
"Timed out waiting for Flux kustomizations to reconcile. Proceeding anyway...");
return false;
}
}
1 change: 1 addition & 0 deletions cli/src/Vdk/Services/IFluxClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace Vdk.Services;
public interface IFluxClient
{
void Bootstrap(string clusterName, string path, string branch = FluxClient.DefaultBranch);
bool WaitForKustomizations(string clusterName, int maxAttempts = 60, int delaySeconds = 5);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The interface should be updated to return Task<bool> instead of bool to properly support async operations. Since this method performs network I/O and waits (currently with Thread.Sleep), it should be async to avoid blocking the calling thread.

Copilot uses AI. Check for mistakes.
}
Loading