Compare commits
8 Commits
codex/s61-
...
codex/s67-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13d8ca8c1a | ||
|
|
b0a3ef7448 | ||
|
|
81ac1f3e4f | ||
| b842738a0e | |||
|
|
f0cb7a5e81 | ||
| ac0f665323 | |||
|
|
c4b08f41ab | ||
|
|
417d3830ae |
16
README.md
16
README.md
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
Infrastructure manifests for ArgoCD. An `ApplicationSet` in `argocd` namespace watches the `apps/*` directories in this repo and creates one `Application` per subdir (prefixed `infra-<name>`).
|
Infrastructure manifests for ArgoCD. An `ApplicationSet` in `argocd` namespace watches the `apps/*` directories in this repo and creates one `Application` per subdir (prefixed `infra-<name>`).
|
||||||
|
|
||||||
|
## Root GitOps ApplicationSet
|
||||||
|
|
||||||
|
`argocd/applicationset-bluejay-infra.yaml` is the root of this GitOps tree, but
|
||||||
|
it is **NOT self-managed** by ArgoCD. Apply it manually when the root generator
|
||||||
|
or sync policy changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n argocd apply -f argocd/applicationset-bluejay-infra.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the per-StatefulSet `ignoreDifferences` entries in that file synced with
|
||||||
|
the live ApplicationSet. They intentionally cover `zabbix-postgres`,
|
||||||
|
`guac-mysql`, `matrix-postgres`, and `authentik-postgres` so ArgoCD does not
|
||||||
|
loop forever on server-side-apply `volumeClaimTemplates` status drift. Every new
|
||||||
|
StatefulSet with `volumeClaimTemplates` needs its own entry appended.
|
||||||
|
|
||||||
## Adding a new service to the cluster
|
## Adding a new service to the cluster
|
||||||
|
|
||||||
Follow these steps in order. **Step 1 must run before step 3** — if you skip it, cert-manager HTTP-01 will silently fail for ~2h per cert (exponential backoff) until someone diagnoses the DNS.
|
Follow these steps in order. **Step 1 must run before step 3** — if you skip it, cert-manager HTTP-01 will silently fail for ~2h per cert (exponential backoff) until someone diagnoses the DNS.
|
||||||
|
|||||||
@@ -1,448 +1,453 @@
|
|||||||
# Authentik OIDC backend
|
# Authentik OIDC backend
|
||||||
# ArgoCD-managed. BlueJay Lab.
|
# ArgoCD-managed. BlueJay Lab.
|
||||||
#
|
#
|
||||||
# Stack:
|
# Stack:
|
||||||
# - PostgreSQL 16 StatefulSet (single replica, Longhorn RWO 5Gi)
|
# - PostgreSQL 16 StatefulSet (single replica, Longhorn RWO 5Gi)
|
||||||
# - Redis 7 Deployment (no persistence — session/cache only)
|
# - Redis 7 Deployment (no persistence — session/cache only)
|
||||||
# - Authentik server + worker Deployments (image ghcr.io/goauthentik/server:2024.12.3)
|
# - Authentik server + worker Deployments (image ghcr.io/goauthentik/server:2024.12.3)
|
||||||
# - Media PVC shared between server + worker (Longhorn RWO 2Gi)
|
# - Media PVC shared between server + worker (Longhorn RWO 2Gi)
|
||||||
# - Certificate via step-ca-acme ClusterIssuer
|
# - Certificate via step-ca-acme ClusterIssuer
|
||||||
# - Traefik IngressRoute at id.iamworkin.lan
|
# - Traefik IngressRoute at id.iamworkin.lan
|
||||||
#
|
#
|
||||||
# Secrets come from 1Password item "authentik-credentials" (IAmWorkin vault, id y6i74ch22q5wvm7znquq4nhhcu)
|
# Secrets come from 1Password item "authentik-credentials" (IAmWorkin vault, id y6i74ch22q5wvm7znquq4nhhcu)
|
||||||
# via the OnePasswordItem CRD, materialized into k8s Secret authentik/authentik-credentials.
|
# via the OnePasswordItem CRD, materialized into k8s Secret authentik/authentik-credentials.
|
||||||
#
|
#
|
||||||
# Why the discovery URL is /application/o/pimanager/ : Authentik issues per-application OIDC providers.
|
# Why the discovery URL is /application/o/pimanager/ : Authentik issues per-application OIDC providers.
|
||||||
# The pimanager OIDC application/provider is created after the cluster pods are healthy (manual or
|
# The pimanager OIDC application/provider is created after the cluster pods are healthy (manual or
|
||||||
# via API once the bootstrap token is available — see Notes substrate).
|
# via API once the bootstrap token is available — see Notes substrate).
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
name: authentik
|
name: authentik
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/part-of: bluejay-infra
|
app.kubernetes.io/part-of: bluejay-infra
|
||||||
|
|
||||||
---
|
---
|
||||||
# 1Password operator pulls the authentik-credentials item into a k8s Secret of the same name.
|
# 1Password operator pulls the authentik-credentials item into a k8s Secret of the same name.
|
||||||
# Field labels in 1P become Secret keys: AUTHENTIK_SECRET_KEY, POSTGRES_PASSWORD, REDIS_PASSWORD,
|
# Field labels in 1P become Secret keys: AUTHENTIK_SECRET_KEY, POSTGRES_PASSWORD, REDIS_PASSWORD,
|
||||||
# BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_TOKEN, BOOTSTRAP_ADMIN_EMAIL.
|
# BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_TOKEN, BOOTSTRAP_ADMIN_EMAIL.
|
||||||
apiVersion: onepassword.com/v1
|
apiVersion: onepassword.com/v1
|
||||||
kind: OnePasswordItem
|
kind: OnePasswordItem
|
||||||
metadata:
|
metadata:
|
||||||
name: authentik-credentials
|
name: authentik-credentials
|
||||||
namespace: authentik
|
namespace: authentik
|
||||||
spec:
|
spec:
|
||||||
itemPath: "vaults/IAmWorkin/items/authentik-credentials"
|
itemPath: "vaults/IAmWorkin/items/authentik-credentials"
|
||||||
|
|
||||||
---
|
---
|
||||||
# Shared media volume for server + worker pods.
|
# Shared media volume for server + worker pods.
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: authentik-media
|
name: authentik-media
|
||||||
namespace: authentik
|
namespace: authentik
|
||||||
spec:
|
spec:
|
||||||
storageClassName: longhorn
|
storageClassName: longhorn
|
||||||
accessModes: [ReadWriteOnce]
|
accessModes: [ReadWriteOnce]
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
|
|
||||||
---
|
---
|
||||||
# PostgreSQL 16 StatefulSet — Authentik's primary store.
|
# PostgreSQL 16 StatefulSet — Authentik's primary store.
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: StatefulSet
|
kind: StatefulSet
|
||||||
metadata:
|
metadata:
|
||||||
name: authentik-postgres
|
name: authentik-postgres
|
||||||
namespace: authentik
|
namespace: authentik
|
||||||
labels:
|
labels:
|
||||||
app: authentik-postgres
|
app: authentik-postgres
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
argocd.argoproj.io/instance: infra-authentik
|
||||||
spec:
|
spec:
|
||||||
persistentVolumeClaimRetentionPolicy:
|
persistentVolumeClaimRetentionPolicy:
|
||||||
whenDeleted: Retain
|
whenDeleted: Retain
|
||||||
whenScaled: Retain
|
whenScaled: Retain
|
||||||
podManagementPolicy: OrderedReady
|
podManagementPolicy: OrderedReady
|
||||||
serviceName: authentik-postgres
|
serviceName: authentik-postgres
|
||||||
replicas: 1
|
replicas: 1
|
||||||
revisionHistoryLimit: 10
|
revisionHistoryLimit: 10
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: authentik-postgres
|
app: authentik-postgres
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: authentik-postgres
|
app: authentik-postgres
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5432
|
- containerPort: 5432
|
||||||
name: postgres
|
name: postgres
|
||||||
env:
|
env:
|
||||||
- name: POSTGRES_USER
|
- name: POSTGRES_USER
|
||||||
value: authentik
|
value: authentik
|
||||||
- name: POSTGRES_PASSWORD
|
- name: POSTGRES_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: authentik-credentials
|
name: authentik-credentials
|
||||||
key: POSTGRES_PASSWORD
|
key: POSTGRES_PASSWORD
|
||||||
- name: POSTGRES_DB
|
- name: POSTGRES_DB
|
||||||
value: authentik
|
value: authentik
|
||||||
- name: POSTGRES_INITDB_ARGS
|
- name: POSTGRES_INITDB_ARGS
|
||||||
value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||||
- name: PGDATA
|
- name: PGDATA
|
||||||
value: /var/lib/postgresql/data/pgdata
|
value: /var/lib/postgresql/data/pgdata
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
exec:
|
exec:
|
||||||
command: ["pg_isready", "-U", "authentik"]
|
command: ["pg_isready", "-U", "authentik"]
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
exec:
|
exec:
|
||||||
command: ["pg_isready", "-U", "authentik"]
|
command: ["pg_isready", "-U", "authentik"]
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
resources:
|
resources:
|
||||||
requests: { cpu: 100m, memory: 256Mi }
|
requests: { cpu: 100m, memory: 256Mi }
|
||||||
limits: { cpu: 1000m, memory: 1Gi }
|
limits: { cpu: 1000m, memory: 1Gi }
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: pgdata
|
- name: pgdata
|
||||||
mountPath: /var/lib/postgresql/data
|
mountPath: /var/lib/postgresql/data
|
||||||
volumeClaimTemplates:
|
volumeClaimTemplates:
|
||||||
- metadata:
|
# apiVersion/kind included deliberately: this STS was created via ArgoCD ServerSideApply,
|
||||||
name: pgdata
|
# so the live object carries PVC TypeMeta inside volumeClaimTemplates; omitting it here
|
||||||
spec:
|
# leaves the app eternally OutOfSync even though kubectl SSA dry-run shows no change.
|
||||||
storageClassName: longhorn
|
- apiVersion: v1
|
||||||
accessModes: [ReadWriteOnce]
|
kind: PersistentVolumeClaim
|
||||||
volumeMode: Filesystem
|
metadata:
|
||||||
resources:
|
name: pgdata
|
||||||
requests:
|
spec:
|
||||||
storage: 5Gi
|
storageClassName: longhorn
|
||||||
|
accessModes: [ReadWriteOnce]
|
||||||
---
|
volumeMode: Filesystem
|
||||||
apiVersion: v1
|
resources:
|
||||||
kind: Service
|
requests:
|
||||||
metadata:
|
storage: 5Gi
|
||||||
name: authentik-postgres
|
|
||||||
namespace: authentik
|
---
|
||||||
spec:
|
apiVersion: v1
|
||||||
clusterIP: None
|
kind: Service
|
||||||
selector:
|
metadata:
|
||||||
app: authentik-postgres
|
name: authentik-postgres
|
||||||
ports:
|
namespace: authentik
|
||||||
- name: postgres
|
spec:
|
||||||
port: 5432
|
clusterIP: None
|
||||||
targetPort: 5432
|
selector:
|
||||||
|
app: authentik-postgres
|
||||||
---
|
ports:
|
||||||
# Redis 7 — session storage + Celery broker. No persistence needed (cache).
|
- name: postgres
|
||||||
apiVersion: apps/v1
|
port: 5432
|
||||||
kind: Deployment
|
targetPort: 5432
|
||||||
metadata:
|
|
||||||
name: authentik-redis
|
---
|
||||||
namespace: authentik
|
# Redis 7 — session storage + Celery broker. No persistence needed (cache).
|
||||||
labels:
|
apiVersion: apps/v1
|
||||||
app: authentik-redis
|
kind: Deployment
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
metadata:
|
||||||
spec:
|
name: authentik-redis
|
||||||
replicas: 1
|
namespace: authentik
|
||||||
strategy:
|
labels:
|
||||||
type: Recreate
|
app: authentik-redis
|
||||||
selector:
|
argocd.argoproj.io/instance: infra-authentik
|
||||||
matchLabels:
|
spec:
|
||||||
app: authentik-redis
|
replicas: 1
|
||||||
template:
|
strategy:
|
||||||
metadata:
|
type: Recreate
|
||||||
labels:
|
selector:
|
||||||
app: authentik-redis
|
matchLabels:
|
||||||
spec:
|
app: authentik-redis
|
||||||
containers:
|
template:
|
||||||
- name: redis
|
metadata:
|
||||||
image: redis:7-alpine
|
labels:
|
||||||
args:
|
app: authentik-redis
|
||||||
- "--save"
|
spec:
|
||||||
- ""
|
containers:
|
||||||
- "--appendonly"
|
- name: redis
|
||||||
- "no"
|
image: redis:7-alpine
|
||||||
- "--requirepass"
|
args:
|
||||||
- "$(REDIS_PASSWORD)"
|
- "--save"
|
||||||
env:
|
- ""
|
||||||
- name: REDIS_PASSWORD
|
- "--appendonly"
|
||||||
valueFrom:
|
- "no"
|
||||||
secretKeyRef:
|
- "--requirepass"
|
||||||
name: authentik-credentials
|
- "$(REDIS_PASSWORD)"
|
||||||
key: REDIS_PASSWORD
|
env:
|
||||||
ports:
|
- name: REDIS_PASSWORD
|
||||||
- containerPort: 6379
|
valueFrom:
|
||||||
name: redis
|
secretKeyRef:
|
||||||
readinessProbe:
|
name: authentik-credentials
|
||||||
tcpSocket: { port: 6379 }
|
key: REDIS_PASSWORD
|
||||||
initialDelaySeconds: 5
|
ports:
|
||||||
periodSeconds: 5
|
- containerPort: 6379
|
||||||
livenessProbe:
|
name: redis
|
||||||
tcpSocket: { port: 6379 }
|
readinessProbe:
|
||||||
initialDelaySeconds: 30
|
tcpSocket: { port: 6379 }
|
||||||
periodSeconds: 30
|
initialDelaySeconds: 5
|
||||||
resources:
|
periodSeconds: 5
|
||||||
requests: { cpu: 50m, memory: 64Mi }
|
livenessProbe:
|
||||||
limits: { cpu: 500m, memory: 256Mi }
|
tcpSocket: { port: 6379 }
|
||||||
|
initialDelaySeconds: 30
|
||||||
---
|
periodSeconds: 30
|
||||||
apiVersion: v1
|
resources:
|
||||||
kind: Service
|
requests: { cpu: 50m, memory: 64Mi }
|
||||||
metadata:
|
limits: { cpu: 500m, memory: 256Mi }
|
||||||
name: authentik-redis
|
|
||||||
namespace: authentik
|
---
|
||||||
spec:
|
apiVersion: v1
|
||||||
selector:
|
kind: Service
|
||||||
app: authentik-redis
|
metadata:
|
||||||
ports:
|
name: authentik-redis
|
||||||
- name: redis
|
namespace: authentik
|
||||||
port: 6379
|
spec:
|
||||||
targetPort: 6379
|
selector:
|
||||||
|
app: authentik-redis
|
||||||
---
|
ports:
|
||||||
# Authentik server Deployment — HTTP frontend on :9000.
|
- name: redis
|
||||||
apiVersion: apps/v1
|
port: 6379
|
||||||
kind: Deployment
|
targetPort: 6379
|
||||||
metadata:
|
|
||||||
name: authentik-server
|
---
|
||||||
namespace: authentik
|
# Authentik server Deployment — HTTP frontend on :9000.
|
||||||
labels:
|
apiVersion: apps/v1
|
||||||
app: authentik-server
|
kind: Deployment
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
metadata:
|
||||||
spec:
|
name: authentik-server
|
||||||
replicas: 1
|
namespace: authentik
|
||||||
strategy:
|
labels:
|
||||||
type: Recreate # shares /media RWO PVC with worker
|
app: authentik-server
|
||||||
selector:
|
argocd.argoproj.io/instance: infra-authentik
|
||||||
matchLabels:
|
spec:
|
||||||
app: authentik-server
|
replicas: 1
|
||||||
template:
|
strategy:
|
||||||
metadata:
|
type: Recreate # shares /media RWO PVC with worker
|
||||||
labels:
|
selector:
|
||||||
app: authentik-server
|
matchLabels:
|
||||||
spec:
|
app: authentik-server
|
||||||
securityContext:
|
template:
|
||||||
# Authentik image runs as uid 1000 "authentik" but the Longhorn PVC mounts
|
metadata:
|
||||||
# root:root by default. fsGroup recursively chgrp + chmod g+rwx so the
|
labels:
|
||||||
# non-root container can mkdir /media/public during the tenant_files migration.
|
app: authentik-server
|
||||||
fsGroup: 1000
|
spec:
|
||||||
containers:
|
securityContext:
|
||||||
- name: server
|
# Authentik image runs as uid 1000 "authentik" but the Longhorn PVC mounts
|
||||||
image: ghcr.io/goauthentik/server:2024.12.3
|
# root:root by default. fsGroup recursively chgrp + chmod g+rwx so the
|
||||||
args: ["server"]
|
# non-root container can mkdir /media/public during the tenant_files migration.
|
||||||
ports:
|
fsGroup: 1000
|
||||||
- containerPort: 9000
|
containers:
|
||||||
name: http
|
- name: server
|
||||||
- containerPort: 9443
|
image: ghcr.io/goauthentik/server:2024.12.3
|
||||||
name: https
|
args: ["server"]
|
||||||
env:
|
ports:
|
||||||
- name: AUTHENTIK_SECRET_KEY
|
- containerPort: 9000
|
||||||
valueFrom:
|
name: http
|
||||||
secretKeyRef:
|
- containerPort: 9443
|
||||||
name: authentik-credentials
|
name: https
|
||||||
key: AUTHENTIK_SECRET_KEY
|
env:
|
||||||
- name: AUTHENTIK_REDIS__HOST
|
- name: AUTHENTIK_SECRET_KEY
|
||||||
value: authentik-redis
|
valueFrom:
|
||||||
- name: AUTHENTIK_REDIS__PASSWORD
|
secretKeyRef:
|
||||||
valueFrom:
|
name: authentik-credentials
|
||||||
secretKeyRef:
|
key: AUTHENTIK_SECRET_KEY
|
||||||
name: authentik-credentials
|
- name: AUTHENTIK_REDIS__HOST
|
||||||
key: REDIS_PASSWORD
|
value: authentik-redis
|
||||||
- name: AUTHENTIK_POSTGRESQL__HOST
|
- name: AUTHENTIK_REDIS__PASSWORD
|
||||||
value: authentik-postgres
|
valueFrom:
|
||||||
- name: AUTHENTIK_POSTGRESQL__NAME
|
secretKeyRef:
|
||||||
value: authentik
|
name: authentik-credentials
|
||||||
- name: AUTHENTIK_POSTGRESQL__USER
|
key: REDIS_PASSWORD
|
||||||
value: authentik
|
- name: AUTHENTIK_POSTGRESQL__HOST
|
||||||
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
value: authentik-postgres
|
||||||
valueFrom:
|
- name: AUTHENTIK_POSTGRESQL__NAME
|
||||||
secretKeyRef:
|
value: authentik
|
||||||
name: authentik-credentials
|
- name: AUTHENTIK_POSTGRESQL__USER
|
||||||
key: POSTGRES_PASSWORD
|
value: authentik
|
||||||
- name: AUTHENTIK_BOOTSTRAP_PASSWORD
|
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: authentik-credentials
|
name: authentik-credentials
|
||||||
key: BOOTSTRAP_ADMIN_PASSWORD
|
key: POSTGRES_PASSWORD
|
||||||
- name: AUTHENTIK_BOOTSTRAP_TOKEN
|
- name: AUTHENTIK_BOOTSTRAP_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: authentik-credentials
|
name: authentik-credentials
|
||||||
key: BOOTSTRAP_ADMIN_TOKEN
|
key: BOOTSTRAP_ADMIN_PASSWORD
|
||||||
- name: AUTHENTIK_BOOTSTRAP_EMAIL
|
- name: AUTHENTIK_BOOTSTRAP_TOKEN
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: authentik-credentials
|
name: authentik-credentials
|
||||||
key: BOOTSTRAP_ADMIN_EMAIL
|
key: BOOTSTRAP_ADMIN_TOKEN
|
||||||
- name: AUTHENTIK_DISABLE_UPDATE_CHECK
|
- name: AUTHENTIK_BOOTSTRAP_EMAIL
|
||||||
value: "true"
|
valueFrom:
|
||||||
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
secretKeyRef:
|
||||||
value: "false"
|
name: authentik-credentials
|
||||||
- name: AUTHENTIK_LOG_LEVEL
|
key: BOOTSTRAP_ADMIN_EMAIL
|
||||||
value: info
|
- name: AUTHENTIK_DISABLE_UPDATE_CHECK
|
||||||
# First-boot Authentik can take 3+ min on the migration phase
|
value: "true"
|
||||||
# (waiting on DB lock while worker also runs migrations). Initial
|
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
||||||
# delays are generous so kubelet doesn't kill the pod mid-migration;
|
value: "false"
|
||||||
# periodSeconds keeps post-startup probing responsive.
|
- name: AUTHENTIK_LOG_LEVEL
|
||||||
readinessProbe:
|
value: info
|
||||||
httpGet:
|
# First-boot Authentik can take 3+ min on the migration phase
|
||||||
path: /-/health/ready/
|
# (waiting on DB lock while worker also runs migrations). Initial
|
||||||
port: 9000
|
# delays are generous so kubelet doesn't kill the pod mid-migration;
|
||||||
initialDelaySeconds: 60
|
# periodSeconds keeps post-startup probing responsive.
|
||||||
periodSeconds: 10
|
readinessProbe:
|
||||||
timeoutSeconds: 5
|
httpGet:
|
||||||
failureThreshold: 12
|
path: /-/health/ready/
|
||||||
livenessProbe:
|
port: 9000
|
||||||
httpGet:
|
initialDelaySeconds: 60
|
||||||
path: /-/health/live/
|
periodSeconds: 10
|
||||||
port: 9000
|
timeoutSeconds: 5
|
||||||
initialDelaySeconds: 300
|
failureThreshold: 12
|
||||||
periodSeconds: 30
|
livenessProbe:
|
||||||
timeoutSeconds: 10
|
httpGet:
|
||||||
failureThreshold: 3
|
path: /-/health/live/
|
||||||
startupProbe:
|
port: 9000
|
||||||
httpGet:
|
initialDelaySeconds: 300
|
||||||
path: /-/health/live/
|
periodSeconds: 30
|
||||||
port: 9000
|
timeoutSeconds: 10
|
||||||
initialDelaySeconds: 30
|
failureThreshold: 3
|
||||||
periodSeconds: 15
|
startupProbe:
|
||||||
timeoutSeconds: 10
|
httpGet:
|
||||||
failureThreshold: 40 # 30s + 40*15s = 10.5 min budget
|
path: /-/health/live/
|
||||||
resources:
|
port: 9000
|
||||||
requests: { cpu: 150m, memory: 512Mi }
|
initialDelaySeconds: 30
|
||||||
limits: { cpu: 1500m, memory: 1Gi }
|
periodSeconds: 15
|
||||||
volumeMounts:
|
timeoutSeconds: 10
|
||||||
- name: media
|
failureThreshold: 40 # 30s + 40*15s = 10.5 min budget
|
||||||
mountPath: /media
|
resources:
|
||||||
volumes:
|
requests: { cpu: 150m, memory: 512Mi }
|
||||||
- name: media
|
limits: { cpu: 1500m, memory: 1Gi }
|
||||||
persistentVolumeClaim:
|
volumeMounts:
|
||||||
claimName: authentik-media
|
- name: media
|
||||||
|
mountPath: /media
|
||||||
---
|
volumes:
|
||||||
# Authentik worker Deployment — runs Celery background tasks.
|
- name: media
|
||||||
apiVersion: apps/v1
|
persistentVolumeClaim:
|
||||||
kind: Deployment
|
claimName: authentik-media
|
||||||
metadata:
|
|
||||||
name: authentik-worker
|
---
|
||||||
namespace: authentik
|
# Authentik worker Deployment — runs Celery background tasks.
|
||||||
labels:
|
apiVersion: apps/v1
|
||||||
app: authentik-worker
|
kind: Deployment
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
metadata:
|
||||||
spec:
|
name: authentik-worker
|
||||||
replicas: 1
|
namespace: authentik
|
||||||
strategy:
|
labels:
|
||||||
type: Recreate # shares /media RWO PVC with server
|
app: authentik-worker
|
||||||
selector:
|
argocd.argoproj.io/instance: infra-authentik
|
||||||
matchLabels:
|
spec:
|
||||||
app: authentik-worker
|
replicas: 1
|
||||||
template:
|
strategy:
|
||||||
metadata:
|
type: Recreate # shares /media RWO PVC with server
|
||||||
labels:
|
selector:
|
||||||
app: authentik-worker
|
matchLabels:
|
||||||
spec:
|
app: authentik-worker
|
||||||
securityContext:
|
template:
|
||||||
# Same as server pod — non-root uid 1000 needs PVC group write.
|
metadata:
|
||||||
fsGroup: 1000
|
labels:
|
||||||
containers:
|
app: authentik-worker
|
||||||
- name: worker
|
spec:
|
||||||
image: ghcr.io/goauthentik/server:2024.12.3
|
securityContext:
|
||||||
args: ["worker"]
|
# Same as server pod — non-root uid 1000 needs PVC group write.
|
||||||
env:
|
fsGroup: 1000
|
||||||
- name: AUTHENTIK_SECRET_KEY
|
containers:
|
||||||
valueFrom:
|
- name: worker
|
||||||
secretKeyRef:
|
image: ghcr.io/goauthentik/server:2024.12.3
|
||||||
name: authentik-credentials
|
args: ["worker"]
|
||||||
key: AUTHENTIK_SECRET_KEY
|
env:
|
||||||
- name: AUTHENTIK_REDIS__HOST
|
- name: AUTHENTIK_SECRET_KEY
|
||||||
value: authentik-redis
|
valueFrom:
|
||||||
- name: AUTHENTIK_REDIS__PASSWORD
|
secretKeyRef:
|
||||||
valueFrom:
|
name: authentik-credentials
|
||||||
secretKeyRef:
|
key: AUTHENTIK_SECRET_KEY
|
||||||
name: authentik-credentials
|
- name: AUTHENTIK_REDIS__HOST
|
||||||
key: REDIS_PASSWORD
|
value: authentik-redis
|
||||||
- name: AUTHENTIK_POSTGRESQL__HOST
|
- name: AUTHENTIK_REDIS__PASSWORD
|
||||||
value: authentik-postgres
|
valueFrom:
|
||||||
- name: AUTHENTIK_POSTGRESQL__NAME
|
secretKeyRef:
|
||||||
value: authentik
|
name: authentik-credentials
|
||||||
- name: AUTHENTIK_POSTGRESQL__USER
|
key: REDIS_PASSWORD
|
||||||
value: authentik
|
- name: AUTHENTIK_POSTGRESQL__HOST
|
||||||
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
value: authentik-postgres
|
||||||
valueFrom:
|
- name: AUTHENTIK_POSTGRESQL__NAME
|
||||||
secretKeyRef:
|
value: authentik
|
||||||
name: authentik-credentials
|
- name: AUTHENTIK_POSTGRESQL__USER
|
||||||
key: POSTGRES_PASSWORD
|
value: authentik
|
||||||
- name: AUTHENTIK_DISABLE_UPDATE_CHECK
|
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
||||||
value: "true"
|
valueFrom:
|
||||||
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
secretKeyRef:
|
||||||
value: "false"
|
name: authentik-credentials
|
||||||
- name: AUTHENTIK_LOG_LEVEL
|
key: POSTGRES_PASSWORD
|
||||||
value: info
|
- name: AUTHENTIK_DISABLE_UPDATE_CHECK
|
||||||
resources:
|
value: "true"
|
||||||
requests: { cpu: 100m, memory: 256Mi }
|
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
||||||
limits: { cpu: 1000m, memory: 768Mi }
|
value: "false"
|
||||||
volumeMounts:
|
- name: AUTHENTIK_LOG_LEVEL
|
||||||
- name: media
|
value: info
|
||||||
mountPath: /media
|
resources:
|
||||||
volumes:
|
requests: { cpu: 100m, memory: 256Mi }
|
||||||
- name: media
|
limits: { cpu: 1000m, memory: 768Mi }
|
||||||
persistentVolumeClaim:
|
volumeMounts:
|
||||||
claimName: authentik-media
|
- name: media
|
||||||
|
mountPath: /media
|
||||||
---
|
volumes:
|
||||||
apiVersion: v1
|
- name: media
|
||||||
kind: Service
|
persistentVolumeClaim:
|
||||||
metadata:
|
claimName: authentik-media
|
||||||
name: authentik-server
|
|
||||||
namespace: authentik
|
---
|
||||||
spec:
|
apiVersion: v1
|
||||||
selector:
|
kind: Service
|
||||||
app: authentik-server
|
metadata:
|
||||||
ports:
|
name: authentik-server
|
||||||
- name: http
|
namespace: authentik
|
||||||
port: 9000
|
spec:
|
||||||
targetPort: 9000
|
selector:
|
||||||
- name: https
|
app: authentik-server
|
||||||
port: 9443
|
ports:
|
||||||
targetPort: 9443
|
- name: http
|
||||||
|
port: 9000
|
||||||
---
|
targetPort: 9000
|
||||||
# step-ca leaf certificate for id.iamworkin.lan.
|
- name: https
|
||||||
# step-ca container resolver uses pfSense Unbound, so the public A record for id.iamworkin.lan
|
port: 9443
|
||||||
# MUST exist before this Certificate is applied (cert-manager HTTP-01 will silently 2h-backoff
|
targetPort: 9443
|
||||||
# otherwise). Added 2026-05-25 via scripts/pfsense-add-id-host.py.
|
|
||||||
apiVersion: cert-manager.io/v1
|
---
|
||||||
kind: Certificate
|
# step-ca leaf certificate for id.iamworkin.lan.
|
||||||
metadata:
|
# step-ca container resolver uses pfSense Unbound, so the public A record for id.iamworkin.lan
|
||||||
name: authentik-tls
|
# MUST exist before this Certificate is applied (cert-manager HTTP-01 will silently 2h-backoff
|
||||||
namespace: authentik
|
# otherwise). Added 2026-05-25 via scripts/pfsense-add-id-host.py.
|
||||||
spec:
|
apiVersion: cert-manager.io/v1
|
||||||
secretName: authentik-tls
|
kind: Certificate
|
||||||
dnsNames:
|
metadata:
|
||||||
- id.iamworkin.lan
|
name: authentik-tls
|
||||||
issuerRef:
|
namespace: authentik
|
||||||
name: step-ca-acme
|
spec:
|
||||||
kind: ClusterIssuer
|
secretName: authentik-tls
|
||||||
|
dnsNames:
|
||||||
---
|
- id.iamworkin.lan
|
||||||
apiVersion: traefik.io/v1alpha1
|
issuerRef:
|
||||||
kind: IngressRoute
|
name: step-ca-acme
|
||||||
metadata:
|
kind: ClusterIssuer
|
||||||
name: authentik
|
|
||||||
namespace: authentik
|
---
|
||||||
spec:
|
apiVersion: traefik.io/v1alpha1
|
||||||
entryPoints: [websecure]
|
kind: IngressRoute
|
||||||
routes:
|
metadata:
|
||||||
- match: Host(`id.iamworkin.lan`)
|
name: authentik
|
||||||
kind: Rule
|
namespace: authentik
|
||||||
services:
|
spec:
|
||||||
- name: authentik-server
|
entryPoints: [websecure]
|
||||||
port: 9000
|
routes:
|
||||||
tls:
|
- match: Host(`id.iamworkin.lan`)
|
||||||
secretName: authentik-tls
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: authentik-server
|
||||||
|
port: 9000
|
||||||
|
tls:
|
||||||
|
secretName: authentik-tls
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ spec:
|
|||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/healthz"
|
||||||
prometheus.io/path: /metrics/prometheus
|
prometheus.io/path: /metrics/prometheus
|
||||||
prometheus.io/port: "5000"
|
prometheus.io/port: "5000"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
@@ -54,6 +56,7 @@ spec:
|
|||||||
app.kubernetes.io/part-of: flowercore
|
app.kubernetes.io/part-of: flowercore
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
- envFrom:
|
- envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: aistation-web-config
|
name: aistation-web-config
|
||||||
@@ -167,3 +170,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: aistation-web-tls
|
secretName: aistation-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose aistation-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: aistation-web-public
|
||||||
|
# namespace: fc-aistation
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`aistation.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: aistation-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: aistation-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ spec:
|
|||||||
app.kubernetes.io/name: chat-web
|
app.kubernetes.io/name: chat-web
|
||||||
app.kubernetes.io/part-of: flowercore
|
app.kubernetes.io/part-of: flowercore
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/healthz"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/metrics/prometheus"
|
prometheus.io/path: "/metrics/prometheus"
|
||||||
@@ -128,6 +130,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: chat-web-config
|
name: chat-web-config
|
||||||
|
|||||||
@@ -51,3 +51,26 @@ spec:
|
|||||||
port: 8080
|
port: 8080
|
||||||
tls:
|
tls:
|
||||||
secretName: remotedesktop-web-tls
|
secretName: remotedesktop-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose remotedesktop-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: remotedesktop-web-public
|
||||||
|
# namespace: fc-desktop
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`desktop.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: remotedesktop-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: remotedesktop-web
|
||||||
|
# port: 8080
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ spec:
|
|||||||
flowercore.io/tenant-id: system
|
flowercore.io/tenant-id: system
|
||||||
flowercore.io/created-by: bluejay-infra
|
flowercore.io/created-by: bluejay-infra
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/healthz"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/metrics"
|
prometheus.io/path: "/metrics"
|
||||||
@@ -67,6 +69,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
env:
|
env:
|
||||||
- name: ASPNETCORE_URLS
|
- name: ASPNETCORE_URLS
|
||||||
value: "http://+:8080"
|
value: "http://+:8080"
|
||||||
|
|||||||
@@ -30,3 +30,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: dms-web-tls
|
secretName: dms-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose dms-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: dms-web-public
|
||||||
|
# namespace: fc-dms
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`dms.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: dms-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: dms-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ spec:
|
|||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/health"
|
||||||
prometheus.io/path: /metrics/prometheus
|
prometheus.io/path: /metrics/prometheus
|
||||||
prometheus.io/port: "5000"
|
prometheus.io/port: "5000"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
@@ -54,6 +56,7 @@ spec:
|
|||||||
app.kubernetes.io/part-of: flowercore
|
app.kubernetes.io/part-of: flowercore
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
- envFrom:
|
- envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: library-web-config
|
name: library-web-config
|
||||||
@@ -167,3 +170,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: library-web-tls
|
secretName: library-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose library-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: library-web-public
|
||||||
|
# namespace: fc-library
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`library.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: library-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: library-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ spec:
|
|||||||
app.kubernetes.io/name: fc-llm-bridge
|
app.kubernetes.io/name: fc-llm-bridge
|
||||||
app.kubernetes.io/part-of: flowercore
|
app.kubernetes.io/part-of: flowercore
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/healthz"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/metrics"
|
prometheus.io/path: "/metrics"
|
||||||
@@ -116,6 +118,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
env:
|
env:
|
||||||
- name: ASPNETCORE_URLS
|
- name: ASPNETCORE_URLS
|
||||||
value: "http://+:8080"
|
value: "http://+:8080"
|
||||||
@@ -281,3 +284,26 @@ spec:
|
|||||||
port: 8080
|
port: 8080
|
||||||
tls:
|
tls:
|
||||||
secretName: fc-llm-bridge-tls
|
secretName: fc-llm-bridge-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose fc-llm-bridge publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: fc-llm-bridge-public
|
||||||
|
# namespace: fc-llm-bridge
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`llm-bridge.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: fc-llm-bridge-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: fc-llm-bridge
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -30,3 +30,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: menuboard-web-tls
|
secretName: menuboard-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose menuboard-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: menuboard-web-public
|
||||||
|
# namespace: fc-menuboard
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`menuboard.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: menuboard-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: menuboard-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: messageboard-web
|
app: messageboard-web
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/health"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/metrics/prometheus"
|
prometheus.io/path: "/metrics/prometheus"
|
||||||
@@ -52,6 +54,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: messageboard-web-config
|
name: messageboard-web-config
|
||||||
@@ -141,3 +144,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: messageboard-web-tls
|
secretName: messageboard-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose messageboard-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: messageboard-web-public
|
||||||
|
# namespace: fc-messageboard
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`messageboard.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: messageboard-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: messageboard-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -30,3 +30,26 @@ spec:
|
|||||||
port: 5300
|
port: 5300
|
||||||
tls:
|
tls:
|
||||||
secretName: mysql-web-tls
|
secretName: mysql-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose mysql-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: mysql-web-public
|
||||||
|
# namespace: fc-mysql
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`mysql.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: mysql-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: mysql-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -30,3 +30,26 @@ spec:
|
|||||||
port: 5400
|
port: 5400
|
||||||
tls:
|
tls:
|
||||||
secretName: php-web-tls
|
secretName: php-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose php-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: php-web-public
|
||||||
|
# namespace: fc-php
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`php.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: php-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: php-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -30,3 +30,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: presentations-web-tls
|
secretName: presentations-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose presentations-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: presentations-web-public
|
||||||
|
# namespace: fc-presentations
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`presentations.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: presentations-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: presentations-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ spec:
|
|||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/healthz"
|
||||||
kubectl.kubernetes.io/restartedAt: "2026-06-02T01:34:08-05:00"
|
kubectl.kubernetes.io/restartedAt: "2026-06-02T01:34:08-05:00"
|
||||||
prometheus.io/path: /metrics/prometheus
|
prometheus.io/path: /metrics/prometheus
|
||||||
prometheus.io/port: "5000"
|
prometheus.io/port: "5000"
|
||||||
@@ -55,6 +57,7 @@ spec:
|
|||||||
app.kubernetes.io/part-of: flowercore
|
app.kubernetes.io/part-of: flowercore
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
- envFrom:
|
- envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: retail-web-config
|
name: retail-web-config
|
||||||
@@ -168,3 +171,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: retail-web-tls
|
secretName: retail-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose retail-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: retail-web-public
|
||||||
|
# namespace: fc-retail
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`retail.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: retail-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: retail-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -30,3 +30,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: scoreboard-web-tls
|
secretName: scoreboard-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose scoreboard-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: scoreboard-web-public
|
||||||
|
# namespace: fc-scoreboard
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`scoreboard.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: scoreboard-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: scoreboard-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -37,3 +37,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: segmentdisplay-web-tls
|
secretName: segmentdisplay-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose segmentdisplay-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: segmentdisplay-web-public
|
||||||
|
# namespace: fc-segmentdisplay
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`segmentdisplay.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: segmentdisplay-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: segmentdisplay-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -46,3 +46,26 @@ spec:
|
|||||||
services:
|
services:
|
||||||
- name: signage-web
|
- name: signage-web
|
||||||
port: 5190
|
port: 5190
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose signage-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: signage-web-public
|
||||||
|
# namespace: fc-signage
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`signage.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: signage-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: signage-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: piper
|
- name: piper
|
||||||
image: rhasspy/wyoming-piper:latest
|
image: rhasspy/wyoming-piper:latest
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
env:
|
env:
|
||||||
- name: PYTHONHTTPSVERIFY
|
- name: PYTHONHTTPSVERIFY
|
||||||
value: "0"
|
value: "0"
|
||||||
@@ -523,6 +524,8 @@ spec:
|
|||||||
app.kubernetes.io/name: ttsreader-web
|
app.kubernetes.io/name: ttsreader-web
|
||||||
app.kubernetes.io/part-of: flowercore
|
app.kubernetes.io/part-of: flowercore
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/health"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "5217"
|
prometheus.io/port: "5217"
|
||||||
prometheus.io/path: "/metrics"
|
prometheus.io/path: "/metrics"
|
||||||
@@ -762,3 +765,26 @@ spec:
|
|||||||
port: 5217
|
port: 5217
|
||||||
tls:
|
tls:
|
||||||
secretName: ttsreader-tls
|
secretName: ttsreader-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose ttsreader-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: ttsreader-web-public
|
||||||
|
# namespace: fc-ttsreader
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`ttsreader.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: ttsreader-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: ttsreader-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ spec:
|
|||||||
app: updatecenter-web
|
app: updatecenter-web
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/"
|
||||||
labels:
|
labels:
|
||||||
app: updatecenter-web
|
app: updatecenter-web
|
||||||
spec:
|
spec:
|
||||||
@@ -63,6 +66,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
env:
|
env:
|
||||||
- name: ASPNETCORE_URLS
|
- name: ASPNETCORE_URLS
|
||||||
value: http://+:8080
|
value: http://+:8080
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ original Longhorn ReadWriteOnce NuGet PVC. Every other repo-scoped runner uses
|
|||||||
two replicas with per-pod `emptyDir` caches. That is the safe backlog-drain
|
two replicas with per-pod `emptyDir` caches. That is the safe backlog-drain
|
||||||
strategy: no two pods share one RWO PVC.
|
strategy: no two pods share one RWO PVC.
|
||||||
|
|
||||||
|
Ephemeral runner pods are expected to register, run one job, deregister, and
|
||||||
|
exit so the Deployment starts a fresh pod for the next registration token. A
|
||||||
|
small amount of exit-1/restart churn from token-expiry or no-work windows is
|
||||||
|
accepted operational noise as long as jobs are not stuck queued and the
|
||||||
|
repo-scoped runner-offline alerts stay quiet.
|
||||||
|
|
||||||
Sprint 32 final long-tail wave adds 16 two-replica Deployments:
|
Sprint 32 final long-tail wave adds 16 two-replica Deployments:
|
||||||
`FlowerCore.Knowledge`, `FlowerCore.LlmBridge`, `FlowerCore.Media`,
|
`FlowerCore.Knowledge`, `FlowerCore.LlmBridge`, `FlowerCore.Media`,
|
||||||
`FlowerCore.Presentations`, `FlowerCore.RemoteDesktop`, `FlowerCore.DNS`,
|
`FlowerCore.Presentations`, `FlowerCore.RemoteDesktop`, `FlowerCore.DNS`,
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ spec:
|
|||||||
app.kubernetes.io/name: knowledge-web
|
app.kubernetes.io/name: knowledge-web
|
||||||
app.kubernetes.io/part-of: bluejay-infra
|
app.kubernetes.io/part-of: bluejay-infra
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/healthz"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/metrics"
|
prometheus.io/path: "/metrics"
|
||||||
@@ -117,6 +119,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
env:
|
env:
|
||||||
- name: ASPNETCORE_URLS
|
- name: ASPNETCORE_URLS
|
||||||
value: "http://+:8080"
|
value: "http://+:8080"
|
||||||
@@ -286,3 +289,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: knowledge-tls
|
secretName: knowledge-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose knowledge-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: knowledge-web-public
|
||||||
|
# namespace: knowledge
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`knowledge.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: knowledge-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: knowledge-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
@@ -843,7 +843,9 @@ data:
|
|||||||
rules:
|
rules:
|
||||||
- alert: PiManagerDown
|
- alert: PiManagerDown
|
||||||
expr: up{job="pimanager-app"} == 0
|
expr: up{job="pimanager-app"} == 0
|
||||||
for: 3m
|
# Sprint 67: delayed behind NodeDown's critical page so a powered-off
|
||||||
|
# Pi does not create the first duplicate page for the same host.
|
||||||
|
for: 8m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
annotations:
|
annotations:
|
||||||
@@ -1242,6 +1244,58 @@ data:
|
|||||||
summary: "Marquee animation duration drifting > 10% on {{ $labels.renderer }} ({{ $labels.phase }})"
|
summary: "Marquee animation duration drifting > 10% on {{ $labels.renderer }} ({{ $labels.phase }})"
|
||||||
description: "Median observed cycle duration deviates from target DurationMs by >10%. Could indicate browser tab throttling, GPU pressure, or phase-advancement bug."
|
description: "Median observed cycle duration deviates from target DurationMs by >10%. Could indicate browser tab throttling, GPU pressure, or phase-advancement bug."
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Update Center public-edge probes
|
||||||
|
# Live-mirrored from FlowerCore.Notes/scripts/monitoring/alerts.yml.
|
||||||
|
# This K8s ConfigMap is the future migration target; live Prometheus
|
||||||
|
# still reads the canonical Notes file from noc1 Podman.
|
||||||
|
# ============================================================
|
||||||
|
- name: update_center
|
||||||
|
rules:
|
||||||
|
# Critical only when the edge is genuinely unreachable. A Cloudflare
|
||||||
|
# HTTP 429 means the prober hit a rate-limit, not that real clients
|
||||||
|
# are down, so the warning rule below owns that signal.
|
||||||
|
- alert: UpdateCenterPublicEdgeDown
|
||||||
|
expr: |
|
||||||
|
(probe_success{job="probe-update-center-public-edge"} == 0)
|
||||||
|
unless on(instance)
|
||||||
|
(probe_http_status_code{job="probe-update-center-public-edge"} == 429)
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
service: update-center
|
||||||
|
alert_channel: irc
|
||||||
|
annotations:
|
||||||
|
summary: "Update Center public edge probe failed for {{ $labels.instance }}"
|
||||||
|
description: >-
|
||||||
|
The external probe for {{ $labels.instance }} failed for 10 minutes with a
|
||||||
|
non-2xx status that is not a rate-limit. Public Update Center clients may be
|
||||||
|
unable to fetch manifest schema metadata through Cloudflare.
|
||||||
|
runbook: >-
|
||||||
|
1. curl -sk https://{{ $labels.instance }}/api/v1/manifests/_schema
|
||||||
|
2. Verify Cloudflare DNS record is proxied and targets the current public edge IP
|
||||||
|
3. kubectl -n fc-updater get ingressroute updatecenter-web-public secret cf-origin-flowercore-io
|
||||||
|
4. Check Traefik logs for Method() or TLS secret errors
|
||||||
|
|
||||||
|
- alert: UpdateCenterPublicEdgeRateLimited
|
||||||
|
expr: probe_http_status_code{job="probe-update-center-public-edge"} == 429
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
service: update-center
|
||||||
|
alert_channel: irc
|
||||||
|
annotations:
|
||||||
|
summary: "Cloudflare is rate-limiting (HTTP 429) the public-edge probe for {{ $labels.instance }}"
|
||||||
|
description: >-
|
||||||
|
The blackbox prober receives HTTP 429 from Cloudflare for {{ $labels.instance }}
|
||||||
|
while the origin is healthy. This is a Cloudflare rate-limit / WAF condition on
|
||||||
|
the public hostname, not an outage.
|
||||||
|
runbook: >-
|
||||||
|
1. curl -sk https://{{ $labels.instance }}/api/v1/manifests/_schema (expect 200 from a normal client)
|
||||||
|
2. Review Cloudflare rate-limit / WAF rules for the hostname; the 5m-cadence prober is tripping a 429
|
||||||
|
3. Add a Cloudflare rate-limit exception for the prober source IP or the /api/v1/manifests/_schema path
|
||||||
|
4. Confirm whether the singular host update.flowercore.io is still required, or only updates.flowercore.io
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ConfigMap: Blackbox Exporter Configuration
|
# ConfigMap: Blackbox Exporter Configuration
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ spec:
|
|||||||
app: telephony-web
|
app: telephony-web
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/health"
|
||||||
labels:
|
labels:
|
||||||
app: telephony-web
|
app: telephony-web
|
||||||
spec:
|
spec:
|
||||||
@@ -161,6 +164,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- containerPort: 5100
|
- containerPort: 5100
|
||||||
name: http
|
name: http
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
env:
|
env:
|
||||||
- name: Telephony__Twilio__AccountSid
|
- name: Telephony__Twilio__AccountSid
|
||||||
valueFrom:
|
valueFrom:
|
||||||
@@ -387,4 +391,3 @@ spec:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ spec:
|
|||||||
flowercore.io/tenant-id: system
|
flowercore.io/tenant-id: system
|
||||||
flowercore.io/created-by: bluejay-infra
|
flowercore.io/created-by: bluejay-infra
|
||||||
annotations:
|
annotations:
|
||||||
|
fc.flowercore.io/healthz-anon: "true"
|
||||||
|
fc.flowercore.io/probe-path: "/healthz"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/metrics/prometheus"
|
prometheus.io/path: "/metrics/prometheus"
|
||||||
@@ -93,6 +95,7 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
|
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||||
env:
|
env:
|
||||||
- name: ASPNETCORE_URLS
|
- name: ASPNETCORE_URLS
|
||||||
value: "http://+:8080"
|
value: "http://+:8080"
|
||||||
@@ -254,3 +257,26 @@ spec:
|
|||||||
port: 80
|
port: 80
|
||||||
tls:
|
tls:
|
||||||
secretName: worldbuilder-web-tls
|
secretName: worldbuilder-web-tls
|
||||||
|
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||||
|
# When the operator decides to expose worldbuilder-web publicly, uncomment + update the host,
|
||||||
|
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||||
|
#
|
||||||
|
# --- IngressRoute ---
|
||||||
|
# apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: worldbuilder-web-public
|
||||||
|
# namespace: worldbuilder
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [websecure]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`worldbuilder.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: worldbuilder-web-public-profile-header # injects entitlement profile
|
||||||
|
# services:
|
||||||
|
# - name: worldbuilder-web
|
||||||
|
# port: 80
|
||||||
|
# tls: {}
|
||||||
|
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||||
|
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||||
|
|||||||
74
argocd/applicationset-bluejay-infra.yaml
Normal file
74
argocd/applicationset-bluejay-infra.yaml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: ApplicationSet
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/refresh: "true"
|
||||||
|
name: bluejay-infra
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
generators:
|
||||||
|
- git:
|
||||||
|
directories:
|
||||||
|
- path: apps/*
|
||||||
|
repoURL: http://gitea-clusterip.gitea.svc:3000/bluejay/bluejay-infra.git
|
||||||
|
revision: main
|
||||||
|
template:
|
||||||
|
metadata: {}
|
||||||
|
spec:
|
||||||
|
destination: {}
|
||||||
|
project: ""
|
||||||
|
goTemplate: true
|
||||||
|
goTemplateOptions:
|
||||||
|
- missingkey=error
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: infra-{{.path.basename}}
|
||||||
|
spec:
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
ignoreDifferences:
|
||||||
|
- group: apps
|
||||||
|
jqPathExpressions:
|
||||||
|
- .spec.volumeClaimTemplates[]?.status
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/volumeClaimTemplates
|
||||||
|
kind: StatefulSet
|
||||||
|
name: zabbix-postgres
|
||||||
|
namespace: zabbix
|
||||||
|
- group: apps
|
||||||
|
jqPathExpressions:
|
||||||
|
- .spec.volumeClaimTemplates[]?.status
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/volumeClaimTemplates
|
||||||
|
kind: StatefulSet
|
||||||
|
name: guac-mysql
|
||||||
|
namespace: guacamole
|
||||||
|
- group: apps
|
||||||
|
jqPathExpressions:
|
||||||
|
- .spec.volumeClaimTemplates[]?.status
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/volumeClaimTemplates
|
||||||
|
kind: StatefulSet
|
||||||
|
name: matrix-postgres
|
||||||
|
namespace: matrix
|
||||||
|
- group: apps
|
||||||
|
jqPathExpressions:
|
||||||
|
- .spec.volumeClaimTemplates[]?.status
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/volumeClaimTemplates
|
||||||
|
kind: StatefulSet
|
||||||
|
name: authentik-postgres
|
||||||
|
namespace: authentik
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
path: '{{.path.path}}'
|
||||||
|
repoURL: http://gitea-clusterip.gitea.svc:3000/bluejay/bluejay-infra.git
|
||||||
|
targetRevision: main
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ServerSideApply=true
|
||||||
|
- RespectIgnoreDifferences=true
|
||||||
@@ -17,21 +17,17 @@ public sealed class FleetManifestLintTests
|
|||||||
"dist.flowercore.io",
|
"dist.flowercore.io",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public hosts that allow a tightly bounded write surface in addition to
|
// Hosts that allow a tightly bounded write surface in addition to GET/HEAD.
|
||||||
// GET/HEAD. updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id}
|
// updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id}
|
||||||
// (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS — but
|
// (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS — but
|
||||||
// PUT/PATCH/DELETE must still 404 at the route. Anything wider than this
|
// PUT/PATCH/DELETE must still 404 at the route. Public
|
||||||
// set should fail this lint.
|
// update.flowercore.io remains a GET/HEAD download surface in the
|
||||||
//
|
// FlowerCore.Updater sibling manifest and is covered by the general
|
||||||
// PUB-1 (2026-05-06): update.flowercore.io / updates.flowercore.io were
|
// public-method allowlist lint instead of this write-surface rule.
|
||||||
// added for the Cloudflare-proxied public Update Center edge. They use the
|
|
||||||
// same bounded read-write allowlist as the LAN pair.
|
|
||||||
private static readonly HashSet<string> PublicReadWriteAllowlistHosts = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> PublicReadWriteAllowlistHosts = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
"updatecenter.iamworkin.lan",
|
"updatecenter.iamworkin.lan",
|
||||||
"updates.iamworkin.lan",
|
"updates.iamworkin.lan",
|
||||||
"update.flowercore.io",
|
|
||||||
"updates.flowercore.io",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly HashSet<string> ApiKeyProtectedDeployments = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> ApiKeyProtectedDeployments = new(StringComparer.Ordinal)
|
||||||
@@ -69,7 +65,7 @@ public sealed class FleetManifestLintTests
|
|||||||
["github-runner-updater"] = "https://github.com/astoltz/FlowerCore.Updater",
|
["github-runner-updater"] = "https://github.com/astoltz/FlowerCore.Updater",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly HashSet<string> ScaledLinuxRunnerDeployments = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> RepoScopedLinuxRunnerDeployments = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
"github-runner-sharedpos",
|
"github-runner-sharedpos",
|
||||||
"github-runner-puppet",
|
"github-runner-puppet",
|
||||||
@@ -83,6 +79,44 @@ public sealed class FleetManifestLintTests
|
|||||||
"github-runner-updater",
|
"github-runner-updater",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, (string Deployment, string ProbePath)> BroaderHardeningDeployments =
|
||||||
|
new Dictionary<string, (string Deployment, string ProbePath)>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["fc-aistation"] = ("aistation-web", "/healthz"),
|
||||||
|
["fc-chat"] = ("chat-web", "/healthz"),
|
||||||
|
["fc-devicemgmt"] = ("fc-devicemgmt-web", "/healthz"),
|
||||||
|
["fc-library"] = ("library-web", "/health"),
|
||||||
|
["fc-llm-bridge"] = ("fc-llm-bridge", "/healthz"),
|
||||||
|
["fc-messageboard"] = ("messageboard-web", "/health"),
|
||||||
|
["fc-retail"] = ("retail-web", "/healthz"),
|
||||||
|
["fc-ttsreader"] = ("ttsreader-web", "/health"),
|
||||||
|
["fc-updater"] = ("updatecenter-web", "/"),
|
||||||
|
["knowledge"] = ("knowledge-web", "/healthz"),
|
||||||
|
["telephony"] = ("telephony-web", "/health"),
|
||||||
|
["worldbuilder"] = ("worldbuilder-web", "/healthz"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> BroaderHardeningInternalPrestageApps = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
"fc-aistation",
|
||||||
|
"fc-desktop",
|
||||||
|
"fc-dms",
|
||||||
|
"fc-library",
|
||||||
|
"fc-llm-bridge",
|
||||||
|
"fc-menuboard",
|
||||||
|
"fc-messageboard",
|
||||||
|
"fc-mysql",
|
||||||
|
"fc-php",
|
||||||
|
"fc-presentations",
|
||||||
|
"fc-retail",
|
||||||
|
"fc-scoreboard",
|
||||||
|
"fc-segmentdisplay",
|
||||||
|
"fc-signage",
|
||||||
|
"fc-ttsreader",
|
||||||
|
"knowledge",
|
||||||
|
"worldbuilder",
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<string, string> WritableRunnerEnv = new Dictionary<string, string>(StringComparer.Ordinal)
|
private static readonly IReadOnlyDictionary<string, string> WritableRunnerEnv = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
["HOME"] = "/home/runner",
|
["HOME"] = "/home/runner",
|
||||||
@@ -271,17 +305,17 @@ public sealed class FleetManifestLintTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForScaledDeployments()
|
public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForRepoScopedDeployments()
|
||||||
{
|
{
|
||||||
var deployments = GitHubRunnerDeployments();
|
var deployments = GitHubRunnerDeployments();
|
||||||
|
|
||||||
foreach (var deploymentName in ScaledLinuxRunnerDeployments)
|
foreach (var deploymentName in RepoScopedLinuxRunnerDeployments)
|
||||||
{
|
{
|
||||||
var deployment = deployments[deploymentName];
|
var deployment = deployments[deploymentName];
|
||||||
// Scaled runners must have >= 2 replicas (avoid single-pod bottleneck).
|
// Sprint 34 ops trimmed runner load while the cluster was degraded
|
||||||
// Individual deployments may be tuned upward per CI activity — see
|
// to two healthy nodes. Repo-scoped runners can be tuned back above
|
||||||
// "runners: right-size replica counts per 14d CI activity (#24)".
|
// one replica, but they must stay RWO-safe before that happens.
|
||||||
ReplicaCount(deployment).Should().BeGreaterOrEqualTo(2, $"{deploymentName} is in the scaled set and must run with at least 2 replicas");
|
ReplicaCount(deployment).Should().BeGreaterOrEqualTo(1, $"{deploymentName} must keep at least one repo-scoped runner online");
|
||||||
|
|
||||||
var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes");
|
var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes");
|
||||||
var claimNames = volumes
|
var claimNames = volumes
|
||||||
@@ -289,7 +323,7 @@ public sealed class FleetManifestLintTests
|
|||||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
claimNames.Should().BeEmpty($"{deploymentName} is scaled and must not share a RWO PVC");
|
claimNames.Should().BeEmpty($"{deploymentName} must remain ready for safe multi-replica scaling without sharing a RWO PVC");
|
||||||
volumes.Should().Contain(volume =>
|
volumes.Should().Contain(volume =>
|
||||||
string.Equals(ManifestNodeExtensions.Scalar(volume, "name"), "nuget-cache", StringComparison.Ordinal)
|
string.Equals(ManifestNodeExtensions.Scalar(volume, "name"), "nuget-cache", StringComparison.Ordinal)
|
||||||
&& ManifestNodeExtensions.Mapping(volume, "emptyDir") != null);
|
&& ManifestNodeExtensions.Mapping(volume, "emptyDir") != null);
|
||||||
@@ -434,6 +468,99 @@ public sealed class FleetManifestLintTests
|
|||||||
monitoring.Should().Contain("dedicated LinuxRunnerOffline/MacMiniRunnerOffline alerts");
|
monitoring.Should().Contain("dedicated LinuxRunnerOffline/MacMiniRunnerOffline alerts");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GithubRunnerReadme_DocumentsAcceptedEphemeralExitChurn()
|
||||||
|
{
|
||||||
|
var readme = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "github-runner", "README.md"));
|
||||||
|
|
||||||
|
readme.Should().Contain("Ephemeral runner pods");
|
||||||
|
readme.Should().Contain("exit-1/restart churn");
|
||||||
|
readme.Should().Contain("accepted operational noise");
|
||||||
|
readme.Should().Contain("repo-scoped runner-offline alerts stay quiet");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Monitoring_PiManagerDownDelayAndUpdateCenterRateLimit_MatchCanonicalAlerts()
|
||||||
|
{
|
||||||
|
var notesAlerts = File.ReadAllText(Path.Combine(
|
||||||
|
Inventory.WorkspaceRoot,
|
||||||
|
"FlowerCore.Notes",
|
||||||
|
"scripts",
|
||||||
|
"monitoring",
|
||||||
|
"alerts.yml"));
|
||||||
|
var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml"));
|
||||||
|
|
||||||
|
notesAlerts.Should().Contain("# Sprint 67: keep this warning behind NodeDown's 5m critical page");
|
||||||
|
notesAlerts.Should().Contain("- alert: PiManagerDown");
|
||||||
|
notesAlerts.Should().Contain("for: 8m");
|
||||||
|
monitoring.Should().Contain("# Sprint 67: delayed behind NodeDown's critical page");
|
||||||
|
monitoring.Should().Contain("- alert: PiManagerDown");
|
||||||
|
monitoring.Should().Contain("for: 8m");
|
||||||
|
|
||||||
|
notesAlerts.Should().Contain("- alert: UpdateCenterPublicEdgeRateLimited");
|
||||||
|
notesAlerts.Should().Contain("expr: probe_http_status_code{job=\"probe-update-center-public-edge\"} == 429");
|
||||||
|
notesAlerts.Should().Contain("for: 15m");
|
||||||
|
monitoring.Should().Contain("- alert: UpdateCenterPublicEdgeRateLimited");
|
||||||
|
monitoring.Should().Contain("expr: probe_http_status_code{job=\"probe-update-center-public-edge\"} == 429");
|
||||||
|
monitoring.Should().Contain("for: 15m");
|
||||||
|
monitoring.Should().Contain("severity: warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplicationSetExport_MustRemainManualRootOfGitOpsTree()
|
||||||
|
{
|
||||||
|
var readme = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "README.md"));
|
||||||
|
var appsetPath = Path.Combine(Inventory.BluejayRoot, "argocd", "applicationset-bluejay-infra.yaml");
|
||||||
|
|
||||||
|
File.Exists(appsetPath).Should().BeTrue();
|
||||||
|
var appset = File.ReadAllText(appsetPath);
|
||||||
|
|
||||||
|
appset.Should().Contain("kind: ApplicationSet");
|
||||||
|
appset.Should().Contain("name: bluejay-infra");
|
||||||
|
appset.Should().NotContain("\nstatus:");
|
||||||
|
appset.Should().NotContain("managedFields:");
|
||||||
|
readme.Should().Contain("root of this GitOps tree");
|
||||||
|
readme.Should().Contain("NOT self-managed");
|
||||||
|
readme.Should().Contain("kubectl -n argocd apply -f argocd/applicationset-bluejay-infra.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplicationSetExport_MustDiscoverAppsDirectoryOnMain()
|
||||||
|
{
|
||||||
|
var appset = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "argocd", "applicationset-bluejay-infra.yaml"));
|
||||||
|
|
||||||
|
appset.Should().Contain("path: apps/*");
|
||||||
|
appset.Should().Contain("revision: main");
|
||||||
|
appset.Should().Contain("repoURL: http://gitea-clusterip.gitea.svc:3000/bluejay/bluejay-infra.git");
|
||||||
|
appset.Should().Contain("path: '{{.path.path}}'");
|
||||||
|
appset.Should().Contain("targetRevision: main");
|
||||||
|
appset.Should().Contain("ServerSideApply=true");
|
||||||
|
appset.Should().Contain("RespectIgnoreDifferences=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplicationSetExport_MustPreserveStatefulSetIgnoreDifferences()
|
||||||
|
{
|
||||||
|
var appset = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "argocd", "applicationset-bluejay-infra.yaml"));
|
||||||
|
|
||||||
|
appset.Should().Contain("jsonPointers:");
|
||||||
|
appset.Should().Contain("- /spec/volumeClaimTemplates");
|
||||||
|
appset.Should().Contain(".spec.volumeClaimTemplates[]?.status");
|
||||||
|
Regex.Matches(appset, "kind: StatefulSet").Should().HaveCount(4);
|
||||||
|
|
||||||
|
foreach (var (name, ns) in new[]
|
||||||
|
{
|
||||||
|
("zabbix-postgres", "zabbix"),
|
||||||
|
("guac-mysql", "guacamole"),
|
||||||
|
("matrix-postgres", "matrix"),
|
||||||
|
("authentik-postgres", "authentik"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
appset.Should().Contain($"name: {name}");
|
||||||
|
appset.Should().Contain($"namespace: {ns}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Monitoring_BlackboxTargetsForOidcSensitiveServices_MustUseAnonymousHealthRoutesWhenAvailable()
|
public void Monitoring_BlackboxTargetsForOidcSensitiveServices_MustUseAnonymousHealthRoutesWhenAvailable()
|
||||||
{
|
{
|
||||||
@@ -612,7 +739,6 @@ public sealed class FleetManifestLintTests
|
|||||||
var expectedFiles = new[]
|
var expectedFiles = new[]
|
||||||
{
|
{
|
||||||
"1password-item.yaml",
|
"1password-item.yaml",
|
||||||
"argocd-application.yaml",
|
|
||||||
"certificate-web.yaml",
|
"certificate-web.yaml",
|
||||||
"clusterrole-operator.yaml",
|
"clusterrole-operator.yaml",
|
||||||
"clusterrolebinding-operator.yaml",
|
"clusterrolebinding-operator.yaml",
|
||||||
@@ -768,17 +894,62 @@ public sealed class FleetManifestLintTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FcDeviceManagement_ArgocdApplicationMustMatchApplicationSetDiscoveryConventions()
|
public void FcDeviceManagement_MustRelyOnApplicationSetDiscovery()
|
||||||
{
|
{
|
||||||
var application = FcDeviceManagementDocuments()
|
var documents = FcDeviceManagementDocuments();
|
||||||
.Single(document => document.Kind == "Application" && document.Name == "infra-fc-devicemgmt");
|
|
||||||
|
|
||||||
application.Namespace.Should().Be("argocd");
|
documents.Should().NotContain(document => document.Kind == "Application");
|
||||||
application.Scalar("spec", "source", "repoURL")
|
|
||||||
.Should()
|
var ns = documents.Single(document => document.Kind == "Namespace" && document.Name == "fc-devicemgmt");
|
||||||
.Be("http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git");
|
ns.FileText.Should().Contain("ArgoCD discovers this directory as Application `infra-fc-devicemgmt`.");
|
||||||
application.Scalar("spec", "source", "path").Should().Be("apps/fc-devicemgmt");
|
}
|
||||||
application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt");
|
|
||||||
|
[Fact]
|
||||||
|
public void BroaderHardeningDeployments_MustAnnotateAnonymousHealthProbeIntent()
|
||||||
|
{
|
||||||
|
foreach (var expected in BroaderHardeningDeployments)
|
||||||
|
{
|
||||||
|
var deployment = AppDocuments(expected.Key)
|
||||||
|
.Single(document => document.Kind == "Deployment" && document.Name == expected.Value.Deployment);
|
||||||
|
|
||||||
|
PodAnnotation(deployment, "fc.flowercore.io/healthz-anon").Should().Be("true");
|
||||||
|
PodAnnotation(deployment, "fc.flowercore.io/probe-path").Should().Be(expected.Value.ProbePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BroaderHardeningDeployments_MustDocumentForwardedProtoAuthPosture()
|
||||||
|
{
|
||||||
|
foreach (var expected in BroaderHardeningDeployments)
|
||||||
|
{
|
||||||
|
var deployment = AppDocuments(expected.Key)
|
||||||
|
.Single(document => document.Kind == "Deployment" && document.Name == expected.Value.Deployment);
|
||||||
|
|
||||||
|
deployment.FileText.Should().Contain(
|
||||||
|
"fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BroaderHardeningInternalApps_MustOnlyPrestageCommentedPublicMethodAllowlist()
|
||||||
|
{
|
||||||
|
foreach (var app in BroaderHardeningInternalPrestageApps)
|
||||||
|
{
|
||||||
|
var documents = AppDocuments(app);
|
||||||
|
var text = string.Join(Environment.NewLine, documents.Select(document => document.FileText));
|
||||||
|
|
||||||
|
text.Should().Contain("PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only)");
|
||||||
|
text.Should().Contain("# - match: Host(`");
|
||||||
|
text.Should().Contain("Method(`GET`) || Method(`HEAD`)");
|
||||||
|
|
||||||
|
documents
|
||||||
|
.Where(document => document.Kind == "IngressRoute")
|
||||||
|
.SelectMany(document => document.MappingSequence("spec", "routes"))
|
||||||
|
.Select(route => ManifestNodeExtensions.Scalar(route, "match") ?? string.Empty)
|
||||||
|
.Should()
|
||||||
|
.NotContain(match => match.Contains(".flowercore.io", StringComparison.Ordinal),
|
||||||
|
"Sprint 61 broader hardening only pre-stages commented public hosts for internal-only apps");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user