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) { "brochure.flowercore.io", "dist.flowercore.io", "dns.iamworkin.lan", }; // Public hosts that allow a tightly bounded write surface in addition to // GET/HEAD. updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id} // (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS โ€” but // PUT/PATCH/DELETE must still 404 at the route. Anything wider than this // set should fail this lint. // // PUB-1 (2026-05-06): update.flowercore.io / updates.flowercore.io were // added for the Cloudflare-proxied public Update Center edge. They use the // same bounded read-write allowlist as the LAN pair. private static readonly HashSet PublicReadWriteAllowlistHosts = new(StringComparer.Ordinal) { "updatecenter.iamworkin.lan", "updates.iamworkin.lan", "update.flowercore.io", "updates.flowercore.io", }; 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", }; private static readonly IReadOnlyDictionary LinuxRunnerRepos = new Dictionary(StringComparer.Ordinal) { ["github-runner"] = "https://github.com/astoltz/FlowerCore.Common", ["github-runner-sharedpos"] = "https://github.com/astoltz/FlowerCore.Shared.Pos", ["github-runner-puppet"] = "https://github.com/astoltz/FlowerCore.Puppet", ["github-runner-signage"] = "https://github.com/astoltz/FlowerCore.Signage", ["github-runner-dms"] = "https://github.com/astoltz/FlowerCore.DMS", ["github-runner-telephony"] = "https://github.com/astoltz/FlowerCore.Telephony", ["github-runner-print-web"] = "https://github.com/astoltz/FlowerCore.Print.Web", ["github-runner-chat"] = "https://github.com/astoltz/FlowerCore.Chat", ["github-runner-mysql"] = "https://github.com/astoltz/FlowerCore.MySQL", ["github-runner-kiosk-linux"] = "https://github.com/astoltz/FlowerCore.Kiosk.Linux", }; private static readonly HashSet ScaledLinuxRunnerDeployments = new(StringComparer.Ordinal) { "github-runner-sharedpos", "github-runner-puppet", "github-runner-signage", "github-runner-dms", "github-runner-telephony", "github-runner-print-web", "github-runner-chat", "github-runner-mysql", "github-runner-kiosk-linux", }; private static readonly IReadOnlyDictionary WritableRunnerEnv = new Dictionary(StringComparer.Ordinal) { ["HOME"] = "/home/runner", ["DOTNET_INSTALL_DIR"] = "/home/runner/.dotnet", ["DOTNET_CLI_HOME"] = "/home/runner", ["NUGET_PACKAGES"] = "/home/runner/.nuget/packages", ["XDG_CACHE_HOME"] = "/home/runner/.cache", ["RUNNER_TOOL_CACHE"] = "/home/runner/_tool", }; [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 PublicReadWriteIngressRoutes_MustPinGetHeadPostOptionsAllowlist() { // For hosts in PublicReadWriteAllowlistHosts, the route match MUST // contain Method(`GET`), Method(`HEAD`), Method(`POST`), and // Method(`OPTIONS`) AND MUST NOT contain Method(`PUT`), // Method(`PATCH`), or Method(`DELETE`). This keeps the public // allowlist invariant against regression โ€” see Track A's // updatecenter-web ingressroute hardening. 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 => PublicReadWriteAllowlistHosts.Any(host => entry.Match.Contains($"Host(`{host}`)", StringComparison.Ordinal))) .SelectMany(entry => { var localViolations = new List(); foreach (var required in new[] { "GET", "HEAD", "POST", "OPTIONS" }) { if (!entry.Match.Contains($"Method(`{required}`)", StringComparison.Ordinal)) { localViolations.Add($"{entry.Document.Descriptor} is missing required Method(`{required}`)."); } } foreach (var forbidden in new[] { "PUT", "PATCH", "DELETE" }) { if (entry.Match.Contains($"Method(`{forbidden}`)", StringComparison.Ordinal)) { localViolations.Add($"{entry.Document.Descriptor} must not include Method(`{forbidden}`) on a public host."); } } return localViolations; }) .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 GitHubRunnerFleet_MustRegisterRequiredReposAsRepoScopedDeployments() { var deployments = GitHubRunnerDeployments(); foreach (var expectedRunner in LinuxRunnerRepos) { deployments.Should().ContainKey(expectedRunner.Key); var container = deployments[expectedRunner.Key].ContainerMappings().Should().ContainSingle().Subject; EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value); EnvValue(container, "EPHEMERAL").Should().Be("true"); EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux"); EnvValue(container, "RUN_AS_ROOT").Should().Be("false"); EnvValue(container, "ACCESS_TOKEN").Should().BeNull("ACCESS_TOKEN must come from github-runner-token Secret, not a literal"); EnvSecretName(container, "ACCESS_TOKEN").Should().Be("github-runner-token"); EnvSecretKey(container, "ACCESS_TOKEN").Should().Be("credential"); } } [Fact] public void GitHubRunnerFleet_MustSetWritableNonRootDotnetAndCachePaths() { foreach (var deployment in GitHubRunnerDeployments().Values) { var container = deployment.ContainerMappings().Should().ContainSingle().Subject; foreach (var expectedEnv in WritableRunnerEnv) { EnvValue(container, expectedEnv.Key).Should().Be(expectedEnv.Value, $"{deployment.Name} must keep .NET paths writable for uid 1001"); } var mounts = ManifestNodeExtensions.MappingSequence(container, "volumeMounts") .ToDictionary( mount => ManifestNodeExtensions.Scalar(mount, "name") ?? string.Empty, mount => ManifestNodeExtensions.Scalar(mount, "mountPath") ?? string.Empty, StringComparer.Ordinal); mounts.Should().Contain("runner-home", "/home/runner"); mounts.Should().Contain("nuget-cache", "/home/runner/.nuget/packages"); mounts.Should().Contain("tmp", "/tmp"); } } [Fact] public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForScaledDeployments() { var deployments = GitHubRunnerDeployments(); foreach (var deploymentName in ScaledLinuxRunnerDeployments) { var deployment = deployments[deploymentName]; ReplicaCount(deployment).Should().Be(2); var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes"); var claimNames = volumes .Select(volume => ManifestNodeExtensions.Scalar(volume, "persistentVolumeClaim", "claimName")) .Where(value => !string.IsNullOrWhiteSpace(value)) .ToList(); claimNames.Should().BeEmpty($"{deploymentName} is scaled and must not share a RWO PVC"); volumes.Should().Contain(volume => string.Equals(ManifestNodeExtensions.Scalar(volume, "name"), "nuget-cache", StringComparison.Ordinal) && ManifestNodeExtensions.Mapping(volume, "emptyDir") != null); } var common = deployments["github-runner"]; ReplicaCount(common).Should().Be(1); common.MappingSequence("spec", "template", "spec", "volumes") .Select(volume => ManifestNodeExtensions.Scalar(volume, "persistentVolumeClaim", "claimName")) .Where(value => !string.IsNullOrWhiteSpace(value)) .Should() .ContainSingle() .Which .Should() .Be("github-runner-nuget-cache"); } [Fact] public void Monitoring_MustAlertWhenLinuxRunnerDeploymentIsUnavailable() { var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml")); monitoring.Should().Contain("MacMiniRunnerOffline"); monitoring.Should().Contain("LinuxRunnerOffline"); monitoring.Should().Contain("kube_deployment_status_replicas_ready"); monitoring.Should().Contain("github-runner(|-(sharedpos|puppet|signage|dms|telephony|print-web|chat|mysql|kiosk-linux))"); monitoring.Should().Contain("folder: CI Alerts"); monitoring.Should().Contain("uid: linux-runner-offline"); monitoring.Should().Contain("alert_channel: irc"); } [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(); } [Fact] public void FcDeviceManagement_MustShipExpectedManifestSet() { var appRoot = Path.Combine(Inventory.BluejayRoot, "apps", "fc-devicemgmt"); Directory.Exists(appRoot).Should().BeTrue("Sprint 8 Cx-5 owns apps/fc-devicemgmt."); var expectedFiles = new[] { "1password-item.yaml", "argocd-application.yaml", "certificate-web.yaml", "clusterissuer-step-ca-agent.yaml", "clusterrole-operator.yaml", "clusterrolebinding-operator.yaml", "deployment-operator.yaml", "deployment-web.yaml", "ingressroute-web.yaml", "namespace.yaml", "network-policy.yaml", "service-web.yaml", "serviceaccount-operator.yaml", }; Directory.GetFiles(appRoot, "*.yaml") .Select(Path.GetFileName) .Should() .BeEquivalentTo(expectedFiles); foreach (var expectedFile in expectedFiles) { FcDeviceManagementDocuments() .Should() .Contain(document => document.RelativePath == $"fc-devicemgmt/{expectedFile}"); } } [Fact] public void FcDeviceManagement_ObjectsMustCarryStandardTraceabilityLabels() { var requiredLabels = new[] { "app.kubernetes.io/name", "app.kubernetes.io/part-of", "app.kubernetes.io/managed-by", "flowercore.io/tenant-id", "flowercore.io/created-by", }; var violations = FcDeviceManagementDocuments() .SelectMany(document => requiredLabels .Where(label => string.IsNullOrWhiteSpace(document.Scalar("metadata", "labels", label))) .Select(label => $"{document.Descriptor} is missing metadata.labels['{label}'].")) .Concat(FcDeviceManagementDocuments() .Where(document => document.Kind == "Deployment") .SelectMany(document => requiredLabels .Where(label => string.IsNullOrWhiteSpace(document.Scalar("spec", "template", "metadata", "labels", label))) .Select(label => $"{document.Descriptor} pod template is missing metadata.labels['{label}']."))) .Concat(FcDeviceManagementDocuments() .Where(document => document.Kind == "Deployment") .Where(document => string.IsNullOrWhiteSpace(document.Scalar("spec", "template", "metadata", "annotations", "flowercore.io/audit-trace-id"))) .Select(document => $"{document.Descriptor} pod template is missing flowercore.io/audit-trace-id.")) .ToList(); violations.Should().BeEmpty(); } [Fact] public void FcDeviceManagement_IngressMustUseCertManagerAndKeepPublicHostDisabled() { var appText = string.Join( Environment.NewLine, Directory.GetFiles(Path.Combine(Inventory.BluejayRoot, "apps", "fc-devicemgmt"), "*.yaml") .Select(File.ReadAllText)); appText.Should().NotContain("certResolver"); appText.Should().Contain("update.flowercore.io"); appText.Should().Contain("disabled-until-Q-OIDC-1"); FcDeviceManagementDocuments() .Where(document => document.Kind == "IngressRoute") .SelectMany(document => document.MappingSequence("spec", "routes")) .Select(route => ManifestNodeExtensions.Scalar(route, "match") ?? string.Empty) .Should() .Contain(match => match.Contains("Host(`devices.iamworkin.lan`)", StringComparison.Ordinal)) .And.NotContain(match => match.Contains("Host(`update.flowercore.io`)", StringComparison.Ordinal)); var certificate = FcDeviceManagementDocuments() .Single(document => document.Kind == "Certificate" && document.Name == "fc-devicemgmt-web-tls"); certificate.Scalar("spec", "issuerRef", "name").Should().Be("step-ca-acme"); certificate.Scalar("spec", "issuerRef", "kind").Should().Be("ClusterIssuer"); ManifestNodeExtensions.ScalarSequence(certificate.Root, "spec", "dnsNames") .Should() .ContainSingle("devices.iamworkin.lan"); } [Fact] public void FcDeviceManagement_StepCaAgentIssuerMustTargetNocProvisioner() { var issuer = FcDeviceManagementDocuments() .Single(document => document.Kind == "StepClusterIssuer" && document.Name == "step-ca-agent"); issuer.Scalar("apiVersion").Should().Be("certmanager.step.sm/v1beta1"); issuer.Scalar("spec", "url").Should().Be("https://10.0.56.10:9443"); issuer.Scalar("spec", "caBundle").Should().NotBeNullOrWhiteSpace(); issuer.Scalar("spec", "provisioner", "name").Should().Be("step-ca-agent"); issuer.Scalar("spec", "provisioner", "kid").Should().Be("RF3A9welUYVOWBX8tr19aWyA2kQlxoGZN1dRwTElUEM"); } [Fact] public void FcDeviceManagement_StepCaAgentIssuerMustReferencePasswordSecretOnly() { var issuer = FcDeviceManagementDocuments() .Single(document => document.Kind == "StepClusterIssuer" && document.Name == "step-ca-agent"); issuer.Scalar("spec", "provisioner", "passwordRef", "name") .Should() .Be("step-ca-agent-provisioner-password"); issuer.Scalar("spec", "provisioner", "passwordRef", "namespace").Should().Be("cert-manager"); issuer.Scalar("spec", "provisioner", "passwordRef", "key").Should().Be("password"); var issuerText = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "fc-devicemgmt", "clusterissuer-step-ca-agent.yaml")); issuerText.Should().NotContain("stringData:"); issuerText.Should().NotContain("password:"); issuerText.Should().NotContain("privateKey"); } [Fact] public void FcDeviceManagement_StepCaAgentIssuerMustCarryTraceabilityMetadata() { var issuer = FcDeviceManagementDocuments() .Single(document => document.Kind == "StepClusterIssuer" && document.Name == "step-ca-agent"); issuer.Scalar("metadata", "labels", "app.kubernetes.io/managed-by").Should().Be("argocd"); issuer.Scalar("metadata", "labels", "flowercore.io/tenant-id").Should().Be("system"); issuer.Scalar("metadata", "annotations", "flowercore.io/provisioner-source") .Should() .Be("profile::pki::stepca"); issuer.Scalar("metadata", "annotations", "flowercore.io/secret-source") .Should() .Be("cert-manager/step-ca-agent-provisioner-password"); } [Fact] public void FcDeviceManagement_OperatorRbacMustCoverDevicesAndOwnerLookup() { var clusterRole = FcDeviceManagementDocuments() .Single(document => document.Kind == "ClusterRole" && document.Name == "fc-devicemgmt-operator"); var allScalars = clusterRole.AllScalars().ToList(); allScalars.Should().Contain("devices.flowercore.io"); allScalars.Should().Contain("*"); allScalars.Should().Contain("deployments"); allScalars.Should().Contain("get"); var operatorDeployment = FcDeviceManagementDocuments() .Single(document => document.Kind == "Deployment" && document.Name == "fc-devicemgmt-operator"); operatorDeployment.AllScalars().Should().Contain("FLOWERCORE_KUBERNETES_OWNER_DEPLOYMENT"); operatorDeployment.AllScalars().Should().Contain("fc-devicemgmt-operator"); } [Fact] public void FcDeviceManagement_RuntimeSecretsMustUseOnePasswordItemPattern() { var item = FcDeviceManagementDocuments() .Single(document => document.Kind == "OnePasswordItem" && document.Name == "fc-devicemgmt-runtime"); item.Scalar("spec", "itemPath") .Should() .Be("vaults/IAmWorkin/items/FlowerCore DeviceManagement Runtime"); var appText = string.Join( Environment.NewLine, Directory.GetFiles(Path.Combine(Inventory.BluejayRoot, "apps", "fc-devicemgmt"), "*.yaml") .Select(File.ReadAllText)); FcDeviceManagementDocuments().Should().NotContain(document => document.Kind == "Secret"); appText.Should().Contain("secretKeyRef:"); appText.Should().Contain("secretName: fc-devicemgmt-runtime"); appText.Should().NotContain("stringData:"); appText.Should().NotContain("from-literal"); appText.Should().NotContain("tls.key:"); } [Fact] public void FcDeviceManagement_NetworkPoliciesMustAllowLanAgentsSynologyAndDnatPorts() { var policies = FcDeviceManagementDocuments() .Where(document => document.Kind == "NetworkPolicy") .ToList(); policies.Should().HaveCount(2); var combinedScalars = policies.SelectMany(policy => policy.AllScalars()).ToList(); combinedScalars.Should().Contain("10.0.56.0/24"); combinedScalars.Should().Contain("10.0.57.0/24"); combinedScalars.Should().Contain("10.0.58.0/24"); combinedScalars.Should().Contain("10.0.68.0/27"); combinedScalars.Should().Contain("10.0.58.3/32"); var combinedEgressPorts = policies.SelectMany(policy => policy.EgressPorts()).ToHashSet(StringComparer.Ordinal); combinedEgressPorts.Should().Contain(new[] { "80", "443", "8080", "8443", "2049", "111" }); var traefikVipPolicies = policies .Where(policy => policy.AllScalars().Any(value => value.Contains("10.0.56.200", StringComparison.Ordinal))) .ToList(); traefikVipPolicies.Should().ContainSingle(); traefikVipPolicies[0].EgressPorts().Should().Contain(new[] { "80", "443", "8080", "8443" }); } [Fact] public void FcDeviceManagement_ArgocdApplicationMustMatchApplicationSetDiscoveryConventions() { var application = FcDeviceManagementDocuments() .Single(document => document.Kind == "Application" && document.Name == "infra-fc-devicemgmt"); application.Namespace.Should().Be("argocd"); application.Scalar("spec", "source", "repoURL") .Should() .Be("http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git"); application.Scalar("spec", "source", "path").Should().Be("apps/fc-devicemgmt"); application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt"); } 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.", }; } private static IReadOnlyDictionary GitHubRunnerDeployments() { return Inventory.Documents .Where(document => document.Kind == "Deployment") .Where(document => document.Namespace == "github-runner") .ToDictionary(document => document.Name, StringComparer.Ordinal); } private static int ReplicaCount(ManifestDocument document) { return int.TryParse(document.Scalar("spec", "replicas"), out var replicas) ? replicas : 1; } private static string? EnvValue(YamlMappingNode container, string name) { return EnvMapping(container, name) is { } env ? ManifestNodeExtensions.Scalar(env, "value") : null; } private static string? EnvSecretName(YamlMappingNode container, string name) { return EnvMapping(container, name) is { } env ? ManifestNodeExtensions.Scalar(env, "valueFrom", "secretKeyRef", "name") : null; } private static string? EnvSecretKey(YamlMappingNode container, string name) { return EnvMapping(container, name) is { } env ? ManifestNodeExtensions.Scalar(env, "valueFrom", "secretKeyRef", "key") : null; } private static YamlMappingNode? EnvMapping(YamlMappingNode container, string name) { return ManifestNodeExtensions.MappingSequence(container, "env") .SingleOrDefault(env => string.Equals(ManifestNodeExtensions.Scalar(env, "name"), name, StringComparison.Ordinal)); } private static IReadOnlyList FcDeviceManagementDocuments() { return Inventory.Documents .Where(document => document.RelativePath.StartsWith("fc-devicemgmt/", StringComparison.Ordinal)) .ToList(); } } 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"), // FlowerCore.Notes/k8s/selenium/ is the live Selenium Grid // manifest tree (consumed by deploy-selenium scripts). // FlowerCore.Notes/k8s/guacamole/ + FlowerCore.Notes/k8s/monitoring/ // are historical scaffolds that have diverged from the live state // (bluejay-infra/apps/guacamole + bluejay-infra/apps/monitoring are // canonical). Operator review is required before bringing them in // line OR decommissioning them โ€” keep them out of the lint scope // until that decision lands. See xxl-regroup-2026-05-03-followup.md // "Codex 7 ยง0 stop conditions" + the C7 close-session output. Path.Combine(workspaceRoot, "FlowerCore.Notes", "k8s", "selenium"), 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; } }