diff --git a/apps/fc-devicemgmt/1password-item.yaml b/apps/fc-devicemgmt/1password-item.yaml new file mode 100644 index 0000000..748bee5 --- /dev/null +++ b/apps/fc-devicemgmt/1password-item.yaml @@ -0,0 +1,26 @@ +# Runtime secrets for FlowerCore.DeviceManagement. +# +# OnePasswordItem operator syncs this item into a Kubernetes Secret with the +# same name. Expected fields: +# DB-Password +# mtls-ca.pem +# mtls-client.crt +# mtls-client.key +# mtls-chain.pem +# +# Do not add literal secret values to this repo. Runtime pods consume the +# synced Secret through env vars and read-only mounts. +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: fc-devicemgmt-runtime + namespace: fc-devicemgmt + labels: + app.kubernetes.io/name: fc-devicemgmt + app.kubernetes.io/component: secrets + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore DeviceManagement Runtime" diff --git a/apps/fc-devicemgmt/argocd-application.yaml b/apps/fc-devicemgmt/argocd-application.yaml new file mode 100644 index 0000000..1e7ede8 --- /dev/null +++ b/apps/fc-devicemgmt/argocd-application.yaml @@ -0,0 +1,33 @@ +# Explicit ArgoCD Application shape for bootstrap/review. +# +# The live bluejay-infra ApplicationSet already discovers apps/* directories +# and creates this same Application name (`infra-fc-devicemgmt`) automatically. +# Keep repoURL on the internal Gitea ClusterIP URL; ArgoCD does not trust the +# external step-ca HTTPS endpoint. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: infra-fc-devicemgmt + namespace: argocd + labels: + app.kubernetes.io/name: fc-devicemgmt + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +spec: + project: default + source: + repoURL: http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git + targetRevision: main + path: apps/fc-devicemgmt + destination: + server: https://kubernetes.default.svc + namespace: fc-devicemgmt + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/apps/fc-devicemgmt/certificate-web.yaml b/apps/fc-devicemgmt/certificate-web.yaml new file mode 100644 index 0000000..4d23c20 --- /dev/null +++ b/apps/fc-devicemgmt/certificate-web.yaml @@ -0,0 +1,30 @@ +# Certificate for devices.iamworkin.lan. +# +# Preflight gate: FlowerCore.DNS / pfSense must contain an explicit A record: +# devices.iamworkin.lan -> 10.0.56.200 +# before this Certificate is synced. step-ca ACME cannot see the CoreDNS +# wildcard, so missing pfSense DNS produces cert-manager HTTP-01 backoff +# (feedback_pfsense_dns_required_for_acme). +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: fc-devicemgmt-web-tls + namespace: fc-devicemgmt + labels: + app.kubernetes.io/name: fc-devicemgmt-web + app.kubernetes.io/component: web + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra + annotations: + flowercore.io/dns-preflight: "devices.iamworkin.lan must resolve to 10.0.56.200 before ACME sync" +spec: + secretName: fc-devicemgmt-web-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - devices.iamworkin.lan + duration: 720h + renewBefore: 240h diff --git a/apps/fc-devicemgmt/clusterrole-operator.yaml b/apps/fc-devicemgmt/clusterrole-operator.yaml new file mode 100644 index 0000000..400f91f --- /dev/null +++ b/apps/fc-devicemgmt/clusterrole-operator.yaml @@ -0,0 +1,81 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: fc-devicemgmt-operator + labels: + app.kubernetes.io/name: fc-devicemgmt-operator + app.kubernetes.io/component: operator + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +rules: + - apiGroups: + - devices.flowercore.io + resources: + - '*' + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - devices.flowercore.io + resources: + - devices/status + - devices/finalizers + - devicegroups/status + - devicegroups/finalizers + - devicepolicies/status + - devicepolicies/finalizers + - remotecommands/status + - remotecommands/finalizers + verbs: + - get + - update + - patch + - apiGroups: + - apps + resources: + - deployments + verbs: + - get + - apiGroups: + - "" + resources: + - pods + - services + - configmaps + - secrets + - events + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - watch diff --git a/apps/fc-devicemgmt/clusterrolebinding-operator.yaml b/apps/fc-devicemgmt/clusterrolebinding-operator.yaml new file mode 100644 index 0000000..2b85897 --- /dev/null +++ b/apps/fc-devicemgmt/clusterrolebinding-operator.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: fc-devicemgmt-operator + labels: + app.kubernetes.io/name: fc-devicemgmt-operator + app.kubernetes.io/component: operator + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: fc-devicemgmt-operator +subjects: + - kind: ServiceAccount + name: fc-devicemgmt-operator + namespace: fc-devicemgmt diff --git a/apps/fc-devicemgmt/deployment-operator.yaml b/apps/fc-devicemgmt/deployment-operator.yaml new file mode 100644 index 0000000..a4ec080 --- /dev/null +++ b/apps/fc-devicemgmt/deployment-operator.yaml @@ -0,0 +1,109 @@ +# FlowerCore.DeviceManagement Operator. +# +# KubeOps controller for devices.flowercore.io resources. Operator-created +# children must set OwnerReferences + traceability labels/annotations per +# k8s-pod-ownership-and-traceability-standard.md. RBAC below grants +# apps/deployments/get so the process can resolve its own Deployment UID. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fc-devicemgmt-operator + namespace: fc-devicemgmt + labels: + app: fc-devicemgmt-operator + app.kubernetes.io/name: fc-devicemgmt-operator + app.kubernetes.io/component: operator + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra + annotations: + flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + app: fc-devicemgmt-operator + template: + metadata: + labels: + app: fc-devicemgmt-operator + app.kubernetes.io/name: fc-devicemgmt-operator + app.kubernetes.io/component: operator + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/metrics" + flowercore.io/audit-trace-id: "runtime-activity-trace" + spec: + serviceAccountName: fc-devicemgmt-operator + securityContext: + fsGroup: 1654 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: operator + image: localhost/fc-devicemgmt-operator:v20260512-cx5 + imagePullPolicy: Never + ports: + - name: metrics + containerPort: 8080 + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT + value: "false" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: FLOWERCORE_KUBERNETES_OWNER_DEPLOYMENT + value: "fc-devicemgmt-operator" + - name: FlowerCore__Service__Name + value: "FlowerCore.DeviceManagement.Operator" + - name: FlowerCore__DeviceManagement__DefaultTenantId + value: "system" + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 20 + periodSeconds: 30 + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /app/logs + volumes: + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} diff --git a/apps/fc-devicemgmt/deployment-web.yaml b/apps/fc-devicemgmt/deployment-web.yaml new file mode 100644 index 0000000..41651cb --- /dev/null +++ b/apps/fc-devicemgmt/deployment-web.yaml @@ -0,0 +1,135 @@ +# FlowerCore.DeviceManagement Web. +# +# Source repo is expected to ship FlowerCore.DeviceManagement.Web in a later +# Sprint 9+ lane. This manifest is static-valid without requiring the image to +# exist yet; import localhost/fc-devicemgmt-web: to all schedulable RKE2 +# nodes before letting ArgoCD sync a live rollout. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fc-devicemgmt-web + namespace: fc-devicemgmt + labels: + app: fc-devicemgmt-web + app.kubernetes.io/name: fc-devicemgmt-web + app.kubernetes.io/component: web + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra + annotations: + flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard +spec: + replicas: 2 + revisionHistoryLimit: 3 + selector: + matchLabels: + app: fc-devicemgmt-web + template: + metadata: + labels: + app: fc-devicemgmt-web + app.kubernetes.io/name: fc-devicemgmt-web + app.kubernetes.io/component: web + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/metrics" + flowercore.io/audit-trace-id: "runtime-activity-trace" + spec: + securityContext: + fsGroup: 1654 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: web + image: localhost/fc-devicemgmt-web:v20260512-cx5 + imagePullPolicy: Never + ports: + - name: http + containerPort: 8080 + env: + - name: ASPNETCORE_URLS + value: "http://+:8080" + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT + value: "false" + - name: FlowerCore__Service__Name + value: "FlowerCore.DeviceManagement.Web" + - name: FlowerCore__DeviceManagement__DefaultTenantId + value: "system" + - name: FlowerCore__Database__Provider + value: "MySql" + - name: FlowerCore__Database__Host + value: "mysql.fc-mysql.svc" + - name: FlowerCore__Database__Database + value: "flowercore_devicemgmt" + - name: FlowerCore__Database__User + value: "fc_devicemgmt" + - name: FlowerCore__Database__Password + valueFrom: + secretKeyRef: + name: fc-devicemgmt-runtime + key: DB-Password + - name: FlowerCore__DeviceManagement__AgentMtls__CaPath + value: "/secrets/devicemgmt-mtls/mtls-ca.pem" + - name: FlowerCore__DeviceManagement__AgentMtls__ClientCertificatePath + value: "/secrets/devicemgmt-mtls/mtls-client.crt" + - name: FlowerCore__DeviceManagement__AgentMtls__ClientKeyPath + value: "/secrets/devicemgmt-mtls/mtls-client.key" + - name: FlowerCore__EventBus__Redis__Configuration + value: "redis.fc-redis.svc:6379" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 768Mi + startupProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + readinessProbe: + tcpSocket: + port: 8080 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 3 + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /app/logs + - name: devicemgmt-mtls + mountPath: /secrets/devicemgmt-mtls + readOnly: true + volumes: + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} + - name: devicemgmt-mtls + secret: + secretName: fc-devicemgmt-runtime + defaultMode: 0400 diff --git a/apps/fc-devicemgmt/ingressroute-web.yaml b/apps/fc-devicemgmt/ingressroute-web.yaml new file mode 100644 index 0000000..0e69b54 --- /dev/null +++ b/apps/fc-devicemgmt/ingressroute-web.yaml @@ -0,0 +1,55 @@ +# LAN ingress for FlowerCore.DeviceManagement Web. +# +# RKE2 Traefik has no built-in ACME resolver configured. Keep TLS certificate +# ownership in cert-manager Certificate/fc-devicemgmt-web-tls. +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: fc-devicemgmt-web + namespace: fc-devicemgmt + labels: + app.kubernetes.io/name: fc-devicemgmt-web + app.kubernetes.io/component: web + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +spec: + entryPoints: + - websecure + routes: + - match: Host(`devices.iamworkin.lan`) + kind: Rule + services: + - name: fc-devicemgmt-web + port: 80 + tls: + secretName: fc-devicemgmt-web-tls + +# Future public agent/update host gate (OFF by default): +# +# Do not enable `update.flowercore.io` here until Authentik OIDC Q-OIDC-1 +# resolves the public-device-management auth model and route ownership with +# UpdateCenter. When enabled, use a separate public IngressRoute with an +# explicit Method allowlist, public-host auth middleware, and public TLS +# certificate strategy. Leaving this as comments keeps ArgoCD from stealing +# live UpdateCenter traffic. +# +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: fc-devicemgmt-web-public +# namespace: fc-devicemgmt +# annotations: +# flowercore.io/public-host-gate: "disabled-until-Q-OIDC-1" +# spec: +# entryPoints: +# - websecure +# routes: +# - match: Host(`update.flowercore.io`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`)) +# kind: Rule +# services: +# - name: fc-devicemgmt-web +# port: 80 +# tls: +# secretName: fc-devicemgmt-public-tls diff --git a/apps/fc-devicemgmt/namespace.yaml b/apps/fc-devicemgmt/namespace.yaml new file mode 100644 index 0000000..b9925c6 --- /dev/null +++ b/apps/fc-devicemgmt/namespace.yaml @@ -0,0 +1,13 @@ +# FlowerCore.DeviceManagement namespace. +# +# ArgoCD discovers this directory as Application `infra-fc-devicemgmt`. +apiVersion: v1 +kind: Namespace +metadata: + name: fc-devicemgmt + labels: + app.kubernetes.io/name: fc-devicemgmt + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra diff --git a/apps/fc-devicemgmt/network-policy.yaml b/apps/fc-devicemgmt/network-policy.yaml new file mode 100644 index 0000000..91e07e0 --- /dev/null +++ b/apps/fc-devicemgmt/network-policy.yaml @@ -0,0 +1,224 @@ +# FlowerCore.DeviceManagement NetworkPolicies. +# +# NetworkPolicies belong in bluejay-infra so ArgoCD owns rebuild state. +# Rules include Traefik post-DNAT backend ports per +# feedback_netpol_dnat_backend_port and Synology NFS egress for the requested +# cold-tier / future artifact path. +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fc-devicemgmt-web-isolation + namespace: fc-devicemgmt + labels: + app.kubernetes.io/name: fc-devicemgmt-web + app.kubernetes.io/component: web + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +spec: + podSelector: + matchLabels: + app: fc-devicemgmt-web + policyTypes: + - Ingress + - Egress + ingress: + # LAN edge: only cluster Traefik should reach the Web pod for + # devices.iamworkin.lan. + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik-system + podSelector: + matchLabels: + app.kubernetes.io/name: traefik + ports: + - port: 8080 + protocol: TCP + # Direct LAN diagnostics are allowed only from FlowerCore LAN/VPN ranges. + - from: + - ipBlock: + cidr: 10.0.56.0/24 + - ipBlock: + cidr: 10.0.57.0/24 + - ipBlock: + cidr: 10.0.58.0/24 + - ipBlock: + cidr: 10.0.68.0/27 + ports: + - port: 8080 + protocol: TCP + egress: + # CoreDNS. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + # Database namespace. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-mysql + ports: + - port: 3306 + protocol: TCP + # Redis backplane for multi-replica SignalR / live-status fan-out. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-redis + ports: + - port: 6379 + protocol: TCP + # Traefik VIP / in-cluster Traefik for self-callbacks and public URL + # generation tests. Include post-DNAT backend ports 8443 + 8080. + - to: + - ipBlock: + cidr: 10.0.56.200/32 + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik-system + podSelector: + matchLabels: + app.kubernetes.io/name: traefik + ports: + - port: 80 + protocol: TCP + - port: 443 + protocol: TCP + - port: 8080 + protocol: TCP + - port: 8443 + protocol: TCP + # Agent egress: LAN/VPN devices may run DM Agent in Generic, Kiosk, Pi, + # ThinClient, or Server mode. Keep this private-range only. + - to: + - ipBlock: + cidr: 10.0.56.0/24 + - ipBlock: + cidr: 10.0.57.0/24 + - ipBlock: + cidr: 10.0.58.0/24 + - ipBlock: + cidr: 10.0.68.0/27 + ports: + - port: 80 + protocol: TCP + - port: 443 + protocol: TCP + - port: 8080 + protocol: TCP + - port: 8443 + protocol: TCP + - port: 5000 + protocol: TCP + - port: 5001 + protocol: TCP + # Synology NFS cold-tier / artifact mount allowance. + - to: + - ipBlock: + cidr: 10.0.58.3/32 + ports: + - port: 2049 + protocol: TCP + - port: 2049 + protocol: UDP + - port: 111 + protocol: TCP + - port: 111 + protocol: UDP +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fc-devicemgmt-operator-isolation + namespace: fc-devicemgmt + labels: + app.kubernetes.io/name: fc-devicemgmt-operator + app.kubernetes.io/component: operator + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +spec: + podSelector: + matchLabels: + app: fc-devicemgmt-operator + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - port: 8080 + protocol: TCP + egress: + # CoreDNS. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + # Kubernetes API for KubeOps reconciliation and Deployment UID lookup. + - to: [] + ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + # Agent egress for operator-initiated probes / fallback command dispatch. + - to: + - ipBlock: + cidr: 10.0.56.0/24 + - ipBlock: + cidr: 10.0.57.0/24 + - ipBlock: + cidr: 10.0.58.0/24 + - ipBlock: + cidr: 10.0.68.0/27 + ports: + - port: 80 + protocol: TCP + - port: 443 + protocol: TCP + - port: 8080 + protocol: TCP + - port: 8443 + protocol: TCP + - port: 5000 + protocol: TCP + - port: 5001 + protocol: TCP + # Synology NFS allowance for future cold-tier/audit archival jobs. + - to: + - ipBlock: + cidr: 10.0.58.3/32 + ports: + - port: 2049 + protocol: TCP + - port: 2049 + protocol: UDP + - port: 111 + protocol: TCP + - port: 111 + protocol: UDP diff --git a/apps/fc-devicemgmt/service-web.yaml b/apps/fc-devicemgmt/service-web.yaml new file mode 100644 index 0000000..2c9eb5d --- /dev/null +++ b/apps/fc-devicemgmt/service-web.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + name: fc-devicemgmt-web + namespace: fc-devicemgmt + labels: + app: fc-devicemgmt-web + app.kubernetes.io/name: fc-devicemgmt-web + app.kubernetes.io/component: web + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra +spec: + type: ClusterIP + selector: + app: fc-devicemgmt-web + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP diff --git a/apps/fc-devicemgmt/serviceaccount-operator.yaml b/apps/fc-devicemgmt/serviceaccount-operator.yaml new file mode 100644 index 0000000..8b1fef5 --- /dev/null +++ b/apps/fc-devicemgmt/serviceaccount-operator.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: fc-devicemgmt-operator + namespace: fc-devicemgmt + labels: + app.kubernetes.io/name: fc-devicemgmt-operator + app.kubernetes.io/component: operator + app.kubernetes.io/part-of: flowercore + app.kubernetes.io/managed-by: argocd + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index 4bba10f..d12bb79 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -291,6 +291,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 ProbeViolations( ManifestDocument document, YamlMappingNode container, @@ -314,6 +492,13 @@ public sealed class FleetManifestLintTests $"{document.Descriptor} container '{containerName}' still uses {probeKey}.httpGet on /health.", }; } + + private static IReadOnlyList FcDeviceManagementDocuments() + { + return Inventory.Documents + .Where(document => document.RelativePath.StartsWith("fc-devicemgmt/", StringComparison.Ordinal)) + .ToList(); + } } internal sealed class ManifestInventory