audit, advisories and doctors/setup work
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
namespace StellaOps.Doctor.Detection;
|
||||
|
||||
/// <summary>
|
||||
/// Detects the runtime environment where Stella Ops is deployed.
|
||||
/// Used to generate runtime-specific remediation commands.
|
||||
/// </summary>
|
||||
public interface IRuntimeDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects the current runtime environment.
|
||||
/// </summary>
|
||||
/// <returns>The detected runtime environment.</returns>
|
||||
RuntimeEnvironment Detect();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Docker is available on the system.
|
||||
/// </summary>
|
||||
bool IsDockerAvailable();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if running within a Kubernetes cluster.
|
||||
/// </summary>
|
||||
bool IsKubernetesContext();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a specific service is managed by systemd.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">The name of the service to check.</param>
|
||||
bool IsSystemdManaged(string serviceName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Docker Compose project path if available.
|
||||
/// </summary>
|
||||
/// <returns>The compose file path, or null if not found.</returns>
|
||||
string? GetComposeProjectPath();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current Kubernetes namespace.
|
||||
/// </summary>
|
||||
/// <returns>The namespace, or null if not in Kubernetes.</returns>
|
||||
string? GetKubernetesNamespace();
|
||||
|
||||
/// <summary>
|
||||
/// Gets environment-specific context values.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of context key-value pairs.</returns>
|
||||
IReadOnlyDictionary<string, string> GetContextValues();
|
||||
}
|
||||
339
src/__Libraries/StellaOps.Doctor/Detection/RuntimeDetector.cs
Normal file
339
src/__Libraries/StellaOps.Doctor/Detection/RuntimeDetector.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Doctor.Detection;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IRuntimeDetector"/>.
|
||||
/// Detects Docker Compose, Kubernetes, systemd, and Windows Service environments.
|
||||
/// </summary>
|
||||
public sealed class RuntimeDetector : IRuntimeDetector
|
||||
{
|
||||
private readonly ILogger<RuntimeDetector> _logger;
|
||||
private readonly Lazy<RuntimeEnvironment> _detectedRuntime;
|
||||
private readonly Lazy<IReadOnlyDictionary<string, string>> _contextValues;
|
||||
|
||||
private static readonly string[] ComposeFileNames =
|
||||
[
|
||||
"docker-compose.yml",
|
||||
"docker-compose.yaml",
|
||||
"compose.yml",
|
||||
"compose.yaml"
|
||||
];
|
||||
|
||||
private static readonly string[] ComposeSearchPaths =
|
||||
[
|
||||
".",
|
||||
"..",
|
||||
"devops/compose",
|
||||
"../devops/compose"
|
||||
];
|
||||
|
||||
public RuntimeDetector(ILogger<RuntimeDetector> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_detectedRuntime = new Lazy<RuntimeEnvironment>(DetectInternal);
|
||||
_contextValues = new Lazy<IReadOnlyDictionary<string, string>>(BuildContextValues);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RuntimeEnvironment Detect() => _detectedRuntime.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDockerAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check for docker command
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "docker.exe" : "docker",
|
||||
Arguments = "--version",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null) return false;
|
||||
|
||||
process.WaitForExit(5000);
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Docker availability check failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsKubernetesContext()
|
||||
{
|
||||
// Check for KUBERNETES_SERVICE_HOST environment variable
|
||||
var kubeHost = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
|
||||
if (!string.IsNullOrEmpty(kubeHost))
|
||||
{
|
||||
_logger.LogDebug("Detected Kubernetes via KUBERNETES_SERVICE_HOST: {Host}", kubeHost);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for kubeconfig file
|
||||
var kubeConfig = Environment.GetEnvironmentVariable("KUBECONFIG");
|
||||
if (!string.IsNullOrEmpty(kubeConfig) && File.Exists(kubeConfig))
|
||||
{
|
||||
_logger.LogDebug("Detected Kubernetes via KUBECONFIG: {Path}", kubeConfig);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check default kubeconfig location
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var defaultKubeConfig = Path.Combine(homeDir, ".kube", "config");
|
||||
if (File.Exists(defaultKubeConfig))
|
||||
{
|
||||
_logger.LogDebug("Detected Kubernetes via default kubeconfig: {Path}", defaultKubeConfig);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSystemdManaged(string serviceName)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "systemctl",
|
||||
Arguments = $"is-enabled {serviceName}",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null) return false;
|
||||
|
||||
process.WaitForExit(5000);
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "systemd check failed for service {ServiceName}", serviceName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetComposeProjectPath()
|
||||
{
|
||||
// Check COMPOSE_FILE environment variable
|
||||
var composeFile = Environment.GetEnvironmentVariable("COMPOSE_FILE");
|
||||
if (!string.IsNullOrEmpty(composeFile) && File.Exists(composeFile))
|
||||
{
|
||||
return composeFile;
|
||||
}
|
||||
|
||||
// Search common locations
|
||||
var currentDir = Directory.GetCurrentDirectory();
|
||||
foreach (var searchPath in ComposeSearchPaths)
|
||||
{
|
||||
var searchDir = Path.GetFullPath(Path.Combine(currentDir, searchPath));
|
||||
if (!Directory.Exists(searchDir)) continue;
|
||||
|
||||
foreach (var fileName in ComposeFileNames)
|
||||
{
|
||||
var fullPath = Path.Combine(searchDir, fileName);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
_logger.LogDebug("Found Docker Compose file at: {Path}", fullPath);
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetKubernetesNamespace()
|
||||
{
|
||||
// Check environment variable
|
||||
var ns = Environment.GetEnvironmentVariable("KUBERNETES_NAMESPACE");
|
||||
if (!string.IsNullOrEmpty(ns))
|
||||
{
|
||||
return ns;
|
||||
}
|
||||
|
||||
// Check namespace file (mounted in pods)
|
||||
const string namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
|
||||
if (File.Exists(namespaceFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ReadAllText(namespaceFile).Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to read Kubernetes namespace file");
|
||||
}
|
||||
}
|
||||
|
||||
// Default namespace
|
||||
return "stellaops";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, string> GetContextValues() => _contextValues.Value;
|
||||
|
||||
private RuntimeEnvironment DetectInternal()
|
||||
{
|
||||
_logger.LogDebug("Detecting runtime environment...");
|
||||
|
||||
// Check if running in Docker container
|
||||
if (File.Exists("/.dockerenv"))
|
||||
{
|
||||
_logger.LogInformation("Detected Docker container environment");
|
||||
return RuntimeEnvironment.DockerCompose;
|
||||
}
|
||||
|
||||
// Check for Kubernetes
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST")))
|
||||
{
|
||||
_logger.LogInformation("Detected Kubernetes environment");
|
||||
return RuntimeEnvironment.Kubernetes;
|
||||
}
|
||||
|
||||
// Check for Docker Compose
|
||||
if (IsDockerAvailable() && GetComposeProjectPath() != null)
|
||||
{
|
||||
_logger.LogInformation("Detected Docker Compose environment");
|
||||
return RuntimeEnvironment.DockerCompose;
|
||||
}
|
||||
|
||||
// Check for systemd (Linux)
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "systemctl",
|
||||
Arguments = "--version",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process != null)
|
||||
{
|
||||
process.WaitForExit(5000);
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
_logger.LogInformation("Detected systemd environment");
|
||||
return RuntimeEnvironment.Systemd;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "systemd detection failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Windows Service
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// If running as a service, parent process is services.exe
|
||||
try
|
||||
{
|
||||
using var current = Process.GetCurrentProcess();
|
||||
var parentId = GetParentProcessId(current.Id);
|
||||
if (parentId > 0)
|
||||
{
|
||||
using var parent = Process.GetProcessById(parentId);
|
||||
if (parent.ProcessName.Equals("services", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Detected Windows Service environment");
|
||||
return RuntimeEnvironment.WindowsService;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Windows Service detection failed");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Detected bare/manual environment");
|
||||
return RuntimeEnvironment.Bare;
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, string> BuildContextValues()
|
||||
{
|
||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var runtime = Detect();
|
||||
|
||||
// Common values
|
||||
values["RUNTIME"] = runtime.ToString();
|
||||
|
||||
switch (runtime)
|
||||
{
|
||||
case RuntimeEnvironment.DockerCompose:
|
||||
var composePath = GetComposeProjectPath();
|
||||
if (composePath != null)
|
||||
{
|
||||
values["COMPOSE_FILE"] = composePath;
|
||||
}
|
||||
var projectName = Environment.GetEnvironmentVariable("COMPOSE_PROJECT_NAME") ?? "stellaops";
|
||||
values["COMPOSE_PROJECT_NAME"] = projectName;
|
||||
break;
|
||||
|
||||
case RuntimeEnvironment.Kubernetes:
|
||||
var ns = GetKubernetesNamespace() ?? "stellaops";
|
||||
values["NAMESPACE"] = ns;
|
||||
var kubeHost = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
|
||||
if (kubeHost != null)
|
||||
{
|
||||
values["KUBERNETES_HOST"] = kubeHost;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Database defaults
|
||||
values["DB_HOST"] = Environment.GetEnvironmentVariable("STELLAOPS_DB_HOST") ?? "localhost";
|
||||
values["DB_PORT"] = Environment.GetEnvironmentVariable("STELLAOPS_DB_PORT") ?? "5432";
|
||||
|
||||
// Valkey defaults
|
||||
values["VALKEY_HOST"] = Environment.GetEnvironmentVariable("STELLAOPS_VALKEY_HOST") ?? "localhost";
|
||||
values["VALKEY_PORT"] = Environment.GetEnvironmentVariable("STELLAOPS_VALKEY_PORT") ?? "6379";
|
||||
|
||||
// Vault defaults
|
||||
var vaultAddr = Environment.GetEnvironmentVariable("VAULT_ADDR");
|
||||
if (vaultAddr != null)
|
||||
{
|
||||
values["VAULT_ADDR"] = vaultAddr;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static int GetParentProcessId(int processId)
|
||||
{
|
||||
// Skip parent process detection - not reliable across platforms
|
||||
// Windows Service detection is done via other signals
|
||||
_ = processId; // Suppress unused parameter warning
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace StellaOps.Doctor.Detection;
|
||||
|
||||
/// <summary>
|
||||
/// The runtime environment where Stella Ops is deployed.
|
||||
/// Used to generate runtime-specific remediation commands.
|
||||
/// </summary>
|
||||
public enum RuntimeEnvironment
|
||||
{
|
||||
/// <summary>Docker Compose deployment.</summary>
|
||||
DockerCompose,
|
||||
|
||||
/// <summary>Kubernetes deployment.</summary>
|
||||
Kubernetes,
|
||||
|
||||
/// <summary>systemd-managed services (Linux).</summary>
|
||||
Systemd,
|
||||
|
||||
/// <summary>Windows Service deployment.</summary>
|
||||
WindowsService,
|
||||
|
||||
/// <summary>Bare metal / manual deployment.</summary>
|
||||
Bare,
|
||||
|
||||
/// <summary>Commands that work in any environment.</summary>
|
||||
Any
|
||||
}
|
||||
Reference in New Issue
Block a user