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
28 changes: 23 additions & 5 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,24 @@ on:
- completed
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
matrix:
rid: [linux-x64, linux-arm, linux-arm64, linux-musl-x64, linux-musl-arm64, osx-x64, osx-arm64]
include:
- rid: linux-x64
runner: ubuntu-latest
- rid: linux-arm
runner: ubuntu-latest
- rid: linux-arm64
runner: ubuntu-latest
- rid: linux-musl-x64
runner: ubuntu-latest
- rid: linux-musl-arm64
runner: ubuntu-latest
- rid: osx-x64
runner: macos-15-large
- rid: osx-arm64
runner: macos-15
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -40,10 +54,14 @@ jobs:
- name: Test
run: dotnet test ./cli --no-build --configuration Release --verbosity normal

- name: Install Zip Utility
run: sudo apt-get install -y zip && mkdir -p ./artifacts
- name: Install Zip Utility and Create Artifacts Directory
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt-get install -y zip
fi
mkdir -p ./artifacts

- name: Publish Linux Build
- name: Publish Build
run: |
RUNTIME_ID=${{ matrix.rid }}
OUTPUT_DIR=./packages/build/${RUNTIME_ID}
Expand Down
2 changes: 1 addition & 1 deletion cli/src/Vdk/Constants/Containers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public static class Containers
public const string RegistryName = "vega-registry";
public const string RegistryImage = "registry:2";
public const int RegistryContainerPort = 5000;
public const int RegistryHostPort = 5000;
public const int RegistryHostPort = 50000;
public const string ProxyName = "vega-proxy";
public const string ProxyImage = "nginx:latest";
public const string CloudProviderKindName = "cloud-provider-kind";
Expand Down
2 changes: 1 addition & 1 deletion cli/src/Vdk/GlobalConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Vdk;

public class GlobalConfiguration
{
private string? _profileDirectory = null;
internal string? _profileDirectory = null;

public string ConfigDirectoryName { get; set; } = Defaults.ConfigDirectoryName;

Expand Down
46 changes: 45 additions & 1 deletion cli/src/Vdk/ServiceProviderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,51 @@ public static IServiceProvider Build()
.AddSingleton<IFluxClient, FluxClient>()
.AddSingleton<IReverseProxyClient, ReverseProxyClient>()
.AddSingleton<IEmbeddedDataReader, EmbeddedDataReader>()
.AddSingleton<IDockerEngine, LocalDockerClient>()
.AddSingleton<IDockerEngine>(provider =>
{
// Intelligent fallback logic
var fileSystem = provider.GetRequiredService<IFileSystem>();
var config = provider.GetRequiredService<GlobalConfiguration>();
var fallbackFile = fileSystem.Path.Combine(config.VegaDirectory, ".vdk_docker_fallback");
DateTime now = DateTime.UtcNow;
DateTime fallbackUntil = DateTime.MinValue;

if (fileSystem.File.Exists(fallbackFile))
{
try
{
var content = fileSystem.File.ReadAllText(fallbackFile).Trim();
if (DateTime.TryParse(content, null, System.Globalization.DateTimeStyles.AdjustToUniversal, out var parsed))
{
fallbackUntil = parsed;
}
}
catch { /* ignore file errors */ }
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

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

Using a bare catch clause without specifying exception types can mask important errors. Consider catching specific exceptions like IOException or UnauthorizedAccessException.

Suggested change
catch { /* ignore file errors */ }
catch (IOException) { /* ignore file errors */ }
catch (UnauthorizedAccessException) { /* ignore file errors */ }

Copilot uses AI. Check for mistakes.
}

if (fallbackUntil > now)
{
// Still in fallback window
return new FallbackDockerEngine();
}

var docker = provider.GetService<IDockerClient>();
if (docker == null) return new FallbackDockerEngine();

var programatic = new LocalDockerClient(docker);
if (!programatic.CanConnect())
{
// Write fallback timestamp for 2 hours
try
{
fileSystem.File.WriteAllText(fallbackFile, now.AddHours(2).ToString("o"));
}
catch { /* ignore file errors */ }
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

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

Using a bare catch clause without specifying exception types can mask important errors. Consider catching specific exceptions like IOException or UnauthorizedAccessException.

Suggested change
catch { /* ignore file errors */ }
catch (IOException) { /* ignore file read/write errors */ }
catch (UnauthorizedAccessException) { /* ignore permission errors */ }

Copilot uses AI. Check for mistakes.
return new FallbackDockerEngine();
}

return programatic;
})
.AddSingleton<IHubClient, DockerHubClient>()
.AddSingleton<IGitHubClient>(gitHubClient)
.AddSingleton<GlobalConfiguration>(new GlobalConfiguration())
Expand Down
91 changes: 91 additions & 0 deletions cli/src/Vdk/Services/FallbackDockerEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Vdk.Models;

namespace Vdk.Services
{
public class FallbackDockerEngine : IDockerEngine
{
internal static bool RunProcess(string fileName, string arguments, out string stdOut, out string stdErr)
{
using var process = new Process();
process.StartInfo.FileName = fileName;
process.StartInfo.Arguments = arguments;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.Start();
stdOut = process.StandardOutput.ReadToEnd();
stdErr = process.StandardError.ReadToEnd();
process.WaitForExit();
return process.ExitCode == 0;
}

public bool Run(string image, string name, PortMapping[]? ports, Dictionary<string, string>? envs, FileMapping[]? volumes, string[]? commands, string? network = null)
{
var args = $"run -d --name {name}";
if (network != null)
args += $" --network {network}";
if (ports != null)
{
foreach (var p in ports)
{
var proto = string.IsNullOrWhiteSpace(p.Protocol) ? "" : "/" + p.Protocol.ToLower();
var listen = string.IsNullOrWhiteSpace(p.ListenAddress) ? "" : p.ListenAddress + ":";
args += $" -p {listen}{p.HostPort}:{p.ContainerPort}{proto}";
}
}
if (envs != null)
{
foreach (var kv in envs)
args += $" -e \"{kv.Key}={kv.Value}\"";
}
if (volumes != null)
{
foreach (var v in volumes)
args += $" -v \"{v.Source}:{v.Destination}\"";
}
args += $" {image}";
if (commands != null && commands.Length > 0)
args += " " + string.Join(" ", commands);
return RunProcess("docker", args, out _, out _);
}

public bool Exists(string name, bool checkRunning = true)
{
var filter = checkRunning ? "--filter \"status=running\"" : "";
var args = $"ps -a {filter} --filter \"name=^{name}$\" --format \"{{{{.Names}}}}\"";
var ok = RunProcess("docker", args, out var output, out _);
if (!ok) return false;
foreach (var line in output.Split('\n'))
if (line.Trim() == name) return true;
return false;
}

public bool Delete(string name)
{
// Try to remove forcefully (stops if running)
return RunProcess("docker", $"rm -f {name}", out _, out _);
}

public bool Stop(string name)
{
return RunProcess("docker", $"stop {name}", out _, out _);
}

public bool Exec(string name, string[] commands)
{
var args = $"exec {name} ";
args += string.Join(" ", commands);
return RunProcess("docker", args, out _, out _);
}

public bool CanConnect()
{
var args = $"ps";
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

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

The trailing space in the string assignment is unnecessary and should be removed for consistency.

Suggested change
var args = $"ps";
var args = "ps";

Copilot uses AI. Check for mistakes.
return RunProcess("docker", args, out _, out _);
}
}
}
2 changes: 2 additions & 0 deletions cli/src/Vdk/Services/IDockerEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public interface IDockerEngine
bool Stop(string name);

bool Exec(string name, string[] commands);

bool CanConnect();
}
14 changes: 14 additions & 0 deletions cli/src/Vdk/Services/LocalDockerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,18 @@ public bool Exec(string name, string[] commands)
}).GetAwaiter().GetResult();
return true;
}

