using FluentAssertions; using System.Text.RegularExpressions; using Xunit; using YamlDotNet.Core; using YamlDotNet.RepresentationModel; namespace BluejayInfraLint.Tests; [Trait("Category", "Unit")] public sealed class FleetManifestLintTests { private static readonly ManifestInventory Inventory = ManifestInventory.Load(); private static readonly HashSet PublicReadOnlyHosts = new(StringComparer.Ordinal) { "dist.flowercore.io", "dns.iamworkin.lan", }; private static readonly HashSet ApiKeyProtectedDeployments = new(StringComparer.Ordinal) { "messageboard-web", "scoreboard-web", "segmentdisplay-web", "signalcontrol-web", }; private static readonly HashSet PublicEgressDeployments = new(StringComparer.Ordinal) { "asterisk", "fc-llm-bridge", "mysql-web", "php-web", "ttsreader-align", "ttsreader-kokoro", "ttsreader-modern", "ttsreader-piper", }; [Fact] public void IngressRoutes_MustKeepServiceReferencesInTheSameNamespace() { var violations = Inventory.Documents .Where(document => document.Kind == "IngressRoute") .SelectMany(document => document.MappingSequence("spec", "routes") .SelectMany(route => route.MappingSequence("services") .Select(service => new { Document = document, ServiceName = ManifestNodeExtensions.Scalar(service, "name"), ServiceNamespace = ManifestNodeExtensions.Scalar(service, "namespace"), }))) .Where(entry => !string.IsNullOrWhiteSpace(entry.ServiceNamespace)) .Where(entry => !string.Equals(entry.ServiceNamespace, entry.Document.Namespace, StringComparison.Ordinal)) .Select(entry => $"{entry.Document.Descriptor} references Service '{entry.ServiceName}' in namespace '{entry.ServiceNamespace}'.") .ToList(); violations.Should().BeEmpty(); } [Fact] public void PublicReadOnlyIngressRoutes_MustExplicitlyAllowOnlyGetAndHead() { var violations = Inventory.Documents .Where(document => document.Kind == "IngressRoute") .SelectMany(document => document.MappingSequence("spec", "routes") .Select(route => new { Document = document, Match = ManifestNodeExtensions.Scalar(route, "match") ?? string.Empty, })) .Where(entry => PublicReadOnlyHosts.Any(host => entry.Match.Contains($"Host(`{host}`)", StringComparison.Ordinal))) .Where(entry => !entry.Match.Contains("Method(`GET`)", StringComparison.Ordinal) || !entry.Match.Contains("Method(`HEAD`)", StringComparison.Ordinal)) .Select(entry => $"{entry.Document.Descriptor} is missing an explicit GET/HEAD method allowlist.") .ToList(); violations.Should().BeEmpty(); } [Fact] public void TraefikVipNetworkPolicies_MustAllowPostDnatBackendPorts() { var violations = Inventory.Documents .Where(document => document.Kind == "NetworkPolicy") .Where(document => document.AllScalars().Any(value => value.Contains("10.0.56.200", StringComparison.Ordinal))) .SelectMany(document => { var ports = document.EgressPorts().ToHashSet(StringComparer.Ordinal); var localViolations = new List(); if (ports.Contains("443") && !ports.Contains("8443")) { localViolations.Add($"{document.Descriptor} allows Traefik VIP 443 without backend port 8443."); } if (ports.Contains("80") && !ports.Contains("8000") && !ports.Contains("8080")) { localViolations.Add($"{document.Descriptor} allows Traefik VIP 80 without a backend HTTP port (8000/8080)."); } return localViolations; }) .ToList(); violations.Should().BeEmpty(); } [Fact] public void ApiKeyProtectedDeployments_MustUseTcpSocketHealthProbes() { var violations = Inventory.Documents .Where(document => document.Kind == "Deployment") .Where(document => ApiKeyProtectedDeployments.Contains(document.Name)) .SelectMany(document => document.ContainerMappings().SelectMany(container => ProbeViolations(document, container, "readinessProbe") .Concat(ProbeViolations(document, container, "livenessProbe")))) .ToList(); violations.Should().BeEmpty(); } [Fact] public void StatefulSets_WithVolumeClaimTemplates_MustDeclareFilesystemDefaults() { var violations = Inventory.Documents .Where(document => document.Kind == "StatefulSet") .Where(document => document.MappingSequence("spec", "volumeClaimTemplates").Count > 0) .SelectMany(document => { var localViolations = new List(); if (string.IsNullOrWhiteSpace(document.Scalar("spec", "podManagementPolicy"))) { localViolations.Add($"{document.Descriptor} is missing spec.podManagementPolicy."); } if (string.IsNullOrWhiteSpace(document.Scalar("spec", "revisionHistoryLimit"))) { localViolations.Add($"{document.Descriptor} is missing spec.revisionHistoryLimit."); } foreach (var claimTemplate in document.MappingSequence("spec", "volumeClaimTemplates")) { if (!string.Equals( ManifestNodeExtensions.Scalar(claimTemplate, "spec", "volumeMode"), "Filesystem", StringComparison.Ordinal)) { var claimName = ManifestNodeExtensions.Scalar(claimTemplate, "metadata", "name") ?? ""; localViolations.Add($"{document.Descriptor} volumeClaimTemplate '{claimName}' is missing volumeMode: Filesystem."); } } return localViolations; }) .ToList(); violations.Should().BeEmpty(); } [Fact] public void LocallyImportedImages_MustUseLocalhostPrefixAndNeverPullPolicy() { var violations = Inventory.Documents .Where(document => document.PodSpec() is not null) .SelectMany(document => document.ContainerSpecs() .Where(container => !string.IsNullOrWhiteSpace(container.Image)) .Select(container => new { Document = document, Container = container, })) .Where(entry => (entry.Container.Image.StartsWith("localhost/", StringComparison.Ordinal) && !string.Equals(entry.Container.ImagePullPolicy, "Never", StringComparison.Ordinal)) || (entry.Container.Image.StartsWith("fc-", StringComparison.Ordinal) && !entry.Container.Image.Contains('/', StringComparison.Ordinal))) .Select(entry => { if (entry.Container.Image.StartsWith("localhost/", StringComparison.Ordinal)) { return $"{entry.Document.Descriptor} container '{entry.Container.Name}' uses {entry.Container.Image} without imagePullPolicy: Never."; } return $"{entry.Document.Descriptor} container '{entry.Container.Name}' uses non-local image '{entry.Container.Image}' for a node-imported FlowerCore workload."; }) .ToList(); violations.Should().BeEmpty(); } [Fact] public void PublicEgressDeployments_MustOptOutOfIamworkinLanSearchSuffixes() { var violations = Inventory.Documents .Where(document => document.PodSpec() is not null) .Where(document => PublicEgressDeployments.Contains(document.Name)) .SelectMany(document => { var localViolations = new List(); var podSpec = document.PodSpec()!; var dnsPolicy = ManifestNodeExtensions.Scalar(podSpec, "dnsPolicy"); var searches = ManifestNodeExtensions.ScalarSequence(podSpec, "dnsConfig", "searches").ToList(); if (!string.Equals(dnsPolicy, "None", StringComparison.Ordinal)) { localViolations.Add($"{document.Descriptor} is missing dnsPolicy: None."); } if (searches.Count == 0) { localViolations.Add($"{document.Descriptor} is missing dnsConfig.searches."); } else if (searches.Any(search => search.Contains("iamworkin.lan", StringComparison.OrdinalIgnoreCase))) { localViolations.Add($"{document.Descriptor} still includes iamworkin.lan in dnsConfig.searches."); } return localViolations; }) .ToList(); violations.Should().BeEmpty(); } private static IEnumerable ProbeViolations( ManifestDocument document, YamlMappingNode container, string probeKey) { if (!ManifestNodeExtensions.TryGetMapping(container, probeKey, out var probe) || !ManifestNodeExtensions.TryGetMapping(probe, "httpGet", out var httpGet)) { return Array.Empty(); } var path = ManifestNodeExtensions.Scalar(httpGet, "path"); if (!string.Equals(path, "/health", StringComparison.Ordinal)) { return Array.Empty(); } var containerName = ManifestNodeExtensions.Scalar(container, "name") ?? ""; return new[] { $"{document.Descriptor} container '{containerName}' still uses {probeKey}.httpGet on /health.", }; } } internal sealed class ManifestInventory { private ManifestInventory(string workspaceRoot, string bluejayRoot, IReadOnlyList documents) { WorkspaceRoot = workspaceRoot; BluejayRoot = bluejayRoot; Documents = documents; } public string WorkspaceRoot { get; } public string BluejayRoot { get; } public IReadOnlyList Documents { get; } public static ManifestInventory Load() { var bluejayRoot = FindBluejayInfraRoot(); var workspaceRoot = Directory.GetParent(bluejayRoot)?.FullName ?? throw new DirectoryNotFoundException($"Could not resolve workspace root from '{bluejayRoot}'."); var documents = ManifestRoots(workspaceRoot, bluejayRoot) .SelectMany(LoadDocumentsFromRoot) .ToList(); return new ManifestInventory(workspaceRoot, bluejayRoot, documents); } private static string FindBluejayInfraRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory); while (current is not null) { if (Directory.Exists(Path.Combine(current.FullName, "apps")) && File.Exists(Path.Combine(current.FullName, "README.md"))) { return current.FullName; } current = current.Parent; } throw new DirectoryNotFoundException("Could not find the bluejay-infra repository root from the test output directory."); } private static IEnumerable ManifestRoots(string workspaceRoot, string bluejayRoot) { var roots = new[] { Path.Combine(bluejayRoot, "apps"), Path.Combine(workspaceRoot, "FlowerCore.Chat", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.DMS", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.DNS", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.Intranet.Web", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.Kiosk", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.Media", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.MenuBoard", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.MessageBoard", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.MySQL", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.PHP", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.Presentations", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.Print.Web", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.RemoteDesktop", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.Scoreboard", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.SegmentDisplay", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.SignalControl", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.TtsReader", "k8s"), Path.Combine(workspaceRoot, "FlowerCore.Updater", "k8s"), }; return roots.Where(Directory.Exists); } private static IEnumerable LoadDocumentsFromRoot(string root) { foreach (var filePath in Directory.EnumerateFiles(root, "*.yaml", SearchOption.AllDirectories)) { var fileText = File.ReadAllText(filePath); var segments = SplitManifestDocuments(fileText); for (var index = 0; index < segments.Count; index++) { var yaml = new YamlStream(); try { using var reader = new StringReader(segments[index]); yaml.Load(reader); } catch (YamlException exception) { _ = exception; continue; } if (yaml.Documents.Count == 0) { continue; } if (yaml.Documents[0].RootNode is YamlMappingNode mapping && ManifestNodeExtensions.Scalar(mapping, "kind") is not null) { yield return new ManifestDocument(root, filePath, index, fileText, mapping); } } } } private static IReadOnlyList SplitManifestDocuments(string fileText) { var documents = new List(); var currentLines = new List(); var seenApiVersion = false; foreach (var line in Regex.Split(fileText, @"\r?\n")) { if (Regex.IsMatch(line, @"^\s*---\s*$")) { FlushCurrentDocument(); continue; } if (Regex.IsMatch(line, @"^\s*apiVersion:\s*") && seenApiVersion && currentLines.Any(existing => !string.IsNullOrWhiteSpace(existing))) { FlushCurrentDocument(); } currentLines.Add(line); if (Regex.IsMatch(line, @"^\s*apiVersion:\s*")) { seenApiVersion = true; } } FlushCurrentDocument(); return documents; void FlushCurrentDocument() { var text = string.Join(Environment.NewLine, currentLines).Trim(); if (!string.IsNullOrWhiteSpace(text)) { documents.Add(text); } currentLines.Clear(); seenApiVersion = false; } } } internal sealed record ManifestDocument( string RootPath, string FilePath, int DocumentIndex, string FileText, YamlMappingNode Root) { public string Kind => Scalar("kind") ?? string.Empty; public string Name => Scalar("metadata", "name") ?? $"document-{DocumentIndex}"; public string Namespace => Scalar("metadata", "namespace") ?? string.Empty; public string RelativePath => Path.GetRelativePath(RootPath, FilePath).Replace('\\', '/'); public string Descriptor => $"{Kind} {Namespace}/{Name} [{RelativePath}#{DocumentIndex + 1}]"; public string? Scalar(params string[] path) => ManifestNodeExtensions.Scalar(Root, path); public IReadOnlyList MappingSequence(params string[] path) => ManifestNodeExtensions.MappingSequence(Root, path); public IEnumerable AllScalars() => ManifestNodeExtensions.AllScalars(Root); public IReadOnlyList EgressPorts() { return MappingSequence("spec", "egress") .SelectMany(egressRule => ManifestNodeExtensions.MappingSequence(egressRule, "ports")) .Select(portMapping => ManifestNodeExtensions.Scalar(portMapping, "port")) .Where(value => !string.IsNullOrWhiteSpace(value)) .Cast() .ToList(); } public YamlMappingNode? PodSpec() { return Kind switch { "Deployment" or "StatefulSet" or "DaemonSet" or "Job" => ManifestNodeExtensions.Mapping(Root, "spec", "template", "spec"), "CronJob" => ManifestNodeExtensions.Mapping(Root, "spec", "jobTemplate", "spec", "template", "spec"), _ => null, }; } public IReadOnlyList ContainerMappings() { var podSpec = PodSpec(); if (podSpec is null) { return Array.Empty(); } return ManifestNodeExtensions.MappingSequence(podSpec, "containers") .Concat(ManifestNodeExtensions.MappingSequence(podSpec, "initContainers")) .ToList(); } public IReadOnlyList ContainerSpecs() { return ContainerMappings() .Select(container => new ContainerSpec( ManifestNodeExtensions.Scalar(container, "name") ?? "", ManifestNodeExtensions.Scalar(container, "image") ?? string.Empty, ManifestNodeExtensions.Scalar(container, "imagePullPolicy") ?? string.Empty)) .ToList(); } } internal sealed record ContainerSpec(string Name, string Image, string ImagePullPolicy); internal static class ManifestNodeExtensions { public static string? Scalar(this YamlMappingNode mapping, params string[] path) { return TryGetNode(mapping, path, out var node) && node is YamlScalarNode scalar ? scalar.Value : null; } public static YamlMappingNode? Mapping(this YamlMappingNode mapping, params string[] path) { return TryGetNode(mapping, path, out var node) ? node as YamlMappingNode : null; } public static bool TryGetMapping(this YamlMappingNode mapping, string key, out YamlMappingNode result) { if (TryGetChild(mapping, key, out var child) && child is YamlMappingNode childMapping) { result = childMapping; return true; } result = null!; return false; } public static IReadOnlyList MappingSequence(this YamlMappingNode mapping, params string[] path) { return TryGetNode(mapping, path, out var node) && node is YamlSequenceNode sequence ? sequence.Children.OfType().ToList() : Array.Empty(); } public static IReadOnlyList ScalarSequence(this YamlMappingNode mapping, params string[] path) { return TryGetNode(mapping, path, out var node) && node is YamlSequenceNode sequence ? sequence.Children.OfType() .Select(child => child.Value) .Where(value => !string.IsNullOrWhiteSpace(value)) .Cast() .ToList() : Array.Empty(); } public static IEnumerable AllScalars(YamlNode node) { return node switch { YamlScalarNode scalar when !string.IsNullOrWhiteSpace(scalar.Value) => new[] { scalar.Value! }, YamlSequenceNode sequence => sequence.Children.SelectMany(AllScalars), YamlMappingNode mapping => mapping.Children.SelectMany(entry => AllScalars(entry.Key).Concat(AllScalars(entry.Value))), _ => Array.Empty(), }; } private static bool TryGetNode(YamlMappingNode mapping, IReadOnlyList path, out YamlNode node) { YamlNode current = mapping; foreach (var segment in path) { if (current is not YamlMappingNode currentMapping || !TryGetChild(currentMapping, segment, out current)) { node = null!; return false; } } node = current; return true; } private static bool TryGetChild(YamlMappingNode mapping, string key, out YamlNode value) { foreach (var entry in mapping.Children) { if (entry.Key is YamlScalarNode scalar && string.Equals(scalar.Value, key, StringComparison.Ordinal)) { value = entry.Value; return true; } } value = null!; return false; } }