Compare commits
1 Commits
04881f46f0
...
codex/ttsr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05af0c48f9 |
@@ -1,18 +1,5 @@
|
|||||||
# FlowerCore Remote Desktop — TLS + Ingress
|
# FlowerCore Remote Desktop — TLS + Ingress
|
||||||
#
|
# Deployment and Service managed by deploy script (not ArgoCD)
|
||||||
# Source-of-truth split:
|
|
||||||
# - bluejay-infra OWNS: Certificate, IngressRoute, all NetworkPolicies
|
|
||||||
# (see network-policies.yaml in this directory).
|
|
||||||
# - FlowerCore.RemoteDesktop scripts/deploy-web.sh OWNS: Deployment +
|
|
||||||
# Service. Reason: image refs like `localhost/fc-desktop:linux-xfce`
|
|
||||||
# only exist on each node's containerd after a manual import, so a
|
|
||||||
# Deployment manifest in bluejay-infra would race the image-import
|
|
||||||
# step and crash-loop.
|
|
||||||
#
|
|
||||||
# NetworkPolicies moved into bluejay-infra 2026-05-07 — previously they
|
|
||||||
# were applied via the deploy script's kubectl apply calls, which broke
|
|
||||||
# cluster-rebuild repeatability. See
|
|
||||||
# feedback_networkpolicies_belong_in_bluejay_infra.md.
|
|
||||||
---
|
---
|
||||||
apiVersion: cert-manager.io/v1
|
apiVersion: cert-manager.io/v1
|
||||||
kind: Certificate
|
kind: Certificate
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
# FlowerCore Remote Desktop — NetworkPolicies (GitOps-managed)
|
|
||||||
#
|
|
||||||
# Moved into bluejay-infra 2026-05-07 as part of the regroup audit. These
|
|
||||||
# four policies were previously applied via FlowerCore.RemoteDesktop's
|
|
||||||
# scripts/deploy-web.sh `kubectl apply` calls, which meant a fresh cluster
|
|
||||||
# rebuild from bluejay-infra alone would miss them — Browser Lab session
|
|
||||||
# isolation, control-plane allow-list, and HTTP-01 cert renewal would all
|
|
||||||
# silently fail to come up.
|
|
||||||
#
|
|
||||||
# Source-of-truth contract:
|
|
||||||
# - bluejay-infra OWNS all NetworkPolicy + Certificate + IngressRoute
|
|
||||||
# resources for fc-desktop.
|
|
||||||
# - FlowerCore.RemoteDesktop's scripts/deploy-web.sh continues to own
|
|
||||||
# the Deployment + Service apply (because the image ref
|
|
||||||
# `localhost/fc-desktop:linux-xfce` only exists on each node's
|
|
||||||
# containerd after a manual import — it can't be pulled from a
|
|
||||||
# registry, so a Deployment manifest in bluejay-infra would race the
|
|
||||||
# image-import step and crash-loop).
|
|
||||||
---
|
|
||||||
# 1) desktop-isolation — Browser Lab session pods.
|
|
||||||
#
|
|
||||||
# Locks down pods labeled `app.kubernetes.io/name=remote-desktop` (every
|
|
||||||
# session pod regardless of template). Allows guacd ingress for the VNC/RDP
|
|
||||||
# display lane and remotedesktop-web's pre-handoff probing. Egress: NFS to
|
|
||||||
# Synology, DNS, Traefik (cluster + LB VIP), Intranet (Browser Lab home).
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: desktop-isolation
|
|
||||||
namespace: fc-desktop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: remotedesktop
|
|
||||||
app.kubernetes.io/component: isolation
|
|
||||||
spec:
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: remote-desktop
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
ingress:
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: guacamole
|
|
||||||
ports:
|
|
||||||
- port: 3000
|
|
||||||
protocol: TCP
|
|
||||||
- port: 3001
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5901
|
|
||||||
protocol: TCP
|
|
||||||
- port: 3389
|
|
||||||
protocol: TCP
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: fc-desktop
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: remotedesktop-web
|
|
||||||
ports:
|
|
||||||
- port: 3000
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5901
|
|
||||||
protocol: TCP
|
|
||||||
egress:
|
|
||||||
# NFS to Synology
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.3/32
|
|
||||||
ports:
|
|
||||||
- port: 2049
|
|
||||||
protocol: TCP
|
|
||||||
- port: 2049
|
|
||||||
protocol: UDP
|
|
||||||
- port: 111
|
|
||||||
protocol: TCP
|
|
||||||
- port: 111
|
|
||||||
protocol: UDP
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.3/32
|
|
||||||
ports:
|
|
||||||
- port: 445
|
|
||||||
protocol: TCP
|
|
||||||
- to: []
|
|
||||||
ports:
|
|
||||||
- port: 53
|
|
||||||
protocol: UDP
|
|
||||||
- port: 53
|
|
||||||
protocol: TCP
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.56.200/32
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.43.33.87/32
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: traefik-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: traefik
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8000
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: intranet
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: intranet-web
|
|
||||||
ports:
|
|
||||||
- port: 5300
|
|
||||||
protocol: TCP
|
|
||||||
---
|
|
||||||
# 2) fc-desktop-default-deny — namespace-wide catch-all.
|
|
||||||
#
|
|
||||||
# Selects every pod EXCEPT remotedesktop-web (the public-surface control
|
|
||||||
# plane) and applies default-deny semantics for both Ingress and Egress.
|
|
||||||
# Closes the gap where session pods land WITHOUT the desktop-isolation
|
|
||||||
# policy's `app.kubernetes.io/name=remote-desktop` label, plus prevents
|
|
||||||
# arbitrary debug sidecars / kubectl debug images from getting cluster
|
|
||||||
# access.
|
|
||||||
#
|
|
||||||
# CRITICAL: also catches transient cm-acme-http-solver pods (that's the
|
|
||||||
# bug this whole regroup chased). The cm-acme-http-solver-allow policy
|
|
||||||
# below is the explicit carve-out.
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: fc-desktop-default-deny
|
|
||||||
namespace: fc-desktop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: remotedesktop
|
|
||||||
app.kubernetes.io/component: isolation
|
|
||||||
spec:
|
|
||||||
podSelector:
|
|
||||||
matchExpressions:
|
|
||||||
- key: app.kubernetes.io/name
|
|
||||||
operator: NotIn
|
|
||||||
values:
|
|
||||||
- remotedesktop-web
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
---
|
|
||||||
# 3) remotedesktop-web-isolation — control plane explicit allow-list.
|
|
||||||
#
|
|
||||||
# remotedesktop-web is the only pod label the default-deny excludes, so
|
|
||||||
# without this policy the control plane would have wide-open Ingress AND
|
|
||||||
# Egress. This re-introduces a tight allow-list:
|
|
||||||
# - Ingress: Traefik only on TCP/8080
|
|
||||||
# - Egress: CoreDNS, K8s API, Guacamole admin, NFS, Intranet,
|
|
||||||
# Traefik (cluster + LB), and the fc-desktop namespace itself
|
|
||||||
# (for session pod readiness probing).
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: remotedesktop-web-isolation
|
|
||||||
namespace: fc-desktop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: remotedesktop
|
|
||||||
app.kubernetes.io/component: isolation
|
|
||||||
spec:
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: remotedesktop-web
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
ingress:
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: traefik-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: traefik
|
|
||||||
ports:
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
egress:
|
|
||||||
# CoreDNS
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: kube-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
ports:
|
|
||||||
- port: 53
|
|
||||||
protocol: UDP
|
|
||||||
- port: 53
|
|
||||||
protocol: TCP
|
|
||||||
# K8s API server
|
|
||||||
- to: []
|
|
||||||
ports:
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 6443
|
|
||||||
protocol: TCP
|
|
||||||
# Guacamole admin
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: guacamole
|
|
||||||
ports:
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
# NFS to Synology
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.58.3/32
|
|
||||||
ports:
|
|
||||||
- port: 2049
|
|
||||||
protocol: TCP
|
|
||||||
- port: 2049
|
|
||||||
protocol: UDP
|
|
||||||
- port: 111
|
|
||||||
protocol: TCP
|
|
||||||
- port: 111
|
|
||||||
protocol: UDP
|
|
||||||
# Intranet web
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: intranet
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: intranet-web
|
|
||||||
ports:
|
|
||||||
- port: 5300
|
|
||||||
protocol: TCP
|
|
||||||
# Cluster Traefik pods (in-cluster service resolution + Guacamole
|
|
||||||
# routing handoff where web app builds URLs against the public host
|
|
||||||
# but resolves internally).
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: traefik-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: traefik
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
# fc-desktop namespace — session pod probing during browser-access
|
|
||||||
# readiness checks.
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: fc-desktop
|
|
||||||
ports:
|
|
||||||
- port: 3000
|
|
||||||
protocol: TCP
|
|
||||||
- port: 3001
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5901
|
|
||||||
protocol: TCP
|
|
||||||
- port: 3389
|
|
||||||
protocol: TCP
|
|
||||||
---
|
|
||||||
# 4) cm-acme-http-solver-allow — cert-manager HTTP-01 carve-out.
|
|
||||||
#
|
|
||||||
# Without this, fc-desktop-default-deny catches the transient solver pods
|
|
||||||
# cert-manager creates for each renewal (they don't carry the
|
|
||||||
# remotedesktop-web label). Caused 8-day silent renewal failure on
|
|
||||||
# desktop.iamworkin.lan in 2026-04-28..2026-05-07 (see
|
|
||||||
# feedback_certmanager_renewal_stuck_when_solver_blocked_by_namespace_default_deny.md).
|
|
||||||
#
|
|
||||||
# Authorizes:
|
|
||||||
# - Ingress on TCP/8089 from cluster Traefik (which proxies the external
|
|
||||||
# HTTP-01 GET on port 80 through to the solver).
|
|
||||||
# - Egress for cluster DNS (defensive — newer cert-manager probes from
|
|
||||||
# inside the solver too).
|
|
||||||
#
|
|
||||||
# The `acme.cert-manager.io/http01-solver=true` label is set by
|
|
||||||
# cert-manager itself on every solver pod automatically.
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: cm-acme-http-solver-allow
|
|
||||||
namespace: fc-desktop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: remotedesktop
|
|
||||||
app.kubernetes.io/component: cert-renewal
|
|
||||||
spec:
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
acme.cert-manager.io/http01-solver: "true"
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
ingress:
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: traefik-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: traefik
|
|
||||||
ports:
|
|
||||||
- port: 8089
|
|
||||||
protocol: TCP
|
|
||||||
egress:
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: kube-system
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
ports:
|
|
||||||
- port: 53
|
|
||||||
protocol: UDP
|
|
||||||
- port: 53
|
|
||||||
protocol: TCP
|
|
||||||
@@ -118,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:v202605061948
|
image: localhost/fc-distribution:v202604240010
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
@@ -151,10 +151,6 @@ spec:
|
|||||||
value: "/signing/aistation-field/chain.pem"
|
value: "/signing/aistation-field/chain.pem"
|
||||||
- name: FlowerCore__Distribution__Signing__EditionCerts__aistation-field__KeyPath
|
- name: FlowerCore__Distribution__Signing__EditionCerts__aistation-field__KeyPath
|
||||||
value: "/signing/aistation-field/private-key.pem"
|
value: "/signing/aistation-field/private-key.pem"
|
||||||
# Public distribution host is GET/HEAD-only at Traefik; this
|
|
||||||
# entitlement list controls which editions are readable there.
|
|
||||||
- name: FlowerCore__Distribution__EntitlementPublic__PublicEditions__0
|
|
||||||
value: "*"
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
@@ -266,12 +262,8 @@ spec:
|
|||||||
kind: ClusterIssuer
|
kind: ClusterIssuer
|
||||||
dnsNames:
|
dnsNames:
|
||||||
- dist.iamworkin.lan
|
- dist.iamworkin.lan
|
||||||
# step-ca ACME caps lifetime at 30d; requesting 90d silently capped
|
duration: 2160h # 90d
|
||||||
# made renewBefore=cert-lifetime → perpetual renewal loop (10880+ CRs
|
renewBefore: 720h # 30d
|
||||||
# in 18h on 2026-05-07). Match working 720h/240h pattern from other
|
|
||||||
# FC services.
|
|
||||||
duration: 720h # 30d (step-ca cap)
|
|
||||||
renewBefore: 240h # 10d
|
|
||||||
---
|
---
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: IngressRoute
|
kind: IngressRoute
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import unicodedata
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
@@ -61,189 +60,6 @@ class TtsRequest(BaseModel):
|
|||||||
volume: int = 100 # 0-200
|
volume: int = 100 # 0-200
|
||||||
|
|
||||||
|
|
||||||
HEBREW_CHAR_RE = re.compile(r"[\u0590-\u05FF]")
|
|
||||||
HEBREW_WORD_RE = re.compile(r"[\u0590-\u05FF]+")
|
|
||||||
|
|
||||||
# eSpeak-NG's Hebrew voice can spell unpointed Hebrew as Unicode character
|
|
||||||
# names on some builds. For source-text study reads, prefer a stable
|
|
||||||
# scholarly transliteration so words sound like words even without niqqud.
|
|
||||||
HEBREW_WORD_TRANSLITERATIONS = {
|
|
||||||
"אב": "av",
|
|
||||||
"אבא": "abba",
|
|
||||||
"אברהם": "Avraham",
|
|
||||||
"אדמה": "adamah",
|
|
||||||
"אדני": "Adonai",
|
|
||||||
"אדם": "adam",
|
|
||||||
"אור": "or",
|
|
||||||
"אלהים": "Elohim",
|
|
||||||
"אלוהים": "Elohim",
|
|
||||||
"אמן": "amen",
|
|
||||||
"אם": "em",
|
|
||||||
"אמת": "emet",
|
|
||||||
"ארץ": "eretz",
|
|
||||||
"אש": "esh",
|
|
||||||
"את": "et",
|
|
||||||
"בית": "beit",
|
|
||||||
"בן": "ben",
|
|
||||||
"ברא": "bara",
|
|
||||||
"בראשית": "bereshit",
|
|
||||||
"ברית": "berit",
|
|
||||||
"ברוך": "barukh",
|
|
||||||
"בת": "bat",
|
|
||||||
"גוי": "goy",
|
|
||||||
"גוים": "goyim",
|
|
||||||
"גויים": "goyim",
|
|
||||||
"דבר": "davar",
|
|
||||||
"דברים": "devarim",
|
|
||||||
"דוד": "David",
|
|
||||||
"הלל": "hallel",
|
|
||||||
"הארץ": "ha-aretz",
|
|
||||||
"הברית": "ha-berit",
|
|
||||||
"החדשה": "ha-chadashah",
|
|
||||||
"השמים": "ha-shamayim",
|
|
||||||
"השמיים": "ha-shamayim",
|
|
||||||
"ויאמר": "vayomer",
|
|
||||||
"יהוה": "Adonai",
|
|
||||||
"יוסף": "Yosef",
|
|
||||||
"יוחנן": "Yochanan",
|
|
||||||
"ישראל": "Yisrael",
|
|
||||||
"ישוע": "Yeshua",
|
|
||||||
"יצחק": "Yitzchak",
|
|
||||||
"יעקב": "Yaakov",
|
|
||||||
"ירושלים": "Yerushalayim",
|
|
||||||
"כהן": "kohen",
|
|
||||||
"כהנים": "kohanim",
|
|
||||||
"מים": "mayim",
|
|
||||||
"מות": "mavet",
|
|
||||||
"מושיע": "moshia",
|
|
||||||
"מלך": "melekh",
|
|
||||||
"מלכות": "malkhut",
|
|
||||||
"מרים": "Miriam",
|
|
||||||
"משה": "Moshe",
|
|
||||||
"משיח": "Mashiach",
|
|
||||||
"נביא": "navi",
|
|
||||||
"נביאים": "neviim",
|
|
||||||
"עם": "am",
|
|
||||||
"עולם": "olam",
|
|
||||||
"צדק": "tzedek",
|
|
||||||
"קדוש": "qadosh",
|
|
||||||
"קדושים": "qedoshim",
|
|
||||||
"קול": "qol",
|
|
||||||
"רוח": "ruach",
|
|
||||||
"שאול": "Shaul",
|
|
||||||
"שמים": "shamayim",
|
|
||||||
"שמיים": "shamayim",
|
|
||||||
"שמעון": "Shimon",
|
|
||||||
"שלום": "Shalom",
|
|
||||||
"תורה": "torah",
|
|
||||||
"חכמה": "chokhmah",
|
|
||||||
"חסד": "chesed",
|
|
||||||
"חיים": "chayim",
|
|
||||||
"חושך": "choshekh",
|
|
||||||
}
|
|
||||||
|
|
||||||
HEBREW_LETTERS = {
|
|
||||||
"א": "a",
|
|
||||||
"ב": "b",
|
|
||||||
"ג": "g",
|
|
||||||
"ד": "d",
|
|
||||||
"ה": "h",
|
|
||||||
"ו": "v",
|
|
||||||
"ז": "z",
|
|
||||||
"ח": "kh",
|
|
||||||
"ט": "t",
|
|
||||||
"י": "y",
|
|
||||||
"כ": "kh",
|
|
||||||
"ך": "kh",
|
|
||||||
"ל": "l",
|
|
||||||
"מ": "m",
|
|
||||||
"ם": "m",
|
|
||||||
"נ": "n",
|
|
||||||
"ן": "n",
|
|
||||||
"ס": "s",
|
|
||||||
"ע": "a",
|
|
||||||
"פ": "p",
|
|
||||||
"ף": "f",
|
|
||||||
"צ": "ts",
|
|
||||||
"ץ": "ts",
|
|
||||||
"ק": "q",
|
|
||||||
"ר": "r",
|
|
||||||
"ש": "sh",
|
|
||||||
"ת": "t",
|
|
||||||
}
|
|
||||||
|
|
||||||
HEBREW_VOWELISH = {"a", "e", "i", "o", "u"}
|
|
||||||
|
|
||||||
|
|
||||||
def _strip_hebrew_marks(value: str) -> str:
|
|
||||||
decomposed = unicodedata.normalize("NFD", value)
|
|
||||||
return "".join(
|
|
||||||
ch for ch in decomposed
|
|
||||||
if unicodedata.category(ch) != "Mn" and ch not in {"׳", "״", "־"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _fallback_hebrew_transliteration(word: str) -> str:
|
|
||||||
tokens: list[str] = []
|
|
||||||
chars = list(word)
|
|
||||||
for index, ch in enumerate(chars):
|
|
||||||
token = HEBREW_LETTERS.get(ch)
|
|
||||||
if token is None:
|
|
||||||
continue
|
|
||||||
if ch == "ה" and index == len(chars) - 1:
|
|
||||||
token = "ah"
|
|
||||||
elif ch == "י" and index > 0:
|
|
||||||
token = "i"
|
|
||||||
elif ch == "ו" and index > 0:
|
|
||||||
token = "o"
|
|
||||||
tokens.append(token)
|
|
||||||
|
|
||||||
if not tokens:
|
|
||||||
return word
|
|
||||||
|
|
||||||
spoken: list[str] = []
|
|
||||||
for index, token in enumerate(tokens):
|
|
||||||
spoken.append(token)
|
|
||||||
next_token = tokens[index + 1] if index + 1 < len(tokens) else ""
|
|
||||||
if (
|
|
||||||
token[-1:] not in HEBREW_VOWELISH
|
|
||||||
and next_token
|
|
||||||
and next_token[:1] not in HEBREW_VOWELISH
|
|
||||||
):
|
|
||||||
spoken.append("a")
|
|
||||||
return "".join(spoken)
|
|
||||||
|
|
||||||
|
|
||||||
def _transliterate_hebrew_word(match: re.Match[str]) -> str:
|
|
||||||
original = match.group(0)
|
|
||||||
normalized = _strip_hebrew_marks(original)
|
|
||||||
if not normalized:
|
|
||||||
return original
|
|
||||||
|
|
||||||
direct = HEBREW_WORD_TRANSLITERATIONS.get(normalized)
|
|
||||||
if direct:
|
|
||||||
return direct
|
|
||||||
|
|
||||||
if normalized.startswith("ו") and len(normalized) > 1:
|
|
||||||
rest = HEBREW_WORD_TRANSLITERATIONS.get(normalized[1:])
|
|
||||||
if rest:
|
|
||||||
return f"ve-{rest}"
|
|
||||||
|
|
||||||
if normalized.startswith("ה") and len(normalized) > 1:
|
|
||||||
rest = HEBREW_WORD_TRANSLITERATIONS.get(normalized[1:])
|
|
||||||
if rest:
|
|
||||||
return f"ha-{rest}"
|
|
||||||
|
|
||||||
return _fallback_hebrew_transliteration(normalized)
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_synthesis_input(text: str, language: str, voice: str) -> tuple[str, str]:
|
|
||||||
if language.lower().startswith("he") and HEBREW_CHAR_RE.search(text):
|
|
||||||
spoken = HEBREW_WORD_RE.sub(_transliterate_hebrew_word, text)
|
|
||||||
return spoken, "en-us"
|
|
||||||
return text, voice
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_voice(req: TtsRequest) -> str:
|
def _resolve_voice(req: TtsRequest) -> str:
|
||||||
if req.voice:
|
if req.voice:
|
||||||
return req.voice.strip()
|
return req.voice.strip()
|
||||||
@@ -299,15 +115,14 @@ def tts(req: TtsRequest) -> Response:
|
|||||||
raise HTTPException(status_code=400, detail="text is required")
|
raise HTTPException(status_code=400, detail="text is required")
|
||||||
|
|
||||||
voice = _resolve_voice(req)
|
voice = _resolve_voice(req)
|
||||||
spoken_text, synth_voice = _prepare_synthesis_input(req.text, req.language, voice)
|
|
||||||
args = [
|
args = [
|
||||||
"--stdout",
|
"--stdout",
|
||||||
"-v", synth_voice,
|
"-v", voice,
|
||||||
"-s", str(max(80, min(450, req.rate))),
|
"-s", str(max(80, min(450, req.rate))),
|
||||||
"-p", str(max(0, min(99, req.pitch))),
|
"-p", str(max(0, min(99, req.pitch))),
|
||||||
"-a", str(max(0, min(200, req.volume))),
|
"-a", str(max(0, min(200, req.volume))),
|
||||||
]
|
]
|
||||||
wav = _run_espeak(args, spoken_text.encode("utf-8"))
|
wav = _run_espeak(args, req.text.encode("utf-8"))
|
||||||
if not wav:
|
if not wav:
|
||||||
raise HTTPException(status_code=500, detail="espeak-ng returned empty stdout")
|
raise HTTPException(status_code=500, detail="espeak-ng returned empty stdout")
|
||||||
return Response(content=wav, media_type="audio/wav")
|
return Response(content=wav, media_type="audio/wav")
|
||||||
@@ -338,9 +153,9 @@ def tts(req: TtsRequest) -> Response:
|
|||||||
PHONEME_DURATION_RE = re.compile(r"^\s*\S+\s+(\d+)\s+", re.MULTILINE)
|
PHONEME_DURATION_RE = re.compile(r"^\s*\S+\s+(\d+)\s+", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
def _estimate_total_ms(req: TtsRequest, voice: str, spoken_text: str) -> int:
|
def _estimate_total_ms(req: TtsRequest, voice: str) -> int:
|
||||||
args = ["--pho", "--quiet", "-v", voice, "-s", str(req.rate)]
|
args = ["--pho", "--quiet", "-v", voice, "-s", str(req.rate)]
|
||||||
out = _run_espeak(args, spoken_text.encode("utf-8"))
|
out = _run_espeak(args, req.text.encode("utf-8"))
|
||||||
text = out.decode("utf-8", errors="replace")
|
text = out.decode("utf-8", errors="replace")
|
||||||
total = 0
|
total = 0
|
||||||
for match in PHONEME_DURATION_RE.finditer(text):
|
for match in PHONEME_DURATION_RE.finditer(text):
|
||||||
@@ -360,8 +175,7 @@ def timings(req: TtsRequest):
|
|||||||
if not req.text.strip():
|
if not req.text.strip():
|
||||||
raise HTTPException(status_code=400, detail="text is required")
|
raise HTTPException(status_code=400, detail="text is required")
|
||||||
voice = _resolve_voice(req)
|
voice = _resolve_voice(req)
|
||||||
spoken_text, synth_voice = _prepare_synthesis_input(req.text, req.language, voice)
|
total_ms = _estimate_total_ms(req, voice)
|
||||||
total_ms = _estimate_total_ms(req, synth_voice, spoken_text)
|
|
||||||
|
|
||||||
# Distribute total_ms across whitespace-split words proportional to
|
# Distribute total_ms across whitespace-split words proportional to
|
||||||
# character count. Punctuation-only tokens are folded into the previous
|
# character count. Punctuation-only tokens are folded into the previous
|
||||||
@@ -390,7 +204,7 @@ def timings(req: TtsRequest):
|
|||||||
{
|
{
|
||||||
"text": req.text,
|
"text": req.text,
|
||||||
"language": req.language,
|
"language": req.language,
|
||||||
"voice": synth_voice,
|
"voice": voice,
|
||||||
"words": out_words,
|
"words": out_words,
|
||||||
"durationMs": total_ms,
|
"durationMs": total_ms,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ spec:
|
|||||||
runAsUser: 1654
|
runAsUser: 1654
|
||||||
containers:
|
containers:
|
||||||
- name: biblical-tts
|
- name: biblical-tts
|
||||||
image: localhost/fc-biblical-tts:v20260506-hebrew-translit
|
image: localhost/fc-biblical-tts:v1
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 10402
|
- containerPort: 10402
|
||||||
@@ -532,7 +532,7 @@ spec:
|
|||||||
fsGroupChangePolicy: OnRootMismatch
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: localhost/fc-ttsreader-web:v20260506-phase6
|
image: localhost/fc-ttsreader-web:v20260506-47a88ae
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5217
|
- containerPort: 5217
|
||||||
@@ -568,14 +568,6 @@ spec:
|
|||||||
value: "http://ttsreader-kokoro.fc-ttsreader.svc.cluster.local.:8880"
|
value: "http://ttsreader-kokoro.fc-ttsreader.svc.cluster.local.:8880"
|
||||||
- name: TtsReader__Kokoro__TimeoutSeconds
|
- name: TtsReader__Kokoro__TimeoutSeconds
|
||||||
value: "120"
|
value: "120"
|
||||||
- name: FlowerCore__Tts__BiblicalTts__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Tts__BiblicalTts__BaseUrl
|
|
||||||
value: "http://ttsreader-biblical.fc-ttsreader.svc.cluster.local.:10402"
|
|
||||||
- name: FlowerCore__Tts__BiblicalTts__TimeoutSeconds
|
|
||||||
value: "60"
|
|
||||||
- name: FlowerCore__Tts__BiblicalTts__DefaultLanguage
|
|
||||||
value: "grc"
|
|
||||||
- name: Speech__Alignment__Enabled
|
- name: Speech__Alignment__Enabled
|
||||||
# Cluster-native faster-whisper (Lane F, 2026-04-25). The
|
# Cluster-native faster-whisper (Lane F, 2026-04-25). The
|
||||||
# ttsreader-align deployment in this manifest wraps
|
# ttsreader-align deployment in this manifest wraps
|
||||||
@@ -611,8 +603,6 @@ spec:
|
|||||||
# the writable PVC mount.
|
# the writable PVC mount.
|
||||||
- name: TtsReader__Preview__CacheDirectory
|
- name: TtsReader__Preview__CacheDirectory
|
||||||
value: "/data/voice-previews"
|
value: "/data/voice-previews"
|
||||||
- name: TtsReader__VoiceLibrary__ReferenceClip__Directory
|
|
||||||
value: "/data/voice-reference-clips"
|
|
||||||
# Sprint E XXL Phase 4γ — content-addressed CDN bundle dir for
|
# Sprint E XXL Phase 4γ — content-addressed CDN bundle dir for
|
||||||
# POST /api/v1/render. Default "wwwroot/cdn" resolves under the
|
# POST /api/v1/render. Default "wwwroot/cdn" resolves under the
|
||||||
# read-only app filesystem, so pin to the writable PVC mount
|
# read-only app filesystem, so pin to the writable PVC mount
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# fc-updater — Update Center GitOps adoption
|
|
||||||
|
|
||||||
**Status:** adopted into `bluejay-infra` on 2026-05-06. The live ArgoCD
|
|
||||||
Application is `infra-fc-updater`, generated by the `bluejay-infra`
|
|
||||||
ApplicationSet with automated sync, `prune: true`, and `selfHeal: true`.
|
|
||||||
|
|
||||||
## Managed manifest set
|
|
||||||
|
|
||||||
`apps/fc-updater/fc-updater.yaml` manages:
|
|
||||||
|
|
||||||
- `Namespace/fc-updater`
|
|
||||||
- `PersistentVolumeClaim/updatecenter-data`
|
|
||||||
- `Deployment/updatecenter-web`
|
|
||||||
- `Service/updatecenter-web`
|
|
||||||
- `Certificate/updatecenter-web-tls`
|
|
||||||
- `Certificate/updatecenter-web-internal-tls`
|
|
||||||
- `IngressRoute/updatecenter-web`
|
|
||||||
- `IngressRoute/updatecenter-web-internal`
|
|
||||||
- `IngressRoute/updatecenter-web-public`
|
|
||||||
|
|
||||||
The Deployment intentionally sets `revisionHistoryLimit: 3` and
|
|
||||||
`strategy.type: Recreate`. The service is singleton + SQLite/local bundle
|
|
||||||
storage on `PersistentVolumeClaim/updatecenter-data`, pinned to
|
|
||||||
`rke2-server`.
|
|
||||||
|
|
||||||
## Runtime dependencies intentionally not stored here
|
|
||||||
|
|
||||||
These live Secrets are pre-existing runtime material and are not committed to
|
|
||||||
Git:
|
|
||||||
|
|
||||||
- `updater-bootstrap-auth`
|
|
||||||
- `updater-signing`
|
|
||||||
- `updater-webhooks`
|
|
||||||
- `cf-origin-flowercore-io`
|
|
||||||
|
|
||||||
Rotate the Cloudflare Origin Certificate through
|
|
||||||
`FlowerCore.Notes/docs/standards/code-signing-rotation-runbook.md`; the
|
|
||||||
shared origin cert must exist in every namespace that serves a
|
|
||||||
`*.flowercore.io` public IngressRoute.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n argocd get application infra-fc-updater
|
|
||||||
kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n fc-updater get deploy,svc,ingressroute,certificate,pvc
|
|
||||||
curl.exe -sk https://update.flowercore.io/api/v1/manifests/_schema
|
|
||||||
```
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
# FlowerCore Update Center
|
|
||||||
# GitOps adoption of the live fc-updater namespace after PUB-1/PUB-3.
|
|
||||||
# Runtime credentials remain in existing K8s Secrets; do not store them here.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: fc-updater
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-data
|
|
||||||
namespace: fc-updater
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: updatecenter-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
storageClassName: longhorn
|
|
||||||
volumeMode: Filesystem
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
# Sized for fleet bundle storage (LocalFsBundleStore.MaxTotalBytes
|
|
||||||
# soft cap at 25 GiB per project_uc_remaining_4_apps_signed_2026_05_06).
|
|
||||||
# Mike Bundle alone is ~5.1 GiB; cluster live capacity is already
|
|
||||||
# 20 GiB after a manual expand. PVCs cannot shrink, so git must track
|
|
||||||
# at least the live size to avoid the OutOfSync loop.
|
|
||||||
storage: 25Gi
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-web
|
|
||||||
namespace: fc-updater
|
|
||||||
labels:
|
|
||||||
app: updatecenter-web
|
|
||||||
app.kubernetes.io/name: updatecenter-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
strategy:
|
|
||||||
# SQLite + local bundle storage live on a single RWO PVC. Recreate avoids
|
|
||||||
# two pods overlapping the same write path during future image bumps.
|
|
||||||
type: Recreate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: updatecenter-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: updatecenter-web
|
|
||||||
spec:
|
|
||||||
nodeName: rke2-server
|
|
||||||
containers:
|
|
||||||
- name: web
|
|
||||||
image: localhost/fc-updater-web:v20260507-public-privacy
|
|
||||||
imagePullPolicy: Never
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
name: http
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_URLS
|
|
||||||
value: http://+:8080
|
|
||||||
- name: FlowerCore__Updater__Database__Provider
|
|
||||||
value: sqlite
|
|
||||||
- name: FlowerCore__Updater__Database__ConnectionString
|
|
||||||
value: Data Source=/data/updatecenter.db
|
|
||||||
- name: FlowerCore__Updater__BundleStorage__LocalFs__RootDirectory
|
|
||||||
value: /data/bundles
|
|
||||||
- name: FlowerCore__Updater__PublicShares__RequirePublicVisibilityOnPublicHosts
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__Code
|
|
||||||
value: 8f3c2a9e7d41
|
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__AppId
|
|
||||||
value: flowercore.faith-ai-mike
|
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__Channel
|
|
||||||
value: stable
|
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__RuntimeId
|
|
||||||
value: win-x64
|
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__DisplayName
|
|
||||||
value: Faith AI Mike Edition
|
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__Headline
|
|
||||||
value: Faith AI Mike Edition
|
|
||||||
- name: FlowerCore__Updater__PublicShares__Links__0__Description
|
|
||||||
value: Private release link for Mike's Faith AI bundle.
|
|
||||||
- name: FlowerCore__Updater__Auth__Bootstrap__Enabled
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Updater__Auth__Bootstrap__Username
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-bootstrap-auth
|
|
||||||
key: username
|
|
||||||
- name: FlowerCore__Updater__Auth__Bootstrap__Password
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-bootstrap-auth
|
|
||||||
key: password
|
|
||||||
- name: FlowerCore__Updater__Auth__Bootstrap__SigningKey
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-bootstrap-auth
|
|
||||||
key: signing-key
|
|
||||||
- name: FlowerCore__Updater__Signing__AutoSignOnPublish
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Updater__Signing__RequireSignatureOnPublish
|
|
||||||
value: "true"
|
|
||||||
- name: FlowerCore__Updater__Signing__PfxBase64
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-signing
|
|
||||||
key: pfx-base64
|
|
||||||
- name: FlowerCore__Updater__Signing__PfxPassword
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-signing
|
|
||||||
key: pfx-password
|
|
||||||
- name: FlowerCore__Updater__Signing__OpItemReference
|
|
||||||
value: op://FlowerCore/step-ca-codesign
|
|
||||||
- name: FlowerCore__Updater__Signing__TrustAnchorPath
|
|
||||||
value: /etc/flowercore-updater/signing/root-ca.pem
|
|
||||||
- name: FlowerCore__Updater__GitHub__Token
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-webhooks
|
|
||||||
key: github-token
|
|
||||||
- name: FlowerCore__Updater__GitHub__WebhookSecret
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-webhooks
|
|
||||||
key: github-webhook-secret
|
|
||||||
- name: FlowerCore__Updater__Gitea__Token
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-webhooks
|
|
||||||
key: gitea-token
|
|
||||||
- name: FlowerCore__Updater__Gitea__WebhookSecret
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: updater-webhooks
|
|
||||||
key: gitea-webhook-secret
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 15
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
- name: signing
|
|
||||||
mountPath: /etc/flowercore-updater/signing
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: updatecenter-data
|
|
||||||
- name: signing
|
|
||||||
secret:
|
|
||||||
secretName: updater-signing
|
|
||||||
items:
|
|
||||||
- key: root-ca.pem
|
|
||||||
path: root-ca.pem
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-web
|
|
||||||
namespace: fc-updater
|
|
||||||
labels:
|
|
||||||
app: updatecenter-web
|
|
||||||
app.kubernetes.io/name: updatecenter-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: updatecenter-web
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 8080
|
|
||||||
targetPort: http
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-web-tls
|
|
||||||
namespace: fc-updater
|
|
||||||
spec:
|
|
||||||
secretName: updatecenter-web-tls
|
|
||||||
issuerRef:
|
|
||||||
name: step-ca-acme
|
|
||||||
kind: ClusterIssuer
|
|
||||||
dnsNames:
|
|
||||||
- updatecenter.iamworkin.lan
|
|
||||||
- updates.iamworkin.lan
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-web-internal-tls
|
|
||||||
namespace: fc-updater
|
|
||||||
spec:
|
|
||||||
secretName: updatecenter-web-internal-tls
|
|
||||||
issuerRef:
|
|
||||||
name: step-ca-acme
|
|
||||||
kind: ClusterIssuer
|
|
||||||
dnsNames:
|
|
||||||
- updatecenter-internal.iamworkin.lan
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-web
|
|
||||||
namespace: fc-updater
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- match: (Host(`updatecenter.iamworkin.lan`) || Host(`updates.iamworkin.lan`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: updatecenter-web
|
|
||||||
port: 8080
|
|
||||||
tls:
|
|
||||||
secretName: updatecenter-web-tls
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-web-internal
|
|
||||||
namespace: fc-updater
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- match: Host(`updatecenter-internal.iamworkin.lan`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: updatecenter-web
|
|
||||||
port: 8080
|
|
||||||
tls:
|
|
||||||
secretName: updatecenter-web-internal-tls
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: updatecenter-web-public
|
|
||||||
namespace: fc-updater
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- match: (Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: updatecenter-web
|
|
||||||
port: 8080
|
|
||||||
tls:
|
|
||||||
secretName: cf-origin-flowercore-io
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# ArgoCD's bluejay-infra ApplicationSet uses a directory generator and does
|
|
||||||
# not require kustomization.yaml. Keep this anyway as the manifest inventory
|
|
||||||
# and for local `kubectl kustomize apps/fc-updater` previews.
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- fc-updater.yaml
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
# FlowerCore Tenant — retired flowercore.io placeholder.
|
# FlowerCore Tenant — flowercore.io (main brand)
|
||||||
#
|
# Public-facing placeholder landing page served by nginx
|
||||||
# Public flowercore.io/www.flowercore.io routing is now owned by
|
# ArgoCD managed - BlueJay Lab
|
||||||
# apps/fc-landing/fc-landing.yaml. This tenant placeholder remains available
|
|
||||||
# only as an in-cluster service; do not create a duplicate public
|
|
||||||
# IngressRoute here because it competes with fc-landing and requires a
|
|
||||||
# namespace-local cf-origin-flowercore-io Secret.
|
|
||||||
# ArgoCD managed - BlueJay Lab
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
@@ -15,9 +10,15 @@ metadata:
|
|||||||
app.kubernetes.io/part-of: bluejay-infra
|
app.kubernetes.io/part-of: bluejay-infra
|
||||||
flowercore.io/tenant: flowercore
|
flowercore.io/tenant: flowercore
|
||||||
---
|
---
|
||||||
# Landing page HTML
|
# NOTE: The existing cf-origin-flowercore-io secret (covering *.flowercore.io)
|
||||||
apiVersion: v1
|
# must be copied into this namespace. It already exists in other namespaces.
|
||||||
kind: ConfigMap
|
# Copy with: kubectl get secret cf-origin-flowercore-io -n fc-system -o yaml \
|
||||||
|
# | sed 's/namespace: .*/namespace: tenant-flowercore/' \
|
||||||
|
# | kubectl apply -f -
|
||||||
|
---
|
||||||
|
# Landing page HTML
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: flowercore-web-html
|
name: flowercore-web-html
|
||||||
namespace: tenant-flowercore
|
namespace: tenant-flowercore
|
||||||
@@ -307,6 +308,25 @@ spec:
|
|||||||
selector:
|
selector:
|
||||||
app: flowercore-web
|
app: flowercore-web
|
||||||
ports:
|
ports:
|
||||||
- port: 80
|
- port: 80
|
||||||
targetPort: 80
|
targetPort: 80
|
||||||
name: http
|
name: http
|
||||||
|
---
|
||||||
|
# Traefik IngressRoute — public via Cloudflare
|
||||||
|
# Uses existing cf-origin-flowercore-io cert (must be copied to this namespace)
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: flowercore-web
|
||||||
|
namespace: tenant-flowercore
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`flowercore.io`) || Host(`www.flowercore.io`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: flowercore-web
|
||||||
|
port: 80
|
||||||
|
tls:
|
||||||
|
secretName: cf-origin-flowercore-io
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: intranet-web
|
- name: intranet-web
|
||||||
image: localhost/fc-intranet-web:v20260508-brochure-w1
|
image: localhost/fc-intranet-web:v20260505-1108
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5300
|
- containerPort: 5300
|
||||||
|
|||||||
@@ -241,12 +241,8 @@ spec:
|
|||||||
kind: ClusterIssuer
|
kind: ClusterIssuer
|
||||||
dnsNames:
|
dnsNames:
|
||||||
- knowledge.iamworkin.lan
|
- knowledge.iamworkin.lan
|
||||||
# step-ca ACME caps lifetime at 30d; requesting 90d silently capped
|
duration: 2160h # 90d
|
||||||
# made renewBefore=cert-lifetime → perpetual renewal loop (10888+ CRs
|
renewBefore: 720h # 30d
|
||||||
# in 18h on 2026-05-07). Match working 720h/240h pattern from other
|
|
||||||
# FC services.
|
|
||||||
duration: 720h # 30d (step-ca cap)
|
|
||||||
renewBefore: 240h # 10d
|
|
||||||
---
|
---
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: IngressRoute
|
kind: IngressRoute
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,210 +0,0 @@
|
|||||||
# Selenium Grid NetworkPolicy.
|
|
||||||
#
|
|
||||||
# Captured into bluejay-infra 2026-05-07 during the regroup audit. This
|
|
||||||
# NetworkPolicy was previously applied via `kubectl apply` directly to
|
|
||||||
# the cluster with no source-of-truth anywhere — a fresh cluster rebuild
|
|
||||||
# would have lost all of it (including the Selenium Grid → Traefik VIP
|
|
||||||
# allow rule for AAT runs against `*.iamworkin.lan` services).
|
|
||||||
#
|
|
||||||
# The Selenium Grid Deployment + Services themselves are still managed
|
|
||||||
# outside ArgoCD (deployed via raw kubectl from the original Selenium
|
|
||||||
# Grid bring-up). Migrating those into bluejay-infra is a separate lane —
|
|
||||||
# this commit only restores GitOps repeatability for the NetworkPolicy.
|
|
||||||
#
|
|
||||||
# Rules captured from the live cluster's `kubectl get netpol -n selenium
|
|
||||||
# selenium-netpol -o yaml` on 2026-05-07. Originally applied 2026-03-15
|
|
||||||
# (from `metadata.creationTimestamp` before the field was stripped).
|
|
||||||
#
|
|
||||||
# Allows:
|
|
||||||
# - Egress: CoreDNS, intra-namespace pod-to-pod (4442/4443/4444/5555),
|
|
||||||
# Traefik VIP for `*.iamworkin.lan` AAT runs, all FC namespaces on
|
|
||||||
# standard FC service ports (5100/5200/5300/5400/8080), pod CIDR
|
|
||||||
# (10.42.0.0/16) + service CIDR (10.43.0.0/16) for the same ports,
|
|
||||||
# LAN gateway range (10.0.56.0/24) for HTTPS, edge2 CUPS print
|
|
||||||
# (10.0.57.16:5200), public internet 80/443 (excluding RFC1918), and
|
|
||||||
# fc-signage:5190 for the signage AAT lane.
|
|
||||||
# - Ingress: Traefik (4444 + 8089 ACME-solver-style), intra-pod,
|
|
||||||
# telephony / gitea / fc-system / fc-signage namespaces on 4444.
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: selenium-netpol
|
|
||||||
namespace: selenium
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: selenium
|
|
||||||
app.kubernetes.io/component: isolation
|
|
||||||
spec:
|
|
||||||
egress:
|
|
||||||
- ports:
|
|
||||||
- port: 53
|
|
||||||
protocol: UDP
|
|
||||||
- port: 53
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: kube-system
|
|
||||||
- ports:
|
|
||||||
- port: 4442
|
|
||||||
protocol: TCP
|
|
||||||
- port: 4443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5555
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- podSelector: {}
|
|
||||||
- ports:
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.56.200/32
|
|
||||||
- ports:
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5200
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5300
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5400
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5100
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- namespaceSelector: {}
|
|
||||||
- ports:
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5200
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5300
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5400
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5100
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.43.0.0/16
|
|
||||||
- ports:
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5200
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5300
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5400
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5100
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.42.0.0/16
|
|
||||||
- ports:
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8443
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.56.0/24
|
|
||||||
- ports:
|
|
||||||
- port: 5200
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 10.0.57.16/32
|
|
||||||
- ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: TCP
|
|
||||||
- port: 443
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 0.0.0.0/0
|
|
||||||
except:
|
|
||||||
- 172.16.0.0/12
|
|
||||||
- 192.168.0.0/16
|
|
||||||
- ports:
|
|
||||||
- port: 5190
|
|
||||||
protocol: TCP
|
|
||||||
to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: fc-signage
|
|
||||||
ingress:
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: traefik-system
|
|
||||||
ports:
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
- port: 8089
|
|
||||||
protocol: TCP
|
|
||||||
- from:
|
|
||||||
- podSelector: {}
|
|
||||||
ports:
|
|
||||||
- port: 4442
|
|
||||||
protocol: TCP
|
|
||||||
- port: 4443
|
|
||||||
protocol: TCP
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
- port: 5555
|
|
||||||
protocol: TCP
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: telephony
|
|
||||||
ports:
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: gitea
|
|
||||||
ports:
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: fc-system
|
|
||||||
ports:
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: fc-signage
|
|
||||||
ports:
|
|
||||||
- port: 4444
|
|
||||||
protocol: TCP
|
|
||||||
podSelector: {}
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# FlowerCore.WorldBuilder
|
|
||||||
|
|
||||||
ArgoCD-managed manifest for FlowerCore.WorldBuilder.Web — comic / storyboard
|
|
||||||
authoring service that drives ComfyUI for panel image generation and
|
|
||||||
QuestPDF for letter / A4 export.
|
|
||||||
|
|
||||||
Source: `D:\git\FlowerCore\FlowerCore.WorldBuilder` (master)
|
|
||||||
|
|
||||||
## Deployment order
|
|
||||||
|
|
||||||
1. **DNS preflight** — `worldbuilder.iamworkin.lan -> 10.0.56.200` MUST exist
|
|
||||||
in pfSense Unbound before this manifest is applied, or cert-manager
|
|
||||||
HTTP-01 silently exponential-backs-off ~2h.
|
|
||||||
Memory: `feedback_pfsense_dns_required_for_acme`.
|
|
||||||
2. **Image import to ALL RKE2 nodes** — pod can schedule to any of
|
|
||||||
`rke2-server` (10.0.56.11), `rke2-agent1` (10.0.56.12),
|
|
||||||
`rke2-agent2` (10.0.56.13). Build with:
|
|
||||||
```bash
|
|
||||||
bash deploy/build.sh # in FlowerCore.WorldBuilder repo
|
|
||||||
podman save localhost/fc-worldbuilder:v<TAG> -o /tmp/fc-worldbuilder-v<TAG>.tar
|
|
||||||
for h in 10.0.56.11 10.0.56.12 10.0.56.13; do
|
|
||||||
scp /tmp/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/tmp/
|
|
||||||
ssh fcadmin@$h \
|
|
||||||
"sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock \
|
|
||||||
-n k8s.io images import /tmp/fc-worldbuilder-v<TAG>.tar"
|
|
||||||
done
|
|
||||||
```
|
|
||||||
Memory: `feedback_rke2_image_import_per_node_scp`.
|
|
||||||
3. **Bump image tag** in `worldbuilder.yaml` and git push.
|
|
||||||
ArgoCD ApplicationSet picks up within ~3 minutes.
|
|
||||||
4. **First production render** — open `https://worldbuilder.iamworkin.lan`,
|
|
||||||
create World → Character → Storyboard → ExportJob, confirm artifact
|
|
||||||
downloads. ComfyUI lives on BLUEJAY-WS at `http://10.0.56.20:8188`.
|
|
||||||
|
|
||||||
## Health probes
|
|
||||||
|
|
||||||
- `startupProbe` + `readinessProbe`: `httpGet /healthz` (registered explicitly
|
|
||||||
in Program.cs — anonymous, no DB or OpenAPI dependency).
|
|
||||||
- `livenessProbe`: `tcpSocket` as a cheap fallback.
|
|
||||||
Memory: `feedback_k8s_probes_must_not_hit_openapi`,
|
|
||||||
`feedback_k8s_probes_behind_auth_middleware`.
|
|
||||||
|
|
||||||
## Storage
|
|
||||||
|
|
||||||
- Longhorn RWO PVC `worldbuilder-data` (5Gi) mounted at `/data`. SQLite DB
|
|
||||||
lives at `/data/worldbuilder.db`, generated images under `/data/gallery/`,
|
|
||||||
PDF/PNG exports under `/data/exports/`.
|
|
||||||
- DataProtection keys persist to the same SQLite via
|
|
||||||
`AddFlowerCoreDataProtection<WorldBuilderDbContext>` — explicit migration
|
|
||||||
`20260429133417_Initial` already creates `fc_dp_keys`.
|
|
||||||
Memory: `feedback_dataprotection_keys_persist_to_app_dbcontext`,
|
|
||||||
`feedback_intranet_dataprotection_table_must_have_explicit_migration`.
|
|
||||||
|
|
||||||
## Image generation backend
|
|
||||||
|
|
||||||
`FlowerCore:WorldBuilder:ImageGeneration:BaseUrl=http://10.0.56.20:8188` —
|
|
||||||
ComfyUI runs on BLUEJAY-WS Windows (R9700 / gfx1201 / ROCm 7.2.1). Pod reaches
|
|
||||||
the workstation directly across the 10.0.56.0/24 VLAN (no Podman-style host-
|
|
||||||
filter issues — K8s pods route via Calico, which is L3-routed across the
|
|
||||||
VLAN).
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
# FlowerCore.WorldBuilder — comic / storyboard authoring service.
|
|
||||||
#
|
|
||||||
# Deployment + Service + PVC + Certificate + IngressRoute. ArgoCD-managed
|
|
||||||
# end-to-end. See apps/worldbuilder/README.md for the per-deploy runbook.
|
|
||||||
#
|
|
||||||
# Image build (BLUEJAY-WS):
|
|
||||||
# bash deploy/build.sh # in FlowerCore.WorldBuilder repo
|
|
||||||
# podman save localhost/fc-worldbuilder:v<TAG> -o /tmp/fc-worldbuilder-v<TAG>.tar
|
|
||||||
# for h in 10.0.56.11 10.0.56.12 10.0.56.13; do
|
|
||||||
# scp /tmp/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/tmp/
|
|
||||||
# ssh fcadmin@$h "sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/fc-worldbuilder-v<TAG>.tar"
|
|
||||||
# done
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: fc-worldbuilder
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
---
|
|
||||||
# SQLite DB + generated image gallery + PDF/PNG exports.
|
|
||||||
# Longhorn RWO — single replica with `Recreate` rollout strategy keeps it safe.
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: worldbuilder-data
|
|
||||||
namespace: fc-worldbuilder
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
storageClassName: longhorn
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: worldbuilder-web
|
|
||||||
namespace: fc-worldbuilder
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: worldbuilder-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
strategy:
|
|
||||||
# RWO PVC + single replica. Recreate avoids multi-attach overlap.
|
|
||||||
type: Recreate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: worldbuilder-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: worldbuilder-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
annotations:
|
|
||||||
prometheus.io/scrape: "true"
|
|
||||||
prometheus.io/port: "8080"
|
|
||||||
prometheus.io/path: "/metrics/prometheus"
|
|
||||||
spec:
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 1654
|
|
||||||
fsGroupChangePolicy: OnRootMismatch
|
|
||||||
containers:
|
|
||||||
- name: web
|
|
||||||
# Bump tag for each rebuild. Initial deploy: v202605062048
|
|
||||||
image: localhost/fc-worldbuilder:v202605062048
|
|
||||||
imagePullPolicy: Never
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
name: http
|
|
||||||
env:
|
|
||||||
- name: ASPNETCORE_URLS
|
|
||||||
value: "http://+:8080"
|
|
||||||
- name: ASPNETCORE_ENVIRONMENT
|
|
||||||
value: "Production"
|
|
||||||
- name: DOTNET_RUNNING_IN_CONTAINER
|
|
||||||
value: "true"
|
|
||||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
|
||||||
value: "false"
|
|
||||||
# SQLite path overrides (default appsettings uses relative paths).
|
|
||||||
- name: ConnectionStrings__DefaultConnection
|
|
||||||
value: "Data Source=/data/worldbuilder.db"
|
|
||||||
- name: FlowerCore__Database__Provider
|
|
||||||
value: "Sqlite"
|
|
||||||
- name: FlowerCore__Database__ConnectionStrings__Sqlite
|
|
||||||
value: "Data Source=/data/worldbuilder.db"
|
|
||||||
# Generated image gallery + exports persist on /data.
|
|
||||||
- name: FlowerCore__WorldBuilder__ImageStore__RootPath
|
|
||||||
value: "/data/gallery"
|
|
||||||
- name: FlowerCore__WorldBuilder__Export__RootPath
|
|
||||||
value: "/data/exports"
|
|
||||||
# ComfyUI on BLUEJAY-WS (R9700 / gfx1201 / ROCm 7.2.1).
|
|
||||||
- name: FlowerCore__WorldBuilder__ImageGeneration__BaseUrl
|
|
||||||
value: "http://10.0.56.20:8188"
|
|
||||||
- name: FlowerCore__WorldBuilder__ImageGeneration__ClientMode
|
|
||||||
value: "comfyui"
|
|
||||||
resources:
|
|
||||||
# Cluster CPU-request budget runs hot (99% on all 3 nodes at deploy
|
|
||||||
# time) while actual CPU usage is well below capacity. Idle Blazor
|
|
||||||
# Server + SignalR + a single ComfyUI poller uses ~5m, so 25m is
|
|
||||||
# generous. Re-evaluate if active rendering/export workers ever
|
|
||||||
# push past the limit.
|
|
||||||
requests:
|
|
||||||
cpu: 25m
|
|
||||||
memory: 256Mi
|
|
||||||
limits:
|
|
||||||
cpu: 1000m
|
|
||||||
memory: 768Mi
|
|
||||||
# /healthz is registered explicitly in Program.cs (anonymous, no DB
|
|
||||||
# or OpenAPI dependency). Liveness uses tcpSocket as a cheap fallback
|
|
||||||
# in case future middleware changes accidentally gate /healthz.
|
|
||||||
# Memory: feedback_k8s_probes_must_not_hit_openapi,
|
|
||||||
# feedback_k8s_probes_behind_auth_middleware.
|
|
||||||
startupProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
failureThreshold: 30
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: 8080
|
|
||||||
periodSeconds: 10
|
|
||||||
failureThreshold: 3
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
failureThreshold: 3
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1654
|
|
||||||
runAsGroup: 1654
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
- name: tmp
|
|
||||||
mountPath: /tmp
|
|
||||||
- name: logs
|
|
||||||
mountPath: /app/logs
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: worldbuilder-data
|
|
||||||
- name: tmp
|
|
||||||
emptyDir: {}
|
|
||||||
- name: logs
|
|
||||||
emptyDir: {}
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: worldbuilder-web
|
|
||||||
namespace: fc-worldbuilder
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: worldbuilder-web
|
|
||||||
app.kubernetes.io/part-of: flowercore
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: worldbuilder-web
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
---
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: worldbuilder-web-tls
|
|
||||||
namespace: fc-worldbuilder
|
|
||||||
spec:
|
|
||||||
secretName: worldbuilder-web-tls
|
|
||||||
issuerRef:
|
|
||||||
name: step-ca-acme
|
|
||||||
kind: ClusterIssuer
|
|
||||||
dnsNames:
|
|
||||||
- worldbuilder.iamworkin.lan
|
|
||||||
# step-ca ACME provisioner caps lifetime at 30d. Requesting 90d
|
|
||||||
# silently capped to 30d, making renewBefore 720h (30d) equal to the
|
|
||||||
# actual cert lifetime — triggered a perpetual renewal loop that
|
|
||||||
# generated 2365+ CertificateRequest objects in 18h. Match the working
|
|
||||||
# 720h/240h pattern used by every other FC service cert.
|
|
||||||
duration: 720h # 30d (step-ca cap)
|
|
||||||
renewBefore: 240h # 10d
|
|
||||||
---
|
|
||||||
apiVersion: traefik.io/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: worldbuilder-web
|
|
||||||
namespace: fc-worldbuilder
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- match: Host(`worldbuilder.iamworkin.lan`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: worldbuilder-web
|
|
||||||
port: 80
|
|
||||||
tls:
|
|
||||||
secretName: worldbuilder-web-tls
|
|
||||||
@@ -22,16 +22,10 @@ public sealed class FleetManifestLintTests
|
|||||||
// (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS — but
|
// (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS — but
|
||||||
// PUT/PATCH/DELETE must still 404 at the route. Anything wider than this
|
// PUT/PATCH/DELETE must still 404 at the route. Anything wider than this
|
||||||
// set should fail this lint.
|
// set should fail this lint.
|
||||||
//
|
|
||||||
// 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)
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ package bluejayinfra.public_readwrite_allowlist
|
|||||||
# PUT/PATCH/DELETE must still 404 at the route. Any host in this set MUST
|
# PUT/PATCH/DELETE must still 404 at the route. Any host in this set MUST
|
||||||
# include all four required methods AND MUST NOT include any forbidden
|
# include all four required methods AND MUST NOT include any forbidden
|
||||||
# method.
|
# method.
|
||||||
public_readwrite_hosts := {
|
public_readwrite_hosts := {"updatecenter.iamworkin.lan", "updates.iamworkin.lan"}
|
||||||
"updatecenter.iamworkin.lan",
|
|
||||||
"updates.iamworkin.lan",
|
|
||||||
"update.flowercore.io",
|
|
||||||
"updates.flowercore.io",
|
|
||||||
}
|
|
||||||
|
|
||||||
required_methods := {"GET", "HEAD", "POST", "OPTIONS"}
|
required_methods := {"GET", "HEAD", "POST", "OPTIONS"}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user