sln build fix (again), tests fixes, audit work and doctors work
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using System.Globalization;
|
||||
using Docker.DotNet;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Docker.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Docker API version compatibility.
|
||||
/// </summary>
|
||||
public sealed class DockerApiVersionCheck : IDoctorCheck
|
||||
{
|
||||
private static readonly Version MinimumApiVersion = new(1, 41);
|
||||
private static readonly Version RecommendedApiVersion = new(1, 43);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.docker.apiversion";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Docker API Version";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates Docker API version meets minimum requirements";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["docker", "api", "compatibility"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.docker", DoctorCategory.Infrastructure.ToString());
|
||||
|
||||
var dockerHost = context.Configuration.GetValue<string>("Docker:Host")
|
||||
?? GetDefaultDockerHost();
|
||||
|
||||
try
|
||||
{
|
||||
using var dockerClient = CreateDockerClient(dockerHost);
|
||||
|
||||
var version = await dockerClient.System.GetVersionAsync(ct);
|
||||
|
||||
if (!Version.TryParse(version.APIVersion, out var apiVersion))
|
||||
{
|
||||
return result
|
||||
.Warn($"Cannot parse API version: {version.APIVersion}")
|
||||
.WithEvidence("Docker API", e =>
|
||||
{
|
||||
e.Add("ReportedVersion", version.APIVersion);
|
||||
e.Add("DockerVersion", version.Version);
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
var issues = new List<string>();
|
||||
|
||||
if (apiVersion < MinimumApiVersion)
|
||||
{
|
||||
issues.Add($"API version {apiVersion} is below minimum required {MinimumApiVersion}");
|
||||
}
|
||||
else if (apiVersion < RecommendedApiVersion)
|
||||
{
|
||||
issues.Add($"API version {apiVersion} is below recommended {RecommendedApiVersion}");
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return result
|
||||
.Warn($"{issues.Count} API version issue(s)")
|
||||
.WithEvidence("Docker API", e =>
|
||||
{
|
||||
e.Add("ApiVersion", apiVersion.ToString());
|
||||
e.Add("MinimumRequired", MinimumApiVersion.ToString());
|
||||
e.Add("Recommended", RecommendedApiVersion.ToString());
|
||||
e.Add("DockerVersion", version.Version);
|
||||
e.Add("Os", version.Os);
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Update Docker", "Install the latest Docker version for your OS")
|
||||
.AddManualStep(2, "Verify version", "Run: docker version"))
|
||||
.WithVerification("stella doctor --check check.docker.apiversion")
|
||||
.Build();
|
||||
}
|
||||
|
||||
return result
|
||||
.Pass($"Docker API version {apiVersion} meets requirements")
|
||||
.WithEvidence("Docker API", e =>
|
||||
{
|
||||
e.Add("ApiVersion", apiVersion.ToString());
|
||||
e.Add("MinimumRequired", MinimumApiVersion.ToString());
|
||||
e.Add("Recommended", RecommendedApiVersion.ToString());
|
||||
e.Add("DockerVersion", version.Version);
|
||||
e.Add("BuildTime", version.BuildTime ?? "(not available)");
|
||||
e.Add("GitCommit", version.GitCommit ?? "(not available)");
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
return result
|
||||
.Skip($"Cannot check API version: {ex.Message}")
|
||||
.WithEvidence("Docker API", e =>
|
||||
{
|
||||
e.Add("Host", dockerHost);
|
||||
e.Add("Error", ex.GetType().Name);
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDefaultDockerHost()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return "npipe://./pipe/docker_engine";
|
||||
}
|
||||
|
||||
return "unix:///var/run/docker.sock";
|
||||
}
|
||||
|
||||
private static DockerClient CreateDockerClient(string host)
|
||||
{
|
||||
var config = new DockerClientConfiguration(new Uri(host));
|
||||
return config.CreateClient();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using Docker.DotNet;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Docker.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Docker daemon availability and responsiveness.
|
||||
/// </summary>
|
||||
public sealed class DockerDaemonCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.docker.daemon";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Docker Daemon";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates Docker daemon is running and responsive";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["docker", "daemon", "container"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.docker", DoctorCategory.Infrastructure.ToString());
|
||||
|
||||
var dockerHost = context.Configuration.GetValue<string>("Docker:Host")
|
||||
?? GetDefaultDockerHost();
|
||||
|
||||
var timeout = context.Configuration.GetValue<int?>("Docker:TimeoutSeconds") ?? 10;
|
||||
|
||||
try
|
||||
{
|
||||
using var dockerClient = CreateDockerClient(dockerHost);
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
||||
|
||||
await dockerClient.System.PingAsync(cts.Token);
|
||||
|
||||
var version = await dockerClient.System.GetVersionAsync(cts.Token);
|
||||
|
||||
return result
|
||||
.Pass("Docker daemon is running and responsive")
|
||||
.WithEvidence("Docker daemon", e =>
|
||||
{
|
||||
e.Add("Host", dockerHost);
|
||||
e.Add("Version", version.Version);
|
||||
e.Add("ApiVersion", version.APIVersion);
|
||||
e.Add("Os", version.Os);
|
||||
e.Add("Arch", version.Arch);
|
||||
e.Add("KernelVersion", version.KernelVersion ?? "(not available)");
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
catch (DockerApiException ex)
|
||||
{
|
||||
return result
|
||||
.Fail($"Docker API error: {ex.Message}")
|
||||
.WithEvidence("Docker daemon", e =>
|
||||
{
|
||||
e.Add("Host", dockerHost);
|
||||
e.Add("StatusCode", ex.StatusCode.ToString());
|
||||
e.Add("ResponseBody", TruncateMessage(ex.ResponseBody ?? "(no body)"));
|
||||
})
|
||||
.WithCauses("Docker daemon returned an error response")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check daemon status", "Run: docker info")
|
||||
.AddManualStep(2, "Restart daemon", "Run: sudo systemctl restart docker"))
|
||||
.WithVerification("stella doctor --check check.docker.daemon")
|
||||
.Build();
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
return result
|
||||
.Fail($"Cannot connect to Docker daemon: {ex.Message}")
|
||||
.WithEvidence("Docker daemon", e =>
|
||||
{
|
||||
e.Add("Host", dockerHost);
|
||||
e.Add("Error", ex.GetType().Name);
|
||||
e.Add("Message", TruncateMessage(ex.Message));
|
||||
})
|
||||
.WithCauses("Docker daemon is not running or not accessible")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Install Docker", "Follow Docker installation guide for your OS")
|
||||
.AddManualStep(2, "Start daemon", "Run: sudo systemctl start docker")
|
||||
.AddManualStep(3, "Verify installation", "Run: docker version"))
|
||||
.WithVerification("stella doctor --check check.docker.daemon")
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDefaultDockerHost()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return "npipe://./pipe/docker_engine";
|
||||
}
|
||||
|
||||
return "unix:///var/run/docker.sock";
|
||||
}
|
||||
|
||||
private static DockerClient CreateDockerClient(string host)
|
||||
{
|
||||
var config = new DockerClientConfiguration(new Uri(host));
|
||||
return config.CreateClient();
|
||||
}
|
||||
|
||||
private static string TruncateMessage(string message, int maxLength = 200)
|
||||
{
|
||||
if (message.Length <= maxLength)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
return message[..maxLength] + "...";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using Docker.DotNet;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Docker.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Docker network configuration.
|
||||
/// </summary>
|
||||
public sealed class DockerNetworkCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.docker.network";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Docker Network";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates Docker network configuration and connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["docker", "network", "connectivity"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.docker", DoctorCategory.Infrastructure.ToString());
|
||||
|
||||
var dockerHost = context.Configuration.GetValue<string>("Docker:Host")
|
||||
?? GetDefaultDockerHost();
|
||||
|
||||
var requiredNetworks = context.Configuration.GetSection("Docker:RequiredNetworks").Get<string[]>()
|
||||
?? ["bridge"];
|
||||
|
||||
try
|
||||
{
|
||||
using var dockerClient = CreateDockerClient(dockerHost);
|
||||
|
||||
var networks = await dockerClient.Networks.ListNetworksAsync(cancellationToken: ct);
|
||||
|
||||
var issues = new List<string>();
|
||||
var foundNetworks = new List<string>();
|
||||
var missingNetworks = new List<string>();
|
||||
|
||||
foreach (var requiredNetwork in requiredNetworks)
|
||||
{
|
||||
var found = networks.Any(n =>
|
||||
n.Name.Equals(requiredNetwork, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (found)
|
||||
{
|
||||
foundNetworks.Add(requiredNetwork);
|
||||
}
|
||||
else
|
||||
{
|
||||
missingNetworks.Add(requiredNetwork);
|
||||
issues.Add($"Required network '{requiredNetwork}' not found");
|
||||
}
|
||||
}
|
||||
|
||||
var bridgeNetwork = networks.FirstOrDefault(n =>
|
||||
n.Driver?.Equals("bridge", StringComparison.OrdinalIgnoreCase) == true);
|
||||
|
||||
if (bridgeNetwork == null)
|
||||
{
|
||||
issues.Add("No bridge network driver available");
|
||||
}
|
||||
|
||||
var totalNetworks = networks.Count;
|
||||
var networkDrivers = networks
|
||||
.Select(n => n.Driver ?? "unknown")
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return result
|
||||
.Warn($"{issues.Count} Docker network issue(s)")
|
||||
.WithEvidence("Docker networks", e =>
|
||||
{
|
||||
e.Add("TotalNetworks", totalNetworks.ToString());
|
||||
e.Add("AvailableDrivers", string.Join(", ", networkDrivers));
|
||||
e.Add("FoundRequired", string.Join(", ", foundNetworks));
|
||||
e.Add("MissingRequired", string.Join(", ", missingNetworks));
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "List networks", "Run: docker network ls")
|
||||
.AddManualStep(2, "Create network", "Run: docker network create <network-name>"))
|
||||
.WithVerification("stella doctor --check check.docker.network")
|
||||
.Build();
|
||||
}
|
||||
|
||||
return result
|
||||
.Pass($"Docker networks configured ({totalNetworks} available)")
|
||||
.WithEvidence("Docker networks", e =>
|
||||
{
|
||||
e.Add("TotalNetworks", totalNetworks.ToString());
|
||||
e.Add("AvailableDrivers", string.Join(", ", networkDrivers));
|
||||
e.Add("RequiredNetworks", string.Join(", ", requiredNetworks));
|
||||
e.Add("BridgeNetwork", bridgeNetwork?.Name ?? "(none)");
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
return result
|
||||
.Skip($"Cannot check Docker networks: {ex.Message}")
|
||||
.WithEvidence("Docker networks", e =>
|
||||
{
|
||||
e.Add("Host", dockerHost);
|
||||
e.Add("Error", ex.GetType().Name);
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDefaultDockerHost()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return "npipe://./pipe/docker_engine";
|
||||
}
|
||||
|
||||
return "unix:///var/run/docker.sock";
|
||||
}
|
||||
|
||||
private static DockerClient CreateDockerClient(string host)
|
||||
{
|
||||
var config = new DockerClientConfiguration(new Uri(host));
|
||||
return config.CreateClient();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Builders;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Docker.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Docker socket accessibility and permissions.
|
||||
/// </summary>
|
||||
public sealed class DockerSocketCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.docker.socket";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Docker Socket";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates Docker socket exists and is accessible";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["docker", "socket", "permissions"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.docker", DoctorCategory.Infrastructure.ToString());
|
||||
|
||||
var dockerHost = context.Configuration.GetValue<string>("Docker:Host")
|
||||
?? GetDefaultDockerHost();
|
||||
|
||||
var issues = new List<string>();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return Task.FromResult(CheckWindowsNamedPipe(result, dockerHost, issues));
|
||||
}
|
||||
|
||||
return Task.FromResult(CheckUnixSocket(result, dockerHost, issues));
|
||||
}
|
||||
|
||||
private static DoctorCheckResult CheckUnixSocket(
|
||||
CheckResultBuilder result,
|
||||
string dockerHost,
|
||||
List<string> issues)
|
||||
{
|
||||
var socketPath = dockerHost.StartsWith("unix://", StringComparison.OrdinalIgnoreCase)
|
||||
? dockerHost["unix://".Length..]
|
||||
: "/var/run/docker.sock";
|
||||
|
||||
var socketExists = File.Exists(socketPath);
|
||||
var socketReadable = false;
|
||||
var socketWritable = false;
|
||||
|
||||
if (socketExists)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(socketPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
socketReadable = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Cannot read socket
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(socketPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
|
||||
socketWritable = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Cannot write to socket
|
||||
}
|
||||
}
|
||||
|
||||
if (!socketExists)
|
||||
{
|
||||
issues.Add($"Docker socket not found at {socketPath}");
|
||||
}
|
||||
else if (!socketReadable || !socketWritable)
|
||||
{
|
||||
issues.Add($"Insufficient permissions on {socketPath}");
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return result
|
||||
.Fail($"{issues.Count} Docker socket issue(s)")
|
||||
.WithEvidence("Docker socket", e =>
|
||||
{
|
||||
e.Add("Path", socketPath);
|
||||
e.Add("Exists", socketExists.ToString());
|
||||
e.Add("Readable", socketReadable.ToString());
|
||||
e.Add("Writable", socketWritable.ToString());
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check Docker installation", "Ensure Docker is installed and running")
|
||||
.AddManualStep(2, "Add user to docker group", "Run: sudo usermod -aG docker $USER")
|
||||
.AddManualStep(3, "Re-login", "Log out and back in for group changes to take effect"))
|
||||
.WithVerification("stella doctor --check check.docker.socket")
|
||||
.Build();
|
||||
}
|
||||
|
||||
return result
|
||||
.Pass("Docker socket is accessible")
|
||||
.WithEvidence("Docker socket", e =>
|
||||
{
|
||||
e.Add("Path", socketPath);
|
||||
e.Add("Exists", socketExists.ToString());
|
||||
e.Add("Readable", socketReadable.ToString());
|
||||
e.Add("Writable", socketWritable.ToString());
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static DoctorCheckResult CheckWindowsNamedPipe(
|
||||
CheckResultBuilder result,
|
||||
string dockerHost,
|
||||
List<string> issues)
|
||||
{
|
||||
var pipePath = dockerHost.StartsWith("npipe://", StringComparison.OrdinalIgnoreCase)
|
||||
? dockerHost
|
||||
: "npipe://./pipe/docker_engine";
|
||||
|
||||
// On Windows, we primarily check via Docker daemon connectivity
|
||||
// Named pipe access is handled by the daemon check
|
||||
return result
|
||||
.Pass("Docker named pipe configured")
|
||||
.WithEvidence("Docker socket", e =>
|
||||
{
|
||||
e.Add("Type", "Named Pipe");
|
||||
e.Add("Path", pipePath);
|
||||
e.Add("Platform", "Windows");
|
||||
e.Add("Note", "Connectivity verified via daemon check");
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static string GetDefaultDockerHost()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return "npipe://./pipe/docker_engine";
|
||||
}
|
||||
|
||||
return "unix:///var/run/docker.sock";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Globalization;
|
||||
using Docker.DotNet;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Docker.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Docker storage and disk space usage.
|
||||
/// </summary>
|
||||
public sealed class DockerStorageCheck : IDoctorCheck
|
||||
{
|
||||
private const double DefaultMaxUsagePercent = 85.0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.docker.storage";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Docker Storage";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates Docker storage driver and disk space usage";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["docker", "storage", "disk"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.docker", DoctorCategory.Infrastructure.ToString());
|
||||
|
||||
var dockerHost = context.Configuration.GetValue<string>("Docker:Host")
|
||||
?? GetDefaultDockerHost();
|
||||
|
||||
var minFreeSpaceGb = context.Configuration.GetValue<double?>("Docker:MinFreeSpaceGb") ?? 10.0;
|
||||
var maxUsagePercent = context.Configuration.GetValue<double?>("Docker:MaxStorageUsagePercent")
|
||||
?? DefaultMaxUsagePercent;
|
||||
|
||||
try
|
||||
{
|
||||
using var dockerClient = CreateDockerClient(dockerHost);
|
||||
|
||||
var systemInfo = await dockerClient.System.GetSystemInfoAsync(ct);
|
||||
|
||||
var issues = new List<string>();
|
||||
|
||||
var storageDriver = systemInfo.Driver ?? "unknown";
|
||||
var dockerRoot = systemInfo.DockerRootDir ?? "/var/lib/docker";
|
||||
|
||||
// Check storage driver
|
||||
var recommendedDrivers = new[] { "overlay2", "btrfs", "zfs" };
|
||||
var isRecommendedDriver = recommendedDrivers.Any(d =>
|
||||
storageDriver.Equals(d, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!isRecommendedDriver)
|
||||
{
|
||||
issues.Add($"Storage driver '{storageDriver}' is not recommended (use overlay2, btrfs, or zfs)");
|
||||
}
|
||||
|
||||
// Get disk info from Docker root directory
|
||||
long? totalSpace = null;
|
||||
long? freeSpace = null;
|
||||
double? usagePercent = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(dockerRoot))
|
||||
{
|
||||
var driveInfo = new DriveInfo(Path.GetPathRoot(dockerRoot) ?? dockerRoot);
|
||||
totalSpace = driveInfo.TotalSize;
|
||||
freeSpace = driveInfo.AvailableFreeSpace;
|
||||
|
||||
if (totalSpace > 0)
|
||||
{
|
||||
usagePercent = ((totalSpace.Value - freeSpace.Value) / (double)totalSpace.Value) * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Disk info may not be available on all platforms
|
||||
}
|
||||
|
||||
if (freeSpace.HasValue)
|
||||
{
|
||||
var minFreeSpaceBytes = (long)(minFreeSpaceGb * 1024 * 1024 * 1024);
|
||||
if (freeSpace.Value < minFreeSpaceBytes)
|
||||
{
|
||||
var freeGb = freeSpace.Value / (1024.0 * 1024 * 1024);
|
||||
issues.Add($"Low disk space: {freeGb:F1} GB free (minimum: {minFreeSpaceGb:F0} GB)");
|
||||
}
|
||||
}
|
||||
|
||||
if (usagePercent.HasValue && usagePercent.Value > maxUsagePercent)
|
||||
{
|
||||
issues.Add($"Disk usage {usagePercent.Value:F1}% exceeds threshold {maxUsagePercent:F0}%");
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return result
|
||||
.Warn($"{issues.Count} Docker storage issue(s)")
|
||||
.WithEvidence("Docker storage", e =>
|
||||
{
|
||||
e.Add("StorageDriver", storageDriver);
|
||||
e.Add("DockerRoot", dockerRoot);
|
||||
e.Add("TotalSpace", FormatBytes(totalSpace));
|
||||
e.Add("FreeSpace", FormatBytes(freeSpace));
|
||||
e.Add("UsagePercent", usagePercent?.ToString("F1", CultureInfo.InvariantCulture) ?? "(unknown)");
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Prune unused data", "Run: docker system prune -a")
|
||||
.AddManualStep(2, "Check disk usage", "Run: docker system df")
|
||||
.AddManualStep(3, "Add storage", "Expand disk or add additional storage"))
|
||||
.WithVerification("stella doctor --check check.docker.storage")
|
||||
.Build();
|
||||
}
|
||||
|
||||
return result
|
||||
.Pass("Docker storage is healthy")
|
||||
.WithEvidence("Docker storage", e =>
|
||||
{
|
||||
e.Add("StorageDriver", storageDriver);
|
||||
e.Add("DockerRoot", dockerRoot);
|
||||
e.Add("TotalSpace", FormatBytes(totalSpace));
|
||||
e.Add("FreeSpace", FormatBytes(freeSpace));
|
||||
e.Add("UsagePercent", usagePercent?.ToString("F1", CultureInfo.InvariantCulture) ?? "(unknown)");
|
||||
e.Add("IsRecommendedDriver", isRecommendedDriver.ToString());
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
return result
|
||||
.Skip($"Cannot check Docker storage: {ex.Message}")
|
||||
.WithEvidence("Docker storage", e =>
|
||||
{
|
||||
e.Add("Host", dockerHost);
|
||||
e.Add("Error", ex.GetType().Name);
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDefaultDockerHost()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return "npipe://./pipe/docker_engine";
|
||||
}
|
||||
|
||||
return "unix:///var/run/docker.sock";
|
||||
}
|
||||
|
||||
private static DockerClient CreateDockerClient(string host)
|
||||
{
|
||||
var config = new DockerClientConfiguration(new Uri(host));
|
||||
return config.CreateClient();
|
||||
}
|
||||
|
||||
private static string FormatBytes(long? bytes)
|
||||
{
|
||||
if (!bytes.HasValue)
|
||||
{
|
||||
return "(unknown)";
|
||||
}
|
||||
|
||||
var b = bytes.Value;
|
||||
string[] suffixes = ["B", "KB", "MB", "GB", "TB"];
|
||||
var i = 0;
|
||||
double size = b;
|
||||
|
||||
while (size >= 1024 && i < suffixes.Length - 1)
|
||||
{
|
||||
size /= 1024;
|
||||
i++;
|
||||
}
|
||||
|
||||
return $"{size:F1} {suffixes[i]}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Docker.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering the Docker diagnostics plugin.
|
||||
/// </summary>
|
||||
public static class DockerPluginExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the Docker diagnostics plugin to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddDoctorDockerPlugin(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IDoctorPlugin, DockerPlugin>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Docker.Checks;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Docker;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin providing Docker container runtime diagnostic checks.
|
||||
/// </summary>
|
||||
public sealed class DockerPlugin : IDoctorPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string PluginId => "stellaops.doctor.docker";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Docker Runtime";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorCategory Category => DoctorCategory.Infrastructure;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version Version => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version MinEngineVersion => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context) =>
|
||||
[
|
||||
new DockerDaemonCheck(),
|
||||
new DockerSocketCheck(),
|
||||
new DockerApiVersionCheck(),
|
||||
new DockerNetworkCheck(),
|
||||
new DockerStorageCheck()
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Docker.DotNet" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user