From bfe42cf44eb311008b1c08996df024a16072f1c0 Mon Sep 17 00:00:00 2001 From: Andrew Stoltz Date: Fri, 12 Jun 2026 14:21:45 -0500 Subject: [PATCH] feat(fc-network): add FlowerCore.Network app (read-only pfSense plane, ADR-189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up the pfSense automation plane (Phase 0, read-only) on RKE2 as an ArgoCD-managed workload at network.iamworkin.lan. - namespace fc-network - Deployment fc-network-web: localhost/fc-network-web:v20260612-0b5b049, imagePullPolicy Never, port 5340, /healthz probes, runAsNonRoot 1654 + readOnlyRootFilesystem, RWO-safe RollingUpdate (maxSurge 0/maxUnavailable 1), auth gate-OFF, SQLite + snapshot-store + intended-model paths under /data. - PVC fc-network-web-data (longhorn, 2Gi): SQLite index + on-box snapshot store (full-fidelity raw config.xml stays on-box; service surfaces redacted only). - Service (ClusterIP 80 -> 5340), Certificate (ClusterIssuer step-ca-acme), IngressRoute (network.iamworkin.lan, all methods — POST ingest is local-only). - kustomization.yaml for local previews / single-app validation. The ApplicationSet git generator picks this up as infra-fc-network; if it lags, the Application is applied manually (documented pattern). --- apps/fc-network/certificate-web.yaml | 33 ++++++ apps/fc-network/deployment-web.yaml | 145 ++++++++++++++++++++++++++ apps/fc-network/ingressroute-web.yaml | 32 ++++++ apps/fc-network/kustomization.yaml | 11 ++ apps/fc-network/namespace.yaml | 8 ++ apps/fc-network/pvc.yaml | 27 +++++ apps/fc-network/service-web.yaml | 21 ++++ 7 files changed, 277 insertions(+) create mode 100644 apps/fc-network/certificate-web.yaml create mode 100644 apps/fc-network/deployment-web.yaml create mode 100644 apps/fc-network/ingressroute-web.yaml create mode 100644 apps/fc-network/kustomization.yaml create mode 100644 apps/fc-network/namespace.yaml create mode 100644 apps/fc-network/pvc.yaml create mode 100644 apps/fc-network/service-web.yaml diff --git a/apps/fc-network/certificate-web.yaml b/apps/fc-network/certificate-web.yaml new file mode 100644 index 0000000..55e3cfb --- /dev/null +++ b/apps/fc-network/certificate-web.yaml @@ -0,0 +1,33 @@ +# Certificate for network.iamworkin.lan. +# +# Preflight gate: network.iamworkin.lan must resolve to 10.0.56.200 before this +# Certificate is synced. step-ca ACME cannot see the CoreDNS wildcard +# (*.iamworkin.lan -> 10.0.56.200) — it does an HTTP-01 challenge against the +# resolved host. The CoreDNS wildcard template covers network.iamworkin.lan, so +# resolution exists fleet-wide; do NOT add a pfSense DNS override (this plane is +# read-only and holds no pfSense creds). If ACME backs off, confirm the wildcard +# resolves first (feedback_pfsense_dns_required_for_acme). +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: fc-network-web-tls + namespace: fc-network + labels: + app: fc-network-web + app.kubernetes.io/name: fc-network-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: "network.iamworkin.lan must resolve to 10.0.56.200 (CoreDNS wildcard) before ACME sync" +spec: + secretName: fc-network-web-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - network.iamworkin.lan + duration: 720h + renewBefore: 240h diff --git a/apps/fc-network/deployment-web.yaml b/apps/fc-network/deployment-web.yaml new file mode 100644 index 0000000..2cc884d --- /dev/null +++ b/apps/fc-network/deployment-web.yaml @@ -0,0 +1,145 @@ +# FlowerCore.Network.Web — the pfSense automation plane (read-only Phase 0, ADR-189). +# +# Phase 0 is READ-ONLY: the service holds NO pfSense credentials and has no write +# path to pfSense anywhere. The only mutating endpoint is POST /api/v1/snapshots, +# which ingests a config.xml the noc1 exporter collected READ-ONLY and stores it +# (redacted projection) on the PVC. Auth ships gate-OFF. +# +# Image localhost/fc-network-web: is built by FlowerCore.Network +# scripts/deploy-k8s.sh and imported to all schedulable RKE2 nodes (rke2-server + +# rke2-agent1; agent2 retired). imagePullPolicy: Never — bump the tag here, sync +# ArgoCD, then scale 0->1 for the RWO PVC and verify the running pod imageID. +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fc-network-web + namespace: fc-network + labels: + app: fc-network-web + app.kubernetes.io/name: fc-network-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: 1 + revisionHistoryLimit: 3 + # RWO PVC: a single replica can't be surged (the new pod can't mount the volume + # while the old one holds it). maxSurge 0 / maxUnavailable 1 is the rwo-safe shape; + # for image bumps scale 0->1 rather than rollout restart. + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + selector: + matchLabels: + app: fc-network-web + template: + metadata: + labels: + app: fc-network-web + app.kubernetes.io/name: fc-network-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: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" + prometheus.io/scrape: "true" + prometheus.io/port: "5340" + prometheus.io/path: "/metrics/prometheus" + flowercore.io/audit-trace-id: "runtime-activity-trace" + spec: + securityContext: + fsGroup: 1654 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: web + image: localhost/fc-network-web:v20260612-0b5b049 + imagePullPolicy: Never + ports: + - name: http + containerPort: 5340 + # fc-safe-to-expose: read-only plane, auth gate-OFF; X-Forwarded-Proto handled + # by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. + env: + - name: ASPNETCORE_URLS + value: "http://+:5340" + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT + value: "false" + - name: HOME + value: "/data" + - name: FlowerCore__Auth__Enabled + value: "false" + - name: FlowerCore__Database__Provider + value: "Sqlite" + - name: FlowerCore__Database__ConnectionStrings__Sqlite + value: "Data Source=/data/network.db" + # Snapshot store + intended-model paths MUST be absolute on the PVC — + # the default is relative to the read-only content root. + - name: FlowerCore__Network__SnapshotStore__RootDirectory + value: "/data/snapshots" + - name: FlowerCore__Network__SnapshotStore__UseGitHistory + value: "true" + - name: FlowerCore__Network__IntendedModel__FilePath + value: "/data/intended.json" + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + startupProbe: + httpGet: + path: /healthz + port: 5340 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /healthz + port: 5340 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /healthz + port: 5340 + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 3 + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /app/logs + volumes: + - name: data + persistentVolumeClaim: + claimName: fc-network-web-data + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} diff --git a/apps/fc-network/ingressroute-web.yaml b/apps/fc-network/ingressroute-web.yaml new file mode 100644 index 0000000..3641331 --- /dev/null +++ b/apps/fc-network/ingressroute-web.yaml @@ -0,0 +1,32 @@ +# LAN ingress for FlowerCore.Network Web (network.iamworkin.lan). +# +# RKE2 Traefik has no built-in ACME resolver; TLS certificate ownership stays in +# cert-manager Certificate/fc-network-web-tls. Phase 0 is read-only but the POST +# ingest endpoint is genuinely needed by the noc1 exporter, so this route allows +# all methods (no GET/HEAD-only restriction like fc-dns) — the service itself has +# NO pfSense write path, so allowing POST here only reaches the local snapshot +# ingest. +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: fc-network-web + namespace: fc-network + labels: + app: fc-network-web + app.kubernetes.io/name: fc-network-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(`network.iamworkin.lan`) + kind: Rule + services: + - name: fc-network-web + port: 80 + tls: + secretName: fc-network-web-tls diff --git a/apps/fc-network/kustomization.yaml b/apps/fc-network/kustomization.yaml new file mode 100644 index 0000000..e6b102f --- /dev/null +++ b/apps/fc-network/kustomization.yaml @@ -0,0 +1,11 @@ +# ArgoCD's bluejay-infra ApplicationSet discovers apps/* directories on main. +# The kustomization is included for local previews and single-app validation. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - pvc.yaml + - deployment-web.yaml + - service-web.yaml + - certificate-web.yaml + - ingressroute-web.yaml diff --git a/apps/fc-network/namespace.yaml b/apps/fc-network/namespace.yaml new file mode 100644 index 0000000..1d08565 --- /dev/null +++ b/apps/fc-network/namespace.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: fc-network + labels: + app.kubernetes.io/part-of: flowercore + flowercore.io/tenant-id: system + flowercore.io/created-by: bluejay-infra diff --git a/apps/fc-network/pvc.yaml b/apps/fc-network/pvc.yaml new file mode 100644 index 0000000..86a81c0 --- /dev/null +++ b/apps/fc-network/pvc.yaml @@ -0,0 +1,27 @@ +# Persistent store for FlowerCore.Network (read-only pfSense automation plane). +# +# Holds the SQLite snapshot INDEX db (network.db) AND the on-box snapshot store +# (data/snapshots): full-fidelity raw config.xml + redacted inventory sidecars + +# an on-box git history. Full-fidelity config is on-box ONLY (this PVC); the +# service DB / REST / MCP / UI only ever surface the REDACTED projection. +# RWO — single replica, scale 0->1 for updates (never rollout restart). +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fc-network-web-data + namespace: fc-network + labels: + app: fc-network-web + app.kubernetes.io/name: fc-network-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: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 2Gi diff --git a/apps/fc-network/service-web.yaml b/apps/fc-network/service-web.yaml new file mode 100644 index 0000000..97a7dd3 --- /dev/null +++ b/apps/fc-network/service-web.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: fc-network-web + namespace: fc-network + labels: + app: fc-network-web + app.kubernetes.io/name: fc-network-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: + selector: + app: fc-network-web + ports: + - name: http + port: 80 + targetPort: 5340 + type: ClusterIP