Compare commits
3 Commits
60f5f8e6f0
...
783720cc58
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783720cc58 | ||
|
|
6fe77225ae | ||
| 634b9c4169 |
26
apps/fc-devicemgmt/1password-item.yaml
Normal file
26
apps/fc-devicemgmt/1password-item.yaml
Normal file
@@ -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"
|
||||||
33
apps/fc-devicemgmt/argocd-application.yaml
Normal file
33
apps/fc-devicemgmt/argocd-application.yaml
Normal file
@@ -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
|
||||||
30
apps/fc-devicemgmt/certificate-web.yaml
Normal file
30
apps/fc-devicemgmt/certificate-web.yaml
Normal file
@@ -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
|
||||||
81
apps/fc-devicemgmt/clusterrole-operator.yaml
Normal file
81
apps/fc-devicemgmt/clusterrole-operator.yaml
Normal file
@@ -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
|
||||||
19
apps/fc-devicemgmt/clusterrolebinding-operator.yaml
Normal file
19
apps/fc-devicemgmt/clusterrolebinding-operator.yaml
Normal file
@@ -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
|
||||||
109
apps/fc-devicemgmt/deployment-operator.yaml
Normal file
109
apps/fc-devicemgmt/deployment-operator.yaml
Normal file
@@ -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: {}
|
||||||
135
apps/fc-devicemgmt/deployment-web.yaml
Normal file
135
apps/fc-devicemgmt/deployment-web.yaml
Normal file
@@ -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:<tag> 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
|
||||||
55
apps/fc-devicemgmt/ingressroute-web.yaml
Normal file
55
apps/fc-devicemgmt/ingressroute-web.yaml
Normal file
@@ -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
|
||||||
13
apps/fc-devicemgmt/namespace.yaml
Normal file
13
apps/fc-devicemgmt/namespace.yaml
Normal file
@@ -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
|
||||||
224
apps/fc-devicemgmt/network-policy.yaml
Normal file
224
apps/fc-devicemgmt/network-policy.yaml
Normal file
@@ -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
|
||||||
22
apps/fc-devicemgmt/service-web.yaml
Normal file
22
apps/fc-devicemgmt/service-web.yaml
Normal file
@@ -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
|
||||||
12
apps/fc-devicemgmt/serviceaccount-operator.yaml
Normal file
12
apps/fc-devicemgmt/serviceaccount-operator.yaml
Normal file
@@ -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
|
||||||
61
apps/github-runner/README.md
Normal file
61
apps/github-runner/README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# GitHub Runner Fleet
|
||||||
|
|
||||||
|
ArgoCD owns `apps/github-runner/github-runner.yaml`. Do not patch live runner
|
||||||
|
Deployments with `kubectl`; update this manifest and let ArgoCD reconcile.
|
||||||
|
|
||||||
|
## Runner Shape
|
||||||
|
|
||||||
|
All repo-scoped Linux runners use:
|
||||||
|
|
||||||
|
- `ACCESS_TOKEN` from the `github-runner-token` Secret
|
||||||
|
- `RUN_AS_ROOT=false`
|
||||||
|
- `EPHEMERAL=true`
|
||||||
|
- `LABELS=self-hosted,linux,fc-build-linux`
|
||||||
|
- writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and
|
||||||
|
Actions tool cache
|
||||||
|
|
||||||
|
`github-runner` for `FlowerCore.Common` is single-replica because it retains the
|
||||||
|
original Longhorn ReadWriteOnce NuGet PVC. `github-runner-sharedpos` and the top
|
||||||
|
Linux-cost repo runners use two replicas with per-pod `emptyDir` caches. That is
|
||||||
|
the safe backlog-drain strategy: no two pods share one RWO PVC.
|
||||||
|
|
||||||
|
## Post-Merge Proof
|
||||||
|
|
||||||
|
After the PR is merged and ArgoCD syncs, verify the runner fleet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n github-runner get deploy,pods,pvc
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify GitHub registration for the repo-scoped runners:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for repo in FlowerCore.Common FlowerCore.Shared.Pos FlowerCore.Puppet FlowerCore.Signage \
|
||||||
|
FlowerCore.DMS FlowerCore.Telephony FlowerCore.Print.Web FlowerCore.Chat \
|
||||||
|
FlowerCore.MySQL FlowerCore.Kiosk.Linux; do
|
||||||
|
echo "=== $repo ==="
|
||||||
|
gh api "/repos/astoltz/$repo/actions/runners" \
|
||||||
|
--jq '.runners[] | select(.labels[].name == "fc-build-linux") | {name,status,busy,labels:[.labels[].name]}'
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Shared.Pos publish proof after the runner pod is online:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh run list --repo astoltz/FlowerCore.Shared.Pos \
|
||||||
|
--workflow "Build, Test & Publish" --branch main --limit 5
|
||||||
|
```
|
||||||
|
|
||||||
|
If the latest run is still queued after runner registration, rerun the workflow
|
||||||
|
from GitHub Actions and verify it lands on an `rke2-linux-*` runner.
|
||||||
|
|
||||||
|
## Failure Notes
|
||||||
|
|
||||||
|
- `actions/setup-dotnet` permission error at `/usr/share/dotnet`: check that
|
||||||
|
`DOTNET_INSTALL_DIR=/home/runner/.dotnet` and related cache env vars are
|
||||||
|
present on the runner pod.
|
||||||
|
- `404` during runner registration: the fine-grained PAT is valid but missing
|
||||||
|
repository access for that repo. Add the repo to the PAT access list; the PAT
|
||||||
|
value does not change.
|
||||||
|
- `Multi-Attach` volume error: only the Common runner uses a RWO PVC and it must
|
||||||
|
stay single-replica. New multi-replica runners use `emptyDir`.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -723,6 +723,24 @@ data:
|
|||||||
summary: "Mac mini GitHub runner offline ({{ $labels.runner }})"
|
summary: "Mac mini GitHub runner offline ({{ $labels.runner }})"
|
||||||
description: "A macmini-* GitHub Actions runner has not reported online for more than 10 minutes. Puppet manages its LaunchDaemon under /Library/LaunchDaemons/io.flowercore.github-runner-<slug>.plist; runners survive reboot and do not require a GUI session."
|
description: "A macmini-* GitHub Actions runner has not reported online for more than 10 minutes. Puppet manages its LaunchDaemon under /Library/LaunchDaemons/io.flowercore.github-runner-<slug>.plist; runners survive reboot and do not require a GUI session."
|
||||||
|
|
||||||
|
- name: linux-runners
|
||||||
|
rules:
|
||||||
|
- alert: LinuxRunnerOffline
|
||||||
|
expr: |
|
||||||
|
kube_deployment_status_replicas_ready{
|
||||||
|
namespace="github-runner",
|
||||||
|
deployment=~"github-runner(|-(sharedpos|puppet|signage|dms|telephony|print-web|chat|mysql|kiosk-linux))"
|
||||||
|
} == 0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
alert_channel: irc
|
||||||
|
service: github-runner
|
||||||
|
team: ci
|
||||||
|
annotations:
|
||||||
|
summary: "Linux CI runner offline: {{ $labels.deployment }}"
|
||||||
|
description: "Deployment {{ $labels.deployment }} in namespace github-runner has 0 ready replicas for more than 5 minutes. CI jobs targeting this repo will queue until the runner pod restarts and re-registers with GitHub. Check pods with: kubectl -n github-runner get pods -l app.kubernetes.io/name={{ $labels.deployment }}. Check logs with: kubectl -n github-runner logs -l app.kubernetes.io/name={{ $labels.deployment }} --tail=50. Common causes: PAT missing repo access, runner CrashLoopBackOff, or node/resource pressure."
|
||||||
|
|
||||||
- name: remote-desktop
|
- name: remote-desktop
|
||||||
rules:
|
rules:
|
||||||
- alert: RemoteDesktopWebDown
|
- alert: RemoteDesktopWebDown
|
||||||
@@ -3421,6 +3439,39 @@ data:
|
|||||||
relativeTimeRange: {from: 120, to: 0}
|
relativeTimeRange: {from: 120, to: 0}
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [600], type: gt}}], refId: C}
|
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [600], type: gt}}], refId: C}
|
||||||
|
- orgId: 1
|
||||||
|
name: CI Runners
|
||||||
|
folder: CI Alerts
|
||||||
|
interval: 1m
|
||||||
|
rules:
|
||||||
|
- uid: linux-runner-offline
|
||||||
|
title: LinuxRunnerOffline
|
||||||
|
condition: C
|
||||||
|
for: 5m
|
||||||
|
noDataState: OK
|
||||||
|
execErrState: Error
|
||||||
|
annotations:
|
||||||
|
summary: "Linux CI runner offline: {{ $labels.deployment }}"
|
||||||
|
description: "A github-runner namespace Deployment has 0 ready replicas for more than 5 minutes. CI jobs targeting that repo will queue until the runner pod restarts and re-registers."
|
||||||
|
runbook: "1. kubectl -n github-runner get pods -l app.kubernetes.io/name={{ $labels.deployment }} 2. kubectl -n github-runner logs -l app.kubernetes.io/name={{ $labels.deployment }} --tail=50 3. Verify PAT repo access if registration returns 404 4. Verify no RWO PVC is shared by scaled runners"
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
service: github-runner
|
||||||
|
alert_channel: irc
|
||||||
|
team: ci
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange: {from: 300, to: 0}
|
||||||
|
datasourceUid: prometheus
|
||||||
|
model: {expr: 'kube_deployment_status_replicas_ready{namespace="github-runner",deployment=~"github-runner(|-(sharedpos|puppet|signage|dms|telephony|print-web|chat|mysql|kiosk-linux))"} == 0', instant: true, refId: A}
|
||||||
|
- refId: B
|
||||||
|
relativeTimeRange: {from: 300, to: 0}
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model: {type: reduce, expression: A, reducer: last, refId: B}
|
||||||
|
- refId: C
|
||||||
|
relativeTimeRange: {from: 300, to: 0}
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model: {type: threshold, expression: B, conditions: [{evaluator: {params: [0], type: gt}}], refId: C}
|
||||||
- orgId: 1
|
- orgId: 1
|
||||||
name: Infrastructure
|
name: Infrastructure
|
||||||
folder: AI Stack Alerts
|
folder: AI Stack Alerts
|
||||||
|
|||||||
@@ -54,6 +54,43 @@ public sealed class FleetManifestLintTests
|
|||||||
"ttsreader-piper",
|
"ttsreader-piper",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> LinuxRunnerRepos = new Dictionary<string, string>(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<string> 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<string, string> WritableRunnerEnv = new Dictionary<string, string>(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]
|
[Fact]
|
||||||
public void IngressRoutes_MustKeepServiceReferencesInTheSameNamespace()
|
public void IngressRoutes_MustKeepServiceReferencesInTheSameNamespace()
|
||||||
{
|
{
|
||||||
@@ -187,6 +224,98 @@ public sealed class FleetManifestLintTests
|
|||||||
violations.Should().BeEmpty();
|
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]
|
[Fact]
|
||||||
public void StatefulSets_WithVolumeClaimTemplates_MustDeclareFilesystemDefaults()
|
public void StatefulSets_WithVolumeClaimTemplates_MustDeclareFilesystemDefaults()
|
||||||
{
|
{
|
||||||
@@ -291,6 +420,184 @@ public sealed class FleetManifestLintTests
|
|||||||
violations.Should().BeEmpty();
|
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(
|
private static IEnumerable<string> ProbeViolations(
|
||||||
ManifestDocument document,
|
ManifestDocument document,
|
||||||
YamlMappingNode container,
|
YamlMappingNode container,
|
||||||
@@ -314,6 +621,51 @@ public sealed class FleetManifestLintTests
|
|||||||
$"{document.Descriptor} container '{containerName}' still uses {probeKey}.httpGet on /health.",
|
$"{document.Descriptor} container '{containerName}' still uses {probeKey}.httpGet on /health.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, ManifestDocument> 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<ManifestDocument> FcDeviceManagementDocuments()
|
||||||
|
{
|
||||||
|
return Inventory.Documents
|
||||||
|
.Where(document => document.RelativePath.StartsWith("fc-devicemgmt/", StringComparison.Ordinal))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class ManifestInventory
|
internal sealed class ManifestInventory
|
||||||
|
|||||||
Reference in New Issue
Block a user