-
Notifications
You must be signed in to change notification settings - Fork 0
Add FallbackDockerEngine with intelligent fallback logic #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
af09c64
cb5984a
a1a3f6a
4fdfa09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 */ } | ||||||||
| } | ||||||||
|
|
||||||||
| 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 */ } | ||||||||
|
||||||||
| catch { /* ignore file errors */ } | |
| catch (IOException) { /* ignore file read/write errors */ } | |
| catch (UnauthorizedAccessException) { /* ignore permission errors */ } |
| 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"; | ||||||
|
||||||
| var args = $"ps"; | |
| var args = "ps"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}"); | ||
|
||
| return false; | ||
| } | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.