Compare commits
1 Commits
codex/s63-
...
sprint37/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a4a8264d9 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,4 +0,0 @@
|
|||||||
/.gitattributes text eol=lf
|
|
||||||
*.yaml text eol=lf
|
|
||||||
*.yml text eol=lf
|
|
||||||
*.sh text eol=lf
|
|
||||||
10
README.md
10
README.md
@@ -116,16 +116,6 @@ dotnet test tests/bluejay-infra-lint/BluejayInfraLint.Tests.csproj -c Release
|
|||||||
|
|
||||||
That test project sweeps `bluejay-infra/apps/**` plus the canonical sibling `FlowerCore.*\\k8s` manifests that share the same workspace. Matching `conftest.dev` policy files live under `tests/bluejay-infra-lint/conftest.dev/` for environments that also have `conftest` or `opa`.
|
That test project sweeps `bluejay-infra/apps/**` plus the canonical sibling `FlowerCore.*\\k8s` manifests that share the same workspace. Matching `conftest.dev` policy files live under `tests/bluejay-infra-lint/conftest.dev/` for environments that also have `conftest` or `opa`.
|
||||||
|
|
||||||
## Non-K8s Pi Artifacts
|
|
||||||
|
|
||||||
Some `apps/*` directories are deployment artifact bundles consumed by Puppet
|
|
||||||
instead of Kubernetes workloads. `apps/fc-signage-pi-player/` carries the
|
|
||||||
Chromium signage Pi player, `apps/fc-divoom-dm-pi-device/` carries the additive
|
|
||||||
edge2 Divoom-as-DeviceManagement-device profile/Hiera contract, and
|
|
||||||
`apps/fc-divoom-tv-pi/` carries the Divoom TV Pi HDMI systemd/Puppet shape.
|
|
||||||
These bundles intentionally avoid Deployment, IngressRoute, Certificate, and
|
|
||||||
OnePasswordItem resources.
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- OpenVox noc1 durability runbook: `docs/runbooks/openvoxserver-quadlet-durability.md`
|
- OpenVox noc1 durability runbook: `docs/runbooks/openvoxserver-quadlet-durability.md`
|
||||||
|
|||||||
@@ -1,448 +0,0 @@
|
|||||||
# Authentik OIDC backend
|
|
||||||
# ArgoCD-managed. BlueJay Lab.
|
|
||||||
#
|
|
||||||
# Stack:
|
|
||||||
# - PostgreSQL 16 StatefulSet (single replica, Longhorn RWO 5Gi)
|
|
||||||
# - Redis 7 Deployment (no persistence — session/cache only)
|
|
||||||
# - Authentik server + worker Deployments (image ghcr.io/goauthentik/server:2024.12.3)
|
|
||||||
# - Media PVC shared between server + worker (Longhorn RWO 2Gi)
|
|
||||||
# - Certificate via step-ca-acme ClusterIssuer
|
|
||||||
# - Traefik IngressRoute at id.iamworkin.lan
|
|
||||||
#
|
|
||||||
# Secrets come from 1Password item "authentik-credentials" (IAmWorkin vault, id y6i74ch22q5wvm7znquq4nhhcu)
|
|
||||||
# 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.
|
|
||||||
# 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).
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: authentik
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: bluejay-infra
|
|
||||||
|
|
||||||
---
|
|
||||||
# 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,
|
|
||||||
# BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_TOKEN, BOOTSTRAP_ADMIN_EMAIL.
|
|
||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: authentik-credentials
|
|
||||||
namespace: authentik
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/authentik-credentials"
|
|
||||||
|
|
||||||
---
|
|
||||||
# Shared media volume for server + worker pods.
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: authentik-media
|
|
||||||
namespace: authentik
|
|
||||||
spec:
|
|
||||||
storageClassName: longhorn
|
|
||||||
accessModes: [ReadWriteOnce]
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 2Gi
|
|
||||||
|
|
||||||
---
|
|
||||||
# PostgreSQL 16 StatefulSet — Authentik's primary store.
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: authentik-postgres
|
|
||||||
namespace: authentik
|
|
||||||
labels:
|
|
||||||
app: authentik-postgres
|
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
|
||||||
spec:
|
|
||||||
persistentVolumeClaimRetentionPolicy:
|
|
||||||
whenDeleted: Retain
|
|
||||||
whenScaled: Retain
|
|
||||||
podManagementPolicy: OrderedReady
|
|
||||||
serviceName: authentik-postgres
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 10
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: authentik-postgres
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: authentik-postgres
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: postgres
|
|
||||||
image: postgres:16-alpine
|
|
||||||
ports:
|
|
||||||
- containerPort: 5432
|
|
||||||
name: postgres
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
value: authentik
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: POSTGRES_PASSWORD
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
value: authentik
|
|
||||||
- name: POSTGRES_INITDB_ARGS
|
|
||||||
value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
|
||||||
- name: PGDATA
|
|
||||||
value: /var/lib/postgresql/data/pgdata
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command: ["pg_isready", "-U", "authentik"]
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command: ["pg_isready", "-U", "authentik"]
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
resources:
|
|
||||||
requests: { cpu: 100m, memory: 256Mi }
|
|
||||||
limits: { cpu: 1000m, memory: 1Gi }
|
|
||||||
volumeMounts:
|
|
||||||
- name: pgdata
|
|
||||||
mountPath: /var/lib/postgresql/data
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- metadata:
|
|
||||||
name: pgdata
|
|
||||||
spec:
|
|
||||||
storageClassName: longhorn
|
|
||||||
accessModes: [ReadWriteOnce]
|
|
||||||
volumeMode: Filesystem
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: authentik-postgres
|
|
||||||
namespace: authentik
|
|
||||||
spec:
|
|
||||||
clusterIP: None
|
|
||||||
selector:
|
|
||||||
app: authentik-postgres
|
|
||||||
ports:
|
|
||||||
- name: postgres
|
|
||||||
port: 5432
|
|
||||||
targetPort: 5432
|
|
||||||
|
|
||||||
---
|
|
||||||
# Redis 7 — session storage + Celery broker. No persistence needed (cache).
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: authentik-redis
|
|
||||||
namespace: authentik
|
|
||||||
labels:
|
|
||||||
app: authentik-redis
|
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: authentik-redis
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: authentik-redis
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: redis
|
|
||||||
image: redis:7-alpine
|
|
||||||
args:
|
|
||||||
- "--save"
|
|
||||||
- ""
|
|
||||||
- "--appendonly"
|
|
||||||
- "no"
|
|
||||||
- "--requirepass"
|
|
||||||
- "$(REDIS_PASSWORD)"
|
|
||||||
env:
|
|
||||||
- name: REDIS_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: REDIS_PASSWORD
|
|
||||||
ports:
|
|
||||||
- containerPort: 6379
|
|
||||||
name: redis
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket: { port: 6379 }
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket: { port: 6379 }
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
resources:
|
|
||||||
requests: { cpu: 50m, memory: 64Mi }
|
|
||||||
limits: { cpu: 500m, memory: 256Mi }
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: authentik-redis
|
|
||||||
namespace: authentik
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: authentik-redis
|
|
||||||
ports:
|
|
||||||
- name: redis
|
|
||||||
port: 6379
|
|
||||||
targetPort: 6379
|
|
||||||
|
|
||||||
---
|
|
||||||
# Authentik server Deployment — HTTP frontend on :9000.
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: authentik-server
|
|
||||||
namespace: authentik
|
|
||||||
labels:
|
|
||||||
app: authentik-server
|
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate # shares /media RWO PVC with worker
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: authentik-server
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: authentik-server
|
|
||||||
spec:
|
|
||||||
securityContext:
|
|
||||||
# Authentik image runs as uid 1000 "authentik" but the Longhorn PVC mounts
|
|
||||||
# root:root by default. fsGroup recursively chgrp + chmod g+rwx so the
|
|
||||||
# non-root container can mkdir /media/public during the tenant_files migration.
|
|
||||||
fsGroup: 1000
|
|
||||||
containers:
|
|
||||||
- name: server
|
|
||||||
image: ghcr.io/goauthentik/server:2024.12.3
|
|
||||||
args: ["server"]
|
|
||||||
ports:
|
|
||||||
- containerPort: 9000
|
|
||||||
name: http
|
|
||||||
- containerPort: 9443
|
|
||||||
name: https
|
|
||||||
env:
|
|
||||||
- name: AUTHENTIK_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: AUTHENTIK_SECRET_KEY
|
|
||||||
- name: AUTHENTIK_REDIS__HOST
|
|
||||||
value: authentik-redis
|
|
||||||
- name: AUTHENTIK_REDIS__PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: REDIS_PASSWORD
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__HOST
|
|
||||||
value: authentik-postgres
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__NAME
|
|
||||||
value: authentik
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__USER
|
|
||||||
value: authentik
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: POSTGRES_PASSWORD
|
|
||||||
- name: AUTHENTIK_BOOTSTRAP_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: BOOTSTRAP_ADMIN_PASSWORD
|
|
||||||
- name: AUTHENTIK_BOOTSTRAP_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: BOOTSTRAP_ADMIN_TOKEN
|
|
||||||
- name: AUTHENTIK_BOOTSTRAP_EMAIL
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: BOOTSTRAP_ADMIN_EMAIL
|
|
||||||
- name: AUTHENTIK_DISABLE_UPDATE_CHECK
|
|
||||||
value: "true"
|
|
||||||
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
|
||||||
value: "false"
|
|
||||||
- name: AUTHENTIK_LOG_LEVEL
|
|
||||||
value: info
|
|
||||||
# First-boot Authentik can take 3+ min on the migration phase
|
|
||||||
# (waiting on DB lock while worker also runs migrations). Initial
|
|
||||||
# delays are generous so kubelet doesn't kill the pod mid-migration;
|
|
||||||
# periodSeconds keeps post-startup probing responsive.
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /-/health/ready/
|
|
||||||
port: 9000
|
|
||||||
initialDelaySeconds: 60
|
|
||||||
periodSeconds: 10
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 12
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /-/health/live/
|
|
||||||
port: 9000
|
|
||||||
initialDelaySeconds: 300
|
|
||||||
periodSeconds: 30
|
|
||||||
timeoutSeconds: 10
|
|
||||||
failureThreshold: 3
|
|
||||||
startupProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /-/health/live/
|
|
||||||
port: 9000
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 15
|
|
||||||
timeoutSeconds: 10
|
|
||||||
failureThreshold: 40 # 30s + 40*15s = 10.5 min budget
|
|
||||||
resources:
|
|
||||||
requests: { cpu: 150m, memory: 512Mi }
|
|
||||||
limits: { cpu: 1500m, memory: 1Gi }
|
|
||||||
volumeMounts:
|
|
||||||
- name: media
|
|
||||||
mountPath: /media
|
|
||||||
volumes:
|
|
||||||
- name: media
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: authentik-media
|
|
||||||
|
|
||||||
---
|
|
||||||
# Authentik worker Deployment — runs Celery background tasks.
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: authentik-worker
|
|
||||||
namespace: authentik
|
|
||||||
labels:
|
|
||||||
app: authentik-worker
|
|
||||||
argocd.argoproj.io/instance: infra-authentik
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate # shares /media RWO PVC with server
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: authentik-worker
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: authentik-worker
|
|
||||||
spec:
|
|
||||||
securityContext:
|
|
||||||
# Same as server pod — non-root uid 1000 needs PVC group write.
|
|
||||||
fsGroup: 1000
|
|
||||||
containers:
|
|
||||||
- name: worker
|
|
||||||
image: ghcr.io/goauthentik/server:2024.12.3
|
|
||||||
args: ["worker"]
|
|
||||||
env:
|
|
||||||
- name: AUTHENTIK_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: AUTHENTIK_SECRET_KEY
|
|
||||||
- name: AUTHENTIK_REDIS__HOST
|
|
||||||
value: authentik-redis
|
|
||||||
- name: AUTHENTIK_REDIS__PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: REDIS_PASSWORD
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__HOST
|
|
||||||
value: authentik-postgres
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__NAME
|
|
||||||
value: authentik
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__USER
|
|
||||||
value: authentik
|
|
||||||
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: authentik-credentials
|
|
||||||
key: POSTGRES_PASSWORD
|
|
||||||
- name: AUTHENTIK_DISABLE_UPDATE_CHECK
|
|
||||||
value: "true"
|
|
||||||
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
|
|
||||||
value: "false"
|
|
||||||
- name: AUTHENTIK_LOG_LEVEL
|
|
||||||
value: info
|
|
||||||
resources:
|
|
||||||
requests: { cpu: 100m, memory: 256Mi }
|
|
||||||
limits: { cpu: 1000m, memory: 768Mi }
|
|
||||||
volumeMounts:
|
|
||||||
- name: media
|
|
||||||
mountPath: /media
|
|
||||||
volumes:
|
|
||||||
- name: media
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: authentik-media
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: authentik-server
|
|
||||||
namespace: authentik
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: authentik-server
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 9000
|
|
||||||
targetPort: 9000
|
|
||||||
- name: https
|
|
||||||
port: 9443
|
|
||||||
targetPort: 9443
|
|
||||||
|
|
||||||
---
|
|
||||||
# step-ca leaf certificate for id.iamworkin.lan.
|
|
||||||
# step-ca container resolver uses pfSense Unbound, so the public A record for id.iamworkin.lan
|
|
||||||
# MUST exist before this Certificate is applied (cert-manager HTTP-01 will silently 2h-backoff
|
|
||||||
# otherwise). Added 2026-05-25 via scripts/pfsense-add-id-host.py.
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: authentik-tls
|
|
||||||
namespace: authentik
|
|
||||||
spec:
|
|
||||||
secretName: authentik-tls
|
|
||||||
dnsNames:
|
|
||||||
- id.iamworkin.lan
|
|
||||||
issuerRef:
|
|
||||||
name: step-ca-acme
|
|
||||||
kind: ClusterIssuer
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: authentik
|
|
||||||
namespace: authentik
|
|
||||||
spec:
|
|
||||||
entryPoints: [websecure]
|
|
||||||
routes:
|
|
||||||
- match: Host(`id.iamworkin.lan`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: authentik-server
|
|
||||||
port: 9000
|
|
||||||
tls:
|
|
||||||
secretName: authentik-tls
|
|
||||||
27
apps/brochure/README.md
Normal file
27
apps/brochure/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# FlowerCore Brochure
|
||||||
|
|
||||||
|
`apps/brochure` hosts the public brochure split from `FlowerCore.Intranet.Web`.
|
||||||
|
ArgoCD's `apps/*` ApplicationSet will create `infra-brochure` after this
|
||||||
|
directory lands on `main`.
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
- Host: `https://brochure.flowercore.io`
|
||||||
|
- Namespace: `brochure`
|
||||||
|
- Deployment: `brochure-web`
|
||||||
|
- Image: `localhost/fc-brochure-web:v20260524-sprint32`
|
||||||
|
- Port: `8080`
|
||||||
|
- Public route method allowlist: `GET` and `HEAD`
|
||||||
|
|
||||||
|
## Operator Actions
|
||||||
|
|
||||||
|
1. Publish and import `localhost/fc-brochure-web:v20260524-sprint32` to every
|
||||||
|
RKE2 node before sync, using the same podman save + `ctr images import`
|
||||||
|
flow as the Intranet deployment.
|
||||||
|
2. Create the Cloudflare DNS record for `brochure.flowercore.io` pointing at
|
||||||
|
the FlowerCore public edge.
|
||||||
|
3. Verify `infra-brochure` appears in ArgoCD, the certificate becomes Ready,
|
||||||
|
and `GET https://brochure.flowercore.io/` returns `200`.
|
||||||
|
|
||||||
|
The route intentionally does not expose `/ops/*` or `/admin/*`; the Brochure
|
||||||
|
web app returns `404` for those paths and Traefik only forwards read methods.
|
||||||
131
apps/brochure/brochure.yaml
Normal file
131
apps/brochure/brochure.yaml
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# FlowerCore Brochure public host
|
||||||
|
#
|
||||||
|
# Thin Blazor host for public What's New, walkthrough, and gallery content
|
||||||
|
# carved out of FlowerCore.Intranet.Web. The ApplicationSet creates
|
||||||
|
# infra-brochure from this directory after merge.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: brochure
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: flowercore
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: brochure-web
|
||||||
|
namespace: brochure
|
||||||
|
labels:
|
||||||
|
app: brochure-web
|
||||||
|
app.kubernetes.io/name: brochure-web
|
||||||
|
app.kubernetes.io/part-of: flowercore
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: brochure-web
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: brochure-web
|
||||||
|
app.kubernetes.io/name: brochure-web
|
||||||
|
app.kubernetes.io/part-of: flowercore
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: brochure-web
|
||||||
|
image: localhost/fc-brochure-web:v20260524-sprint32
|
||||||
|
imagePullPolicy: Never
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: http
|
||||||
|
env:
|
||||||
|
- name: ASPNETCORE_ENVIRONMENT
|
||||||
|
value: Production
|
||||||
|
- name: ASPNETCORE_URLS
|
||||||
|
value: "http://+:8080"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "25m"
|
||||||
|
memory: "128Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "512Mi"
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1654
|
||||||
|
runAsGroup: 1654
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
volumeMounts:
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
|
volumes:
|
||||||
|
- name: tmp
|
||||||
|
emptyDir: {}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: brochure-web
|
||||||
|
namespace: brochure
|
||||||
|
labels:
|
||||||
|
app: brochure-web
|
||||||
|
app.kubernetes.io/name: brochure-web
|
||||||
|
app.kubernetes.io/part-of: flowercore
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: brochure-web
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8080
|
||||||
|
targetPort: http
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: brochure-web-tls
|
||||||
|
namespace: brochure
|
||||||
|
spec:
|
||||||
|
secretName: brochure-web-tls
|
||||||
|
issuerRef:
|
||||||
|
name: step-ca-acme
|
||||||
|
kind: ClusterIssuer
|
||||||
|
dnsNames:
|
||||||
|
- brochure.flowercore.io
|
||||||
|
duration: 720h
|
||||||
|
renewBefore: 240h
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: brochure-web-public
|
||||||
|
namespace: brochure
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`brochure.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: brochure-web
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
secretName: brochure-web-tls
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
# FlowerCore.AiStation.Web GitOps adoption manifest.
|
|
||||||
#
|
|
||||||
# Authored from the already-live fc-aistation resources on 2026-06-04.
|
|
||||||
# Keep the live image tag, Service ClusterIP, and PVC volumeName unchanged so
|
|
||||||
# ArgoCD adopts in place instead of replacing the workload or data volume.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: aistation-web-data
|
|
||||||
namespace: fc-aistation
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: aistation-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-aistation
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
storageClassName: longhorn
|
|
||||||
volumeMode: Filesystem
|
|
||||||
volumeName: pvc-27448d6f-6e66-42a7-a293-73dd8bbd6b3e
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: aistation-web
|
|
||||||
namespace: fc-aistation
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: aistation-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-aistation
|
|
||||||
spec:
|
|
||||||
progressDeadlineSeconds: 600
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: aistation-web
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
fc.flowercore.io/healthz-anon: "true"
|
|
||||||
fc.flowercore.io/probe-path: "/healthz"
|
|
||||||
prometheus.io/path: /metrics/prometheus
|
|
||||||
prometheus.io/port: "5000"
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: aistation-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
|
||||||
- envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: aistation-web-config
|
|
||||||
image: localhost/fc-aistation-web:v20260602-aistation-owned-deploy-fix2
|
|
||||||
imagePullPolicy: Never
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 3
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 5000
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 5
|
|
||||||
name: aistation-web
|
|
||||||
ports:
|
|
||||||
- containerPort: 5000
|
|
||||||
name: http
|
|
||||||
protocol: TCP
|
|
||||||
readinessProbe:
|
|
||||||
failureThreshold: 6
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 5000
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 5
|
|
||||||
resources: {}
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /data
|
|
||||||
name: data
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
securityContext: {}
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: aistation-web-data
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: aistation-web
|
|
||||||
namespace: fc-aistation
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: aistation-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-aistation
|
|
||||||
spec:
|
|
||||||
clusterIP: 10.43.211.127
|
|
||||||
clusterIPs:
|
|
||||||
- 10.43.211.127
|
|
||||||
internalTrafficPolicy: Cluster
|
|
||||||
ipFamilies:
|
|
||||||
- IPv4
|
|
||||||
ipFamilyPolicy: SingleStack
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 5000
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: aistation-web
|
|
||||||
sessionAffinity: None
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: aistation-web-tls
|
|
||||||
namespace: fc-aistation
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: aistation-web-tls
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-aistation
|
|
||||||
spec:
|
|
||||||
dnsNames:
|
|
||||||
- aistation.iamworkin.lan
|
|
||||||
issuerRef:
|
|
||||||
kind: ClusterIssuer
|
|
||||||
name: step-ca-acme
|
|
||||||
secretName: aistation-web-tls
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: aistation-web
|
|
||||||
namespace: fc-aistation
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: aistation-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-aistation
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- kind: Rule
|
|
||||||
match: Host(`aistation.iamworkin.lan`)
|
|
||||||
services:
|
|
||||||
- name: aistation-web
|
|
||||||
port: 80
|
|
||||||
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).
|
|
||||||
@@ -1,209 +1,5 @@
|
|||||||
# FlowerCore Chat
|
# FlowerCore Chat — TLS + Ingress
|
||||||
#
|
# Deployment and Service managed by deploy script (not ArgoCD)
|
||||||
# ArgoCD-managed workload plus TLS/Ingress. The chat-web-secret remains an
|
|
||||||
# out-of-band Secret until the values are moved into a 1Password-backed item;
|
|
||||||
# the Deployment references it as optional so GitOps can own the workload
|
|
||||||
# without storing secret material in this repo.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: fc-chat
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: chat-web-config
|
|
||||||
namespace: fc-chat
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: chat-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
data:
|
|
||||||
ASPNETCORE_ENVIRONMENT: Production
|
|
||||||
ASPNETCORE_URLS: "http://+:8080"
|
|
||||||
ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
|
|
||||||
FlowerCore__Auth__Enabled: "false"
|
|
||||||
FlowerCore__Auth__Oidc__Enabled: "true"
|
|
||||||
FlowerCore__Auth__Oidc__Authority: "https://id.iamworkin.lan/application/o/chat/"
|
|
||||||
FlowerCore__Auth__Oidc__Audience: "chat"
|
|
||||||
FlowerCore__Auth__Oidc__ClientId: "chat"
|
|
||||||
FlowerCore__Database__ConnectionStrings__Sqlite: "Data Source=/data/chat.db"
|
|
||||||
# Ollama target. Switched 2026-04-25 from edge1 Pi5 (10.0.57.17) to BLUEJAY-WS
|
|
||||||
# workstation (10.0.56.20, RX 9070 XT 16GB, OLLAMA_HOST=0.0.0.0:11434, Vulkan
|
|
||||||
# backend per feedback_rdna4_vulkan_broken). The Pi5 was timing out every team-
|
|
||||||
# round speaker at the 300s per-turn cap (live-proven 2026-04-25 03:53 UTC,
|
|
||||||
# see feedback_chat_team_round_edge1_too_slow). Workstation has gemma3:4b for
|
|
||||||
# the Cheap tier, plus gemma3:27b/phi4:14b/qwen3:14b for Default/Balanced/Deep.
|
|
||||||
# Piper TTS stays on edge1 below (different service, Pi handles TTS fine).
|
|
||||||
FlowerCore__AI__OllamaBaseUrl: "http://10.0.56.20:11434"
|
|
||||||
FlowerCore__AI__DefaultModelName: "phi4:14b"
|
|
||||||
ChatOptions__BehaviorRuleEngine__OllamaBaseUrl: "http://10.0.56.20:11434"
|
|
||||||
ChatOptions__BehaviorRuleEngine__FallbackOllamaBaseUrl: "http://10.0.57.17:11434"
|
|
||||||
ChatOptions__BehaviorRuleEngine__ModelName: "gemma3:12b"
|
|
||||||
FlowerCore__AI__Memory__UseSharedIndexingAdapter: "true"
|
|
||||||
FlowerCore__AI__Memory__UseOllamaEmbeddings: "true"
|
|
||||||
FlowerCore__AI__Memory__EmbeddingModel: "nomic-embed-text"
|
|
||||||
FlowerCore__AI__Memory__EnableSharedIndexingBackfill: "true"
|
|
||||||
FlowerCore__AI__Memory__SharedIndexingDatabasePath: "/data/chat-memory-index.db"
|
|
||||||
FlowerCore__AI__Skills__Library__LibraryApiUrl: "http://library-web.fc-library.svc.cluster.local"
|
|
||||||
FlowerCore__AI__Skills__Retail__RetailApiUrl: "http://retail-web.fc-retail.svc.cluster.local"
|
|
||||||
FlowerCore__AI__Skills__Intranet__IntranetBaseUrl: "http://intranet-web.intranet.svc.cluster.local"
|
|
||||||
FlowerCore__AI__Skills__Print__PrintMcpBaseUrl: "http://10.0.57.16:5200"
|
|
||||||
FlowerCore__AI__IrcBridge__Enabled: "true"
|
|
||||||
FlowerCore__AI__IrcBridge__DefaultProfileSlug: "it-helpdesk"
|
|
||||||
FlowerCore__AI__IrcBridge__MentionProfileSlug: "it-helpdesk"
|
|
||||||
FlowerCore__AI__IrcBridge__MentionReactiveMode: "mentions-only"
|
|
||||||
FlowerCore__AI__IrcBridge__AllowActionExecution: "false"
|
|
||||||
FlowerCore__AI__Voice__Piper__Host: "10.0.57.17"
|
|
||||||
FlowerCore__AI__Voice__Piper__Port: "10400"
|
|
||||||
FlowerCore__AI__Voice__OutputRoot: "/data/audio"
|
|
||||||
FlowerCore__AI__Voice__RetentionDays: "30"
|
|
||||||
# LLM provider abstraction (ADR-088). Anthropic stays disabled here -- when
|
|
||||||
# an operator wants to enable Claude, they flip Enabled=true and mount
|
|
||||||
# FlowerCore__Anthropic__ApiKey from the onepassword-synced Secret (see
|
|
||||||
# docs/ai-agents/anthropic-integration.md).
|
|
||||||
FlowerCore__Anthropic__Enabled: "false"
|
|
||||||
FlowerCore__Anthropic__BaseUrl: "https://api.anthropic.com"
|
|
||||||
FlowerCore__Anthropic__DefaultModel: "claude-sonnet-4-6"
|
|
||||||
FlowerCore__Anthropic__CheapModel: "claude-haiku-4-5-20251001"
|
|
||||||
FlowerCore__Anthropic__DeepModel: "claude-opus-4-7"
|
|
||||||
FlowerCore__Budget__ResponseCacheEnabled: "true"
|
|
||||||
OTEL_SERVICE_NAME: FlowerCore.Chat
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.monitoring.svc.cluster.local:4317"
|
|
||||||
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: chat-web-data
|
|
||||||
namespace: fc-chat
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: chat-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
storageClassName: longhorn
|
|
||||||
volumeMode: Filesystem
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: chat-web
|
|
||||||
namespace: fc-chat
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: chat-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: chat-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: chat-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
annotations:
|
|
||||||
fc.flowercore.io/healthz-anon: "true"
|
|
||||||
fc.flowercore.io/probe-path: "/healthz"
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
prometheus.io/port: "8080"
|
|
||||||
prometheus.io/path: "/metrics/prometheus"
|
|
||||||
spec:
|
|
||||||
nodeSelector:
|
|
||||||
kubernetes.io/hostname: rke2-server
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 1654
|
|
||||||
fsGroupChangePolicy: OnRootMismatch
|
|
||||||
containers:
|
|
||||||
- name: chat-web
|
|
||||||
image: localhost/fc-chat-web:v20260603-oidc-authentik
|
|
||||||
imagePullPolicy: Never
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 8080
|
|
||||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
|
||||||
envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: chat-web-config
|
|
||||||
- secretRef:
|
|
||||||
name: chat-web-secret
|
|
||||||
optional: true
|
|
||||||
env:
|
|
||||||
- name: FlowerCore__Auth__Oidc__Authority
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: chat-oidc-client
|
|
||||||
key: issuer_url
|
|
||||||
optional: true
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientId
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: chat-oidc-client
|
|
||||||
key: client_id
|
|
||||||
optional: true
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientSecret
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: chat-oidc-client
|
|
||||||
key: client_secret
|
|
||||||
optional: true
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: "128Mi"
|
|
||||||
cpu: "100m"
|
|
||||||
limits:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "500m"
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 6
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 3
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: chat-web-data
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: chat-web
|
|
||||||
namespace: fc-chat
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: chat-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: chat-web
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
protocol: TCP
|
|
||||||
---
|
---
|
||||||
apiVersion: cert-manager.io/v1
|
apiVersion: cert-manager.io/v1
|
||||||
kind: Certificate
|
kind: Certificate
|
||||||
|
|||||||
@@ -51,26 +51,3 @@ 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).
|
|
||||||
|
|||||||
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
|
||||||
@@ -47,7 +47,7 @@ spec:
|
|||||||
fsGroupChangePolicy: OnRootMismatch
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
containers:
|
containers:
|
||||||
- name: operator
|
- name: operator
|
||||||
image: localhost/fc-devicemgmt-operator:v20260519-sp34cl3-fix
|
image: localhost/fc-devicemgmt-operator:v20260512-cx5
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- name: metrics
|
- name: metrics
|
||||||
|
|||||||
@@ -4,22 +4,6 @@
|
|||||||
# Sprint 9+ lane. This manifest is static-valid without requiring the image to
|
# 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
|
# exist yet; import localhost/fc-devicemgmt-web:<tag> to all schedulable RKE2
|
||||||
# nodes before letting ArgoCD sync a live rollout.
|
# nodes before letting ArgoCD sync a live rollout.
|
||||||
#
|
|
||||||
# SCALED TO 0 — 2026-05-19 morning-routine cleanup.
|
|
||||||
# The Web pod cannot start until TWO upstream gaps close:
|
|
||||||
# 1. MySQL DB instance `flowercore_devicemgmt` (user `fc_devicemgmt`) is
|
|
||||||
# provisioned via fc-mysql Manager. The cluster currently has ZERO
|
|
||||||
# MySqlInstanceCrds and no `mysql.fc-mysql.svc:3306` Service, so the
|
|
||||||
# deployment-web container env `FlowerCore__Database__Host=mysql.fc-mysql.svc`
|
|
||||||
# points at nothing. Provision via the fc-mysql Manager UI/REST/MCP.
|
|
||||||
# 2. 1Password vault item `IAmWorkin/FlowerCore DeviceManagement Runtime`
|
|
||||||
# with 5 fields (DB-Password, mtls-ca.pem, mtls-client.crt, mtls-client.key,
|
|
||||||
# mtls-chain.pem) — see apps/fc-devicemgmt/1password-item.yaml. Mint mTLS
|
|
||||||
# from step-ca-agent ClusterIssuer per ADR-126; DB-Password must match the
|
|
||||||
# password configured for the MySQL user.
|
|
||||||
# Re-enable: change replicas back to 2 after both gaps close. The image tag
|
|
||||||
# in this file (v20260512-cx5) MAY also need a refresh — it predates the
|
|
||||||
# Sprint 34 Cl-3 operator fix; Web may have an analogous bug.
|
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -36,7 +20,7 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
||||||
spec:
|
spec:
|
||||||
replicas: 0
|
replicas: 2
|
||||||
revisionHistoryLimit: 3
|
revisionHistoryLimit: 3
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
@@ -52,8 +36,6 @@ 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"
|
||||||
@@ -69,7 +51,6 @@ 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"
|
||||||
|
|||||||
@@ -74,14 +74,6 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
itemPath: "vaults/IAmWorkin/items/FlowerCore Edition Signing Key - edition:aistation-field"
|
itemPath: "vaults/IAmWorkin/items/FlowerCore Edition Signing Key - edition:aistation-field"
|
||||||
---
|
---
|
||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: distribution-oidc-client
|
|
||||||
namespace: fc-distribution
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/distribution-oidc-client"
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -109,7 +101,6 @@ spec:
|
|||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/metrics"
|
prometheus.io/path: "/metrics"
|
||||||
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
|
||||||
spec:
|
spec:
|
||||||
# Synology NFS export `/volume1/kubernetes` ACL only allows rke2-server
|
# Synology NFS export `/volume1/kubernetes` ACL only allows rke2-server
|
||||||
# (10.0.56.11) right now. Until the ACL is widened in DSM (admin only),
|
# (10.0.56.11) right now. Until the ACL is widened in DSM (admin only),
|
||||||
@@ -127,7 +118,7 @@ spec:
|
|||||||
# dotnet.exe publish -c Release -o deploy/app \
|
# dotnet.exe publish -c Release -o deploy/app \
|
||||||
# src/FlowerCore.Distribution.Web/FlowerCore.Distribution.Web.csproj
|
# src/FlowerCore.Distribution.Web/FlowerCore.Distribution.Web.csproj
|
||||||
# podman build -t localhost/fc-distribution:v<tag> -f deploy/Dockerfile.deploy deploy
|
# podman build -t localhost/fc-distribution:v<tag> -f deploy/Dockerfile.deploy deploy
|
||||||
image: localhost/fc-distribution:v20260604-oidc-root-anon
|
image: localhost/fc-distribution:v202605061948
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
@@ -139,25 +130,6 @@ spec:
|
|||||||
value: "Production"
|
value: "Production"
|
||||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
||||||
value: "false"
|
value: "false"
|
||||||
# Authentik/OIDC enforcement. Public read/entitlement + the
|
|
||||||
# dist.flowercore.io Method() allowlist stay open; OIDC gates the
|
|
||||||
# operator/admin surface while /healthz remains anonymous.
|
|
||||||
- name: FlowerCore__Auth__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Authority
|
|
||||||
value: "https://id.iamworkin.lan/application/o/distribution/"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Audience
|
|
||||||
value: "distribution"
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientId
|
|
||||||
value: "distribution"
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientSecret
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: distribution-oidc-client
|
|
||||||
key: client_secret
|
|
||||||
optional: true
|
|
||||||
# SQLite connection (catalog + data-protection keys via FlowerCoreDbContext).
|
# SQLite connection (catalog + data-protection keys via FlowerCoreDbContext).
|
||||||
# Read by Data/DatabaseProviderExtensions.cs in precedence order; Sqlite key wins.
|
# Read by Data/DatabaseProviderExtensions.cs in precedence order; Sqlite key wins.
|
||||||
- name: FlowerCore__Database__Provider
|
- name: FlowerCore__Database__Provider
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# FlowerCore Divoom DM Pi Device
|
|
||||||
|
|
||||||
Source-controlled Puppet/Hiera deployment contract for registering the edge2
|
|
||||||
Divoom MiniToo panel as a FlowerCore DeviceManagement-managed Pi device.
|
|
||||||
|
|
||||||
This is not a Kubernetes application. The live panel remains the existing
|
|
||||||
edge2 `flowercore-divoom.service` managed by `FlowerCore.Puppet`
|
|
||||||
`profile::pi::service::divoom`, with the .NET payload deployed out of band
|
|
||||||
and `/opt/flowercore/divoom/data` plus the Bluetooth shell wrappers preserved.
|
|
||||||
Because edge2 is already Hiera-driven through `profile::pi::service::apps`,
|
|
||||||
the deploy home is additive `profile::pi::service` data/profile source, not
|
|
||||||
`profile::edge::service::apps` and not an ArgoCD/K8s app.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- Stage DeviceManagement registration metadata for the edge2 Divoom MiniToo.
|
|
||||||
- Stage a separate, disabled-by-default DM Agent executor unit for privileged
|
|
||||||
Bluetooth operations once the DM-RPC lane lands.
|
|
||||||
- Keep `flowercore-divoom.service` and `flowercore-divoom-bt.service`
|
|
||||||
untouched: no service replacement, no restart subscription, no K8s surface.
|
|
||||||
- Preserve the current wrapper contract:
|
|
||||||
`/opt/flowercore/divoom/bt-link.sh`,
|
|
||||||
`/opt/flowercore/divoom/bt-reset.sh`, and
|
|
||||||
`/opt/flowercore/divoom/audio-link.sh`.
|
|
||||||
- Keep FM radio disabled and require visible render proof; device-info echo is
|
|
||||||
not render proof.
|
|
||||||
|
|
||||||
## Artifact Map
|
|
||||||
|
|
||||||
| Path | Use |
|
|
||||||
| --- | --- |
|
|
||||||
| `hiera/edge2-divoom-dm-device.overlay.yaml` | Additive Hiera overlay for edge2. Merge into the existing node YAML without removing `fc-pimanager` or `fc-divoom`. |
|
|
||||||
| `puppet/profile/pi/service/divoom_dm_device.pp` | Puppet profile shape to vendor into `FlowerCore.Puppet` after the DM-RPC executor binary exists. |
|
|
||||||
| `puppet/templates/divoom-device-registration.json.epp` | DM device registration metadata rendered on edge2. |
|
|
||||||
| `puppet/templates/flowercore-divoom-dm-agent.service.epp` | Separate DM Agent systemd unit. Defaults are stopped and disabled until a later cutover. |
|
|
||||||
|
|
||||||
## Rollout Notes
|
|
||||||
|
|
||||||
1. Land these artifacts in bluejay-infra as the deploy contract.
|
|
||||||
2. Vendor the Puppet profile and EPP templates into `FlowerCore.Puppet`.
|
|
||||||
3. Merge the Hiera overlay into `data/nodes/edge2.iamworkin.lan.yaml`.
|
|
||||||
4. Run Puppet in noop first, preferably with a node-local validation directory
|
|
||||||
under `~/.fcv` rather than `/tmp`.
|
|
||||||
5. Only enable the DM Agent service after the DeviceManagement BT executor has
|
|
||||||
landed and passed operator-eyeball render proof.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
# Merge into FlowerCore.Puppet data/nodes/edge2.iamworkin.lan.yaml.
|
|
||||||
# Additive overlay only: keep the existing fc-pimanager version/tarball entry,
|
|
||||||
# keep fc-divoom enabled, and do not move Divoom into Kubernetes.
|
|
||||||
|
|
||||||
profile::pi::service::apps:
|
|
||||||
fc-pimanager:
|
|
||||||
binary: 'FlowerCore.PiManager.Web'
|
|
||||||
install_dir: '/opt/fc-pimanager'
|
|
||||||
port: 5000
|
|
||||||
environment: 'edge2'
|
|
||||||
version: '2026.05.28.1646'
|
|
||||||
tarball_source: 'puppet:///modules/profile/pi/builds/fc-pimanager.tar.gz'
|
|
||||||
fc-divoom:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
profile::pi::service::divoom_dm_device::ensure: 'present'
|
|
||||||
profile::pi::service::divoom_dm_device::service_enabled: false
|
|
||||||
profile::pi::service::divoom_dm_device::service_ensure: 'stopped'
|
|
||||||
profile::pi::service::divoom_dm_device::device_id: 'edge2-divoom-minitoo'
|
|
||||||
profile::pi::service::divoom_dm_device::display_name: 'edge2 Divoom MiniToo'
|
|
||||||
profile::pi::service::divoom_dm_device::host_fqdn: 'edge2.iamworkin.lan'
|
|
||||||
profile::pi::service::divoom_dm_device::dm_web_url: 'https://devicemgmt.iamworkin.lan'
|
|
||||||
profile::pi::service::divoom_dm_device::divoom_install_dir: '/opt/flowercore/divoom'
|
|
||||||
profile::pi::service::divoom_dm_device::agent_install_dir: '/opt/flowercore/devicemanagement-agent'
|
|
||||||
profile::pi::service::divoom_dm_device::bt_candidate_channels:
|
|
||||||
- '1'
|
|
||||||
- '10'
|
|
||||||
profile::pi::service::divoom_dm_device::default_bt_channel: '1'
|
|
||||||
profile::pi::service::divoom_dm_device::a2dp_default_state: 'off'
|
|
||||||
profile::pi::service::divoom_dm_device::fm_radio_enabled: false
|
|
||||||
profile::pi::service::divoom_dm_device::visible_render_proof_required: true
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# Drop into FlowerCore.Puppet site-modules/profile/manifests/pi/service/divoom_dm_device.pp.
|
|
||||||
# This profile is additive to profile::pi::service::divoom. It must not manage,
|
|
||||||
# restart, replace, or subscribe the existing flowercore-divoom.service.
|
|
||||||
class profile::pi::service::divoom_dm_device (
|
|
||||||
Enum['present', 'absent'] $ensure = 'present',
|
|
||||||
Boolean $service_enabled = false,
|
|
||||||
Enum['running', 'stopped'] $service_ensure = 'stopped',
|
|
||||||
String $service_name = 'flowercore-divoom-dm-agent',
|
|
||||||
String $device_id = 'edge2-divoom-minitoo',
|
|
||||||
String $display_name = 'edge2 Divoom MiniToo',
|
|
||||||
String $host_fqdn = 'edge2.iamworkin.lan',
|
|
||||||
String $dm_web_url = 'https://devicemgmt.iamworkin.lan',
|
|
||||||
String $divoom_install_dir = '/opt/flowercore/divoom',
|
|
||||||
String $agent_install_dir = '/opt/flowercore/devicemanagement-agent',
|
|
||||||
String $agent_binary = 'FlowerCore.DeviceManagement.Agent',
|
|
||||||
Array[String] $bt_candidate_channels = ['1', '10'],
|
|
||||||
String $default_bt_channel = '1',
|
|
||||||
Enum['on', 'off'] $a2dp_default_state = 'off',
|
|
||||||
Boolean $fm_radio_enabled = false,
|
|
||||||
Boolean $visible_render_proof_required = true,
|
|
||||||
) {
|
|
||||||
include profile::workstation::safe_account_exclusion
|
|
||||||
|
|
||||||
$safe_account = $profile::workstation::safe_account_exclusion::safe_account
|
|
||||||
$config_dir = '/etc/flowercore/device-management/devices'
|
|
||||||
$state_dir = '/var/lib/flowercore/divoom-dm-agent'
|
|
||||||
$log_dir = '/var/log/flowercore/divoom-dm-agent'
|
|
||||||
$registration_path = "${config_dir}/${device_id}.json"
|
|
||||||
$agent_binary_path = "${agent_install_dir}/${agent_binary}"
|
|
||||||
$bt_channels_json = inline_template('[<%= @bt_candidate_channels.map { |c| "\"#{c}\"" }.join(", ") %>]')
|
|
||||||
|
|
||||||
if $safe_account {
|
|
||||||
notify { 'fc-divoom-dm-device safe-account exclusion':
|
|
||||||
message => 'SAFE-ACCOUNT-EXCLUSION: Divoom DM Pi device profile refused to apply on operator workstation',
|
|
||||||
}
|
|
||||||
|
|
||||||
if $facts['os']['family'] != 'windows' {
|
|
||||||
ensure_resource('file', '/var/log/flowercore-audit', {
|
|
||||||
'ensure' => 'directory',
|
|
||||||
'owner' => 'root',
|
|
||||||
'group' => 'root',
|
|
||||||
'mode' => '0755',
|
|
||||||
})
|
|
||||||
|
|
||||||
file { '/var/log/flowercore-audit/safe-account-noop-fc-divoom-dm-device.log':
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0644',
|
|
||||||
content => "noop: divoom dm pi device profile refused to apply on safe-account host\n",
|
|
||||||
require => File['/var/log/flowercore-audit'],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elsif $ensure == 'absent' {
|
|
||||||
service { $service_name:
|
|
||||||
ensure => stopped,
|
|
||||||
enable => false,
|
|
||||||
}
|
|
||||||
|
|
||||||
file { [
|
|
||||||
"/etc/systemd/system/${service_name}.service",
|
|
||||||
$registration_path,
|
|
||||||
]:
|
|
||||||
ensure => absent,
|
|
||||||
}
|
|
||||||
|
|
||||||
exec { 'fc-divoom-dm-agent-systemd-reload':
|
|
||||||
command => '/usr/bin/systemctl daemon-reload',
|
|
||||||
refreshonly => true,
|
|
||||||
path => ['/usr/bin', '/bin'],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
case $facts['os']['family'] {
|
|
||||||
'Debian': {}
|
|
||||||
default: { fail("profile::pi::service::divoom_dm_device only supports Debian-family OS, got ${facts['os']['family']}") }
|
|
||||||
}
|
|
||||||
|
|
||||||
file { [$config_dir, $state_dir, $log_dir]:
|
|
||||||
ensure => directory,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0755',
|
|
||||||
}
|
|
||||||
|
|
||||||
file { $registration_path:
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0644',
|
|
||||||
content => epp('profile/pi/fc_divoom_dm/divoom-device-registration.json.epp', {
|
|
||||||
'device_id' => $device_id,
|
|
||||||
'display_name' => $display_name,
|
|
||||||
'host_fqdn' => $host_fqdn,
|
|
||||||
'divoom_install_dir' => $divoom_install_dir,
|
|
||||||
'bt_channels_json' => $bt_channels_json,
|
|
||||||
'default_bt_channel' => $default_bt_channel,
|
|
||||||
'a2dp_default_state' => $a2dp_default_state,
|
|
||||||
'fm_radio_enabled' => $fm_radio_enabled,
|
|
||||||
'visible_render_proof_required' => $visible_render_proof_required,
|
|
||||||
}),
|
|
||||||
require => File[$config_dir],
|
|
||||||
}
|
|
||||||
|
|
||||||
file { "/etc/systemd/system/${service_name}.service":
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0644',
|
|
||||||
content => epp('profile/pi/fc_divoom_dm/flowercore-divoom-dm-agent.service.epp', {
|
|
||||||
'service_name' => $service_name,
|
|
||||||
'device_id' => $device_id,
|
|
||||||
'dm_web_url' => $dm_web_url,
|
|
||||||
'registration_path' => $registration_path,
|
|
||||||
'divoom_install_dir' => $divoom_install_dir,
|
|
||||||
'agent_install_dir' => $agent_install_dir,
|
|
||||||
'agent_binary_path' => $agent_binary_path,
|
|
||||||
'state_dir' => $state_dir,
|
|
||||||
'log_dir' => $log_dir,
|
|
||||||
}),
|
|
||||||
notify => Exec['fc-divoom-dm-agent-systemd-reload'],
|
|
||||||
require => File[$registration_path],
|
|
||||||
}
|
|
||||||
|
|
||||||
exec { 'fc-divoom-dm-agent-systemd-reload':
|
|
||||||
command => '/usr/bin/systemctl daemon-reload',
|
|
||||||
refreshonly => true,
|
|
||||||
path => ['/usr/bin', '/bin'],
|
|
||||||
}
|
|
||||||
|
|
||||||
service { $service_name:
|
|
||||||
ensure => $service_ensure,
|
|
||||||
enable => $service_enabled,
|
|
||||||
require => [
|
|
||||||
File["/etc/systemd/system/${service_name}.service"],
|
|
||||||
File[$registration_path],
|
|
||||||
Exec['fc-divoom-dm-agent-systemd-reload'],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"deviceId": "<%= $device_id %>",
|
|
||||||
"displayName": "<%= $display_name %>",
|
|
||||||
"hostFqdn": "<%= $host_fqdn %>",
|
|
||||||
"kind": "DivoomMiniToo",
|
|
||||||
"managedBy": "FlowerCore.DeviceManagement",
|
|
||||||
"executionMode": "Pi",
|
|
||||||
"transport": {
|
|
||||||
"kind": "BluetoothSerial",
|
|
||||||
"candidateChannels": <%= $bt_channels_json %>,
|
|
||||||
"defaultChannel": "<%= $default_bt_channel %>",
|
|
||||||
"deviceInfoIsRenderProof": false,
|
|
||||||
"visibleRenderProofRequired": <%= $visible_render_proof_required %>
|
|
||||||
},
|
|
||||||
"paths": {
|
|
||||||
"divoomInstallDir": "<%= $divoom_install_dir %>",
|
|
||||||
"btLink": "<%= $divoom_install_dir %>/bt-link.sh",
|
|
||||||
"btReset": "<%= $divoom_install_dir %>/bt-reset.sh",
|
|
||||||
"audioLink": "<%= $divoom_install_dir %>/audio-link.sh"
|
|
||||||
},
|
|
||||||
"capabilities": {
|
|
||||||
"supportsBluetoothSerial": true,
|
|
||||||
"supportsBtChannelRedetect": true,
|
|
||||||
"supportsBtHardReset": true,
|
|
||||||
"supportsBtAudioProfileSwitch": true,
|
|
||||||
"a2dpDefaultState": "<%= $a2dp_default_state %>",
|
|
||||||
"fmRadioEnabled": <%= $fm_radio_enabled %>
|
|
||||||
},
|
|
||||||
"safety": {
|
|
||||||
"preserveExistingService": "flowercore-divoom.service",
|
|
||||||
"preserveDataDirectory": "<%= $divoom_install_dir %>/data",
|
|
||||||
"doNotEnableFmRadio": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Divoom DM Agent Bluetooth executor
|
|
||||||
Documentation=https://github.com/astoltz/FlowerCore.Notes/blob/master/docs/standards/divoom-tv-hdmi-multitarget-render-substrate.md
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target bluetooth.service
|
|
||||||
Requires=bluetooth.service
|
|
||||||
ConditionPathExists=<%= $agent_binary_path %>
|
|
||||||
ConditionPathExists=<%= $registration_path %>
|
|
||||||
ConditionPathExists=<%= $divoom_install_dir %>/bt-link.sh
|
|
||||||
ConditionPathExists=<%= $divoom_install_dir %>/bt-reset.sh
|
|
||||||
ConditionPathExists=<%= $divoom_install_dir %>/audio-link.sh
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=stoltz
|
|
||||||
Group=stoltz
|
|
||||||
WorkingDirectory=<%= $agent_install_dir %>
|
|
||||||
Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1
|
|
||||||
Environment=FLOWERCORE_DM_DEVICE_REGISTRATION=<%= $registration_path %>
|
|
||||||
Environment=Divoom__Bluetooth__DeviceInfoIsRenderProof=false
|
|
||||||
Environment=Divoom__Bluetooth__VisibleRenderProofRequired=true
|
|
||||||
Environment=Divoom__Bluetooth__A2dpDefaultState=off
|
|
||||||
ExecStart=<%= $agent_binary_path %> --mode=Pi --device-id=<%= $device_id %> --dm-web-url=<%= $dm_web_url %> --registration=<%= $registration_path %>
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=10s
|
|
||||||
StartLimitBurst=3
|
|
||||||
StartLimitIntervalSec=300s
|
|
||||||
SupplementaryGroups=bluetooth audio dialout
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=<%= $state_dir %> <%= $log_dir %>
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# FlowerCore Divoom TV Pi HDMI
|
|
||||||
|
|
||||||
Source-controlled deploy shape for the native `FlowerCore.Divoom.Tv`
|
|
||||||
Avalonia HDMI renderer on a Raspberry Pi connected to a TV.
|
|
||||||
|
|
||||||
This is a Puppet/systemd appliance bundle, not a Kubernetes application. It
|
|
||||||
mirrors the existing `fc-signage-pi-player` pattern: bluejay-infra carries the
|
|
||||||
systemd units, scripts, Hiera shape, and Puppet profile source that
|
|
||||||
`FlowerCore.Puppet` vendors and installs.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- Launch the future `FlowerCore.Divoom.Tv` linux-arm64 self-contained payload
|
|
||||||
from `/opt/flowercore/divoom-tv/FlowerCore.Divoom.Tv`.
|
|
||||||
- Prefer `cage` as the Wayland fullscreen compositor, with direct app launch as
|
|
||||||
a fallback for development images.
|
|
||||||
- Restart the app after HDMI hotplug with a 2 second DRM settle delay.
|
|
||||||
- Keep all runtime state local: `/var/lib/fc-divoom-tv` and
|
|
||||||
`/var/log/fc-divoom-tv`.
|
|
||||||
- Avoid CDN/runtime fetches; the app renders the in-house Divoom scene catalog
|
|
||||||
locally.
|
|
||||||
|
|
||||||
## Artifact Map
|
|
||||||
|
|
||||||
| Path | Use |
|
|
||||||
| --- | --- |
|
|
||||||
| `systemd/flowercore-divoom-tv.service` | Fullscreen Avalonia HDMI app service. |
|
|
||||||
| `systemd/flowercore-divoom-tv-hdmi.service` | HDMI hotplug responder service. |
|
|
||||||
| `systemd/99-flowercore-divoom-tv-hdmi.rules` | DRM udev hotplug rule. |
|
|
||||||
| `scripts/flowercore-divoom-tv-prelaunch.sh` | Preflight checks and local directory creation. |
|
|
||||||
| `scripts/flowercore-divoom-tv-launch.sh` | Cage-first fullscreen launcher. |
|
|
||||||
| `scripts/flowercore-divoom-tv-hdmi-respond.sh` | Hotplug settle and restart script. |
|
|
||||||
| `puppet/profile/pi/service/divoom_tv.pp` | Puppet profile shape to vendor into `FlowerCore.Puppet`. |
|
|
||||||
| `hiera/example-divoom-tv-pi.iamworkin.lan.yaml` | Example node Hiera for a Divoom TV Pi. |
|
|
||||||
|
|
||||||
## Rollout Notes
|
|
||||||
|
|
||||||
1. Build `FlowerCore.Divoom.Tv` with `dotnet.exe publish -c Release -r linux-arm64 --self-contained`.
|
|
||||||
2. Stage the payload to `/opt/flowercore/divoom-tv/` through the standard noc1
|
|
||||||
jump path and avoid `/tmp` for unprivileged Pi scratch.
|
|
||||||
3. Vendor the profile and static files into `FlowerCore.Puppet`.
|
|
||||||
4. Run Puppet noop, then apply on the target Pi.
|
|
||||||
5. Prove deployment with `systemctl is-active flowercore-divoom-tv.service`,
|
|
||||||
journal lines showing frames presented, and a visible HDMI display check.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
# Example node data for a dedicated Pi -> HDMI -> TV Divoom renderer.
|
|
||||||
# Copy into FlowerCore.Puppet data/nodes/<hostname>.iamworkin.lan.yaml only
|
|
||||||
# after the Pi has a static DHCP/DNS entry and the linux-arm64 payload exists.
|
|
||||||
|
|
||||||
facts:
|
|
||||||
role: pi_prototype
|
|
||||||
|
|
||||||
profile::motd::role: 'Divoom TV HDMI Renderer'
|
|
||||||
|
|
||||||
profile::pi::service::divoom_tv::ensure: 'present'
|
|
||||||
profile::pi::service::divoom_tv::service_enabled: true
|
|
||||||
profile::pi::service::divoom_tv::service_ensure: 'running'
|
|
||||||
profile::pi::service::divoom_tv::install_dir: '/opt/flowercore/divoom-tv'
|
|
||||||
profile::pi::service::divoom_tv::state_dir: '/var/lib/fc-divoom-tv'
|
|
||||||
profile::pi::service::divoom_tv::log_dir: '/var/log/fc-divoom-tv'
|
|
||||||
profile::pi::service::divoom_tv::presentation_mode: 'PillarboxSquare'
|
|
||||||
profile::pi::service::divoom_tv::startup_scene: 'bluejay-clock'
|
|
||||||
profile::pi::service::divoom_tv::reduced_motion: false
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# Drop into FlowerCore.Puppet site-modules/profile/manifests/pi/service/divoom_tv.pp.
|
|
||||||
# Static files come from profile/pi/fc_divoom_tv/ after this bluejay-infra
|
|
||||||
# bundle is vendored into the Puppet control repo.
|
|
||||||
class profile::pi::service::divoom_tv (
|
|
||||||
Enum['present', 'absent'] $ensure = 'present',
|
|
||||||
Boolean $service_enabled = false,
|
|
||||||
Enum['running', 'stopped'] $service_ensure = 'stopped',
|
|
||||||
String $service_name = 'flowercore-divoom-tv',
|
|
||||||
String $user = 'fc-divoom-tv',
|
|
||||||
String $group = 'fc-divoom-tv',
|
|
||||||
String $install_dir = '/opt/flowercore/divoom-tv',
|
|
||||||
String $state_dir = '/var/lib/fc-divoom-tv',
|
|
||||||
String $log_dir = '/var/log/fc-divoom-tv',
|
|
||||||
String $presentation_mode = 'PillarboxSquare',
|
|
||||||
String $startup_scene = 'bluejay-clock',
|
|
||||||
Boolean $reduced_motion = false,
|
|
||||||
) {
|
|
||||||
include profile::workstation::safe_account_exclusion
|
|
||||||
|
|
||||||
$safe_account = $profile::workstation::safe_account_exclusion::safe_account
|
|
||||||
|
|
||||||
if $safe_account {
|
|
||||||
notify { 'fc-divoom-tv safe-account exclusion':
|
|
||||||
message => 'SAFE-ACCOUNT-EXCLUSION: Divoom TV Pi profile refused to apply on operator workstation',
|
|
||||||
}
|
|
||||||
} elsif $ensure == 'absent' {
|
|
||||||
service { $service_name:
|
|
||||||
ensure => stopped,
|
|
||||||
enable => false,
|
|
||||||
}
|
|
||||||
|
|
||||||
file { [
|
|
||||||
"/etc/systemd/system/${service_name}.service",
|
|
||||||
"/etc/systemd/system/${service_name}-hdmi.service",
|
|
||||||
'/etc/udev/rules.d/99-flowercore-divoom-tv-hdmi.rules',
|
|
||||||
'/usr/local/bin/flowercore-divoom-tv-prelaunch.sh',
|
|
||||||
'/usr/local/bin/flowercore-divoom-tv-launch.sh',
|
|
||||||
'/usr/local/bin/flowercore-divoom-tv-hdmi-respond.sh',
|
|
||||||
'/etc/flowercore/divoom-tv.env',
|
|
||||||
]:
|
|
||||||
ensure => absent,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
case $facts['os']['family'] {
|
|
||||||
'Debian': {}
|
|
||||||
default: { fail("profile::pi::service::divoom_tv only supports Debian-family OS, got ${facts['os']['family']}") }
|
|
||||||
}
|
|
||||||
|
|
||||||
package { ['cage', 'libgbm1', 'libdrm2', 'libxkbcommon0', 'fonts-dejavu-core']:
|
|
||||||
ensure => installed,
|
|
||||||
}
|
|
||||||
|
|
||||||
group { $group:
|
|
||||||
ensure => present,
|
|
||||||
system => true,
|
|
||||||
}
|
|
||||||
|
|
||||||
user { $user:
|
|
||||||
ensure => present,
|
|
||||||
system => true,
|
|
||||||
gid => $group,
|
|
||||||
home => $state_dir,
|
|
||||||
managehome => false,
|
|
||||||
shell => '/usr/sbin/nologin',
|
|
||||||
require => Group[$group],
|
|
||||||
}
|
|
||||||
|
|
||||||
file { [$install_dir, $state_dir, $log_dir, '/etc/flowercore']:
|
|
||||||
ensure => directory,
|
|
||||||
owner => $user,
|
|
||||||
group => $group,
|
|
||||||
mode => '0755',
|
|
||||||
}
|
|
||||||
|
|
||||||
file { '/etc/flowercore/divoom-tv.env':
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0644',
|
|
||||||
content => "FC_DIVOOM_TV_PRESENTATION_MODE=${presentation_mode}\nFC_DIVOOM_TV_START_SCENE=${startup_scene}\nFC_DIVOOM_TV_REDUCED_MOTION=${reduced_motion}\n",
|
|
||||||
require => File['/etc/flowercore'],
|
|
||||||
}
|
|
||||||
|
|
||||||
$script_map = {
|
|
||||||
'/usr/local/bin/flowercore-divoom-tv-prelaunch.sh' => 'profile/pi/fc_divoom_tv/flowercore-divoom-tv-prelaunch.sh',
|
|
||||||
'/usr/local/bin/flowercore-divoom-tv-launch.sh' => 'profile/pi/fc_divoom_tv/flowercore-divoom-tv-launch.sh',
|
|
||||||
'/usr/local/bin/flowercore-divoom-tv-hdmi-respond.sh' => 'profile/pi/fc_divoom_tv/flowercore-divoom-tv-hdmi-respond.sh',
|
|
||||||
}
|
|
||||||
|
|
||||||
$script_map.each |$dest, $src| {
|
|
||||||
file { $dest:
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0755',
|
|
||||||
source => "puppet:///modules/${src}",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$unit_map = {
|
|
||||||
"/etc/systemd/system/${service_name}.service" => 'profile/pi/fc_divoom_tv/flowercore-divoom-tv.service',
|
|
||||||
"/etc/systemd/system/${service_name}-hdmi.service" => 'profile/pi/fc_divoom_tv/flowercore-divoom-tv-hdmi.service',
|
|
||||||
}
|
|
||||||
|
|
||||||
$unit_map.each |$dest, $src| {
|
|
||||||
file { $dest:
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0644',
|
|
||||||
source => "puppet:///modules/${src}",
|
|
||||||
notify => Exec['fc-divoom-tv-systemd-reload'],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file { '/etc/udev/rules.d/99-flowercore-divoom-tv-hdmi.rules':
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0644',
|
|
||||||
source => 'puppet:///modules/profile/pi/fc_divoom_tv/99-flowercore-divoom-tv-hdmi.rules',
|
|
||||||
notify => Exec['fc-divoom-tv-udev-reload'],
|
|
||||||
}
|
|
||||||
|
|
||||||
exec { 'fc-divoom-tv-systemd-reload':
|
|
||||||
command => '/usr/bin/systemctl daemon-reload',
|
|
||||||
refreshonly => true,
|
|
||||||
path => ['/usr/bin', '/bin'],
|
|
||||||
}
|
|
||||||
|
|
||||||
exec { 'fc-divoom-tv-udev-reload':
|
|
||||||
command => '/usr/bin/udevadm control --reload-rules',
|
|
||||||
refreshonly => true,
|
|
||||||
path => ['/usr/bin', '/bin'],
|
|
||||||
}
|
|
||||||
|
|
||||||
service { $service_name:
|
|
||||||
ensure => $service_ensure,
|
|
||||||
enable => $service_enabled,
|
|
||||||
require => [
|
|
||||||
File["/etc/systemd/system/${service_name}.service"],
|
|
||||||
File['/etc/flowercore/divoom-tv.env'],
|
|
||||||
File['/usr/local/bin/flowercore-divoom-tv-prelaunch.sh'],
|
|
||||||
File['/usr/local/bin/flowercore-divoom-tv-launch.sh'],
|
|
||||||
Exec['fc-divoom-tv-systemd-reload'],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
systemctl restart flowercore-divoom-tv.service
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
APP_BIN="${FC_DIVOOM_TV_BIN:-/opt/flowercore/divoom-tv/FlowerCore.Divoom.Tv}"
|
|
||||||
STATE_DIR="${FC_DIVOOM_TV_STATE_DIR:-/var/lib/fc-divoom-tv}"
|
|
||||||
LOG_DIR="${FC_DIVOOM_TV_LOG_DIR:-/var/log/fc-divoom-tv}"
|
|
||||||
PRESENTATION_MODE="${FC_DIVOOM_TV_PRESENTATION_MODE:-PillarboxSquare}"
|
|
||||||
START_SCENE="${FC_DIVOOM_TV_START_SCENE:-bluejay-clock}"
|
|
||||||
REDUCED_MOTION="${FC_DIVOOM_TV_REDUCED_MOTION:-false}"
|
|
||||||
|
|
||||||
COMMON_ARGS=(
|
|
||||||
"--target=hdmi"
|
|
||||||
"--presentation-mode=${PRESENTATION_MODE}"
|
|
||||||
"--startup-scene=${START_SCENE}"
|
|
||||||
"--reduced-motion=${REDUCED_MOTION}"
|
|
||||||
"--state-dir=${STATE_DIR}"
|
|
||||||
"--log-dir=${LOG_DIR}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if command -v cage >/dev/null 2>&1; then
|
|
||||||
exec cage -- "${APP_BIN}" "${COMMON_ARGS[@]}" "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$(date -Is)] cage not found; launching FlowerCore.Divoom.Tv directly" >&2
|
|
||||||
exec "${APP_BIN}" "${COMMON_ARGS[@]}" "$@"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
APP_BIN="${FC_DIVOOM_TV_BIN:-/opt/flowercore/divoom-tv/FlowerCore.Divoom.Tv}"
|
|
||||||
STATE_DIR="${FC_DIVOOM_TV_STATE_DIR:-/var/lib/fc-divoom-tv}"
|
|
||||||
LOG_DIR="${FC_DIVOOM_TV_LOG_DIR:-/var/log/fc-divoom-tv}"
|
|
||||||
|
|
||||||
mkdir -p "${STATE_DIR}" "${LOG_DIR}"
|
|
||||||
|
|
||||||
if [[ ! -x "${APP_BIN}" ]]; then
|
|
||||||
echo "[$(date -Is)] missing executable ${APP_BIN}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d /sys/class/drm ]] && ! find /sys/class/drm -maxdepth 1 -name 'card*-HDMI-A-*' -print -quit | grep -q .; then
|
|
||||||
echo "[$(date -Is)] no HDMI connector visible yet; continuing so the app can wait for display" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v cage >/dev/null 2>&1; then
|
|
||||||
echo "[$(date -Is)] cage available for fullscreen Wayland launch"
|
|
||||||
else
|
|
||||||
echo "[$(date -Is)] cage not installed; direct launch fallback will be used" >&2
|
|
||||||
fi
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Settle DRM for 2s before restarting the fullscreen Avalonia renderer.
|
|
||||||
SUBSYSTEM=="drm", KERNEL=="card?-HDMI-A-?", ACTION=="change", RUN+="/usr/bin/systemctl start flowercore-divoom-tv-hdmi.service"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Divoom TV HDMI hotplug responder
|
|
||||||
DefaultDependencies=no
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/flowercore-divoom-tv-hdmi-respond.sh
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=FlowerCore Divoom TV HDMI Renderer (Avalonia fullscreen)
|
|
||||||
Documentation=https://github.com/astoltz/FlowerCore.Notes/blob/master/docs/standards/divoom-tv-hdmi-multitarget-render-substrate.md
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target systemd-user-sessions.service
|
|
||||||
ConditionPathExists=/opt/flowercore/divoom-tv/FlowerCore.Divoom.Tv
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=fc-divoom-tv
|
|
||||||
Group=fc-divoom-tv
|
|
||||||
WorkingDirectory=/opt/flowercore/divoom-tv
|
|
||||||
EnvironmentFile=-/etc/flowercore/divoom-tv.env
|
|
||||||
Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1
|
|
||||||
Environment=XDG_RUNTIME_DIR=/run/fc-divoom-tv
|
|
||||||
RuntimeDirectory=fc-divoom-tv
|
|
||||||
RuntimeDirectoryMode=0700
|
|
||||||
ExecStartPre=/usr/local/bin/flowercore-divoom-tv-prelaunch.sh
|
|
||||||
ExecStart=/usr/local/bin/flowercore-divoom-tv-launch.sh
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10s
|
|
||||||
StartLimitBurst=5
|
|
||||||
StartLimitIntervalSec=300s
|
|
||||||
MemoryMax=2G
|
|
||||||
MemoryHigh=1500M
|
|
||||||
PrivateTmp=true
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/var/lib/fc-divoom-tv /var/log/fc-divoom-tv /run/fc-divoom-tv
|
|
||||||
TTYPath=/dev/tty1
|
|
||||||
StandardInput=tty
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
TTYReset=yes
|
|
||||||
TTYVHangup=yes
|
|
||||||
TTYVTDisallocate=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=graphical.target
|
|
||||||
@@ -30,26 +30,3 @@ 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).
|
|
||||||
|
|||||||
@@ -1,481 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: fc-dns
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
---
|
|
||||||
# 1Password-backed Secret for the pfSense admin password.
|
|
||||||
# The operator watches this CRD, resolves the vault item, and produces a
|
|
||||||
# K8s Secret of the same name with each 1P field as a key. The `password`
|
|
||||||
# field of the "pfSense Admin" item becomes Secret key `password`.
|
|
||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: pfsense-admin
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/pfSense Admin"
|
|
||||||
---
|
|
||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: dns-oidc-client
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/dns-oidc-client"
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: dns-web-data
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
accessModes: [ReadWriteOnce]
|
|
||||||
storageClassName: longhorn
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: dns-web-config
|
|
||||||
namespace: fc-dns
|
|
||||||
data:
|
|
||||||
appsettings.Production.json: |
|
|
||||||
{
|
|
||||||
"FlowerCore": {
|
|
||||||
"Auth": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Oidc": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Audience": "dns",
|
|
||||||
"RequireHttpsMetadata": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Database": {
|
|
||||||
"Provider": "Sqlite",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"Sqlite": "Data Source=/data/dns.db"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Tenant": {
|
|
||||||
"DefaultTenantId": "default",
|
|
||||||
"JwtClaimsEnabled": false,
|
|
||||||
"DefaultTenantHosts": [
|
|
||||||
"dns.iamworkin.lan"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Audit": {
|
|
||||||
"HashChain": {
|
|
||||||
"BridgeSensitivity": {
|
|
||||||
"Distribution": "Warn"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: dns-web
|
|
||||||
namespace: fc-dns
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: dns-web
|
|
||||||
app.kubernetes.io/managed-by: flowercore
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: dns-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: dns-web
|
|
||||||
annotations:
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
prometheus.io/port: "5320"
|
|
||||||
prometheus.io/path: "/metrics/prometheus"
|
|
||||||
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
|
||||||
spec:
|
|
||||||
serviceAccountName: dns-web
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1654
|
|
||||||
runAsGroup: 1654
|
|
||||||
fsGroup: 1654
|
|
||||||
containers:
|
|
||||||
- name: dns-web
|
|
||||||
image: localhost/fc-dns-web:v20260604-oidc-proper
|
|
||||||
imagePullPolicy: Never
|
|
||||||
securityContext:
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
capabilities:
|
|
||||||
drop: [ALL]
|
|
||||||
ports:
|
|
||||||
- containerPort: 5320
|
|
||||||
env:
|
|
||||||
# pfSense admin password resolved by the 1Password operator.
|
|
||||||
# `FallbackPassword` is the Slice A seam exposed by
|
|
||||||
# OptionsFallbackPasswordResolver; Slice B will replace it with
|
|
||||||
# a pull-at-runtime 1P Connect resolver once Shared.Vault ships.
|
|
||||||
- name: FlowerCore__Dns__Providers__PfSenseUnbound__FallbackPassword
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: pfsense-admin
|
|
||||||
key: password
|
|
||||||
- name: FlowerCore__Auth__Oidc__Authority
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: dns-oidc-client
|
|
||||||
key: issuer_url
|
|
||||||
optional: true
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientId
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: dns-oidc-client
|
|
||||||
key: client_id
|
|
||||||
optional: true
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientSecret
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: dns-oidc-client
|
|
||||||
key: client_secret
|
|
||||||
optional: true
|
|
||||||
- name: FlowerCore__Auth__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Audience
|
|
||||||
value: "dns"
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
- name: tmp
|
|
||||||
mountPath: /tmp
|
|
||||||
- name: logs
|
|
||||||
mountPath: /app/logs
|
|
||||||
- name: config
|
|
||||||
mountPath: /app/appsettings.Production.json
|
|
||||||
subPath: appsettings.Production.json
|
|
||||||
readOnly: true
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 96Mi
|
|
||||||
limits:
|
|
||||||
cpu: 300m
|
|
||||||
memory: 384Mi
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 5320
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 5320
|
|
||||||
initialDelaySeconds: 20
|
|
||||||
periodSeconds: 30
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: dns-web-data
|
|
||||||
- name: tmp
|
|
||||||
emptyDir: {}
|
|
||||||
- name: logs
|
|
||||||
emptyDir: {}
|
|
||||||
- name: config
|
|
||||||
configMap:
|
|
||||||
name: dns-web-config
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: dns-web
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: dns-web
|
|
||||||
ports:
|
|
||||||
- port: 5320
|
|
||||||
targetPort: 5320
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: dns-web
|
|
||||||
namespace: fc-dns
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRole
|
|
||||||
metadata:
|
|
||||||
name: dns-web
|
|
||||||
rules:
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["namespaces", "pods", "services", "secrets", "configmaps"]
|
|
||||||
verbs: ["get", "list", "watch"]
|
|
||||||
- apiGroups: ["cert-manager.io"]
|
|
||||||
resources: ["certificates"]
|
|
||||||
verbs: ["get", "list", "watch"]
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
metadata:
|
|
||||||
name: dns-web
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: dns-web
|
|
||||||
namespace: fc-dns
|
|
||||||
roleRef:
|
|
||||||
kind: ClusterRole
|
|
||||||
name: dns-web
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: dns-web-cert
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
secretName: dns-web-tls
|
|
||||||
issuerRef:
|
|
||||||
name: step-ca-dns01
|
|
||||||
kind: ClusterIssuer
|
|
||||||
dnsNames:
|
|
||||||
- dns.iamworkin.lan
|
|
||||||
duration: 720h
|
|
||||||
renewBefore: 240h
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: dns-web
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
entryPoints: [websecure]
|
|
||||||
routes:
|
|
||||||
- match: Host(`dns.iamworkin.lan`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: dns-web
|
|
||||||
port: 5320
|
|
||||||
tls:
|
|
||||||
secretName: dns-web-tls
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook
|
|
||||||
namespace: fc-dns
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook
|
|
||||||
namespace: fc-dns
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: dns-acme-webhook
|
|
||||||
app.kubernetes.io/managed-by: flowercore
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: dns-acme-webhook
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: dns-acme-webhook
|
|
||||||
spec:
|
|
||||||
serviceAccountName: dns-acme-webhook
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1654
|
|
||||||
runAsGroup: 1654
|
|
||||||
fsGroup: 1654
|
|
||||||
containers:
|
|
||||||
- name: dns-acme-webhook
|
|
||||||
image: localhost/fc-dns-acme-webhook:v202604290845
|
|
||||||
imagePullPolicy: Never
|
|
||||||
securityContext:
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
capabilities:
|
|
||||||
drop: [ALL]
|
|
||||||
ports:
|
|
||||||
- containerPort: 9443
|
|
||||||
name: https
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_URLS
|
|
||||||
value: https://+:9443
|
|
||||||
- name: Kestrel__Certificates__Default__Path
|
|
||||||
value: /tls/tls.crt
|
|
||||||
- name: Kestrel__Certificates__Default__KeyPath
|
|
||||||
value: /tls/tls.key
|
|
||||||
- name: FlowerCore__Dns__AcmeWebhook__ServiceBaseUrl
|
|
||||||
value: http://dns-web:5320
|
|
||||||
- name: FlowerCore__Dns__AcmeWebhook__GroupName
|
|
||||||
value: acme.flowercore.io
|
|
||||||
- name: FlowerCore__Dns__AcmeWebhook__SolverName
|
|
||||||
value: flowercore-dns
|
|
||||||
- name: FlowerCore__Dns__AcmeWebhook__Version
|
|
||||||
value: v1alpha1
|
|
||||||
volumeMounts:
|
|
||||||
- name: tls
|
|
||||||
mountPath: /tls
|
|
||||||
readOnly: true
|
|
||||||
- name: tmp
|
|
||||||
mountPath: /tmp
|
|
||||||
- name: logs
|
|
||||||
mountPath: /app/logs
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 25m
|
|
||||||
memory: 64Mi
|
|
||||||
limits:
|
|
||||||
cpu: 200m
|
|
||||||
memory: 256Mi
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
scheme: HTTPS
|
|
||||||
path: /readyz
|
|
||||||
port: https
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
timeoutSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
scheme: HTTPS
|
|
||||||
path: /healthz
|
|
||||||
port: https
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 20
|
|
||||||
timeoutSeconds: 5
|
|
||||||
volumes:
|
|
||||||
- name: tls
|
|
||||||
secret:
|
|
||||||
secretName: dns-acme-webhook-tls
|
|
||||||
- name: tmp
|
|
||||||
emptyDir: {}
|
|
||||||
- name: logs
|
|
||||||
emptyDir: {}
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: dns-acme-webhook
|
|
||||||
ports:
|
|
||||||
- port: 443
|
|
||||||
targetPort: https
|
|
||||||
name: https
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Issuer
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook-selfsigned
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
selfSigned: {}
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook-ca
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
secretName: dns-acme-webhook-ca
|
|
||||||
duration: 43800h
|
|
||||||
issuerRef:
|
|
||||||
name: dns-acme-webhook-selfsigned
|
|
||||||
commonName: ca.dns-acme-webhook.fc-dns
|
|
||||||
isCA: true
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Issuer
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook-ca-issuer
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
ca:
|
|
||||||
secretName: dns-acme-webhook-ca
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook-serving-cert
|
|
||||||
namespace: fc-dns
|
|
||||||
spec:
|
|
||||||
secretName: dns-acme-webhook-tls
|
|
||||||
duration: 8760h
|
|
||||||
issuerRef:
|
|
||||||
name: dns-acme-webhook-ca-issuer
|
|
||||||
dnsNames:
|
|
||||||
- dns-acme-webhook
|
|
||||||
- dns-acme-webhook.fc-dns
|
|
||||||
- dns-acme-webhook.fc-dns.svc
|
|
||||||
---
|
|
||||||
apiVersion: apiregistration.k8s.io/v1
|
|
||||||
kind: APIService
|
|
||||||
metadata:
|
|
||||||
name: v1alpha1.acme.flowercore.io
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/inject-ca-from: fc-dns/dns-acme-webhook-serving-cert
|
|
||||||
spec:
|
|
||||||
group: acme.flowercore.io
|
|
||||||
groupPriorityMinimum: 1000
|
|
||||||
service:
|
|
||||||
name: dns-acme-webhook
|
|
||||||
namespace: fc-dns
|
|
||||||
version: v1alpha1
|
|
||||||
versionPriority: 15
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRole
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook-solver
|
|
||||||
rules:
|
|
||||||
- apiGroups: ["acme.flowercore.io"]
|
|
||||||
resources: ["flowercore-dns"]
|
|
||||||
verbs: ["create"]
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
metadata:
|
|
||||||
name: dns-acme-webhook-solver
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: cert-manager
|
|
||||||
namespace: cert-manager
|
|
||||||
roleRef:
|
|
||||||
kind: ClusterRole
|
|
||||||
name: dns-acme-webhook-solver
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: ClusterIssuer
|
|
||||||
metadata:
|
|
||||||
name: step-ca-dns01
|
|
||||||
spec:
|
|
||||||
acme:
|
|
||||||
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ4RENDQVdxZ0F3SUJBZ0lSQVBZMzU3RzZvdzZ6TUFMNSs0YlMya2t3Q2dZSUtvWkl6ajBFQXdJd1FERWEKTUJnR0ExVUVDaE1SU1VGdFYyOXlhMmx1SUVGRFRVVWdRMEV4SWpBZ0JnTlZCQU1UR1VsQmJWZHZjbXRwYmlCQgpRMDFGSUVOQklGSnZiM1FnUTBFd0hoY05Nall3TXpBNE1UZ3dOekV4V2hjTk16WXdNekExTVRnd056RXhXakJBCk1Sb3dHQVlEVlFRS0V4RkpRVzFYYjNKcmFXNGdRVU5OUlNCRFFURWlNQ0FHQTFVRUF4TVpTVUZ0VjI5eWEybHUKSUVGRFRVVWdRMEVnVW05dmRDQkRRVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSjJuMDRYMQpKWm81WmRxL2kxSWR2OCtmcXdaeUF6Qmg3d2hicWowU1dzSkw4VVdSYWJDTXFZQ3M3K2RYTzB4UlN6cWt3RkRMCngrdm9vT2FpOFJnUk5oYWpSVEJETUE0R0ExVWREd0VCL3dRRUF3SUJCakFTQmdOVkhSTUJBZjhFQ0RBR0FRSC8KQWdFQk1CMEdBMVVkRGdRV0JCUm51UFBRUjZpTS9INnZPbHVpVTNTeWdheXo4akFLQmdncWhrak9QUVFEQWdOSQpBREJGQWlFQXJRSzlkWVBHbUFac2RZbmp6aXVGVlZFNU5LWlVjY2VZdkdmR0MrdExYVXNDSUF1ZEYyekpyQ1JxCjNtSzUwWlpFVC9md1RrSndpRUY0ODI0bWpQOHAxQ0tNCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
|
||||||
privateKeySecretRef:
|
|
||||||
name: step-ca-dns01-account-key
|
|
||||||
server: https://10.0.56.10:9443/acme/acme/directory
|
|
||||||
solvers:
|
|
||||||
- dns01:
|
|
||||||
webhook:
|
|
||||||
groupName: acme.flowercore.io
|
|
||||||
solverName: flowercore-dns
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# ArgoCD's bluejay-infra ApplicationSet discovers apps/* directories on main.
|
|
||||||
# The kustomization is included for local previews and single-app validation.
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- fc-dns.yaml
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
# FlowerCore.Library.Web GitOps adoption manifest.
|
|
||||||
#
|
|
||||||
# Authored from the already-live fc-library resources on 2026-06-04.
|
|
||||||
# Keep the live image tag, Service ClusterIP, and PVC volumeName unchanged so
|
|
||||||
# ArgoCD adopts in place instead of replacing the workload or data volume.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: library-web-data
|
|
||||||
namespace: fc-library
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: library-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-library
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
storageClassName: longhorn
|
|
||||||
volumeMode: Filesystem
|
|
||||||
volumeName: pvc-2690bae2-4ee0-417a-b95f-50ec5c632b63
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: library-web
|
|
||||||
namespace: fc-library
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: library-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-library
|
|
||||||
spec:
|
|
||||||
progressDeadlineSeconds: 600
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: library-web
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
fc.flowercore.io/healthz-anon: "true"
|
|
||||||
fc.flowercore.io/probe-path: "/health"
|
|
||||||
prometheus.io/path: /metrics/prometheus
|
|
||||||
prometheus.io/port: "5000"
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: library-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
|
||||||
- envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: library-web-config
|
|
||||||
image: localhost/fc-library-web:v20260602-library-owned-deploy-fix1
|
|
||||||
imagePullPolicy: Never
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 3
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 5000
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 5
|
|
||||||
name: library-web
|
|
||||||
ports:
|
|
||||||
- containerPort: 5000
|
|
||||||
name: http
|
|
||||||
protocol: TCP
|
|
||||||
readinessProbe:
|
|
||||||
failureThreshold: 6
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 5000
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 5
|
|
||||||
resources: {}
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /data
|
|
||||||
name: data
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
securityContext: {}
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: library-web-data
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: library-web
|
|
||||||
namespace: fc-library
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: library-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-library
|
|
||||||
spec:
|
|
||||||
clusterIP: 10.43.179.63
|
|
||||||
clusterIPs:
|
|
||||||
- 10.43.179.63
|
|
||||||
internalTrafficPolicy: Cluster
|
|
||||||
ipFamilies:
|
|
||||||
- IPv4
|
|
||||||
ipFamilyPolicy: SingleStack
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 5000
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: library-web
|
|
||||||
sessionAffinity: None
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: library-web-tls
|
|
||||||
namespace: fc-library
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: library-web-tls
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-library
|
|
||||||
spec:
|
|
||||||
dnsNames:
|
|
||||||
- library.iamworkin.lan
|
|
||||||
issuerRef:
|
|
||||||
kind: ClusterIssuer
|
|
||||||
name: step-ca-acme
|
|
||||||
secretName: library-web-tls
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: library-web
|
|
||||||
namespace: fc-library
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: library-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-library
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- kind: Rule
|
|
||||||
match: Host(`library.iamworkin.lan`)
|
|
||||||
services:
|
|
||||||
- name: library-web
|
|
||||||
port: 80
|
|
||||||
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,8 +83,6 @@ 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"
|
||||||
@@ -118,7 +116,6 @@ 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"
|
||||||
@@ -284,26 +281,3 @@ 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).
|
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: fc-media
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-media
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
---
|
|
||||||
apiVersion: onepassword.com/v1
|
|
||||||
kind: OnePasswordItem
|
|
||||||
metadata:
|
|
||||||
name: media-oidc-client
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
itemPath: "vaults/IAmWorkin/items/media-oidc-client"
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: fc-media-config
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
data:
|
|
||||||
appsettings.Production.json: |
|
|
||||||
{
|
|
||||||
"DatabaseProvider": "Sqlite",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"Sqlite": "Data Source=/data/media.db"
|
|
||||||
},
|
|
||||||
"FlowerCore": {
|
|
||||||
"Auth": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Oidc": {
|
|
||||||
"Authority": "https://id.iamworkin.lan/application/o/media/",
|
|
||||||
"ClientId": "media",
|
|
||||||
"ClientSecret": "",
|
|
||||||
"Audience": "media",
|
|
||||||
"RequireHttpsMetadata": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Tenant": {
|
|
||||||
"JwtClaimsEnabled": false,
|
|
||||||
"DefaultTenantHosts": [ "media.iamworkin.lan" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Media": {
|
|
||||||
"LibraryRoot": "/media/library",
|
|
||||||
"Sources": [
|
|
||||||
{
|
|
||||||
"Name": "BlueJayNAS Video",
|
|
||||||
"Driver": "Nfs",
|
|
||||||
"MountedPath": "/media/library",
|
|
||||||
"RemotePath": "nfs://10.0.58.3/volume1/video",
|
|
||||||
"IsEnabled": true,
|
|
||||||
"IsDefault": true,
|
|
||||||
"Notes": "Synology NFS media share mounted read-only inside the cluster."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"GeneratedRoot": "/data/generated",
|
|
||||||
"TranscodeRoot": "/data/transcodes",
|
|
||||||
"InboxPath": "/media/inbox",
|
|
||||||
"InboxScanIntervalMinutes": 5,
|
|
||||||
"ScanOnStartup": false,
|
|
||||||
"ComputeChecksums": false,
|
|
||||||
"FfmpegCommand": "ffmpeg",
|
|
||||||
"FfprobeCommand": "ffprobe",
|
|
||||||
"Hls": {
|
|
||||||
"MaxConcurrentJobs": 1
|
|
||||||
},
|
|
||||||
"DefaultViewerName": "BlueJay",
|
|
||||||
"Dlna": {
|
|
||||||
"IsEnabled": true,
|
|
||||||
"MulticastAddress": "239.255.255.250",
|
|
||||||
"Port": 1900,
|
|
||||||
"DiscoveryTimeoutSeconds": 2,
|
|
||||||
"DescriptionFetchTimeoutSeconds": 2,
|
|
||||||
"MaxResponsesPerSearchTarget": 32,
|
|
||||||
"SearchTargets": [
|
|
||||||
"urn:schemas-upnp-org:device:MediaRenderer:1",
|
|
||||||
"urn:schemas-upnp-org:device:MediaServer:1"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: fc-media-data
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 20Gi
|
|
||||||
storageClassName: longhorn
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: fc-media-web
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app: fc-media-web
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: fc-media-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: fc-media-web
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
annotations:
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
prometheus.io/port: "5200"
|
|
||||||
prometheus.io/path: "/metrics"
|
|
||||||
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
|
||||||
spec:
|
|
||||||
nodeSelector:
|
|
||||||
kubernetes.io/hostname: rke2-server
|
|
||||||
containers:
|
|
||||||
- name: fc-media-web
|
|
||||||
image: localhost/fc-media-web:v20260604-oidc-proper
|
|
||||||
imagePullPolicy: Never
|
|
||||||
ports:
|
|
||||||
- containerPort: 5200
|
|
||||||
name: http
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_ENVIRONMENT
|
|
||||||
value: Production
|
|
||||||
- name: ASPNETCORE_URLS
|
|
||||||
value: http://+:5200
|
|
||||||
- name: FlowerCore__Auth__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Audience
|
|
||||||
value: "media"
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientId
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: media-oidc-client
|
|
||||||
key: client_id
|
|
||||||
optional: true
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientSecret
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: media-oidc-client
|
|
||||||
key: client_secret
|
|
||||||
optional: true
|
|
||||||
- name: FlowerCore__Auth__Oidc__Authority
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: media-oidc-client
|
|
||||||
key: issuer_url
|
|
||||||
optional: true
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 1Gi
|
|
||||||
limits:
|
|
||||||
cpu: "4"
|
|
||||||
memory: 4Gi
|
|
||||||
volumeMounts:
|
|
||||||
- name: config
|
|
||||||
mountPath: /app/appsettings.Production.json
|
|
||||||
subPath: appsettings.Production.json
|
|
||||||
readOnly: true
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
- name: transcodes
|
|
||||||
mountPath: /data/transcodes
|
|
||||||
- name: media-library
|
|
||||||
mountPath: /media/library
|
|
||||||
readOnly: true
|
|
||||||
- name: media-inbox
|
|
||||||
mountPath: /media/inbox
|
|
||||||
startupProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 5200
|
|
||||||
httpHeaders:
|
|
||||||
- name: X-Forwarded-Proto
|
|
||||||
value: https
|
|
||||||
failureThreshold: 18
|
|
||||||
periodSeconds: 10
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 5200
|
|
||||||
httpHeaders:
|
|
||||||
- name: X-Forwarded-Proto
|
|
||||||
value: https
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 5200
|
|
||||||
httpHeaders:
|
|
||||||
- name: X-Forwarded-Proto
|
|
||||||
value: https
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
volumes:
|
|
||||||
- name: config
|
|
||||||
configMap:
|
|
||||||
name: fc-media-config
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: fc-media-data
|
|
||||||
- name: transcodes
|
|
||||||
nfs:
|
|
||||||
server: 10.0.58.3
|
|
||||||
path: /volume1/kubernetes/fc-media-transcodes
|
|
||||||
- name: media-inbox
|
|
||||||
nfs:
|
|
||||||
server: 10.0.58.3
|
|
||||||
path: /volume1/kubernetes/fc-media-inbox
|
|
||||||
- name: media-library
|
|
||||||
nfs:
|
|
||||||
server: 10.0.58.3
|
|
||||||
path: /volume1/video
|
|
||||||
readOnly: true
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: fc-media-web
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app: fc-media-web
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: fc-media-web
|
|
||||||
ports:
|
|
||||||
- port: 5200
|
|
||||||
targetPort: 5200
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: fc-media-tls
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
secretName: fc-media-tls
|
|
||||||
issuerRef:
|
|
||||||
name: step-ca-acme
|
|
||||||
kind: ClusterIssuer
|
|
||||||
dnsNames:
|
|
||||||
- media.iamworkin.lan
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: fc-media-web
|
|
||||||
namespace: fc-media
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: fc-media-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- match: Host(`media.iamworkin.lan`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: fc-media-web
|
|
||||||
port: 5200
|
|
||||||
tls:
|
|
||||||
secretName: fc-media-tls
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# ArgoCD's bluejay-infra ApplicationSet discovers apps/* directories on main.
|
|
||||||
# The kustomization is included for local previews and single-app validation.
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- fc-media.yaml
|
|
||||||
@@ -30,26 +30,3 @@ 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,8 +41,6 @@ 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"
|
||||||
@@ -54,7 +52,6 @@ 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
|
||||||
@@ -144,26 +141,3 @@ 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,26 +30,3 @@ 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,26 +30,3 @@ 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,26 +30,3 @@ 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).
|
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
# FlowerCore.Retail.Web GitOps adoption manifest.
|
|
||||||
#
|
|
||||||
# Authored from the already-live fc-retail resources on 2026-06-04.
|
|
||||||
# Keep the live image tag, Service ClusterIP, and PVC volumeName unchanged so
|
|
||||||
# ArgoCD adopts in place instead of replacing the workload or data volume.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: retail-web-data
|
|
||||||
namespace: fc-retail
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: retail-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-retail
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
storageClassName: longhorn
|
|
||||||
volumeMode: Filesystem
|
|
||||||
volumeName: pvc-3d40b336-eab4-41b3-812c-d5e9413ce0ab
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: retail-web
|
|
||||||
namespace: fc-retail
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: retail-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-retail
|
|
||||||
spec:
|
|
||||||
progressDeadlineSeconds: 600
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: retail-web
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
fc.flowercore.io/healthz-anon: "true"
|
|
||||||
fc.flowercore.io/probe-path: "/healthz"
|
|
||||||
kubectl.kubernetes.io/restartedAt: "2026-06-02T01:34:08-05:00"
|
|
||||||
prometheus.io/path: /metrics/prometheus
|
|
||||||
prometheus.io/port: "5000"
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: retail-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
|
||||||
- envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: retail-web-config
|
|
||||||
image: localhost/fc-retail-web:v20260602-retail-owned-deploy-fix5
|
|
||||||
imagePullPolicy: Never
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 3
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 5000
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 5
|
|
||||||
name: retail-web
|
|
||||||
ports:
|
|
||||||
- containerPort: 5000
|
|
||||||
name: http
|
|
||||||
protocol: TCP
|
|
||||||
readinessProbe:
|
|
||||||
failureThreshold: 6
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 5000
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 5
|
|
||||||
resources: {}
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /data
|
|
||||||
name: data
|
|
||||||
dnsPolicy: ClusterFirst
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
securityContext: {}
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: retail-web-data
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: retail-web
|
|
||||||
namespace: fc-retail
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: retail-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-retail
|
|
||||||
spec:
|
|
||||||
clusterIP: 10.43.239.8
|
|
||||||
clusterIPs:
|
|
||||||
- 10.43.239.8
|
|
||||||
internalTrafficPolicy: Cluster
|
|
||||||
ipFamilies:
|
|
||||||
- IPv4
|
|
||||||
ipFamilyPolicy: SingleStack
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 5000
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: retail-web
|
|
||||||
sessionAffinity: None
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: retail-web-tls
|
|
||||||
namespace: fc-retail
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: retail-web-tls
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-retail
|
|
||||||
spec:
|
|
||||||
dnsNames:
|
|
||||||
- retail.iamworkin.lan
|
|
||||||
issuerRef:
|
|
||||||
kind: ClusterIssuer
|
|
||||||
name: step-ca-acme
|
|
||||||
secretName: retail-web-tls
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: retail-web
|
|
||||||
namespace: fc-retail
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: retail-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
argocd.argoproj.io/instance: infra-fc-retail
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- kind: Rule
|
|
||||||
match: Host(`retail.iamworkin.lan`)
|
|
||||||
services:
|
|
||||||
- name: retail-web
|
|
||||||
port: 80
|
|
||||||
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,26 +30,3 @@ 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,26 +37,3 @@ 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,26 +46,3 @@ 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,7 +97,6 @@ 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"
|
||||||
@@ -524,8 +523,6 @@ 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"
|
||||||
@@ -535,7 +532,7 @@ spec:
|
|||||||
fsGroupChangePolicy: OnRootMismatch
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: localhost/fc-ttsreader-web:v20260603-s54cx14-pr29-schema
|
image: localhost/fc-ttsreader-web:v20260518-sprint36-demo-finish-b132cbf
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5217
|
- containerPort: 5217
|
||||||
@@ -557,8 +554,6 @@ spec:
|
|||||||
value: "/data/chapter-context.db"
|
value: "/data/chapter-context.db"
|
||||||
- name: TtsReader__Jobs__Root
|
- name: TtsReader__Jobs__Root
|
||||||
value: "/data/jobs"
|
value: "/data/jobs"
|
||||||
- name: TtsReader__Export__LocalCasRoot
|
|
||||||
value: "/data/bundles/cas"
|
|
||||||
- name: TtsReader__Piper__Host
|
- name: TtsReader__Piper__Host
|
||||||
value: "10.0.57.17"
|
value: "10.0.57.17"
|
||||||
- name: TtsReader__Piper__Port
|
- name: TtsReader__Piper__Port
|
||||||
@@ -765,26 +760,3 @@ 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,21 +52,17 @@ 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:
|
||||||
nodeName: rke2-server
|
nodeName: rke2-server
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: localhost/fc-updater-web:v202605310029-7974fc4
|
image: localhost/fc-updater-web:v20260509-4162dca-authgate
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
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
|
||||||
@@ -92,8 +88,6 @@ spec:
|
|||||||
value: Faith AI Mike Edition
|
value: Faith AI Mike Edition
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__Description
|
- name: FlowerCore__Updater__PublicShares__Links__0__Description
|
||||||
value: Private release link for Mike's Faith AI bundle.
|
value: Private release link for Mike's Faith AI bundle.
|
||||||
- name: FlowerCore__Audit__Sinks__Loki__Enabled
|
|
||||||
value: "false"
|
|
||||||
- name: FlowerCore__Updater__Auth__Bootstrap__Enabled
|
- name: FlowerCore__Updater__Auth__Bootstrap__Enabled
|
||||||
value: "true"
|
value: "true"
|
||||||
- name: FlowerCore__Updater__Auth__Bootstrap__Username
|
- name: FlowerCore__Updater__Auth__Bootstrap__Username
|
||||||
|
|||||||
2
apps/github-runner/.gitattributes
vendored
2
apps/github-runner/.gitattributes
vendored
@@ -1,2 +0,0 @@
|
|||||||
*.sh text eol=lf
|
|
||||||
Dockerfile text eol=lf
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
FROM myoung34/github-runner:latest
|
|
||||||
|
|
||||||
ARG RUBY_VERSION=3.3.11
|
|
||||||
ARG RUBY_MINOR=3.3
|
|
||||||
ARG RUBY_BUILD_VERSION=v20260326
|
|
||||||
ARG RUNNER_UID=1001
|
|
||||||
ARG RUNNER_GID=1001
|
|
||||||
|
|
||||||
ENV RUNNER_TOOL_CACHE=/home/runner/_tool
|
|
||||||
ENV RUNNER_RUBY_TOOLCACHE=/opt/runner-toolcache
|
|
||||||
ENV PATH="/home/runner/_tool/Ruby/${RUBY_MINOR}/x64/bin:/opt/runner-toolcache/Ruby/${RUBY_MINOR}/x64/bin:${PATH}"
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
# Bake the IAmWorkin step-ca root CA into the system trust store. Without
|
|
||||||
# this, .NET HttpClient calls from CI tests against *.iamworkin.lan
|
|
||||||
# (e.g. https://selenium.iamworkin.lan/session) fail with `PartialChain`
|
|
||||||
# because the runner image's default Ubuntu trust bundle doesn't include
|
|
||||||
# our internal Root CA. update-ca-certificates regenerates
|
|
||||||
# /etc/ssl/certs/ca-certificates.crt, which OpenSSL + .NET on Linux read
|
|
||||||
# automatically — no SSL_CERT_FILE env var needed.
|
|
||||||
COPY step-ca-root.crt /usr/local/share/ca-certificates/iamworkin-step-ca-root.crt
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
||||||
autoconf \
|
|
||||||
bison \
|
|
||||||
build-essential \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
libdb-dev \
|
|
||||||
libffi-dev \
|
|
||||||
libgdbm-dev \
|
|
||||||
libgmp-dev \
|
|
||||||
libncurses-dev \
|
|
||||||
libreadline-dev \
|
|
||||||
libssl-dev \
|
|
||||||
libyaml-dev \
|
|
||||||
patch \
|
|
||||||
pkg-config \
|
|
||||||
uuid-dev \
|
|
||||||
zlib1g-dev \
|
|
||||||
&& update-ca-certificates \
|
|
||||||
&& curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/${RUBY_BUILD_VERSION}.tar.gz" -o /tmp/ruby-build.tar.gz \
|
|
||||||
&& mkdir -p /tmp/ruby-build \
|
|
||||||
&& tar -xzf /tmp/ruby-build.tar.gz --strip-components=1 -C /tmp/ruby-build \
|
|
||||||
&& /tmp/ruby-build/install.sh \
|
|
||||||
&& rm -rf /tmp/ruby-build /tmp/ruby-build.tar.gz /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY install-ruby-toolcache.sh /usr/local/bin/install-ruby-toolcache.sh
|
|
||||||
|
|
||||||
RUN chmod +x /usr/local/bin/install-ruby-toolcache.sh \
|
|
||||||
&& RUBY_VERSION="${RUBY_VERSION}" RUBY_MINOR="${RUBY_MINOR}" TOOLCACHE_ROOT="${RUNNER_RUBY_TOOLCACHE}" RUNNER_UID="${RUNNER_UID}" RUNNER_GID="${RUNNER_GID}" /usr/local/bin/install-ruby-toolcache.sh \
|
|
||||||
&& ruby -v
|
|
||||||
@@ -7,17 +7,12 @@ Deployments with `kubectl`; update this manifest and let ArgoCD reconcile.
|
|||||||
|
|
||||||
All repo-scoped Linux runners use:
|
All repo-scoped Linux runners use:
|
||||||
|
|
||||||
- `localhost/fc-github-runner:v20260525-ruby3.3.11-stepca`, derived from
|
|
||||||
`myoung34/github-runner:latest`
|
|
||||||
- `ACCESS_TOKEN` from the `github-runner-token` Secret
|
- `ACCESS_TOKEN` from the `github-runner-token` Secret
|
||||||
- `RUN_AS_ROOT=false`
|
- `RUN_AS_ROOT=false`
|
||||||
- `EPHEMERAL=true`
|
- `EPHEMERAL=true`
|
||||||
- `LABELS=self-hosted,linux,fc-build-linux`
|
- `LABELS=self-hosted,linux,fc-build-linux`
|
||||||
- writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and
|
- writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and
|
||||||
Actions tool cache
|
Actions tool cache
|
||||||
- Ruby 3.3.11 seeded into `/home/runner/_tool/Ruby/3.3/x64` from the baked
|
|
||||||
`/opt/runner-toolcache` copy so `ruby/setup-ruby@v1` can discover it on
|
|
||||||
self-hosted `ubuntu-20.04-x64` runners
|
|
||||||
|
|
||||||
`github-runner` for `FlowerCore.Common` is single-replica because it retains the
|
`github-runner` for `FlowerCore.Common` is single-replica because it retains the
|
||||||
original Longhorn ReadWriteOnce NuGet PVC. Every other repo-scoped runner uses
|
original Longhorn ReadWriteOnce NuGet PVC. Every other repo-scoped runner uses
|
||||||
@@ -33,45 +28,9 @@ Sprint 32 final long-tail wave adds 16 two-replica Deployments:
|
|||||||
`FlowerCore.Provisioning`, `FlowerCore.Redis`, `FlowerCore.MessageBoard`, and
|
`FlowerCore.Provisioning`, `FlowerCore.Redis`, `FlowerCore.MessageBoard`, and
|
||||||
`FlowerCore.MenuBoard`.
|
`FlowerCore.MenuBoard`.
|
||||||
|
|
||||||
## Image Build
|
Sprint 37 Cx-2 closes the audited Linux runner gaps for
|
||||||
|
`FlowerCore.DeviceManagement` and `FlowerCore.WorldBuilder` with the same
|
||||||
Ruby is baked with a pinned `ruby-build` release and Ruby patch version. The pod
|
two-replica `emptyDir` pattern.
|
||||||
still mounts an `emptyDir` over `/home/runner`, so the `setup-runner-home` init
|
|
||||||
container copies the baked toolcache from `/opt/runner-toolcache/Ruby` into
|
|
||||||
`/home/runner/_tool/Ruby` before the runner container starts.
|
|
||||||
|
|
||||||
The IAmWorkin step-ca root CA is also baked into the system trust store
|
|
||||||
(`/usr/local/share/ca-certificates/iamworkin-step-ca-root.crt`, registered by
|
|
||||||
`update-ca-certificates`). Without it, .NET HttpClient calls from CI tests
|
|
||||||
against `*.iamworkin.lan` (e.g. `https://selenium.iamworkin.lan/session`)
|
|
||||||
fail with `PartialChain`. To refresh the bundled cert when the root rotates,
|
|
||||||
re-extract from the cluster and overwrite `step-ca-root.crt`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl get secret -n cert-manager step-ca-root \
|
|
||||||
-o jsonpath='{.data.ca\.crt}' | base64 -d > step-ca-root.crt
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/github-runner
|
|
||||||
podman build -t localhost/fc-github-runner:v20260525-ruby3.3.11-stepca .
|
|
||||||
podman run --rm localhost/fc-github-runner:v20260525-ruby3.3.11-stepca ruby -v
|
|
||||||
podman run --rm localhost/fc-github-runner:v20260525-ruby3.3.11-stepca \
|
|
||||||
test -f /opt/runner-toolcache/Ruby/3.3/x64.complete
|
|
||||||
podman save localhost/fc-github-runner:v20260525-ruby3.3.11-stepca \
|
|
||||||
-o fc-github-runner-v20260525-ruby3.3.11-stepca.tar
|
|
||||||
```
|
|
||||||
|
|
||||||
Import the saved image on every schedulable RKE2 node before ArgoCD rolls the
|
|
||||||
Deployments:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
for node in rke2-server rke2-agent1 rke2-agent2; do
|
|
||||||
scp fc-github-runner-v20260525-ruby3.3.11-stepca.tar "$node:/tmp/"
|
|
||||||
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images rm localhost/fc-github-runner:v20260525-ruby3.3.11-stepca || true'
|
|
||||||
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/fc-github-runner-v20260525-ruby3.3.11-stepca.tar'
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
## Post-Merge Proof
|
## Post-Merge Proof
|
||||||
|
|
||||||
@@ -81,14 +40,6 @@ After the PR is merged and ArgoCD syncs, verify the runner fleet:
|
|||||||
kubectl -n github-runner get deploy,pods,pvc
|
kubectl -n github-runner get deploy,pods,pvc
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify the Ruby toolcache in a fresh pod:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl -n github-runner exec deploy/github-runner-puppet -c runner -- ruby -v
|
|
||||||
kubectl -n github-runner exec deploy/github-runner-puppet -c runner -- sh -c \
|
|
||||||
'echo "$RUNNER_TOOL_CACHE" && test -f "$RUNNER_TOOL_CACHE/Ruby/3.3/x64.complete"'
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify GitHub registration for the repo-scoped runners:
|
Verify GitHub registration for the repo-scoped runners:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -100,7 +51,7 @@ for repo in FlowerCore.Common FlowerCore.Shared.Pos FlowerCore.Puppet FlowerCore
|
|||||||
FlowerCore.Distribution FlowerCore.Scoreboard FlowerCore.SegmentDisplay \
|
FlowerCore.Distribution FlowerCore.Scoreboard FlowerCore.SegmentDisplay \
|
||||||
FlowerCore.Signage.Contracts FlowerCore.SignalControl FlowerCore.Intranet.Web \
|
FlowerCore.Signage.Contracts FlowerCore.SignalControl FlowerCore.Intranet.Web \
|
||||||
FlowerCore.Provisioning FlowerCore.Redis FlowerCore.MessageBoard \
|
FlowerCore.Provisioning FlowerCore.Redis FlowerCore.MessageBoard \
|
||||||
FlowerCore.MenuBoard; do
|
FlowerCore.MenuBoard FlowerCore.DeviceManagement FlowerCore.WorldBuilder; do
|
||||||
echo "=== $repo ==="
|
echo "=== $repo ==="
|
||||||
gh api "/repos/astoltz/$repo/actions/runners" \
|
gh api "/repos/astoltz/$repo/actions/runners" \
|
||||||
--jq '.runners[] | select(.labels[].name == "fc-build-linux") | {name,status,busy,labels:[.labels[].name]}'
|
--jq '.runners[] | select(.labels[].name == "fc-build-linux") | {name,status,busy,labels:[.labels[].name]}'
|
||||||
@@ -117,15 +68,25 @@ gh run list --repo astoltz/FlowerCore.Shared.Pos \
|
|||||||
If the latest run is still queued after runner registration, rerun the workflow
|
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.
|
from GitHub Actions and verify it lands on an `rke2-linux-*` runner.
|
||||||
|
|
||||||
|
## Sprint 37 Cx-2 Gap Audit
|
||||||
|
|
||||||
|
The 2026-05-18 GitHub workflow scan found these remaining repos with
|
||||||
|
`runs-on: [self-hosted, linux, fc-build-linux]` but no K8s runner Deployment:
|
||||||
|
`FlowerCore.AiStation.Linux`, `FlowerCore.PHP`, `FlowerCore.PiManager`,
|
||||||
|
`FlowerCore.Shared.Barcodes`, `FlowerCore.Shared.Lookup`,
|
||||||
|
`FlowerCore.Shared.Nodes`, `FlowerCore.Shared.PrintClient`,
|
||||||
|
`FlowerCore.Shared.Relay`, `FlowerCore.Shared.ShowRunner`, and
|
||||||
|
`FlowerCore.Shared.Storage`.
|
||||||
|
|
||||||
|
Mixed/platform repos also have Linux workflow legs but need owner review before
|
||||||
|
adding Linux runner Deployments: `FlowerCore.Library.Mac`,
|
||||||
|
`FlowerCore.Signage.Agent.AppleTv`, and `FlowerCore.Signage.Player.Wpf`.
|
||||||
|
|
||||||
## Failure Notes
|
## Failure Notes
|
||||||
|
|
||||||
- `actions/setup-dotnet` permission error at `/usr/share/dotnet`: check that
|
- `actions/setup-dotnet` permission error at `/usr/share/dotnet`: check that
|
||||||
`DOTNET_INSTALL_DIR=/home/runner/.dotnet` and related cache env vars are
|
`DOTNET_INSTALL_DIR=/home/runner/.dotnet` and related cache env vars are
|
||||||
present on the runner pod.
|
present on the runner pod.
|
||||||
- `ruby/setup-ruby@v1` says self-hosted runners must install Ruby in
|
|
||||||
`$RUNNER_TOOL_CACHE`: check that the init container copied
|
|
||||||
`/opt/runner-toolcache/Ruby` into `/home/runner/_tool/Ruby` and that
|
|
||||||
`/home/runner/_tool/Ruby/3.3/x64.complete` exists.
|
|
||||||
- `404` during runner registration: the fine-grained PAT is valid but missing
|
- `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
|
repository access for that repo. Add the repo to the PAT access list; the PAT
|
||||||
value does not change.
|
value does not change.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
RUBY_VERSION="${RUBY_VERSION:-3.3.11}"
|
|
||||||
RUBY_MINOR="${RUBY_MINOR:-3.3}"
|
|
||||||
TOOLCACHE_ROOT="${TOOLCACHE_ROOT:-/opt/runner-toolcache}"
|
|
||||||
RUNNER_UID="${RUNNER_UID:-1001}"
|
|
||||||
RUNNER_GID="${RUNNER_GID:-1001}"
|
|
||||||
RUBY_PREFIX="${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64"
|
|
||||||
|
|
||||||
mkdir -p "${TOOLCACHE_ROOT}/Ruby"
|
|
||||||
RUBY_CONFIGURE_OPTS="${RUBY_CONFIGURE_OPTS:---disable-install-doc --disable-yjit}" ruby-build "${RUBY_VERSION}" "${RUBY_PREFIX}"
|
|
||||||
|
|
||||||
touch "${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64.complete"
|
|
||||||
ln -sfn "${RUBY_VERSION}" "${TOOLCACHE_ROOT}/Ruby/${RUBY_MINOR}"
|
|
||||||
|
|
||||||
"${RUBY_PREFIX}/bin/ruby" -v
|
|
||||||
chown -R "${RUNNER_UID}:${RUNNER_GID}" "${TOOLCACHE_ROOT}"
|
|
||||||
chmod -R a+rX "${TOOLCACHE_ROOT}"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIBxDCCAWqgAwIBAgIRAPY357G6ow6zMAL5+4bS2kkwCgYIKoZIzj0EAwIwQDEa
|
|
||||||
MBgGA1UEChMRSUFtV29ya2luIEFDTUUgQ0ExIjAgBgNVBAMTGUlBbVdvcmtpbiBB
|
|
||||||
Q01FIENBIFJvb3QgQ0EwHhcNMjYwMzA4MTgwNzExWhcNMzYwMzA1MTgwNzExWjBA
|
|
||||||
MRowGAYDVQQKExFJQW1Xb3JraW4gQUNNRSBDQTEiMCAGA1UEAxMZSUFtV29ya2lu
|
|
||||||
IEFDTUUgQ0EgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ2n04X1
|
|
||||||
JZo5Zdq/i1Idv8+fqwZyAzBh7whbqj0SWsJL8UWRabCMqYCs7+dXO0xRSzqkwFDL
|
|
||||||
x+vooOai8RgRNhajRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/
|
|
||||||
AgEBMB0GA1UdDgQWBBRnuPPQR6iM/H6vOluiU3Sygayz8jAKBggqhkjOPQQDAgNI
|
|
||||||
ADBFAiEArQK9dYPGmAZsdYnjziuFVVE5NKZUcceYvGfGC+tLXUsCIAudF2zJrCRq
|
|
||||||
3mK50ZZET/fwTkJwiEF4824mjP8p1CKM
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -46,7 +46,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: intranet-web
|
- name: intranet-web
|
||||||
image: localhost/fc-intranet-web:v20260531-ttsreader-bridge
|
image: localhost/fc-intranet-web:v20260508-brochure-w1
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5300
|
- containerPort: 5300
|
||||||
|
|||||||
@@ -90,12 +90,9 @@ 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"
|
||||||
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
|
||||||
spec:
|
spec:
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
@@ -105,7 +102,7 @@ spec:
|
|||||||
- name: web
|
- name: web
|
||||||
# Placeholder tag — bump to the image you built + imported to ALL
|
# Placeholder tag — bump to the image you built + imported to ALL
|
||||||
# RKE2 nodes via scripts/deploy-knowledge.sh before applying.
|
# RKE2 nodes via scripts/deploy-knowledge.sh before applying.
|
||||||
image: localhost/fc-knowledge-web:v20260603-oidc-authentik-auditfix
|
image: localhost/fc-knowledge-web:v20260429232635
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
@@ -119,7 +116,6 @@ 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"
|
||||||
@@ -127,25 +123,6 @@ spec:
|
|||||||
value: "Production"
|
value: "Production"
|
||||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
||||||
value: "false"
|
value: "false"
|
||||||
# AuthentiK/OIDC is enforced. /healthz stays anonymous by contract;
|
|
||||||
# see flowercore.io/healthz-auth-policy above and the Sprint 58
|
|
||||||
# OIDC readiness probe audit.
|
|
||||||
- name: FlowerCore__Auth__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Authority
|
|
||||||
value: "https://id.iamworkin.lan/application/o/knowledge/"
|
|
||||||
- name: FlowerCore__Auth__Oidc__Audience
|
|
||||||
value: "knowledge"
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientId
|
|
||||||
value: "knowledge"
|
|
||||||
- name: FlowerCore__Auth__Oidc__ClientSecret
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: knowledge-oidc-client
|
|
||||||
key: client_secret
|
|
||||||
optional: true
|
|
||||||
# Vector-store directory + embedding model + edition profile dir.
|
# Vector-store directory + embedding model + edition profile dir.
|
||||||
# Profile JSON is baked into the image at /home/app/editions via the
|
# Profile JSON is baked into the image at /home/app/editions via the
|
||||||
# csproj Content-link from FlowerCore.Common/editions/.
|
# csproj Content-link from FlowerCore.Common/editions/.
|
||||||
@@ -157,8 +134,6 @@ spec:
|
|||||||
value: "5"
|
value: "5"
|
||||||
- name: Knowledge__MaxLimit
|
- name: Knowledge__MaxLimit
|
||||||
value: "50"
|
value: "50"
|
||||||
- name: Knowledge__Federation__DatabasePath
|
|
||||||
value: "/data/vector-stores/knowledge-federation.db"
|
|
||||||
- name: FlowerCore__Editions__ProfileDirectory
|
- name: FlowerCore__Editions__ProfileDirectory
|
||||||
value: "/home/app/editions"
|
value: "/home/app/editions"
|
||||||
# Embed via edge1 Pi 5 + AI HAT+ (10.0.57.17:11434). Cluster
|
# Embed via edge1 Pi 5 + AI HAT+ (10.0.57.17:11434). Cluster
|
||||||
@@ -289,26 +264,3 @@ 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).
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ metadata:
|
|||||||
role: github-actions-runner
|
role: github-actions-runner
|
||||||
flowercore.io/managed-by: bluejay-infra
|
flowercore.io/managed-by: bluejay-infra
|
||||||
spec:
|
spec:
|
||||||
runStrategy: Halted
|
runStrategy: Always
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -207,13 +207,20 @@ spec:
|
|||||||
- port: 993
|
- port: 993
|
||||||
targetPort: 993
|
targetPort: 993
|
||||||
name: imaps
|
name: imaps
|
||||||
# --- mail-tls Certificate REMOVED 2026-06-01 ---
|
---
|
||||||
# mail-tls is now managed OUTSIDE cert-manager: issued from step-ca's JWK 'admin'
|
# TLS Certificate via cert-manager
|
||||||
# provisioner and auto-renewed by a systemd timer on noc1 (step ca renew), which
|
apiVersion: cert-manager.io/v1
|
||||||
# writes the mail-tls secret directly. step-ca-acme only has an HTTP-01 (Traefik)
|
kind: Certificate
|
||||||
# solver, but mail.iamworkin.lan must resolve to the dedicated MetalLB IP 10.0.56.202
|
metadata:
|
||||||
# (SMTP/IMAP), so HTTP-01 cannot validate. Do NOT re-add a cert-manager Certificate
|
name: mail-tls
|
||||||
# here unless a DNS-01 solver is deployed for step-ca-acme.
|
namespace: mail
|
||||||
|
spec:
|
||||||
|
secretName: mail-tls
|
||||||
|
issuerRef:
|
||||||
|
name: step-ca-acme
|
||||||
|
kind: ClusterIssuer
|
||||||
|
dnsNames:
|
||||||
|
- mail.iamworkin.lan
|
||||||
---
|
---
|
||||||
# Traefik IngressRoute - Webmail placeholder
|
# Traefik IngressRoute - Webmail placeholder
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
|||||||
@@ -216,24 +216,19 @@ data:
|
|||||||
- job_name: "pimanager-app"
|
- job_name: "pimanager-app"
|
||||||
scrape_interval: 15s
|
scrape_interval: 15s
|
||||||
metrics_path: /metrics
|
metrics_path: /metrics
|
||||||
scheme: https
|
|
||||||
tls_config:
|
|
||||||
insecure_skip_verify: true
|
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["piez.iamworkin.lan"]
|
- targets: ["10.0.58.25:5000"]
|
||||||
labels:
|
labels:
|
||||||
instance: "piez"
|
instance: "piez"
|
||||||
service: "signalcontrol"
|
service: "pimanager"
|
||||||
vlan: "home"
|
vlan: "home"
|
||||||
device: "pi4-ezconnect"
|
device: "pi4-ezconnect"
|
||||||
rig: "signal-b"
|
- targets: ["10.0.58.113:5100"]
|
||||||
- targets: ["pirelay.iamworkin.lan"]
|
|
||||||
labels:
|
labels:
|
||||||
instance: "pirelay"
|
instance: "pirelay"
|
||||||
service: "signalcontrol"
|
service: "pimanager"
|
||||||
vlan: "home"
|
vlan: "home"
|
||||||
device: "pi3-ks0212"
|
device: "pi3-ks0212"
|
||||||
rig: "signal-a"
|
|
||||||
|
|
||||||
# Epson ET-3750 EcoTank Printer SNMP
|
# Epson ET-3750 EcoTank Printer SNMP
|
||||||
- job_name: "snmp-printer"
|
- job_name: "snmp-printer"
|
||||||
@@ -285,14 +280,13 @@ data:
|
|||||||
printer_model: "NuPrint 210"
|
printer_model: "NuPrint 210"
|
||||||
|
|
||||||
# Print.Web health (Blazor app on edge2:5200)
|
# Print.Web health (Blazor app on edge2:5200)
|
||||||
# Target `/health` (anonymous) — root path requires API key auth and returns 401.
|
|
||||||
- job_name: "probe-printweb"
|
- job_name: "probe-printweb"
|
||||||
metrics_path: /probe
|
metrics_path: /probe
|
||||||
params:
|
params:
|
||||||
module: [http_2xx]
|
module: [http_2xx]
|
||||||
scrape_interval: 30s
|
scrape_interval: 30s
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["http://10.0.57.16:5200/health"]
|
- targets: ["http://10.0.57.16:5200/"]
|
||||||
labels:
|
labels:
|
||||||
instance: "print-web"
|
instance: "print-web"
|
||||||
service: "print-web"
|
service: "print-web"
|
||||||
@@ -484,33 +478,24 @@ data:
|
|||||||
- "https://gitea.iamworkin.lan/"
|
- "https://gitea.iamworkin.lan/"
|
||||||
- "https://argocd.iamworkin.lan/"
|
- "https://argocd.iamworkin.lan/"
|
||||||
- "https://intranet.iamworkin.lan/"
|
- "https://intranet.iamworkin.lan/"
|
||||||
- "https://signage.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200
|
- "https://signage.iamworkin.lan/"
|
||||||
- "https://kiosk.iamworkin.lan/"
|
- "https://kiosk.iamworkin.lan/"
|
||||||
- "https://media.iamworkin.lan/healthz" # root auth-gated by OIDC; /healthz anonymous 200
|
- "https://media.iamworkin.lan/"
|
||||||
- "https://mysql.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200
|
- "https://mysql.iamworkin.lan/"
|
||||||
- "https://php.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200
|
- "https://php.iamworkin.lan/"
|
||||||
- "https://zabbix.iamworkin.lan/"
|
- "https://zabbix.iamworkin.lan/"
|
||||||
- "https://desktop.iamworkin.lan/"
|
- "https://desktop.iamworkin.lan/"
|
||||||
- "https://print.iamworkin.lan/healthz" # root 401 behind API key auth; /healthz anonymous 200
|
- "https://print.iamworkin.lan/"
|
||||||
- "https://dns.iamworkin.lan/healthz" # root auth-gated by OIDC; /healthz anonymous 200
|
- "https://dns.iamworkin.lan/"
|
||||||
- "https://signalcontrol.iamworkin.lan/health" # FlowerCore.SignalControl Pi control plane
|
- "https://chat.iamworkin.lan/"
|
||||||
- "https://flowercore.iamworkin.lan/healthz" # FlowerCore landing
|
- "https://dist.iamworkin.lan/"
|
||||||
- "https://replay.iamworkin.lan/healthz" # FlowerCore.Signage replay surface
|
- "https://dms.iamworkin.lan/"
|
||||||
- "https://worldbuilder.iamworkin.lan/healthz" # FlowerCore.WorldBuilder
|
|
||||||
- "https://updates.iamworkin.lan/api/v1/manifests/_schema" # UpdateCenter plural LAN alias
|
|
||||||
- "https://updatecenter-internal.iamworkin.lan/api/v1/manifests/_schema" # internal UC schema route
|
|
||||||
- "https://chat.iamworkin.lan/healthz" # OIDC staged; keep blackbox off root before enforcement flips
|
|
||||||
- "https://dist.iamworkin.lan/healthz" # root/admin auth-gated by OIDC; /healthz anonymous 200
|
|
||||||
- "https://dms.iamworkin.lan/healthz" # future OIDC posture; health route is already anonymous/live
|
|
||||||
- "https://menuboard.iamworkin.lan/"
|
- "https://menuboard.iamworkin.lan/"
|
||||||
- "https://messageboard.iamworkin.lan/"
|
- "https://messageboard.iamworkin.lan/"
|
||||||
- "https://presentations.iamworkin.lan/"
|
- "https://presentations.iamworkin.lan/"
|
||||||
- "https://retail.iamworkin.lan/"
|
- "https://retail.iamworkin.lan/"
|
||||||
- "https://ttsreader.iamworkin.lan/"
|
- "https://ttsreader.iamworkin.lan/"
|
||||||
# Explicit healthcheck paths
|
# Explicit healthcheck paths
|
||||||
- "https://library.iamworkin.lan/health"
|
|
||||||
- "https://aistation.iamworkin.lan/healthz"
|
|
||||||
- "https://knowledge.iamworkin.lan/healthz"
|
|
||||||
- "https://fc-llm-bridge.iamworkin.lan/healthz"
|
- "https://fc-llm-bridge.iamworkin.lan/healthz"
|
||||||
- "https://acme.iamworkin.lan/health"
|
- "https://acme.iamworkin.lan/health"
|
||||||
# NOTE: services intentionally NOT in this probe surface
|
# NOTE: services intentionally NOT in this probe surface
|
||||||
@@ -744,7 +729,7 @@ data:
|
|||||||
expr: |
|
expr: |
|
||||||
kube_deployment_status_replicas_ready{
|
kube_deployment_status_replicas_ready{
|
||||||
namespace="github-runner",
|
namespace="github-runner",
|
||||||
deployment=~"github-runner(|-(sharedpos|puppet|signage|dms|telephony|print-web|chat|mysql|kiosk-linux))"
|
deployment=~"github-runner(|-.+)"
|
||||||
} == 0
|
} == 0
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
@@ -922,13 +907,12 @@ data:
|
|||||||
# of idle and SNMP times out, so 5m for: would page nightly. A
|
# of idle and SNMP times out, so 5m for: would page nightly. A
|
||||||
# genuine printer outage (jam, disconnected) lasts well over 30m.
|
# genuine printer outage (jam, disconnected) lasts well over 30m.
|
||||||
- alert: EpsonPrinterDown
|
- alert: EpsonPrinterDown
|
||||||
expr: (max_over_time(up{job="snmp-printer"}[35m]) == bool 0) == 1 and (hour() >= 13 or hour() < 1)
|
expr: up{job="snmp-printer"} == 0
|
||||||
for: 30m
|
for: 30m
|
||||||
labels:
|
labels:
|
||||||
severity: info
|
severity: warning
|
||||||
alert_channel: irc
|
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Epson ET-3750 SNMP unreachable during waking hours (30m)"
|
summary: "Epson ET-3750 SNMP unreachable for >30m (likely actual fault, not sleep)"
|
||||||
|
|
||||||
- alert: SynologyDiskLow
|
- alert: SynologyDiskLow
|
||||||
expr: hrStorageUsed{job="snmp-nas"} / hrStorageSize{job="snmp-nas"} * 100 > 85
|
expr: hrStorageUsed{job="snmp-nas"} / hrStorageSize{job="snmp-nas"} * 100 > 85
|
||||||
@@ -1035,12 +1019,7 @@ data:
|
|||||||
- name: kubernetes-state
|
- name: kubernetes-state
|
||||||
rules:
|
rules:
|
||||||
- alert: KubeContainerRestartingFrequently
|
- alert: KubeContainerRestartingFrequently
|
||||||
# Exclude github-runner: ephemeral runners register, run one job,
|
expr: increase(kube_pod_container_status_restarts_total[1h]) > 5
|
||||||
# exit cleanly, and restart by design. Also require kube_pod_info so
|
|
||||||
# deleted rollout pods do not keep firing from retained restart series.
|
|
||||||
expr: |
|
|
||||||
increase(kube_pod_container_status_restarts_total{namespace!="github-runner"}[1h]) > 5
|
|
||||||
and on(namespace, pod) kube_pod_info
|
|
||||||
for: 15m
|
for: 15m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
@@ -1049,12 +1028,7 @@ data:
|
|||||||
description: "Container {{ $labels.container }} in pod {{ $labels.namespace }}/{{ $labels.pod }} has restarted {{ $value | printf \"%.0f\" }} times in the last hour. Check 'kubectl describe pod' + last-state termination reason."
|
description: "Container {{ $labels.container }} in pod {{ $labels.namespace }}/{{ $labels.pod }} has restarted {{ $value | printf \"%.0f\" }} times in the last hour. Check 'kubectl describe pod' + last-state termination reason."
|
||||||
|
|
||||||
- alert: KubeContainerCrashLooping
|
- alert: KubeContainerCrashLooping
|
||||||
# Same github-runner/delete-retention exclusions as the hourly
|
expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
|
||||||
# restart rule above; real runner failures are covered by the
|
|
||||||
# dedicated LinuxRunnerOffline/MacMiniRunnerOffline alerts.
|
|
||||||
expr: |
|
|
||||||
increase(kube_pod_container_status_restarts_total{namespace!="github-runner"}[15m]) > 3
|
|
||||||
and on(namespace, pod) kube_pod_info
|
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: critical
|
severity: critical
|
||||||
@@ -1082,10 +1056,7 @@ data:
|
|||||||
description: "Pod can't pull image. Check the image ref (often a stale tag or unreachable registry) and clean up if it's an orphan."
|
description: "Pod can't pull image. Check the image ref (often a stale tag or unreachable registry) and clean up if it's an orphan."
|
||||||
|
|
||||||
- alert: KubeDeploymentReplicasMismatch
|
- alert: KubeDeploymentReplicasMismatch
|
||||||
# github-runner has explicit runner-offline alerts; the generic
|
expr: kube_deployment_spec_replicas != kube_deployment_status_replicas_available
|
||||||
# replica-mismatch rule should not page on intentionally ephemeral
|
|
||||||
# 0/1 runner churn between CI jobs.
|
|
||||||
expr: kube_deployment_spec_replicas{namespace!="github-runner"} != kube_deployment_status_replicas_available{namespace!="github-runner"}
|
|
||||||
for: 15m
|
for: 15m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
@@ -1302,55 +1273,24 @@ metadata:
|
|||||||
data:
|
data:
|
||||||
notify.py: |
|
notify.py: |
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""HTTP->IRC alert relay with thermal-printer DIGEST forwarding.
|
"""HTTP->IRC alert relay with thermal printer forwarding for Grafana webhooks.
|
||||||
|
Listens on :9119, posts to #alerts on UnrealIRCd via raw IRC protocol.
|
||||||
Listens on :9119, posts to #alerts on UnrealIRCd, forwards to Print.Web
|
Alerts tagged alert_channel=thermal_print also POST to Print.Web /api/print/alert.
|
||||||
/api/print/alert. Thermal printing is BATCHED into hourly digests by
|
|
||||||
default so the printer no longer spam-fires per Grafana webhook.
|
|
||||||
|
|
||||||
Routing (per Grafana webhook alert):
|
|
||||||
- IRC: always per-event (operator likes the stream)
|
|
||||||
- Thermal printer:
|
|
||||||
* severity in {critical,disaster,page} OR
|
|
||||||
label alert_channel=thermal_print_immediate -> print NOW
|
|
||||||
* label alert_channel=thermal_print -> enqueue into hourly digest
|
|
||||||
* everything else -> IRC only
|
|
||||||
- RESOLVED webhooks remove the alert from the digest buffer
|
|
||||||
|
|
||||||
Env vars (defaults preserve old behavior on first deploy):
|
|
||||||
THERMAL_PRINT_ENABLED default "true" - master kill switch
|
|
||||||
BATCH_INTERVAL_MIN default "60" - minutes between digest prints
|
|
||||||
BATCH_MAX_PENDING default "50" - force-flush threshold
|
|
||||||
|
|
||||||
HTTP surface:
|
|
||||||
POST / - Grafana webhook entry
|
|
||||||
POST /flush - manual digest flush (idempotent)
|
|
||||||
GET / - status + config + buffer depth + stats
|
|
||||||
"""
|
"""
|
||||||
import json, os, socket, sys, threading, time
|
import json, socket, sys, time
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
from urllib.error import URLError
|
||||||
|
|
||||||
THERMAL_PRINT_ENABLED = os.environ.get("THERMAL_PRINT_ENABLED", "true").lower() == "true"
|
IRC_HOST = "unrealircd.irc.svc" # short name: CoreDNS ndots:5 + iamworkin.lan template hijacks full .cluster.local (see memory)
|
||||||
BATCH_INTERVAL_MIN = int(os.environ.get("BATCH_INTERVAL_MIN", "60"))
|
IRC_PORT = 6667
|
||||||
BATCH_MAX_PENDING = int(os.environ.get("BATCH_MAX_PENDING", "50"))
|
IRC_NICK = "grafana-bot"
|
||||||
|
IRC_CHANNEL = "#alerts"
|
||||||
IRC_HOST = os.environ.get("IRC_HOST", "unrealircd.irc.svc")
|
PRINT_WEB_URL = "http://10.0.57.16:5200/api/print/alert"
|
||||||
IRC_PORT = int(os.environ.get("IRC_PORT", "6667"))
|
PRINT_ENABLED = True
|
||||||
IRC_NICK = os.environ.get("IRC_NICK", "grafana-bot")
|
|
||||||
IRC_CHANNEL = os.environ.get("IRC_CHANNEL", "#alerts")
|
|
||||||
PRINT_WEB_URL = os.environ.get("PRINT_WEB_URL", "http://10.0.57.16:5200/api/print/alert")
|
|
||||||
|
|
||||||
_buffer_lock = threading.Lock()
|
|
||||||
_buffer = {} # fingerprint -> {"alert": dict, "first_seen": float, "last_seen": float}
|
|
||||||
_last_flush_time = time.time()
|
|
||||||
_stats = {"webhooks_received": 0, "irc_sent": 0, "print_immediate": 0,
|
|
||||||
"digest_flushed": 0, "buffer_dedup": 0, "buffer_added": 0,
|
|
||||||
"buffer_resolved": 0, "started_at": time.time()}
|
|
||||||
|
|
||||||
def send_irc(message):
|
def send_irc(message):
|
||||||
|
"""Connect, handle PING, join, send, quit."""
|
||||||
try:
|
try:
|
||||||
sock = socket.create_connection((IRC_HOST, IRC_PORT), timeout=15)
|
sock = socket.create_connection((IRC_HOST, IRC_PORT), timeout=15)
|
||||||
sock.sendall(f"NICK {IRC_NICK}\r\n".encode())
|
sock.sendall(f"NICK {IRC_NICK}\r\n".encode())
|
||||||
@@ -1383,137 +1323,52 @@ data:
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
sock.sendall(b"QUIT :alert delivered\r\n")
|
sock.sendall(b"QUIT :alert delivered\r\n")
|
||||||
sock.close()
|
sock.close()
|
||||||
_stats["irc_sent"] += 1
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[irc-notify] IRC send failed: {e}", file=sys.stderr)
|
print(f"[irc-notify] IRC send failed: {e}", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def post_thermal(payload, kind):
|
def send_thermal_print(alert):
|
||||||
if not THERMAL_PRINT_ENABLED:
|
if not PRINT_ENABLED: return
|
||||||
print(f"[irc-notify] thermal disabled; skip {kind} ({payload.get('title','?')[:40]})", file=sys.stderr)
|
labels = alert.get("labels", {})
|
||||||
return False
|
annotations = alert.get("annotations", {})
|
||||||
|
status = alert.get("status", "firing").upper()
|
||||||
|
summary = annotations.get("summary", "")
|
||||||
|
description = annotations.get("description", "")
|
||||||
|
runbook = annotations.get("runbook", "")
|
||||||
|
# Build a useful message: summary + description + runbook steps
|
||||||
|
parts = []
|
||||||
|
if summary: parts.append(summary)
|
||||||
|
if description and description != summary: parts.append(description)
|
||||||
|
if runbook: parts.append("STEPS: " + runbook)
|
||||||
|
message = " | ".join(parts) if parts else labels.get("alertname", "Unknown alert")
|
||||||
|
payload = {
|
||||||
|
"title": labels.get("alertname", "Unknown"),
|
||||||
|
"severity": labels.get("severity", "warning").capitalize(),
|
||||||
|
"host": labels.get("instance", labels.get("host", "unknown")),
|
||||||
|
"message": message,
|
||||||
|
"eventId": alert.get("fingerprint", ""),
|
||||||
|
"source": "Grafana",
|
||||||
|
"status": "RESOLVED" if status == "RESOLVED" else "PROBLEM",
|
||||||
|
"acknowledged": False
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
req = Request(PRINT_WEB_URL, data=json.dumps(payload).encode("utf-8"),
|
req = Request(PRINT_WEB_URL, data=json.dumps(payload).encode("utf-8"),
|
||||||
headers={"Content-Type": "application/json"}, method="POST")
|
headers={"Content-Type": "application/json"}, method="POST")
|
||||||
resp = urlopen(req, timeout=10)
|
resp = urlopen(req, timeout=10)
|
||||||
if kind == "immediate": _stats["print_immediate"] += 1
|
print(f"[irc-notify] Thermal print sent: {resp.read().decode()}", file=sys.stderr)
|
||||||
print(f"[irc-notify] thermal {kind} sent: {payload.get('title','?')[:50]}", file=sys.stderr)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[irc-notify] thermal {kind} failed: {e}", file=sys.stderr)
|
print(f"[irc-notify] Thermal print failed: {e}", file=sys.stderr)
|
||||||
return False
|
|
||||||
|
|
||||||
def fingerprint_of(alert):
|
def should_print(alert):
|
||||||
fp = alert.get("fingerprint", "")
|
|
||||||
if fp: return fp
|
|
||||||
labels = alert.get("labels", {})
|
labels = alert.get("labels", {})
|
||||||
target = labels.get("pod") or labels.get("instance") or labels.get("deployment") or labels.get("statefulset") or labels.get("namespace") or ""
|
if labels.get("alert_channel") == "thermal_print": return True
|
||||||
return f"{labels.get('alertname','?')}/{labels.get('namespace','')}/{target}"
|
if labels.get("severity", "").lower() in ("critical", "disaster"): return True
|
||||||
|
if alert.get("status", "").upper() == "RESOLVED": return False
|
||||||
def is_critical(alert):
|
|
||||||
return alert.get("labels", {}).get("severity", "").lower() in ("critical", "disaster", "page")
|
|
||||||
|
|
||||||
def is_immediate_label(alert):
|
|
||||||
return alert.get("labels", {}).get("alert_channel") == "thermal_print_immediate"
|
|
||||||
|
|
||||||
def is_batched_label(alert):
|
|
||||||
return alert.get("labels", {}).get("alert_channel") == "thermal_print"
|
|
||||||
|
|
||||||
def add_to_digest(alert):
|
|
||||||
"""Add an alert to the digest buffer. Returns True if the buffer GREW
|
|
||||||
(new fingerprint), False if it was a dedup, resolution, or no-op.
|
|
||||||
"""
|
|
||||||
if not THERMAL_PRINT_ENABLED: return False
|
|
||||||
fp = fingerprint_of(alert)
|
|
||||||
status = alert.get("status", "firing").lower()
|
|
||||||
with _buffer_lock:
|
|
||||||
if status == "resolved":
|
|
||||||
if fp in _buffer:
|
|
||||||
del _buffer[fp]
|
|
||||||
_stats["buffer_resolved"] += 1
|
|
||||||
return False
|
return False
|
||||||
if fp in _buffer:
|
|
||||||
_buffer[fp]["last_seen"] = time.time()
|
|
||||||
_buffer[fp]["alert"] = alert
|
|
||||||
_stats["buffer_dedup"] += 1
|
|
||||||
return False
|
|
||||||
_buffer[fp] = {"alert": alert, "first_seen": time.time(), "last_seen": time.time()}
|
|
||||||
_stats["buffer_added"] += 1
|
|
||||||
return True
|
|
||||||
|
|
||||||
def build_digest_payload():
|
|
||||||
with _buffer_lock:
|
|
||||||
items = list(_buffer.values())
|
|
||||||
if not items: return None
|
|
||||||
by_name = defaultdict(list)
|
|
||||||
for item in items:
|
|
||||||
labels = item["alert"].get("labels", {})
|
|
||||||
by_name[labels.get("alertname", "Unknown")].append(item)
|
|
||||||
lines = []
|
|
||||||
for name, group in sorted(by_name.items()):
|
|
||||||
targets = []
|
|
||||||
for it in group[:5]:
|
|
||||||
labels = it["alert"].get("labels", {})
|
|
||||||
t = (labels.get("pod") or labels.get("instance") or labels.get("deployment")
|
|
||||||
or labels.get("statefulset") or labels.get("namespace") or "?")
|
|
||||||
targets.append(t)
|
|
||||||
more = f" (+{len(group)-5})" if len(group) > 5 else ""
|
|
||||||
sevs = sorted({it["alert"].get("labels", {}).get("severity", "warning") for it in group})
|
|
||||||
lines.append(f"[{'/'.join(sevs)}] {name} x{len(group)}: {', '.join(targets)}{more}")
|
|
||||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
||||||
title = f"Alert digest: {len(items)} firing"
|
|
||||||
body = "\n".join([
|
|
||||||
f"=== {title} ===",
|
|
||||||
f"as of {now}",
|
|
||||||
"",
|
|
||||||
*lines,
|
|
||||||
"",
|
|
||||||
"Stream: #alerts (IRC) | Triage: grafana-noc1.iamworkin.lan",
|
|
||||||
"Force-flush: POST irc-notify.monitoring.svc:9119/flush",
|
|
||||||
])
|
|
||||||
return {"title": title, "severity": "Warning", "host": "monitoring",
|
|
||||||
"message": body, "eventId": f"digest-{int(time.time())}",
|
|
||||||
"source": "Grafana digest", "status": "PROBLEM", "acknowledged": False}
|
|
||||||
|
|
||||||
def flush_digest():
|
|
||||||
payload = build_digest_payload()
|
|
||||||
if payload is None:
|
|
||||||
print("[irc-notify] flush: buffer empty, no digest sent", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
sent = post_thermal(payload, "digest")
|
|
||||||
with _buffer_lock:
|
|
||||||
_buffer.clear()
|
|
||||||
if sent: _stats["digest_flushed"] += 1
|
|
||||||
return sent
|
|
||||||
|
|
||||||
def digest_loop():
|
|
||||||
global _last_flush_time
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
now = time.time()
|
|
||||||
elapsed = now - _last_flush_time
|
|
||||||
if elapsed >= BATCH_INTERVAL_MIN * 60:
|
|
||||||
print(f"[irc-notify] digest tick: interval reached ({BATCH_INTERVAL_MIN}m); buffer={len(_buffer)}", file=sys.stderr)
|
|
||||||
flush_digest()
|
|
||||||
_last_flush_time = now
|
|
||||||
elif len(_buffer) >= BATCH_MAX_PENDING:
|
|
||||||
print(f"[irc-notify] digest tick: buffer full ({len(_buffer)}); force flush", file=sys.stderr)
|
|
||||||
flush_digest()
|
|
||||||
_last_flush_time = now
|
|
||||||
time.sleep(15)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[irc-notify] digest loop error: {e}", file=sys.stderr)
|
|
||||||
time.sleep(60)
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if self.path == "/flush":
|
|
||||||
ok = flush_digest()
|
|
||||||
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
|
|
||||||
self.wfile.write(json.dumps({"flushed": ok, "buffer_after": len(_buffer)}).encode())
|
|
||||||
return
|
|
||||||
_stats["webhooks_received"] += 1
|
|
||||||
length = int(self.headers.get("Content-Length", 0))
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
body = json.loads(self.rfile.read(length)) if length else {}
|
body = json.loads(self.rfile.read(length)) if length else {}
|
||||||
for alert in body.get("alerts", []):
|
for alert in body.get("alerts", []):
|
||||||
@@ -1528,56 +1383,22 @@ data:
|
|||||||
msg = f"{icon}{sev_tag} {name}: {summary}"
|
msg = f"{icon}{sev_tag} {name}: {summary}"
|
||||||
if desc: msg += f"\n {desc}"
|
if desc: msg += f"\n {desc}"
|
||||||
send_irc(msg)
|
send_irc(msg)
|
||||||
# Thermal routing — EVERYTHING (including criticals) goes into
|
if should_print(alert): send_thermal_print(alert)
|
||||||
# the hourly digest. Only the explicit `alert_channel=thermal_print_immediate`
|
self.send_response(200)
|
||||||
# label bypasses, and even that flushes-the-current-digest rather
|
self.send_header("Content-Type", "application/json")
|
||||||
# than printing a standalone job, so the same fingerprint can't
|
self.end_headers()
|
||||||
# spam the printer per webhook cycle.
|
|
||||||
if status == "RESOLVED":
|
|
||||||
add_to_digest(alert) # removes from buffer
|
|
||||||
continue
|
|
||||||
if is_immediate_label(alert):
|
|
||||||
# Explicit opt-in for "paper this NOW" — first arrival of a
|
|
||||||
# new fingerprint triggers an immediate digest flush; repeat
|
|
||||||
# webhooks for the same fingerprint dedupe in the buffer
|
|
||||||
# until the next interval or until the alert resolves.
|
|
||||||
new_in_buffer = add_to_digest(alert)
|
|
||||||
if new_in_buffer:
|
|
||||||
global _last_flush_time
|
|
||||||
flush_digest()
|
|
||||||
_last_flush_time = time.time()
|
|
||||||
elif is_critical(alert) or is_batched_label(alert):
|
|
||||||
add_to_digest(alert)
|
|
||||||
# else: IRC-only (warnings without thermal_print label)
|
|
||||||
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
|
|
||||||
self.wfile.write(b'{"status":"ok"}')
|
self.wfile.write(b'{"status":"ok"}')
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
|
self.send_response(200)
|
||||||
with _buffer_lock:
|
self.send_header("Content-Type", "application/json")
|
||||||
alertnames = sorted({it["alert"].get("labels", {}).get("alertname", "?") for it in _buffer.values()})
|
self.end_headers()
|
||||||
depth = len(_buffer)
|
self.wfile.write(json.dumps({"service":"irc-notify","thermal_print":PRINT_ENABLED}).encode())
|
||||||
info = {
|
|
||||||
"service": "irc-notify",
|
|
||||||
"config": {"thermal_print_enabled": THERMAL_PRINT_ENABLED,
|
|
||||||
"batch_interval_min": BATCH_INTERVAL_MIN,
|
|
||||||
"batch_max_pending": BATCH_MAX_PENDING,
|
|
||||||
"irc_target": f"{IRC_HOST}:{IRC_PORT} {IRC_CHANNEL}",
|
|
||||||
"print_web_url": PRINT_WEB_URL},
|
|
||||||
"buffer": {"depth": depth, "alertnames": alertnames,
|
|
||||||
"seconds_since_last_flush": int(time.time() - _last_flush_time),
|
|
||||||
"seconds_until_next_flush": max(0, int(BATCH_INTERVAL_MIN*60 - (time.time() - _last_flush_time)))},
|
|
||||||
"stats": _stats,
|
|
||||||
}
|
|
||||||
self.wfile.write(json.dumps(info, indent=2).encode())
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
print(f"[irc-notify] {args[0]}", file=sys.stderr)
|
print(f"[irc-notify] {args[0]}", file=sys.stderr)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
threading.Thread(target=digest_loop, daemon=True).start()
|
|
||||||
server = HTTPServer(("0.0.0.0", 9119), Handler)
|
server = HTTPServer(("0.0.0.0", 9119), Handler)
|
||||||
print(f"[irc-notify] :9119 -> IRC {IRC_HOST}:{IRC_PORT} {IRC_CHANNEL} | thermal={'ON' if THERMAL_PRINT_ENABLED else 'OFF'} | digest={BATCH_INTERVAL_MIN}m max={BATCH_MAX_PENDING}", file=sys.stderr)
|
print(f"IRC alert relay :9119 -> {IRC_HOST}:{IRC_PORT} {IRC_CHANNEL} (thermal: {PRINT_ENABLED})")
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -3688,7 +3509,7 @@ data:
|
|||||||
- refId: A
|
- refId: A
|
||||||
relativeTimeRange: {from: 300, to: 0}
|
relativeTimeRange: {from: 300, to: 0}
|
||||||
datasourceUid: prometheus
|
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}
|
model: {expr: 'kube_deployment_status_replicas_ready{namespace="github-runner",deployment=~"github-runner(|-.+)"} == 0', instant: true, refId: A}
|
||||||
- refId: B
|
- refId: B
|
||||||
relativeTimeRange: {from: 300, to: 0}
|
relativeTimeRange: {from: 300, to: 0}
|
||||||
datasourceUid: __expr__
|
datasourceUid: __expr__
|
||||||
|
|||||||
@@ -24,16 +24,7 @@
|
|||||||
# (10.0.57.16:5200), public internet 80/443 (excluding RFC1918), and
|
# (10.0.57.16:5200), public internet 80/443 (excluding RFC1918), and
|
||||||
# fc-signage:5190 for the signage AAT lane.
|
# fc-signage:5190 for the signage AAT lane.
|
||||||
# - Ingress: Traefik (4444 + 8089 ACME-solver-style), intra-pod,
|
# - Ingress: Traefik (4444 + 8089 ACME-solver-style), intra-pod,
|
||||||
# telephony / gitea / fc-system / fc-signage / github-runner namespaces
|
# telephony / gitea / fc-system / fc-signage namespaces on 4444.
|
||||||
# on 4444.
|
|
||||||
#
|
|
||||||
# 2026-05-25: added github-runner ingress on 4444 so CI jobs running in
|
|
||||||
# self-hosted runner pods (e.g. FlowerCore.Print.Web `help-screenshots`)
|
|
||||||
# can reach the grid. Without this allow, the session POST to
|
|
||||||
# `selenium-hub.selenium.svc.cluster.local:4444` was DNAT'd to the hub
|
|
||||||
# pod IP and then dropped at the Calico ingress hook — Selenium UI showed
|
|
||||||
# 0/4 sessions while the .NET HTTP client timed out at 60s. Same family
|
|
||||||
# as `feedback_netpol_dnat_backend_port`, wrong-source-namespace flavor.
|
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
@@ -212,13 +203,6 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- port: 4444
|
- port: 4444
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: github-runner
|
|
||||||
ports:
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
podSelector: {}
|
podSelector: {}
|
||||||
policyTypes:
|
policyTypes:
|
||||||
- Ingress
|
- Ingress
|
||||||
|
|||||||
@@ -1,427 +0,0 @@
|
|||||||
# Selenium Grid 4 — RKE2 deployment
|
|
||||||
#
|
|
||||||
# Hub + chrome + firefox + edge browser nodes serving fleet-wide AAT runs from
|
|
||||||
# the GitHub Actions self-hosted runners. ArgoCD owns this namespace from
|
|
||||||
# 2026-05-25 (`infra-selenium` Application; previously these resources were
|
|
||||||
# orphan kubectl-applied since 2026-03-15).
|
|
||||||
#
|
|
||||||
# Endpoints:
|
|
||||||
# - Internal cluster: http://selenium-hub.selenium.svc.cluster.local:4444
|
|
||||||
# - LAN LoadBalancer (MetalLB): http://10.0.56.208:4444
|
|
||||||
# - Traefik public: https://selenium.iamworkin.lan
|
|
||||||
#
|
|
||||||
# Browser maxSessions:
|
|
||||||
# - chrome 2 (bumped from 1 on 2026-05-25 morning-routine — AAT-heavy
|
|
||||||
# Print.Web help-screenshots was the global bottleneck;
|
|
||||||
# see commit history for ops/runner-replica-rightsize)
|
|
||||||
# - firefox 1
|
|
||||||
# - edge 1
|
|
||||||
#
|
|
||||||
# Screenshots + video recording write to NFS via the chrome video sidecar.
|
|
||||||
# See: CLAUDE.md "Selenium Grid & Visual AAT Testing" + bluejay-infra ADR notes.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-hub
|
|
||||||
app.kubernetes.io/name: selenium-hub
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
name: selenium-hub
|
|
||||||
namespace: selenium
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: web
|
|
||||||
port: 4444
|
|
||||||
targetPort: 4444
|
|
||||||
- name: publish
|
|
||||||
port: 4442
|
|
||||||
targetPort: 4442
|
|
||||||
- name: subscribe
|
|
||||||
port: 4443
|
|
||||||
targetPort: 4443
|
|
||||||
selector:
|
|
||||||
app: selenium-hub
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
metallb.io/ip-allocated-from-pool: bluejay-pool
|
|
||||||
metallb.universe.tf/loadBalancerIPs: 10.0.56.208
|
|
||||||
labels:
|
|
||||||
app: selenium-hub
|
|
||||||
component: external-access
|
|
||||||
name: selenium-hub-external
|
|
||||||
namespace: selenium
|
|
||||||
spec:
|
|
||||||
clusterIP: 10.43.90.147
|
|
||||||
clusterIPs:
|
|
||||||
- 10.43.90.147
|
|
||||||
externalTrafficPolicy: Local
|
|
||||||
healthCheckNodePort: 32213
|
|
||||||
ports:
|
|
||||||
- name: web
|
|
||||||
nodePort: 32411
|
|
||||||
port: 4444
|
|
||||||
targetPort: 4444
|
|
||||||
- name: publish
|
|
||||||
nodePort: 32068
|
|
||||||
port: 4442
|
|
||||||
targetPort: 4442
|
|
||||||
- name: subscribe
|
|
||||||
nodePort: 31000
|
|
||||||
port: 4443
|
|
||||||
targetPort: 4443
|
|
||||||
selector:
|
|
||||||
app: selenium-hub
|
|
||||||
type: LoadBalancer
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-hub
|
|
||||||
app.kubernetes.io/name: selenium-hub
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
name: selenium-hub
|
|
||||||
namespace: selenium
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: selenium-hub
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-hub
|
|
||||||
app.kubernetes.io/name: selenium-hub
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- env:
|
|
||||||
- name: SE_NODE_SESSION_TIMEOUT
|
|
||||||
value: '300'
|
|
||||||
- name: SE_SESSION_REQUEST_TIMEOUT
|
|
||||||
value: '300'
|
|
||||||
- name: SE_SESSION_RETRY_INTERVAL
|
|
||||||
value: '5'
|
|
||||||
- name: JAVA_OPTS
|
|
||||||
value: -Xmx512m
|
|
||||||
image: selenium/hub:4.27.0
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /wd/hub/status
|
|
||||||
port: 4444
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 15
|
|
||||||
timeoutSeconds: 5
|
|
||||||
name: selenium-hub
|
|
||||||
ports:
|
|
||||||
- containerPort: 4444
|
|
||||||
name: web
|
|
||||||
- containerPort: 4442
|
|
||||||
name: publish
|
|
||||||
- containerPort: 4443
|
|
||||||
name: subscribe
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /wd/hub/status
|
|
||||||
port: 4444
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 5
|
|
||||||
timeoutSeconds: 5
|
|
||||||
# Hub baseline working set ~766Mi on 2026-05-25 (75% of prior 1Gi
|
|
||||||
# limit). Bump to 1.5Gi / 1Gi to keep ~50% headroom; matches the
|
|
||||||
# stampede-buffer pattern documented for multus
|
|
||||||
# (feedback_k8s_cni_multus_sizing). CPU left alone — observed 54m
|
|
||||||
# against a 500m limit, no contention.
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 1536Mi
|
|
||||||
requests:
|
|
||||||
cpu: 250m
|
|
||||||
memory: 1Gi
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-node-chrome
|
|
||||||
app.kubernetes.io/name: selenium-node-chrome
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
name: selenium-node-chrome
|
|
||||||
namespace: selenium
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: selenium-node-chrome
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-node-chrome
|
|
||||||
app.kubernetes.io/name: selenium-node-chrome
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- env:
|
|
||||||
- name: SE_EVENT_BUS_HOST
|
|
||||||
value: selenium-hub
|
|
||||||
- name: SE_EVENT_BUS_PUBLISH_PORT
|
|
||||||
value: '4442'
|
|
||||||
- name: SE_EVENT_BUS_SUBSCRIBE_PORT
|
|
||||||
value: '4443'
|
|
||||||
- name: SE_NODE_MAX_SESSIONS
|
|
||||||
value: '2'
|
|
||||||
- name: SE_NODE_OVERRIDE_MAX_SESSIONS
|
|
||||||
value: 'false'
|
|
||||||
- name: SE_VNC_NO_PASSWORD
|
|
||||||
value: '1'
|
|
||||||
- name: SE_SCREEN_WIDTH
|
|
||||||
value: '1920'
|
|
||||||
- name: SE_SCREEN_HEIGHT
|
|
||||||
value: '1080'
|
|
||||||
- name: SE_NODE_SESSION_TIMEOUT
|
|
||||||
value: '300'
|
|
||||||
image: selenium/node-chrome:4.27.0
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /status
|
|
||||||
port: 5555
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 15
|
|
||||||
name: selenium-chrome
|
|
||||||
ports:
|
|
||||||
- containerPort: 5555
|
|
||||||
name: node
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /status
|
|
||||||
port: 5555
|
|
||||||
initialDelaySeconds: 15
|
|
||||||
periodSeconds: 5
|
|
||||||
# Chromium-based browser node. Bumped from 1Gi -> 2Gi (req 512Mi
|
|
||||||
# -> 1Gi) on 2026-05-25 — Edge had 51 OOMKills in 5d on the
|
|
||||||
# original 1Gi cap (~1 OOM every 2.4h), and Chrome at maxSessions=2
|
|
||||||
# was running 684Mi idle on the same cap. Matches the Firefox node's
|
|
||||||
# tested-stable 2Gi limit. CPU unchanged.
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 1Gi
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /dev/shm
|
|
||||||
name: dshm
|
|
||||||
- env:
|
|
||||||
- name: DISPLAY_CONTAINER_NAME
|
|
||||||
value: localhost
|
|
||||||
- name: SE_SCREEN_WIDTH
|
|
||||||
value: '1920'
|
|
||||||
- name: SE_SCREEN_HEIGHT
|
|
||||||
value: '1080'
|
|
||||||
- name: SE_VIDEO_FILE_NAME
|
|
||||||
value: auto
|
|
||||||
- name: SE_VIDEO_UPLOAD_ENABLED
|
|
||||||
value: 'false'
|
|
||||||
image: selenium/video:ffmpeg-7.1-20250101
|
|
||||||
name: video
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 768Mi
|
|
||||||
requests:
|
|
||||||
cpu: 250m
|
|
||||||
memory: 384Mi
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /videos
|
|
||||||
name: selenium-videos
|
|
||||||
volumes:
|
|
||||||
- emptyDir:
|
|
||||||
medium: Memory
|
|
||||||
sizeLimit: 2Gi
|
|
||||||
name: dshm
|
|
||||||
- emptyDir:
|
|
||||||
sizeLimit: 5Gi
|
|
||||||
name: selenium-videos
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-node-firefox
|
|
||||||
app.kubernetes.io/name: selenium-node-firefox
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
name: selenium-node-firefox
|
|
||||||
namespace: selenium
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: selenium-node-firefox
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-node-firefox
|
|
||||||
app.kubernetes.io/name: selenium-node-firefox
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- env:
|
|
||||||
- name: SE_EVENT_BUS_HOST
|
|
||||||
value: selenium-hub
|
|
||||||
- name: SE_EVENT_BUS_PUBLISH_PORT
|
|
||||||
value: '4442'
|
|
||||||
- name: SE_EVENT_BUS_SUBSCRIBE_PORT
|
|
||||||
value: '4443'
|
|
||||||
- name: SE_NODE_MAX_SESSIONS
|
|
||||||
value: '1'
|
|
||||||
- name: SE_NODE_OVERRIDE_MAX_SESSIONS
|
|
||||||
value: 'true'
|
|
||||||
- name: SE_VNC_NO_PASSWORD
|
|
||||||
value: '1'
|
|
||||||
- name: SE_START_VNC
|
|
||||||
value: 'false'
|
|
||||||
- name: SE_SCREEN_WIDTH
|
|
||||||
value: '1920'
|
|
||||||
- name: SE_SCREEN_HEIGHT
|
|
||||||
value: '1080'
|
|
||||||
- name: SE_NODE_SESSION_TIMEOUT
|
|
||||||
value: '300'
|
|
||||||
image: selenium/node-firefox:4.27.0
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 5
|
|
||||||
httpGet:
|
|
||||||
path: /status
|
|
||||||
port: 5555
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 15
|
|
||||||
timeoutSeconds: 5
|
|
||||||
name: selenium-firefox
|
|
||||||
ports:
|
|
||||||
- containerPort: 5555
|
|
||||||
name: node
|
|
||||||
readinessProbe:
|
|
||||||
failureThreshold: 5
|
|
||||||
httpGet:
|
|
||||||
path: /status
|
|
||||||
port: 5555
|
|
||||||
initialDelaySeconds: 15
|
|
||||||
periodSeconds: 5
|
|
||||||
timeoutSeconds: 5
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 1Gi
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /dev/shm
|
|
||||||
name: dshm
|
|
||||||
volumes:
|
|
||||||
- emptyDir:
|
|
||||||
medium: Memory
|
|
||||||
sizeLimit: 2Gi
|
|
||||||
name: dshm
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-node-edge
|
|
||||||
app.kubernetes.io/name: selenium-node-edge
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
name: selenium-node-edge
|
|
||||||
namespace: selenium
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: selenium-node-edge
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: selenium-node-edge
|
|
||||||
app.kubernetes.io/name: selenium-node-edge
|
|
||||||
app.kubernetes.io/part-of: selenium-grid
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- env:
|
|
||||||
- name: SE_EVENT_BUS_HOST
|
|
||||||
value: selenium-hub
|
|
||||||
- name: SE_EVENT_BUS_PUBLISH_PORT
|
|
||||||
value: '4442'
|
|
||||||
- name: SE_EVENT_BUS_SUBSCRIBE_PORT
|
|
||||||
value: '4443'
|
|
||||||
- name: SE_NODE_MAX_SESSIONS
|
|
||||||
value: '1'
|
|
||||||
- name: SE_NODE_OVERRIDE_MAX_SESSIONS
|
|
||||||
value: 'true'
|
|
||||||
- name: SE_VNC_NO_PASSWORD
|
|
||||||
value: '1'
|
|
||||||
- name: SE_SCREEN_WIDTH
|
|
||||||
value: '1920'
|
|
||||||
- name: SE_SCREEN_HEIGHT
|
|
||||||
value: '1080'
|
|
||||||
- name: SE_NODE_SESSION_TIMEOUT
|
|
||||||
value: '300'
|
|
||||||
image: selenium/node-edge:4.27.0
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /status
|
|
||||||
port: 5555
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 15
|
|
||||||
name: selenium-edge
|
|
||||||
ports:
|
|
||||||
- containerPort: 5555
|
|
||||||
name: node
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /status
|
|
||||||
port: 5555
|
|
||||||
initialDelaySeconds: 15
|
|
||||||
periodSeconds: 5
|
|
||||||
# Chromium-based browser node. Bumped from 1Gi -> 2Gi (req 512Mi
|
|
||||||
# -> 1Gi) on 2026-05-25 — Edge had 51 OOMKills in 5d on the
|
|
||||||
# original 1Gi cap (~1 OOM every 2.4h), and Chrome at maxSessions=2
|
|
||||||
# was running 684Mi idle on the same cap. Matches the Firefox node's
|
|
||||||
# tested-stable 2Gi limit. CPU unchanged.
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 1Gi
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /dev/shm
|
|
||||||
name: dshm
|
|
||||||
volumes:
|
|
||||||
- emptyDir:
|
|
||||||
medium: Memory
|
|
||||||
sizeLimit: 2Gi
|
|
||||||
name: dshm
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: selenium-hub
|
|
||||||
namespace: selenium
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- kind: Rule
|
|
||||||
match: Host(`selenium.iamworkin.lan`)
|
|
||||||
services:
|
|
||||||
- name: selenium-hub
|
|
||||||
port: 4444
|
|
||||||
tls:
|
|
||||||
secretName: selenium-tls
|
|
||||||
@@ -114,9 +114,6 @@ 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:
|
||||||
@@ -164,7 +161,6 @@ 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:
|
||||||
@@ -391,3 +387,4 @@ spec:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ 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"
|
||||||
@@ -95,7 +93,6 @@ 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"
|
||||||
@@ -257,26 +254,3 @@ 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).
|
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace BluejayInfraLint.Tests;
|
|
||||||
|
|
||||||
[Trait("Category", "Unit")]
|
|
||||||
public sealed class DivoomPiDeployArtifactTests
|
|
||||||
{
|
|
||||||
private static readonly string Root = FindRepoRoot();
|
|
||||||
private static readonly string DmRoot = Path.Combine(Root, "apps", "fc-divoom-dm-pi-device");
|
|
||||||
private static readonly string TvRoot = Path.Combine(Root, "apps", "fc-divoom-tv-pi");
|
|
||||||
|
|
||||||
public static TheoryData<string> DmRequiredArtifacts => new()
|
|
||||||
{
|
|
||||||
"README.md",
|
|
||||||
"hiera/edge2-divoom-dm-device.overlay.yaml",
|
|
||||||
"puppet/profile/pi/service/divoom_dm_device.pp",
|
|
||||||
"puppet/templates/divoom-device-registration.json.epp",
|
|
||||||
"puppet/templates/flowercore-divoom-dm-agent.service.epp",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static TheoryData<string> TvRequiredArtifacts => new()
|
|
||||||
{
|
|
||||||
"README.md",
|
|
||||||
"hiera/example-divoom-tv-pi.iamworkin.lan.yaml",
|
|
||||||
"puppet/profile/pi/service/divoom_tv.pp",
|
|
||||||
"systemd/flowercore-divoom-tv.service",
|
|
||||||
"systemd/flowercore-divoom-tv-hdmi.service",
|
|
||||||
"systemd/99-flowercore-divoom-tv-hdmi.rules",
|
|
||||||
"scripts/flowercore-divoom-tv-prelaunch.sh",
|
|
||||||
"scripts/flowercore-divoom-tv-launch.sh",
|
|
||||||
"scripts/flowercore-divoom-tv-hdmi-respond.sh",
|
|
||||||
};
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(DmRequiredArtifacts))]
|
|
||||||
public void DmDeviceArtifacts_ArePresent(string relativePath)
|
|
||||||
{
|
|
||||||
File.Exists(Path.Combine(DmRoot, relativePath.Replace('/', Path.DirectorySeparatorChar))).Should().BeTrue(relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(TvRequiredArtifacts))]
|
|
||||||
public void TvPiArtifacts_ArePresent(string relativePath)
|
|
||||||
{
|
|
||||||
File.Exists(Path.Combine(TvRoot, relativePath.Replace('/', Path.DirectorySeparatorChar))).Should().BeTrue(relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DmDeviceReadme_DeclaresPuppetSystemdNotKubernetes()
|
|
||||||
{
|
|
||||||
var readme = ReadDm("README.md");
|
|
||||||
|
|
||||||
readme.Should().Contain("not a Kubernetes application");
|
|
||||||
readme.Should().Contain("profile::pi::service::divoom");
|
|
||||||
readme.Should().Contain("no K8s surface");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DmHieraOverlay_PreservesExistingEdge2DivoomService()
|
|
||||||
{
|
|
||||||
var hiera = ReadDm("hiera/edge2-divoom-dm-device.overlay.yaml");
|
|
||||||
|
|
||||||
hiera.Should().Contain("fc-pimanager:");
|
|
||||||
hiera.Should().Contain("fc-divoom:");
|
|
||||||
hiera.Should().Contain("enabled: true");
|
|
||||||
hiera.Should().Contain("profile::pi::service::divoom_dm_device::service_enabled: false");
|
|
||||||
hiera.Should().Contain("profile::pi::service::divoom_dm_device::service_ensure: 'stopped'");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DmPuppetProfile_DefaultsToStoppedDisabledService()
|
|
||||||
{
|
|
||||||
var profile = ReadDm("puppet/profile/pi/service/divoom_dm_device.pp");
|
|
||||||
|
|
||||||
profile.Should().Contain("Boolean $service_enabled = false");
|
|
||||||
profile.Should().Contain("Enum['running', 'stopped'] $service_ensure = 'stopped'");
|
|
||||||
profile.Should().Contain("service { $service_name:");
|
|
||||||
profile.Should().Contain("ensure => $service_ensure");
|
|
||||||
profile.Should().Contain("enable => $service_enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DmPuppetProfile_DoesNotManageLiveDivoomWebUnit()
|
|
||||||
{
|
|
||||||
var profile = ReadDm("puppet/profile/pi/service/divoom_dm_device.pp");
|
|
||||||
|
|
||||||
profile.Should().NotContain("Service['flowercore-divoom.service']");
|
|
||||||
profile.Should().NotContain("service { 'flowercore-divoom.service'");
|
|
||||||
profile.Should().NotContain("notify => Service");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DmAgentUnit_IsSeparateAndGatedByExistingWrappers()
|
|
||||||
{
|
|
||||||
var unit = ReadDm("puppet/templates/flowercore-divoom-dm-agent.service.epp");
|
|
||||||
|
|
||||||
unit.Should().Contain("ConditionPathExists=<%= $divoom_install_dir %>/bt-link.sh");
|
|
||||||
unit.Should().Contain("ConditionPathExists=<%= $divoom_install_dir %>/bt-reset.sh");
|
|
||||||
unit.Should().Contain("ConditionPathExists=<%= $divoom_install_dir %>/audio-link.sh");
|
|
||||||
unit.Should().Contain("ExecStart=<%= $agent_binary_path %> --mode=Pi");
|
|
||||||
unit.Should().NotContain("flowercore-divoom.service");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DmRegistration_CarriesRenderProofAndSafetyPolicy()
|
|
||||||
{
|
|
||||||
var registration = ReadDm("puppet/templates/divoom-device-registration.json.epp");
|
|
||||||
|
|
||||||
registration.Should().Contain("\"candidateChannels\": <%= $bt_channels_json %>");
|
|
||||||
registration.Should().Contain("\"deviceInfoIsRenderProof\": false");
|
|
||||||
registration.Should().Contain("\"visibleRenderProofRequired\": <%= $visible_render_proof_required %>");
|
|
||||||
registration.Should().Contain("\"preserveExistingService\": \"flowercore-divoom.service\"");
|
|
||||||
registration.Should().Contain("\"doNotEnableFmRadio\": true");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TvService_UsesAvaloniaHdmiSafetyGates()
|
|
||||||
{
|
|
||||||
var unit = ReadTv("systemd/flowercore-divoom-tv.service");
|
|
||||||
|
|
||||||
unit.Should().Contain("ConditionPathExists=/opt/flowercore/divoom-tv/FlowerCore.Divoom.Tv");
|
|
||||||
unit.Should().Contain("Environment=XDG_RUNTIME_DIR=/run/fc-divoom-tv");
|
|
||||||
unit.Should().Contain("RuntimeDirectoryMode=0700");
|
|
||||||
unit.Should().Contain("ExecStartPre=/usr/local/bin/flowercore-divoom-tv-prelaunch.sh");
|
|
||||||
unit.Should().Contain("ExecStart=/usr/local/bin/flowercore-divoom-tv-launch.sh");
|
|
||||||
unit.Should().Contain("MemoryMax=2G");
|
|
||||||
unit.Should().Contain("PrivateTmp=true");
|
|
||||||
unit.Should().NotContain("/tmp");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TvLauncher_PrefersCageAndFallsBackToDirectLaunch()
|
|
||||||
{
|
|
||||||
var script = ReadTv("scripts/flowercore-divoom-tv-launch.sh");
|
|
||||||
|
|
||||||
script.Should().Contain("command -v cage");
|
|
||||||
script.Should().Contain("exec cage --");
|
|
||||||
script.Should().Contain("launching FlowerCore.Divoom.Tv directly");
|
|
||||||
script.Should().Contain("--target=hdmi");
|
|
||||||
script.Should().Contain("--presentation-mode=${PRESENTATION_MODE}");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TvHotplugRule_SettlesAndRestartsRenderer()
|
|
||||||
{
|
|
||||||
var rule = ReadTv("systemd/99-flowercore-divoom-tv-hdmi.rules");
|
|
||||||
var responder = ReadTv("scripts/flowercore-divoom-tv-hdmi-respond.sh");
|
|
||||||
|
|
||||||
rule.Should().Contain("KERNEL==\"card?-HDMI-A-?\"");
|
|
||||||
rule.Should().Contain("start flowercore-divoom-tv-hdmi.service");
|
|
||||||
responder.Should().Contain("sleep 2");
|
|
||||||
responder.Should().Contain("systemctl restart flowercore-divoom-tv.service");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TvPuppetProfile_InstallsCageAndStaticArtifacts()
|
|
||||||
{
|
|
||||||
var profile = ReadTv("puppet/profile/pi/service/divoom_tv.pp");
|
|
||||||
|
|
||||||
profile.Should().Contain("package { ['cage', 'libgbm1', 'libdrm2', 'libxkbcommon0', 'fonts-dejavu-core']");
|
|
||||||
profile.Should().Contain("'profile/pi/fc_divoom_tv/flowercore-divoom-tv.service'");
|
|
||||||
profile.Should().Contain("'profile/pi/fc_divoom_tv/flowercore-divoom-tv-launch.sh'");
|
|
||||||
profile.Should().Contain("profile/pi/fc_divoom_tv/99-flowercore-divoom-tv-hdmi.rules");
|
|
||||||
profile.Should().Contain("Boolean $service_enabled = false");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DivoomArtifacts_DoNotAddKubernetesWorkloads()
|
|
||||||
{
|
|
||||||
var allText = Directory.GetFiles(DmRoot, "*", SearchOption.AllDirectories)
|
|
||||||
.Concat(Directory.GetFiles(TvRoot, "*", SearchOption.AllDirectories))
|
|
||||||
.Select(File.ReadAllText);
|
|
||||||
|
|
||||||
foreach (var text in allText)
|
|
||||||
{
|
|
||||||
text.Should().NotContain("kind: Deployment");
|
|
||||||
text.Should().NotContain("kind: IngressRoute");
|
|
||||||
text.Should().NotContain("kind: Certificate");
|
|
||||||
text.Should().NotContain("kind: OnePasswordItem");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadDm(string relativePath)
|
|
||||||
=> File.ReadAllText(Path.Combine(DmRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
|
||||||
|
|
||||||
private static string ReadTv(string relativePath)
|
|
||||||
=> File.ReadAllText(Path.Combine(TvRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
|
||||||
|
|
||||||
private static string FindRepoRoot()
|
|
||||||
{
|
|
||||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
|
||||||
while (current is not null)
|
|
||||||
{
|
|
||||||
if (Directory.Exists(Path.Combine(current.FullName, "apps"))
|
|
||||||
&& File.Exists(Path.Combine(current.FullName, "README.md")))
|
|
||||||
{
|
|
||||||
return current.FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Could not find bluejay-infra root.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,19 +15,24 @@ public sealed class FleetManifestLintTests
|
|||||||
{
|
{
|
||||||
"brochure.flowercore.io",
|
"brochure.flowercore.io",
|
||||||
"dist.flowercore.io",
|
"dist.flowercore.io",
|
||||||
|
"dns.iamworkin.lan",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hosts that allow a tightly bounded write surface in addition to GET/HEAD.
|
// Public hosts that allow a tightly bounded write surface in addition to
|
||||||
// updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id}
|
// GET/HEAD. 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. Public
|
// PUT/PATCH/DELETE must still 404 at the route. Anything wider than this
|
||||||
// update.flowercore.io remains a GET/HEAD download surface in the
|
// set should fail this lint.
|
||||||
// FlowerCore.Updater sibling manifest and is covered by the general
|
//
|
||||||
// public-method allowlist lint instead of this write-surface rule.
|
// PUB-1 (2026-05-06): update.flowercore.io / updates.flowercore.io were
|
||||||
|
// 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)
|
||||||
@@ -62,10 +67,11 @@ public sealed class FleetManifestLintTests
|
|||||||
["github-runner-chat"] = "https://github.com/astoltz/FlowerCore.Chat",
|
["github-runner-chat"] = "https://github.com/astoltz/FlowerCore.Chat",
|
||||||
["github-runner-mysql"] = "https://github.com/astoltz/FlowerCore.MySQL",
|
["github-runner-mysql"] = "https://github.com/astoltz/FlowerCore.MySQL",
|
||||||
["github-runner-kiosk-linux"] = "https://github.com/astoltz/FlowerCore.Kiosk.Linux",
|
["github-runner-kiosk-linux"] = "https://github.com/astoltz/FlowerCore.Kiosk.Linux",
|
||||||
["github-runner-updater"] = "https://github.com/astoltz/FlowerCore.Updater",
|
["github-runner-devicemgmt"] = "https://github.com/astoltz/FlowerCore.DeviceManagement",
|
||||||
|
["github-runner-worldbuilder"] = "https://github.com/astoltz/FlowerCore.WorldBuilder",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly HashSet<string> RepoScopedLinuxRunnerDeployments = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> ScaledLinuxRunnerDeployments = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
"github-runner-sharedpos",
|
"github-runner-sharedpos",
|
||||||
"github-runner-puppet",
|
"github-runner-puppet",
|
||||||
@@ -76,45 +82,8 @@ public sealed class FleetManifestLintTests
|
|||||||
"github-runner-chat",
|
"github-runner-chat",
|
||||||
"github-runner-mysql",
|
"github-runner-mysql",
|
||||||
"github-runner-kiosk-linux",
|
"github-runner-kiosk-linux",
|
||||||
"github-runner-updater",
|
"github-runner-devicemgmt",
|
||||||
};
|
"github-runner-worldbuilder",
|
||||||
|
|
||||||
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)
|
||||||
@@ -269,7 +238,7 @@ public sealed class FleetManifestLintTests
|
|||||||
{
|
{
|
||||||
deployments.Should().ContainKey(expectedRunner.Key);
|
deployments.Should().ContainKey(expectedRunner.Key);
|
||||||
|
|
||||||
var container = deployments[expectedRunner.Key].MainContainerMappings().Should().ContainSingle().Subject;
|
var container = RunnerContainer(deployments[expectedRunner.Key]);
|
||||||
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
|
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
|
||||||
EnvValue(container, "EPHEMERAL").Should().Be("true");
|
EnvValue(container, "EPHEMERAL").Should().Be("true");
|
||||||
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
|
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
|
||||||
@@ -285,7 +254,7 @@ public sealed class FleetManifestLintTests
|
|||||||
{
|
{
|
||||||
foreach (var deployment in GitHubRunnerDeployments().Values)
|
foreach (var deployment in GitHubRunnerDeployments().Values)
|
||||||
{
|
{
|
||||||
var container = deployment.MainContainerMappings().Should().ContainSingle().Subject;
|
var container = RunnerContainer(deployment);
|
||||||
|
|
||||||
foreach (var expectedEnv in WritableRunnerEnv)
|
foreach (var expectedEnv in WritableRunnerEnv)
|
||||||
{
|
{
|
||||||
@@ -305,17 +274,14 @@ public sealed class FleetManifestLintTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForRepoScopedDeployments()
|
public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForScaledDeployments()
|
||||||
{
|
{
|
||||||
var deployments = GitHubRunnerDeployments();
|
var deployments = GitHubRunnerDeployments();
|
||||||
|
|
||||||
foreach (var deploymentName in RepoScopedLinuxRunnerDeployments)
|
foreach (var deploymentName in ScaledLinuxRunnerDeployments)
|
||||||
{
|
{
|
||||||
var deployment = deployments[deploymentName];
|
var deployment = deployments[deploymentName];
|
||||||
// Sprint 34 ops trimmed runner load while the cluster was degraded
|
ReplicaCount(deployment).Should().Be(2);
|
||||||
// to two healthy nodes. Repo-scoped runners can be tuned back above
|
|
||||||
// one replica, but they must stay RWO-safe before that happens.
|
|
||||||
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
|
||||||
@@ -323,7 +289,7 @@ public sealed class FleetManifestLintTests
|
|||||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
claimNames.Should().BeEmpty($"{deploymentName} must remain ready for safe multi-replica scaling without sharing a RWO PVC");
|
claimNames.Should().BeEmpty($"{deploymentName} is scaled and must not share 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);
|
||||||
@@ -341,108 +307,6 @@ public sealed class FleetManifestLintTests
|
|||||||
.Be("github-runner-nuget-cache");
|
.Be("github-runner-nuget-cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Runners_MustNotPinToOperatorWorkstationHosts()
|
|
||||||
{
|
|
||||||
// CRITICAL SAFETY (operator directive 2026-05-26): BLUEJAY-WS is the
|
|
||||||
// operator's primary workstation — host of the 1Password Connect
|
|
||||||
// bearer token, fcadmin SSH keys to noc1, signing CA private keys,
|
|
||||||
// and source for every FC repo. A self-hosted GitHub Actions runner
|
|
||||||
// there would execute arbitrary PR code with that local access.
|
|
||||||
// Build-side analog of the Sprint 9 NEW safe-account exclusion gate
|
|
||||||
// (Puppet GPO/AppLocker/WDAC/audit-forwarder modules refuse to apply
|
|
||||||
// on BLUEJAY-WS). This lint asserts no GitHub-runner Deployment in
|
|
||||||
// apps/github-runner/ pins to a forbidden operator-workstation host
|
|
||||||
// via nodeName, nodeSelector, nodeAffinity, or tolerations.
|
|
||||||
// Existing legacy `bluejay-ws-sandbox-1` GitHub-registered runner is
|
|
||||||
// out of scope here (it's a runtime registration, not a K8s
|
|
||||||
// Deployment) — see CLAUDE.md "Common Mistakes" entry and
|
|
||||||
// feedback_bluejay_ws_never_public_runner.md.
|
|
||||||
var forbiddenHostPatterns = new[]
|
|
||||||
{
|
|
||||||
"bluejay-ws",
|
|
||||||
"BLUEJAY-WS",
|
|
||||||
"bluejay-ws.iamworkin.lan",
|
|
||||||
"iamworkin-ws",
|
|
||||||
};
|
|
||||||
|
|
||||||
bool ContainsForbidden(string? value) =>
|
|
||||||
!string.IsNullOrWhiteSpace(value)
|
|
||||||
&& forbiddenHostPatterns.Any(pattern => value!.Contains(pattern, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
var violations = GitHubRunnerDeployments().Values.SelectMany(deployment =>
|
|
||||||
{
|
|
||||||
var local = new List<string>();
|
|
||||||
var podSpec = ManifestNodeExtensions.Mapping(deployment.Root, "spec", "template", "spec");
|
|
||||||
if (podSpec is null)
|
|
||||||
{
|
|
||||||
return local;
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeName: pins the pod to a specific node by name.
|
|
||||||
var nodeName = ManifestNodeExtensions.Scalar(podSpec, "nodeName");
|
|
||||||
if (ContainsForbidden(nodeName))
|
|
||||||
{
|
|
||||||
local.Add($"{deployment.Name} sets nodeName='{nodeName}' which targets a forbidden operator-workstation host.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeSelector: dict of label → value pinning the pod to nodes
|
|
||||||
// carrying matching labels. Examples that would trip this:
|
|
||||||
// kubernetes.io/hostname: bluejay-ws
|
|
||||||
// flowercore.io/host: bluejay-ws.iamworkin.lan
|
|
||||||
var nodeSelector = ManifestNodeExtensions.Mapping(podSpec, "nodeSelector");
|
|
||||||
if (nodeSelector is not null)
|
|
||||||
{
|
|
||||||
foreach (var entry in nodeSelector.Children)
|
|
||||||
{
|
|
||||||
var key = entry.Key is YamlScalarNode keyScalar ? keyScalar.Value : null;
|
|
||||||
var value = entry.Value is YamlScalarNode valueScalar ? valueScalar.Value : null;
|
|
||||||
if (ContainsForbidden(value))
|
|
||||||
{
|
|
||||||
local.Add($"{deployment.Name} has nodeSelector entry '{key}: {value}' which targets a forbidden operator-workstation host.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeAffinity: matchExpressions over node labels.
|
|
||||||
foreach (var term in ManifestNodeExtensions.MappingSequence(podSpec, "affinity", "nodeAffinity", "requiredDuringSchedulingIgnoredDuringExecution", "nodeSelectorTerms"))
|
|
||||||
{
|
|
||||||
foreach (var expr in ManifestNodeExtensions.MappingSequence(term, "matchExpressions"))
|
|
||||||
{
|
|
||||||
var key = ManifestNodeExtensions.Scalar(expr, "key");
|
|
||||||
foreach (var valueNode in ManifestNodeExtensions.ScalarSequence(expr, "values"))
|
|
||||||
{
|
|
||||||
if (ContainsForbidden(valueNode))
|
|
||||||
{
|
|
||||||
local.Add($"{deployment.Name} has nodeAffinity matchExpression '{key}' value '{valueNode}' which targets a forbidden operator-workstation host.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tolerations: scheduling onto a tainted operator-workstation
|
|
||||||
// node would let the runner run there. Forbid any toleration
|
|
||||||
// value that names the workstation.
|
|
||||||
foreach (var toleration in ManifestNodeExtensions.MappingSequence(podSpec, "tolerations"))
|
|
||||||
{
|
|
||||||
var key = ManifestNodeExtensions.Scalar(toleration, "key");
|
|
||||||
var value = ManifestNodeExtensions.Scalar(toleration, "value");
|
|
||||||
if (ContainsForbidden(key))
|
|
||||||
{
|
|
||||||
local.Add($"{deployment.Name} has toleration key '{key}' which targets a forbidden operator-workstation host.");
|
|
||||||
}
|
|
||||||
if (ContainsForbidden(value))
|
|
||||||
{
|
|
||||||
local.Add($"{deployment.Name} has toleration value '{value}' which targets a forbidden operator-workstation host.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return local;
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
violations.Should().BeEmpty("BLUEJAY-WS / iamworkin-ws must never host a fleet GitHub Actions runner; see CLAUDE.md 'Registering BLUEJAY-WS as a fleet GitHub Actions runner' and feedback_bluejay_ws_never_public_runner.md");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Monitoring_MustAlertWhenLinuxRunnerDeploymentIsUnavailable()
|
public void Monitoring_MustAlertWhenLinuxRunnerDeploymentIsUnavailable()
|
||||||
{
|
{
|
||||||
@@ -451,88 +315,12 @@ public sealed class FleetManifestLintTests
|
|||||||
monitoring.Should().Contain("MacMiniRunnerOffline");
|
monitoring.Should().Contain("MacMiniRunnerOffline");
|
||||||
monitoring.Should().Contain("LinuxRunnerOffline");
|
monitoring.Should().Contain("LinuxRunnerOffline");
|
||||||
monitoring.Should().Contain("kube_deployment_status_replicas_ready");
|
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("github-runner(|-.+)");
|
||||||
monitoring.Should().Contain("folder: CI Alerts");
|
monitoring.Should().Contain("folder: CI Alerts");
|
||||||
monitoring.Should().Contain("uid: linux-runner-offline");
|
monitoring.Should().Contain("uid: linux-runner-offline");
|
||||||
monitoring.Should().Contain("alert_channel: irc");
|
monitoring.Should().Contain("alert_channel: irc");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Monitoring_GenericKubernetesAlerts_MustExcludeEphemeralGithubRunnerNamespace()
|
|
||||||
{
|
|
||||||
var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml"));
|
|
||||||
|
|
||||||
monitoring.Should().Contain("kube_pod_container_status_restarts_total{namespace!=\"github-runner\"}");
|
|
||||||
monitoring.Should().Contain("and on(namespace, pod) kube_pod_info");
|
|
||||||
monitoring.Should().Contain("kube_deployment_spec_replicas{namespace!=\"github-runner\"} != kube_deployment_status_replicas_available{namespace!=\"github-runner\"}");
|
|
||||||
monitoring.Should().Contain("dedicated LinuxRunnerOffline/MacMiniRunnerOffline alerts");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Monitoring_BlackboxTargetsForOidcSensitiveServices_MustUseAnonymousHealthRoutesWhenAvailable()
|
|
||||||
{
|
|
||||||
var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml"));
|
|
||||||
|
|
||||||
monitoring.Should().Contain("https://chat.iamworkin.lan/healthz");
|
|
||||||
monitoring.Should().Contain("https://dist.iamworkin.lan/healthz");
|
|
||||||
monitoring.Should().Contain("https://dms.iamworkin.lan/healthz");
|
|
||||||
monitoring.Should().Contain("https://print.iamworkin.lan/healthz");
|
|
||||||
monitoring.Should().Contain("https://knowledge.iamworkin.lan/healthz");
|
|
||||||
monitoring.Should().Contain("https://library.iamworkin.lan/health");
|
|
||||||
monitoring.Should().Contain("https://aistation.iamworkin.lan/healthz");
|
|
||||||
monitoring.Should().NotContain("https://print.iamworkin.lan/\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OidcEnforcedDeployments_WithHttpHealthzProbes_MustDeclareAnonymousHealthzContract()
|
|
||||||
{
|
|
||||||
var violations = Inventory.Documents
|
|
||||||
.Where(document => document.Kind == "Deployment")
|
|
||||||
.SelectMany(document => document.MainContainerMappings()
|
|
||||||
.Where(container => string.Equals(EnvValue(container, "FlowerCore__Auth__Enabled"), "true", StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Where(container => string.Equals(EnvValue(container, "FlowerCore__Auth__Oidc__Enabled"), "true", StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Where(container => ProbeHttpGetPath(container, "readinessProbe") == "/healthz"
|
|
||||||
|| ProbeHttpGetPath(container, "startupProbe") == "/healthz")
|
|
||||||
.Where(_ => !string.Equals(
|
|
||||||
PodAnnotation(document, "flowercore.io/healthz-auth-policy"),
|
|
||||||
"allow-anonymous",
|
|
||||||
StringComparison.Ordinal))
|
|
||||||
.Select(container =>
|
|
||||||
{
|
|
||||||
var containerName = ManifestNodeExtensions.Scalar(container, "name") ?? "<unnamed>";
|
|
||||||
return $"{document.Descriptor} container '{containerName}' enforces OIDC while probing /healthz but lacks flowercore.io/healthz-auth-policy: allow-anonymous.";
|
|
||||||
}))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
violations.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Knowledge_OidcEnforcement_MustKeepHealthzAnonymousContractVisibleInManifest()
|
|
||||||
{
|
|
||||||
var knowledge = Inventory.Documents
|
|
||||||
.Single(document => document.Kind == "Deployment" && document.Namespace == "knowledge" && document.Name == "knowledge-web");
|
|
||||||
var container = knowledge.MainContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
|
|
||||||
EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be("true");
|
|
||||||
EnvValue(container, "FlowerCore__Auth__Oidc__Enabled").Should().Be("true");
|
|
||||||
ProbeHttpGetPath(container, "readinessProbe").Should().Be("/healthz");
|
|
||||||
PodAnnotation(knowledge, "flowercore.io/healthz-auth-policy").Should().Be("allow-anonymous");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Distribution_OidcEnforcement_MustKeepHealthzAnonymousContractVisibleInManifest()
|
|
||||||
{
|
|
||||||
var distribution = Inventory.Documents
|
|
||||||
.Single(document => document.Kind == "Deployment" && document.Namespace == "fc-distribution" && document.Name == "fc-distribution");
|
|
||||||
var container = distribution.MainContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
|
|
||||||
EnvValue(container, "FlowerCore__Auth__Oidc__Enabled").Should().Be("true");
|
|
||||||
EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be("true");
|
|
||||||
ProbeHttpGetPath(container, "readinessProbe").Should().Be("/healthz");
|
|
||||||
PodAnnotation(distribution, "flowercore.io/healthz-auth-policy").Should().Be("allow-anonymous");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StatefulSets_WithVolumeClaimTemplates_MustDeclareFilesystemDefaults()
|
public void StatefulSets_WithVolumeClaimTemplates_MustDeclareFilesystemDefaults()
|
||||||
{
|
{
|
||||||
@@ -646,6 +434,7 @@ 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",
|
||||||
@@ -801,202 +590,17 @@ public sealed class FleetManifestLintTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FcDeviceManagement_MustRelyOnApplicationSetDiscovery()
|
public void FcDeviceManagement_ArgocdApplicationMustMatchApplicationSetDiscoveryConventions()
|
||||||
{
|
{
|
||||||
var documents = FcDeviceManagementDocuments();
|
var application = FcDeviceManagementDocuments()
|
||||||
|
.Single(document => document.Kind == "Application" && document.Name == "infra-fc-devicemgmt");
|
||||||
|
|
||||||
documents.Should().NotContain(document => document.Kind == "Application");
|
application.Namespace.Should().Be("argocd");
|
||||||
|
application.Scalar("spec", "source", "repoURL")
|
||||||
var ns = documents.Single(document => document.Kind == "Namespace" && document.Name == "fc-devicemgmt");
|
|
||||||
ns.FileText.Should().Contain("ArgoCD discovers this directory as Application `infra-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()
|
.Should()
|
||||||
.NotContain(match => match.Contains(".flowercore.io", StringComparison.Ordinal),
|
.Be("http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git");
|
||||||
"Sprint 61 broader hardening only pre-stages commented public hosts for internal-only apps");
|
application.Scalar("spec", "source", "path").Should().Be("apps/fc-devicemgmt");
|
||||||
}
|
application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt");
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OidcFlipServices_AreGitOpsManagedWithHealthzProbes()
|
|
||||||
{
|
|
||||||
var deployments = new[]
|
|
||||||
{
|
|
||||||
(App: "fc-dns", Name: "dns-web", Slug: "dns", Secret: "dns-oidc-client"),
|
|
||||||
(App: "fc-media", Name: "fc-media-web", Slug: "media", Secret: "media-oidc-client"),
|
|
||||||
(App: "fc-distribution", Name: "fc-distribution", Slug: "distribution", Secret: "distribution-oidc-client"),
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var expected in deployments)
|
|
||||||
{
|
|
||||||
var deployment = AppDocuments(expected.App)
|
|
||||||
.Single(document => document.Kind == "Deployment" && document.Name == expected.Name);
|
|
||||||
var container = deployment.MainContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
|
|
||||||
EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be("true");
|
|
||||||
EnvValue(container, "FlowerCore__Auth__Oidc__Enabled").Should().Be("true");
|
|
||||||
(EnvValue(container, "FlowerCore__Auth__Oidc__Audience") ?? EnvValue(container, "FlowerCore__Auth__Oidc__ClientId"))
|
|
||||||
.Should()
|
|
||||||
.Be(expected.Slug);
|
|
||||||
EnvSecretName(container, "FlowerCore__Auth__Oidc__ClientSecret").Should().Be(expected.Secret);
|
|
||||||
EnvSecretOptional(container, "FlowerCore__Auth__Oidc__ClientSecret").Should().Be("true");
|
|
||||||
|
|
||||||
ProbePath(container, "readinessProbe").Should().Be("/healthz");
|
|
||||||
if (ProbePath(container, "startupProbe") is { } startupProbePath)
|
|
||||||
{
|
|
||||||
startupProbePath.Should().Be("/healthz");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ProbePath(container, "livenessProbe") is { } livenessProbePath)
|
|
||||||
{
|
|
||||||
livenessProbePath.Should().Be("/healthz");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OidcFlipServices_UseOnePasswordItemClientSecrets()
|
|
||||||
{
|
|
||||||
var expectedItems = new Dictionary<string, (string Name, string ItemPath)>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
["fc-dns"] = ("dns-oidc-client", "vaults/IAmWorkin/items/dns-oidc-client"),
|
|
||||||
["fc-media"] = ("media-oidc-client", "vaults/IAmWorkin/items/media-oidc-client"),
|
|
||||||
["fc-distribution"] = ("distribution-oidc-client", "vaults/IAmWorkin/items/distribution-oidc-client"),
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var expected in expectedItems)
|
|
||||||
{
|
|
||||||
var item = AppDocuments(expected.Key)
|
|
||||||
.Single(document => document.Kind == "OnePasswordItem" && document.Name == expected.Value.Name);
|
|
||||||
|
|
||||||
item.Scalar("spec", "itemPath").Should().Be(expected.Value.ItemPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DnsAndMediaGitOpsAdoption_PreservesLiveStorageAndImageShape()
|
|
||||||
{
|
|
||||||
var dnsDeployment = AppDocuments("fc-dns")
|
|
||||||
.Single(document => document.Kind == "Deployment" && document.Name == "dns-web");
|
|
||||||
var dnsContainer = dnsDeployment.MainContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
var dnsPvc = AppDocuments("fc-dns")
|
|
||||||
.Single(document => document.Kind == "PersistentVolumeClaim" && document.Name == "dns-web-data");
|
|
||||||
|
|
||||||
ManifestNodeExtensions.Scalar(dnsContainer, "image").Should().Be("localhost/fc-dns-web:v20260604-oidc-proper");
|
|
||||||
dnsPvc.Scalar("spec", "storageClassName").Should().Be("longhorn");
|
|
||||||
dnsPvc.Scalar("spec", "resources", "requests", "storage").Should().Be("1Gi");
|
|
||||||
|
|
||||||
var mediaDeployment = AppDocuments("fc-media")
|
|
||||||
.Single(document => document.Kind == "Deployment" && document.Name == "fc-media-web");
|
|
||||||
var mediaContainer = mediaDeployment.MainContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
var mediaPvc = AppDocuments("fc-media")
|
|
||||||
.Single(document => document.Kind == "PersistentVolumeClaim" && document.Name == "fc-media-data");
|
|
||||||
|
|
||||||
ManifestNodeExtensions.Scalar(mediaContainer, "image").Should().Be("localhost/fc-media-web:v20260604-oidc-proper");
|
|
||||||
mediaPvc.Scalar("spec", "storageClassName").Should().Be("longhorn");
|
|
||||||
mediaPvc.Scalar("spec", "resources", "requests", "storage").Should().Be("20Gi");
|
|
||||||
|
|
||||||
mediaDeployment.AllScalars().Should().Contain(new[]
|
|
||||||
{
|
|
||||||
"/volume1/kubernetes/fc-media-transcodes",
|
|
||||||
"/volume1/kubernetes/fc-media-inbox",
|
|
||||||
"/volume1/video",
|
|
||||||
});
|
|
||||||
|
|
||||||
var distributionDeployment = AppDocuments("fc-distribution")
|
|
||||||
.Single(document => document.Kind == "Deployment" && document.Name == "fc-distribution");
|
|
||||||
var distributionContainer = distributionDeployment.MainContainerMappings().Should().ContainSingle().Subject;
|
|
||||||
|
|
||||||
ManifestNodeExtensions.Scalar(distributionContainer, "image").Should().Be("localhost/fc-distribution:v20260604-oidc-root-anon");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MonitoringProbes_UseHealthzForOidcGatedHosts()
|
|
||||||
{
|
|
||||||
var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml"));
|
|
||||||
|
|
||||||
monitoring.Should().Contain("\"https://dns.iamworkin.lan/healthz\"");
|
|
||||||
monitoring.Should().Contain("\"https://dist.iamworkin.lan/healthz\"");
|
|
||||||
monitoring.Should().Contain("\"https://media.iamworkin.lan/healthz\"");
|
|
||||||
monitoring.Should().NotContain("\"https://dns.iamworkin.lan/\"");
|
|
||||||
monitoring.Should().NotContain("\"https://dist.iamworkin.lan/\"");
|
|
||||||
monitoring.Should().NotContain("\"https://media.iamworkin.lan/\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DistributionPublicIngress_KeepsGetHeadMethodAllowlist()
|
|
||||||
{
|
|
||||||
var publicIngress = AppDocuments("fc-distribution")
|
|
||||||
.Single(document => document.Kind == "IngressRoute" && document.Name == "fc-distribution-public");
|
|
||||||
var route = publicIngress.MappingSequence("spec", "routes").Should().ContainSingle().Subject;
|
|
||||||
var match = ManifestNodeExtensions.Scalar(route, "match");
|
|
||||||
|
|
||||||
match.Should().Contain("Host(`dist.flowercore.io`)");
|
|
||||||
match.Should().Contain("Method(`GET`)");
|
|
||||||
match.Should().Contain("Method(`HEAD`)");
|
|
||||||
match.Should().NotContain("Method(`POST`)");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DnsAndMediaIngressRoutes_MatchLiveInternalHosts()
|
|
||||||
{
|
|
||||||
var dnsRoute = AppDocuments("fc-dns")
|
|
||||||
.Single(document => document.Kind == "IngressRoute" && document.Name == "dns-web")
|
|
||||||
.MappingSequence("spec", "routes")
|
|
||||||
.Should()
|
|
||||||
.ContainSingle()
|
|
||||||
.Subject;
|
|
||||||
var mediaRoute = AppDocuments("fc-media")
|
|
||||||
.Single(document => document.Kind == "IngressRoute" && document.Name == "fc-media-web")
|
|
||||||
.MappingSequence("spec", "routes")
|
|
||||||
.Should()
|
|
||||||
.ContainSingle()
|
|
||||||
.Subject;
|
|
||||||
|
|
||||||
ManifestNodeExtensions.Scalar(dnsRoute, "match").Should().Be("Host(`dns.iamworkin.lan`)");
|
|
||||||
ManifestNodeExtensions.Scalar(mediaRoute, "match").Should().Be("Host(`media.iamworkin.lan`)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> ProbeViolations(
|
private static IEnumerable<string> ProbeViolations(
|
||||||
@@ -1041,6 +645,15 @@ public sealed class FleetManifestLintTests
|
|||||||
return EnvMapping(container, name) is { } env ? ManifestNodeExtensions.Scalar(env, "value") : null;
|
return EnvMapping(container, name) is { } env ? ManifestNodeExtensions.Scalar(env, "value") : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static YamlMappingNode RunnerContainer(ManifestDocument deployment)
|
||||||
|
{
|
||||||
|
return deployment.ContainerMappings()
|
||||||
|
.Where(container => string.Equals(ManifestNodeExtensions.Scalar(container, "name"), "runner", StringComparison.Ordinal))
|
||||||
|
.Should()
|
||||||
|
.ContainSingle($"{deployment.Name} must keep exactly one main runner container")
|
||||||
|
.Subject;
|
||||||
|
}
|
||||||
|
|
||||||
private static string? EnvSecretName(YamlMappingNode container, string name)
|
private static string? EnvSecretName(YamlMappingNode container, string name)
|
||||||
{
|
{
|
||||||
return EnvMapping(container, name) is { } env
|
return EnvMapping(container, name) is { } env
|
||||||
@@ -1055,44 +668,12 @@ public sealed class FleetManifestLintTests
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? EnvSecretOptional(YamlMappingNode container, string name)
|
|
||||||
{
|
|
||||||
return EnvMapping(container, name) is { } env
|
|
||||||
? ManifestNodeExtensions.Scalar(env, "valueFrom", "secretKeyRef", "optional")
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ProbePath(YamlMappingNode container, string probeKey)
|
|
||||||
{
|
|
||||||
return ManifestNodeExtensions.Scalar(container, probeKey, "httpGet", "path");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<ManifestDocument> AppDocuments(string app)
|
|
||||||
{
|
|
||||||
return Inventory.Documents
|
|
||||||
.Where(document => document.RelativePath.StartsWith($"{app}/", StringComparison.Ordinal))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static YamlMappingNode? EnvMapping(YamlMappingNode container, string name)
|
private static YamlMappingNode? EnvMapping(YamlMappingNode container, string name)
|
||||||
{
|
{
|
||||||
return ManifestNodeExtensions.MappingSequence(container, "env")
|
return ManifestNodeExtensions.MappingSequence(container, "env")
|
||||||
.SingleOrDefault(env => string.Equals(ManifestNodeExtensions.Scalar(env, "name"), name, StringComparison.Ordinal));
|
.SingleOrDefault(env => string.Equals(ManifestNodeExtensions.Scalar(env, "name"), name, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? PodAnnotation(ManifestDocument document, string name)
|
|
||||||
{
|
|
||||||
return document.Scalar("spec", "template", "metadata", "annotations", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ProbeHttpGetPath(YamlMappingNode container, string probeKey)
|
|
||||||
{
|
|
||||||
return ManifestNodeExtensions.TryGetMapping(container, probeKey, out var probe)
|
|
||||||
&& ManifestNodeExtensions.TryGetMapping(probe, "httpGet", out var httpGet)
|
|
||||||
? ManifestNodeExtensions.Scalar(httpGet, "path")
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<ManifestDocument> FcDeviceManagementDocuments()
|
private static IReadOnlyList<ManifestDocument> FcDeviceManagementDocuments()
|
||||||
{
|
{
|
||||||
return Inventory.Documents
|
return Inventory.Documents
|
||||||
@@ -1322,22 +903,6 @@ internal sealed record ManifestDocument(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// MainContainerMappings excludes initContainers. Use this when asserting
|
|
||||||
// properties of the primary container (env, image, volumeMounts) where an
|
|
||||||
// initContainer would be a false-positive match — e.g. the GitHub runner
|
|
||||||
// image's `setup-runner-home` initContainer should not count toward the
|
|
||||||
// single-container assertions on the runner deployments.
|
|
||||||
public IReadOnlyList<YamlMappingNode> MainContainerMappings()
|
|
||||||
{
|
|
||||||
var podSpec = PodSpec();
|
|
||||||
if (podSpec is null)
|
|
||||||
{
|
|
||||||
return Array.Empty<YamlMappingNode>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ManifestNodeExtensions.MappingSequence(podSpec, "containers").ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<ContainerSpec> ContainerSpecs()
|
public IReadOnlyList<ContainerSpec> ContainerSpecs()
|
||||||
{
|
{
|
||||||
return ContainerMappings()
|
return ContainerMappings()
|
||||||
|
|||||||
Reference in New Issue
Block a user