public bool CanConnect()
{
try
{
_dockerClient.Images.ListImagesAsync(new ImagesListParameters()).GetAwaiter().GetResult();
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error in CanConnect: {ex.Message}");
Copy link

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

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

[nitpick] Writing errors directly to the console can make testing and logging harder. Consider injecting an ILogger to handle error reporting in a more flexible way.

Copilot uses AI. Check for mistakes.
return false;
}
}
}
10 changes: 5 additions & 5 deletions cli/src/Vdk/Vdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@

<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="KubeOps.KubernetesClient" Version="9.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="KubeOps.KubernetesClient" Version="9.10.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="SemanticVersion" Version="2.1.0" />
<PackageReference Include="Shell.NET" Version="0.2.2" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.14" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.14" />
<PackageReference Include="YamlDotNet" Version="15.1.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.15" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
60 changes: 60 additions & 0 deletions cli/tests/Vdk.Tests/FallbackDockerEngineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Vdk.Models;
using Vdk.Services;

namespace Vdk.Tests
{
public class FallbackDockerEngineTests : IDisposable
{
private readonly FallbackDockerEngine _engine = new();
private readonly string _containerName = $"vdk_test_{Guid.NewGuid().ToString().Substring(0, 8)}";
private readonly string _image = "alpine";

public FallbackDockerEngineTests()
{
// Pull image to avoid network flakiness in tests
FallbackDockerEngine.RunProcess("docker", $"pull {_image}", out _, out _);
}

[Fact]
public void Run_And_Exists_And_Delete_Works()
{
var result = _engine.Run(_image, _containerName, null, null, null, new[] { "sleep", "60" });
Assert.True(result, "Container should start");
Assert.True(_engine.Exists(_containerName), "Container should exist and be running");
Assert.True(_engine.Delete(_containerName), "Container should be deleted");
Assert.False(_engine.Exists(_containerName, false), "Container should not exist after delete");
}

[Fact]
public void Stop_And_Exec_Works()
{
var started = _engine.Run(_image, _containerName, null, null, null, new[] { "sleep", "60" });
Assert.True(started, "Container should start");
// Exec a command
Assert.True(_engine.Exec(_containerName, new[] { "sh", "-c", "echo hello" }), "Exec should succeed");
// Stop
Assert.True(_engine.Stop(_containerName), "Stop should succeed");
// After stop, Exists with checkRunning=true should be false
Assert.False(_engine.Exists(_containerName, true), "Container should not be running after stop");
// Delete
Assert.True(_engine.Delete(_containerName), "Container should be deleted");
}

[Fact]
public void Run_With_Ports_And_Volumes_Works()
{
var ports = new[] { new PortMapping { HostPort = 12345, ContainerPort = 80 } };
var volumes = new[] { new FileMapping { Source = "/tmp", Destination = "/mnt" } };
var result = _engine.Run(_image, _containerName, ports, null, volumes, new[] { "sleep", "60" });
Assert.True(result, "Container should start with ports and volumes");
Assert.True(_engine.Exists(_containerName), "Container should exist");
Assert.True(_engine.Delete(_containerName), "Container should be deleted");
}

public void Dispose()
{
// Cleanup in case test fails
_engine.Delete(_containerName);
}
}
}
Loading
Loading