feat(fc-devicemgmt): add Kubernetes deployment manifests (#1)
Sprint 8 IMPL lane Cx-5: fc-devicemgmt K8s manifests (rebased onto main 2026-05-18; 13 files, +944).
Namespace + Web Deployment (replicas:2, MySQL backend) + Operator Deployment (replicas:1, KubeOps leader-elect) + Service + Certificate (step-ca-acme ClusterIssuer) + Traefik IngressRoute (devices.iamworkin.lan internal) + ServiceAccount + ClusterRole + ClusterRoleBinding + NetworkPolicy (CNI DNAT-aware backend ports) + OnePasswordItem (5-field consolidated) + ArgoCD Application bootstrap shape + lint coverage.
Follow-ups (not merge blockers):
- localhost/fc-devicemgmt-{web,operator}:v20260512-cx5 must be imported to all 3 RKE2 nodes; pods will ErrImageNeverPull until imported.
- 1Password vault item 'FlowerCore DeviceManagement Runtime' must be created with 5 fields before pods can start.
- DNS devices.iamworkin.lan -> 10.0.56.200 already present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -420,6 +420,184 @@ public sealed class FleetManifestLintTests
|
||||
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",
|
||||
"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_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<string> ProbeViolations(
|
||||
ManifestDocument document,
|
||||
YamlMappingNode container,
|
||||
@@ -481,6 +659,13 @@ public sealed class FleetManifestLintTests
|
||||
return ManifestNodeExtensions.MappingSequence(container, "env")
|
||||
.SingleOrDefault(env => string.Equals(ManifestNodeExtensions.Scalar(env, "name"), name, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ManifestDocument> FcDeviceManagementDocuments()
|
||||
{
|
||||
return Inventory.Documents
|
||||
.Where(document => document.RelativePath.StartsWith("fc-devicemgmt/", StringComparison.Ordinal))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ManifestInventory
|
||||
|
||||
Reference in New Issue
Block a user