From df02b4c3c34b1b36c8c1bf1986bfeaa556c22508 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 6 May 2026 15:56:59 -0500 Subject: [PATCH] feat(worldbuilder): add fc-worldbuilder ArgoCD app FlowerCore.WorldBuilder runtime deploy: Namespace + Longhorn PVC + Deployment + Service + step-ca Certificate + Traefik IngressRoute. ArgoCD ApplicationSet picks up apps/worldbuilder/ within ~3 minutes. Source: D:\git\FlowerCore\FlowerCore.WorldBuilder@6ed6d26 Initial image: localhost/fc-worldbuilder:v202605062048 (already imported on all 3 RKE2 nodes via ctr images import). DNS preflight done: worldbuilder.iamworkin.lan -> 10.0.56.200 (Traefik VIP) in pfSense Unbound (FlowerCore.DNS provider was 502 at deploy time, fell back to direct pfSense PHP exec via diag_command.php). ImageGen backend: BLUEJAY-WS http://10.0.56.20:8188 (R9700 / gfx1201 / ROCm 7.2.1). One real branding render confirmed working 2026-05-06T20:36:47Z. Memory references in README: - feedback_pfsense_dns_required_for_acme - feedback_rke2_image_import_per_node_scp - feedback_k8s_probes_must_not_hit_openapi - feedback_k8s_probes_behind_auth_middleware - feedback_dataprotection_keys_persist_to_app_dbcontext Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/worldbuilder/README.md | 60 ++++++++ apps/worldbuilder/worldbuilder.yaml | 203 ++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 apps/worldbuilder/README.md create mode 100644 apps/worldbuilder/worldbuilder.yaml diff --git a/apps/worldbuilder/README.md b/apps/worldbuilder/README.md new file mode 100644 index 0000000..60dc5f6 --- /dev/null +++ b/apps/worldbuilder/README.md @@ -0,0 +1,60 @@ +# 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 -o /tmp/fc-worldbuilder-v.tar + for h in 10.0.56.11 10.0.56.12 10.0.56.13; do + scp /tmp/fc-worldbuilder-v.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.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` — 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). diff --git a/apps/worldbuilder/worldbuilder.yaml b/apps/worldbuilder/worldbuilder.yaml new file mode 100644 index 0000000..bfea238 --- /dev/null +++ b/apps/worldbuilder/worldbuilder.yaml @@ -0,0 +1,203 @@ +# 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 -o /tmp/fc-worldbuilder-v.tar +# for h in 10.0.56.11 10.0.56.12 10.0.56.13; do +# scp /tmp/fc-worldbuilder-v.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.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: + requests: + cpu: 100m + 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 + duration: 2160h # 90d + renewBefore: 720h # 30d +--- +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