Files
OTSSignsOrchestrator/OTSSignsOrchestrator.Core/Services/ComposeValidationService.cs
Matt Batchelder 45c94b6536
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
feat: Add main application views and structure
- Implemented CreateInstanceView for creating new instances.
- Added HostsView for managing SSH hosts with CRUD operations.
- Created InstancesView for displaying and managing instances.
- Developed LogsView for viewing operation logs.
- Introduced SecretsView for managing secrets associated with hosts.
- Established SettingsView for configuring application settings.
- Created MainWindow as the main application window with navigation.
- Added app manifest and configuration files for logging and settings.
2026-02-18 10:43:27 -05:00

117 lines
4.2 KiB
C#

using Microsoft.Extensions.Logging;
using YamlDotNet.RepresentationModel;
namespace OTSSignsOrchestrator.Core.Services;
/// <summary>
/// Validates a rendered Compose YAML before deployment.
/// </summary>
public class ComposeValidationService
{
private readonly ILogger<ComposeValidationService> _logger;
public ComposeValidationService(ILogger<ComposeValidationService> logger)
{
_logger = logger;
}
public ValidationResult Validate(string composeYaml, string? customerAbbrev = null)
{
var errors = new List<string>();
var warnings = new List<string>();
YamlStream yamlStream;
try
{
yamlStream = new YamlStream();
using var reader = new StringReader(composeYaml);
yamlStream.Load(reader);
}
catch (Exception ex)
{
errors.Add($"YAML parse error: {ex.Message}");
return new ValidationResult { Errors = errors, Warnings = warnings };
}
if (yamlStream.Documents.Count == 0)
{
errors.Add("YAML document is empty.");
return new ValidationResult { Errors = errors, Warnings = warnings };
}
var root = yamlStream.Documents[0].RootNode as YamlMappingNode;
if (root == null)
{
errors.Add("YAML root is not a mapping node.");
return new ValidationResult { Errors = errors, Warnings = warnings };
}
if (!HasKey(root, "services"))
errors.Add("Missing required top-level key: 'services'.");
if (!HasKey(root, "secrets"))
warnings.Add("Missing top-level key: 'secrets'. Secrets may not be available.");
if (HasKey(root, "services") && root.Children[new YamlScalarNode("services")] is YamlMappingNode services)
{
var presentServices = services.Children.Keys
.OfType<YamlScalarNode>()
.Select(k => k.Value!)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
// Determine required service suffixes
var requiredSuffixes = new[] { "-web", "-memcached", "-quickchart" };
var prefix = customerAbbrev ?? string.Empty;
if (!string.IsNullOrEmpty(prefix))
{
foreach (var suffix in requiredSuffixes)
{
if (!presentServices.Contains($"{prefix}{suffix}"))
errors.Add($"Missing required service: '{prefix}{suffix}'.");
}
}
else
{
// Fallback: at least check that there are web/memcached/quickchart services
if (!presentServices.Any(s => s.EndsWith("-web", StringComparison.OrdinalIgnoreCase)))
errors.Add("Missing a '-web' service.");
if (!presentServices.Any(s => s.EndsWith("-memcached", StringComparison.OrdinalIgnoreCase)))
errors.Add("Missing a '-memcached' service.");
}
foreach (var (key, value) in services.Children)
{
if (key is YamlScalarNode keyNode && value is YamlMappingNode svcNode)
{
if (!HasKey(svcNode, "image"))
errors.Add($"Service '{keyNode.Value}' is missing 'image'.");
}
}
}
if (HasKey(root, "secrets") && root.Children[new YamlScalarNode("secrets")] is YamlMappingNode secretsNode)
{
foreach (var (key, value) in secretsNode.Children)
{
if (value is YamlMappingNode secretNode)
{
if (!HasKey(secretNode, "external"))
warnings.Add($"Secret '{(key as YamlScalarNode)?.Value}' is not external.");
}
}
}
return new ValidationResult { Errors = errors, Warnings = warnings };
}
private static bool HasKey(YamlMappingNode node, string key)
=> node.Children.ContainsKey(new YamlScalarNode(key));
}
public class ValidationResult
{
public List<string> Errors { get; set; } = new();
public List<string> Warnings { get; set; } = new();
public bool IsValid => Errors.Count == 0;
}