Compare commits
1 Commits
codex/regr
...
b87df27844
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b87df27844 |
@@ -139,20 +139,6 @@ metadata:
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore Knowledge MCP Tokens"
|
||||
|
||||
---
|
||||
# FlowerCore DMS Manager MCP key (product-manager fan-out). Synced from the
|
||||
# 1Password "FlowerCore DMS MCP Keys" item (field `credential`) into Secret
|
||||
# `dms-mcp-keys`; the deployment reads it as DMS_MCP_API_KEY for the fc_dms
|
||||
# MCP server. presentations/messageboard/segmentdisplay/telephony 1P MCP-key
|
||||
# items also exist and follow this same pattern when added.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: dms-mcp-keys
|
||||
namespace: agent-zero
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore DMS MCP Keys"
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -262,7 +248,7 @@ spec:
|
||||
# use the bridge's Ollama-compatible root via OLLAMA_HOST.
|
||||
mkdir -p /a0/usr/plugins/_model_config
|
||||
cat > /a0/usr/plugins/_model_config/config.json << 'MODELCFG'
|
||||
{"allow_chat_override":true,"chat_model":{"provider":"openai","name":"fc:balanced","api_base":"http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1","ctx_length":32768,"ctx_history":0.7,"vision":false,"kwargs":{"temperature":0,"num_ctx":32768}},"utility_model":{"provider":"openai","name":"fc:cheap","api_base":"http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1","ctx_length":8192,"ctx_input":0.7,"kwargs":{"num_ctx":8192}},"embedding_model":{"provider":"openai","name":"openai/fc:embedding","api_base":"http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1","kwargs":{}}}
|
||||
{"allow_chat_override":true,"chat_model":{"provider":"openai","name":"fc:balanced","api_base":"http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1","ctx_length":8192,"ctx_history":0.7,"vision":false,"kwargs":{"temperature":0,"num_ctx":8192}},"utility_model":{"provider":"openai","name":"fc:cheap","api_base":"http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1","ctx_length":8192,"ctx_input":0.7,"kwargs":{"num_ctx":8192}},"embedding_model":{"provider":"openai","name":"openai/fc:embedding","api_base":"http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1","kwargs":{}}}
|
||||
MODELCFG
|
||||
# Strip heredoc indentation
|
||||
sed -i 's/^ //' /a0/usr/plugins/_model_config/config.json
|
||||
@@ -290,7 +276,7 @@ spec:
|
||||
fi
|
||||
|
||||
export A0_SET_mcp_servers="$(
|
||||
python3 -c 'import json, os; servers = {}; chat_key = os.getenv("CHAT_MCP_API_KEY"); knowledge_enabled = os.getenv("KNOWLEDGE_MCP_ENABLED", "false").lower() == "true"; token = os.getenv("KNOWLEDGE_MCP_BEARER_TOKEN", "") if knowledge_enabled else ""; chat_key and servers.setdefault("fc_chat", {"type": "streamable-http", "url": "http://chat-web.fc-chat.svc/mcp", "headers": {"X-Api-Key": chat_key}}); token and servers.setdefault("fc_knowledge", {"type": "streamable-http", "url": os.getenv("KNOWLEDGE_MCP_URL", "http://knowledge-web.knowledge.svc/mcp"), "headers": {"Authorization": f"Bearer {token}"}}); dms_key = os.getenv("DMS_MCP_API_KEY"); dms_key and servers.setdefault("fc_dms", {"type": "streamable-http", "url": os.getenv("DMS_MCP_URL", "http://dms-web.fc-dms.svc/mcp"), "headers": {"X-Api-Key": dms_key}}); print(json.dumps({"mcpServers": servers}, separators=(",", ":")))'
|
||||
python3 -c 'import json, os; servers = {}; chat_key = os.getenv("CHAT_MCP_API_KEY"); knowledge_enabled = os.getenv("KNOWLEDGE_MCP_ENABLED", "false").lower() == "true"; token = os.getenv("KNOWLEDGE_MCP_BEARER_TOKEN", "") if knowledge_enabled else ""; chat_key and servers.setdefault("fc_chat", {"type": "streamable-http", "url": "http://chat-web.fc-chat.svc/mcp", "headers": {"X-Api-Key": chat_key}}); token and servers.setdefault("fc_knowledge", {"type": "streamable-http", "url": os.getenv("KNOWLEDGE_MCP_URL", "http://knowledge-web.knowledge.svc/mcp"), "headers": {"Authorization": f"Bearer {token}"}}); print(json.dumps({"mcpServers": servers}, separators=(",", ":")))'
|
||||
)"
|
||||
# Run the original entrypoint
|
||||
exec /exe/initialize.sh $BRANCH
|
||||
@@ -299,7 +285,7 @@ spec:
|
||||
env:
|
||||
# Agent identity
|
||||
- name: AGENT_NAME
|
||||
value: "Blue Jay"
|
||||
value: "Blue Jay (NUC)"
|
||||
# Chat model — routed through FlowerCore LLM Bridge (ADR-088)
|
||||
# so spend is tracked and tier aliases (fc:cheap/fc:balanced/fc:deep)
|
||||
# dispatch to Ollama or Anthropic via a single OpenAI-compat endpoint.
|
||||
@@ -358,7 +344,7 @@ spec:
|
||||
- name: A0_SET_browser_model_provider
|
||||
value: "ollama"
|
||||
- name: A0_SET_browser_model_name
|
||||
value: "qwen2.5:7b"
|
||||
value: "gemma3:4b"
|
||||
- name: A0_SET_browser_model_api_base
|
||||
value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080"
|
||||
- name: A0_SET_browser_model_api_key
|
||||
@@ -367,7 +353,7 @@ spec:
|
||||
name: fc-llm-bridge-api-keys
|
||||
key: agent-zero-k8s
|
||||
- name: A0_SET_browser_model_vision
|
||||
value: "false"
|
||||
value: "true"
|
||||
- name: OLLAMA_HOST
|
||||
value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080"
|
||||
- name: FLOWERCORE_AGENTZERO_OLLAMA_URL
|
||||
@@ -407,20 +393,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: knowledge-mcp-tokens
|
||||
key: password
|
||||
# FlowerCore DMS Manager MCP (dynamic message signs) — first of the
|
||||
# product-manager MCP fan-out. dms-web /mcp requires X-Api-Key; the key
|
||||
# is synced from 1Password "FlowerCore DMS MCP Keys" (field credential)
|
||||
# by the dms-mcp-keys OnePasswordItem CRD above. Same builder+env+netpol
|
||||
# pattern extends to presentations/messageboard/segmentdisplay/telephony
|
||||
# (all have 1P MCP-key items). MySQL + Signage still need 1P MCP items
|
||||
# provisioned before they can join (mysql-web /mcp 401s with no key today).
|
||||
- name: DMS_MCP_URL
|
||||
value: "http://dms-web.fc-dms.svc/mcp"
|
||||
- name: DMS_MCP_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dms-mcp-keys
|
||||
key: credential
|
||||
# Print.Web — Thermal printer service on edge2.
|
||||
# PRINT_WEB_URL: internal HTTP (bypasses Traefik TLS — print_web.py
|
||||
# runs in-cluster and can reach edge2 directly on the PROD VLAN).
|
||||
@@ -665,19 +637,6 @@ spec:
|
||||
ports:
|
||||
- port: 5300
|
||||
protocol: TCP
|
||||
# FlowerCore DMS Manager MCP (product-manager fan-out) — in-cluster
|
||||
# dms-web. NetworkPolicy matches the destination POD port: dms-web svc:80
|
||||
# targets containerPort 8080, so the egress MUST allow 8080 (not the svc
|
||||
# port 80) — same as the fc-chat rule. Allow both for parity.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-dms
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
# Allow internet (for kubectl image pull, etc)
|
||||
- to:
|
||||
- ipBlock:
|
||||
|
||||
@@ -13736,15 +13736,20 @@ data:
|
||||
|
||||
### Active Services
|
||||
|
||||
The fleet spans dozens of services -- Signage (Web + WPF Player), Common Libraries, MySQL Manager, PHP Manager, Telephony, Chat, AiStation, PiManager, Print.Web, Divoom, TtsReader, WorldBuilder, Library, Retail, and more. Each carries hundreds-to-thousands of xUnit tests; the fleet total runs to many thousands of passing tests.
|
||||
|
||||
**Never quote a hard test count from memory** -- counts drift between sprints and stale numbers look more authoritative than they are. Use range language ("dozens of controllers", "hundreds of tests", "thousands fleet-wide") and, when a number actually matters, run the test command and read the live result. The canonical state of counts lives in `MEMORY.md` and `docs/standards/feature-backlog.md`, not in this prompt.
|
||||
| Service | Tests | Key Facts |
|
||||
|---------|-------|-----------|
|
||||
| Signage Web | 3,127 | 17 controllers, 33 services, 26 entities, 32 pages, 154 MCP tools |
|
||||
| Signage WPF Player | 1,700 | 12 screen types, 12 zone controls, LibVLC video, HtmlBundleRenderer |
|
||||
| Common Libraries | 1,189 | UI.Components (427), Operator.Sdk (61), Security (110) |
|
||||
| MySQL Manager | 508 | 135 Operator + 373 Web |
|
||||
| PHP Manager | 423 | 32 Operator + 391 Web |
|
||||
| **Total** | **6,947** | 0 skipped, 0 failures |
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **.NET 10 LTS** -- target `net10.0`, SDK 10.0.100
|
||||
- **Blazor Server** -- Web UI with Blue Jay theme
|
||||
- **WPF** -- Desktop apps (must build with `dotnet.exe` on Windows -- the Linux SDK cannot compile WPF/WinForms)
|
||||
- **WPF** -- Desktop apps (must build with `dotnet.exe` from WSL)
|
||||
- **Entity Framework Core** -- Multi-provider (SQLite, MySQL Pomelo, PostgreSQL, SQL Server)
|
||||
- **gRPC** -- HTTP/2 bidirectional streaming (port 5191)
|
||||
- **KubeOps 9.x** -- C# Kubernetes operators
|
||||
@@ -13764,9 +13769,9 @@ data:
|
||||
|------|---------|
|
||||
| 5190 | HTTP/REST |
|
||||
| 5191 | gRPC/HTTP2 |
|
||||
| 11434 | Ollama API (fleet AI hub VIP `10.0.57.201:11434`) |
|
||||
|
||||
You reach the fleet via Traefik IngressRoutes on `*.iamworkin.lan` (TLS via step-ca). Your own UI is `https://agent-zero.iamworkin.lan`. Don't surface raw NodePort numbers -- they drift.
|
||||
| 30050 | Agent Zero UI |
|
||||
| 11434 | Ollama API |
|
||||
| 30052 | Piper TTS |
|
||||
|
||||
## Technical Standards (Non-Negotiable)
|
||||
|
||||
@@ -13798,32 +13803,6 @@ data:
|
||||
- **`new X509Certificate2(byte[])` in .NET 10** -- Use `X509CertificateLoader.LoadPkcs12()`
|
||||
- **ToString("P0") non-breaking space** -- U+00A0 before percent sign breaks assertions
|
||||
|
||||
## Session Continuity: HANDOFF.md
|
||||
|
||||
When another agent (Claude Code or Codex) runs out of credits or hands off work mid-task, they write a checkpoint to `HANDOFF.md` in the FlowerCore.Notes repo.
|
||||
|
||||
**Location:** `/a0/work/repos/FlowerCore/FlowerCore.Notes/HANDOFF.md`
|
||||
|
||||
**When to read it:**
|
||||
- At the start of any session where you're asked to continue or pick up work
|
||||
- When a user says "Claude ran out of credits" or "pick up where we left off"
|
||||
- When `HANDOFF.md` status field shows `credits-exhausted` or `handed-off`
|
||||
|
||||
**Key sections to check:**
|
||||
- **Reasoning Chain** — what the previous agent figured out (root cause, failed attempts, working hypothesis)
|
||||
- **Suggested Next Steps** — ordered list of what to do, prioritized
|
||||
- **Uncommitted Changes** — work that may exist on disk but not in git
|
||||
- **Blockers** — anything preventing progress
|
||||
|
||||
**What you can do with it:**
|
||||
- Handle quick tasks listed in "Suggested Next Steps" (YAML gen, doc formatting, SSH checks)
|
||||
- Escalate to Claude Code or Codex if the task requires multi-file code changes (beyond your 32K context)
|
||||
- Report findings back by updating the handoff file or telling the user
|
||||
|
||||
**What you should NOT do:**
|
||||
- Don't attempt multi-file refactors from a handoff — escalate those
|
||||
- Don't ignore the "Failed Attempts" section — repeating them wastes time
|
||||
|
||||
## Repository Access
|
||||
|
||||
All of Andrew's git repositories are mounted at `/a0/work/repos/` (read-only):
|
||||
@@ -13848,51 +13827,47 @@ data:
|
||||
| PHP Manager | `/a0/work/repos/FlowerCore/FlowerCore.PHP/` |
|
||||
| Notes / Docs | `/a0/work/repos/FlowerCore/FlowerCore.Notes/` |
|
||||
|
||||
## The AI Hub -- GX10 (fleet Ollama)
|
||||
## Available Ollama Models
|
||||
|
||||
The fleet AI runs on the **GX10** -- an ASUS Ascent GX10 = NVIDIA DGX Spark (GB10 Grace-Blackwell, ARM64, CUDA 13, **121 GiB unified memory**) at `10.0.56.14`. Ollama serves on the fleet VIP **`http://10.0.57.201:11434`** with models warm-pinned (`OLLAMA_KEEP_ALIVE=-1`) on local NVMe.
|
||||
Access via `http://host.docker.internal:11434`:
|
||||
|
||||
This GX10 hub **supersedes the retired BLUEJAY-WS R9700 and BLUEJAY-AI (.132) AI roles.** There is no `host.docker.internal`, no port-30050 lane, no edge1-as-Ollama-host story, and no WSL/K3s deployment. The single live deployment is the RKE2 cluster lane (`https://agent-zero.iamworkin.lan`), which reaches Ollama through the FlowerCore LLM Bridge tier router.
|
||||
| Model | Size | Role | Speed | Status |
|
||||
|-------|------|------|-------|--------|
|
||||
| qwen2.5:3b | 1.9 GB | Quick utility tasks | ~190 tok/s | 100% GPU |
|
||||
| mistral:7b | 4.4 GB | Fast summarization | ~110 tok/s | 100% GPU |
|
||||
| granite3.1-dense:8b | 5 GB | Structured JSON/YAML, tool calling | ~92 tok/s | 100% GPU |
|
||||
| deepseek-r1:8b | 5.2 GB | Reasoning (compact) | ~73 tok/s | 100% GPU |
|
||||
| qwen3-vl:8b | 6.1 GB | Fast lightweight vision | ~76 tok/s | 100% GPU |
|
||||
| deepseek-ocr | 6.7 GB | Document OCR | ~167 tok/s | 100% GPU |
|
||||
| translategemma:12b | 8.1 GB | Translation (55 languages) | ~54 tok/s | 100% GPU |
|
||||
| phi4:14b | 9.1 GB | .NET-focused reasoning, architecture | ~60 tok/s | 100% GPU |
|
||||
| devstral:24b | 14 GB | Agentic coding specialist (Mistral) | needs ReBAR | blocked |
|
||||
| gemma3:27b | 17 GB | Vision + text, browser model | needs ReBAR | blocked |
|
||||
| qwen3-coder:30b | 19 GB | Advanced code generation | needs ReBAR | blocked |
|
||||
| deepseek-r1:32b | 20 GB | Deep reasoning (direct API) | needs ReBAR | blocked |
|
||||
| qwen3:32b | 20 GB | Chat brain (JSON tool-call mode) | needs ReBAR | blocked |
|
||||
| nomic-embed-text | 274 MB | Embeddings (768 dims, RAG/memory) | N/A | 100% GPU |
|
||||
|
||||
| Model | Role | Tool-calling? |
|
||||
|-------|------|---------------|
|
||||
| `qwen2.5:14b` | **Chat brain** (`fc:balanced`) -- agentic loop, code, architecture | YES (proven live) |
|
||||
| `qwen2.5:7b` | **Utility + browser** (`fc:cheap`) -- fast tool-capable tier | YES |
|
||||
| `gemma3:12b` | Vision / image description ONLY (non-agentic path) | NO -- 400 on tools |
|
||||
| `gemma3:4b` | Lightweight vision fallback | NO -- 400 on tools |
|
||||
| `nomic-embed-text` | Embeddings (768 dims) for memory / RAG | N/A (embeddings only) |
|
||||
| `llama3.2:1b` | Tiny utility -- garbles tool output, avoid for the loop | NO (too small) |
|
||||
|
||||
With 121 GiB unified memory, VRAM is never the bottleneck -- `nvidia-smi` reports VRAM "Not Supported"; use `free -h`. Multiple models stay resident at once; Ollama does not need to swap.
|
||||
**VRAM budget**: AMD Radeon AI PRO R9700 32GB -- 3-4 models fit simultaneously. Ollama swaps models automatically.
|
||||
|
||||
### Model Selection by Task
|
||||
|
||||
| Task | Primary | Notes |
|
||||
|------|---------|-------|
|
||||
| C#/.NET code gen | `qwen2.5:14b` | Tool-capable, free/local |
|
||||
| Agentic coding / tool loop | `qwen2.5:14b` | Must be tool-capable -- see rule below |
|
||||
| Code review | `qwen2.5:14b` | Falls back to `qwen2.5:7b` for speed |
|
||||
| Architecture decisions | `qwen2.5:14b` | -- |
|
||||
| K8s manifests / YAML | `qwen2.5:7b` | Fast structured output |
|
||||
| Fast utility | `qwen2.5:7b` | -- |
|
||||
| Screenshot / image description | `gemma3:12b` | Vision-only, NO tool calls in this path |
|
||||
| Embeddings | `nomic-embed-text` | -- |
|
||||
|
||||
## RULE: Models & Tool-Calling (non-negotiable)
|
||||
|
||||
**The whole point of Agent Zero is the agentic tool-calling loop, and it MUST run on a tool-capable model.** The fleet learned this the hard way:
|
||||
|
||||
- **Use the `qwen2.5` family for any turn that may call a tool** -- chat goes through `fc:balanced` -> `qwen2.5:14b`, utility/browser through `fc:cheap` -> `qwen2.5:7b`. Both return proper `tool_calls`. `qwen2.5:14b` tool-calling is **proven live**.
|
||||
- **`gemma3:*` CANNOT call tools.** Ollama returns `400: does not support tools` (even `"tools": null`/`[]`) for the whole gemma3 family. Use it ONLY behind a non-agentic vision/image-description path -- never as the agent brain.
|
||||
- **Models <=3B garble tool output.** `llama3.2:1b` and any sub-3B model will mangle JSON tool calls. Don't route the loop through them.
|
||||
- **`nomic-embed-text` is embeddings-only.** It powers memory/RAG vectors; it cannot chat or call tools.
|
||||
- **qwen2.5 instruct does NOT need `think`.** Do not add a `think` kwarg (that's a qwen3/reasoning gate). Chat kwargs are `{"temperature":0,"num_ctx":32768}`.
|
||||
|
||||
If a turn unexpectedly hits `400: does not support tools` or the model emits literal `<tool_call>` text instead of structured calls, the wiring drifted to a non-tool model -- mob it: report the slot, don't silently degrade.
|
||||
| Task | Primary | Quick Alternative |
|
||||
|------|---------|-------------------|
|
||||
| C#/.NET code gen | qwen3-coder:30b | devstral:24b |
|
||||
| Agentic coding | devstral:24b | qwen3-coder:30b |
|
||||
| Code review | phi4:14b | qwen3-coder:30b |
|
||||
| Architecture decisions | phi4:14b | deepseek-r1:32b |
|
||||
| K8s manifests / YAML | granite3.1-dense:8b | qwen3-coder:30b |
|
||||
| Screenshot analysis | gemma3:27b | qwen3-vl:8b |
|
||||
| Translation | translategemma:12b | -- |
|
||||
| Fast summarization | mistral:7b | qwen2.5:3b |
|
||||
| Deep reasoning | deepseek-r1:32b | phi4:14b |
|
||||
| Embeddings | nomic-embed-text | -- |
|
||||
|
||||
## The Blue Jay Agent Team
|
||||
|
||||
The "Blu" roles below are a **persona vocabulary** for focused sub-agent spawns -- labels for scoped tasks, not a standing fixed-size team. When you are the orchestrator, you spawn focused agents for parallel development using these personas:
|
||||
You work as part of a 14-agent squad. When you are the orchestrator, you spawn focused agents for parallel development:
|
||||
|
||||
### Tier 1 -- Core Development
|
||||
|
||||
@@ -13974,106 +13949,6 @@ data:
|
||||
FlowerCore.{Service}.Operator.Tests/
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
You have custom tools that give you real capabilities. When a user asks you to do something, USE the appropriate tool -- do not say you cannot do it. You are not a generic chatbot; you have hardware access and infrastructure control.
|
||||
|
||||
### print_web -- Thermal Printer (NuPrint 210, 58mm)
|
||||
|
||||
Connected to a real thermal receipt printer. You CAN print barcodes, QR codes, labels, receipts, images, and more.
|
||||
|
||||
| Action | What It Does | Key Args |
|
||||
|--------|-------------|----------|
|
||||
| `barcode` | Print a barcode label | `data`, `symbology` (Code128/UpcA/Ean13/Ean8/Code39/Codabar), `title`, `copies` |
|
||||
| `qr` | Print a QR code | `data`, `label`, `module_size` |
|
||||
| `label` | Print a text label | `title`, `subtitle`, `copies` |
|
||||
| `receipt` | Print a formatted receipt | `header`, `lines` [{left, right, bold?, separator?}], `footer` |
|
||||
| `image` | Print an image | `image_base64` or `image_path`, `label` |
|
||||
| `test` | Print a test page | (no args) |
|
||||
| `url` | Print URL as receipt + QR | `url`, `title` |
|
||||
| `recipe` | Scrape and print a recipe | `url` |
|
||||
| `recipe_print` | Enhanced recipe (Selenium fallback) | `url` |
|
||||
| `ai_summary` | AI-summarize text, optionally print | `text`, `url`, `print_result` |
|
||||
| `product` | Look up product by barcode | `barcode` |
|
||||
| `product_search` | Search product by name | `query` |
|
||||
| `status` | Printer connection status | (no args) |
|
||||
| `paper` | Paper roll level | (no args) |
|
||||
| `queue` | Print queue depth | (no args) |
|
||||
| `hardware` | Hardware diagnostics | (no args) |
|
||||
| `waste` | Paper waste report | `days` |
|
||||
| `drawer` | Open cash drawer | (no args) |
|
||||
| `clear_queue` | Clear print queue | `source` |
|
||||
|
||||
**Barcode auto-detection:** 13 digits = EAN-13, 12 digits = UPC-A, starts with 978/979 = ISBN, otherwise Code128.
|
||||
|
||||
**Example:** User says "print a barcode for 20612000248789" → use `print_web` with `action="barcode"`, `data="20612000248789"`, `symbology="Ean13"`.
|
||||
|
||||
### ssh_remote -- SSH to Infrastructure Nodes
|
||||
|
||||
Execute commands on remote servers via SSH.
|
||||
|
||||
### kubectl_manager -- Kubernetes Cluster
|
||||
|
||||
Manage RKE2 cluster resources, pods, deployments.
|
||||
|
||||
### ollama_model_switch -- Ollama Model Management
|
||||
|
||||
Switch models, check loaded models, manage VRAM.
|
||||
|
||||
### flowercore_build / flowercore_test -- Build and Test
|
||||
|
||||
Build .NET projects and run test suites.
|
||||
|
||||
### qrcode_generator -- Generate QR Code Images
|
||||
|
||||
Generate QR code image files locally.
|
||||
|
||||
### kiwix_search -- Offline Knowledge Base
|
||||
|
||||
Search offline Wikipedia, documentation archives.
|
||||
|
||||
### corpus_search -- Fleet Vector Corpus (Bible / Lexicons / Morphology)
|
||||
|
||||
Semantic search over the fleet knowledge DB at `/a0/usr/vectors/<slug>.db`
|
||||
(Strong's, macula-greek/hebrew, aquifer-bible-dictionary/translation-words/acai,
|
||||
WEB + Berean Bibles). Uses Ollama `nomic-embed-text` to embed the query,
|
||||
computes cosine in Python, returns ranked chunks with source + passage + score.
|
||||
Use this for "what does Genesis 1:1 say", "show me every use of agape",
|
||||
"find dictionary entries for covenant", etc. Faster and more offline-friendly
|
||||
than `intranet_search` for scripture/lexicon queries.
|
||||
|
||||
| Arg | Description |
|
||||
|-----|-------------|
|
||||
| `query` | Search text. Required. |
|
||||
| `limit` | Top-K results (default 8). |
|
||||
| `index` | Optional: `bible-texts`, `lexicons`, `dictionaries`, `morphology`. |
|
||||
| `repo` | Optional repo substring filter (e.g. `world-english-bible`). |
|
||||
| `db` | Optional DB override (absolute path or filename inside `/a0/usr/vectors`). Default picks the largest fleet tier present (workstation-full → pi-edge → bmo-bot). |
|
||||
| `action` | Optional. `stats` returns a markdown inventory of every fleet DB (name/size/index/chunk counts/last-built) without doing a query. Useful for "what's in the corpus?" before picking a specific query. |
|
||||
|
||||
## RULE: Knowledge & RAG (which source to reach for)
|
||||
|
||||
When a question needs grounding in FlowerCore knowledge, reach for sources in this order:
|
||||
|
||||
1. **`fc_knowledge` MCP -- the PRIMARY RAG.** This is the fleet's canonical retrieval layer: vector indexes over the Notes and docs corpora (`notes-md`, `notes-html`, and friends), embedded with `nomic-embed-text` on the GX10 hub. Use it first for "where is X documented", "what does the standard say about Y", ADRs, runbooks, gotchas, and any project/infra knowledge. Embeddings run on the GX10 (`10.0.57.201`) so they are fast now -- no more slow Pi5 embed waits.
|
||||
2. **`corpus_search` (fallback / scripture & lexicons).** Offline vector search over the Bible/lexicon/morphology corpus DBs. Prefer this for scripture, Strong's, Greek/Hebrew word studies, and dictionary lookups. Faster and more offline-friendly than the intranet for those queries.
|
||||
3. **`intranet_search` (fallback).** HTTP search against the Blue Jay Lab Intranet (`https://intranet.iamworkin.lan/api/v1/search`) when `fc_knowledge` is unavailable or the answer lives in intranet-only content.
|
||||
4. **`kiwix_search` (general reference).** Offline Wikipedia/Wiktionary when the question is general-knowledge, not FlowerCore-specific.
|
||||
|
||||
### Offline datasets in the fleet corpus cache
|
||||
|
||||
The shared cache (`corpus-cache/`, manifest: its own `README.md`; see `docs/standards/shared-datasets.md`) holds open-licensed offline data you can query via `corpus_search` / Knowledge indexes:
|
||||
|
||||
- **Bibles:** Berean Standard Bible, World English Bible (public domain), Reina-Valera (Spanish).
|
||||
- **Greek / Hebrew morphology:** MACULA Greek (NT) and MACULA Hebrew (OT) -- morphology + syntax trees, Strong's numbers embedded.
|
||||
- **Strong's & lexicons:** Strong's Exhaustive Concordance (Greek + Hebrew), Tyndale Brief lexicon (TBESG), STEPBible tables.
|
||||
- **Notes / dictionaries / cross-refs:** unfoldingWord Translation Notes/Words, Aquifer Bible Dictionary, Aquifer Study Notes, ACAI entity graph, OpenBible cross-refs, Treasury of Scripture Knowledge.
|
||||
- **General reference:** Wikipedia and Wiktionary ZIMs (via `kiwix_search`).
|
||||
|
||||
The indexing tiers are `bible-texts`, `translation-notes`, `dictionaries`, `morphology`, `strongs`, and `wikipedia`. **Gotcha:** a corpus is queryable only when its on-disk directory name matches the index config exactly -- a mismatch makes the indexer silently skip it.
|
||||
|
||||
**Rule: Never say "I cannot" for something a tool can do.** Check your tools first.
|
||||
|
||||
## Remember
|
||||
|
||||
You are Blue Jay. You guard the nest. You cache knowledge. You mob bugs fearlessly. You sing when the build is green. And you always, always keep one eye on the squirrels.
|
||||
|
||||
@@ -113,12 +113,7 @@ spec:
|
||||
- name: pgdata
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumeClaimTemplates:
|
||||
# apiVersion/kind included deliberately: this STS was created via ArgoCD ServerSideApply,
|
||||
# so the live object carries PVC TypeMeta inside volumeClaimTemplates; omitting it here
|
||||
# leaves the app eternally OutOfSync even though kubectl SSA dry-run shows no change.
|
||||
- apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
- metadata:
|
||||
name: pgdata
|
||||
spec:
|
||||
storageClassName: longhorn
|
||||
|
||||
@@ -46,8 +46,6 @@ spec:
|
||||
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"
|
||||
@@ -56,7 +54,6 @@ spec:
|
||||
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
|
||||
@@ -170,26 +167,3 @@ spec:
|
||||
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).
|
||||
|
||||
@@ -30,15 +30,18 @@ data:
|
||||
FlowerCore__Auth__Oidc__Audience: "chat"
|
||||
FlowerCore__Auth__Oidc__ClientId: "chat"
|
||||
FlowerCore__Database__ConnectionStrings__Sqlite: "Data Source=/data/chat.db"
|
||||
# Ollama target. BLUEJAY-WS remains faster from the workstation, but this lane
|
||||
# proved Chat pods time out reaching 10.0.56.20:11434. Keep generation and
|
||||
# behavior-rule checks on the cluster-routable edge1 endpoint until that route
|
||||
# is fixed; choose models that edge1 actually hosts.
|
||||
FlowerCore__AI__OllamaBaseUrl: "http://10.0.57.201:11434"
|
||||
FlowerCore__AI__DefaultModelName: "gemma3:12b"
|
||||
ChatOptions__BehaviorRuleEngine__OllamaBaseUrl: "http://10.0.57.201:11434"
|
||||
ChatOptions__BehaviorRuleEngine__FallbackOllamaBaseUrl: "http://10.0.57.201:11434"
|
||||
ChatOptions__BehaviorRuleEngine__ModelName: "gemma3:4b"
|
||||
# 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"
|
||||
@@ -48,7 +51,6 @@ data:
|
||||
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__Helpdesk__SentimentEscalation__Enabled: "true"
|
||||
FlowerCore__AI__IrcBridge__Enabled: "true"
|
||||
FlowerCore__AI__IrcBridge__DefaultProfileSlug: "it-helpdesk"
|
||||
FlowerCore__AI__IrcBridge__MentionProfileSlug: "it-helpdesk"
|
||||
@@ -110,8 +112,6 @@ spec:
|
||||
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"
|
||||
@@ -123,12 +123,11 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: chat-web
|
||||
image: localhost/fc-chat-web:v20260614-regroup-ch3-0479a31
|
||||
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
|
||||
|
||||
@@ -14,20 +14,6 @@
|
||||
# cluster-rebuild repeatability. See
|
||||
# feedback_networkpolicies_belong_in_bluejay_infra.md.
|
||||
---
|
||||
# OIDC client secret for the RemoteDesktop end-user sign-in (fleet regroup L9,
|
||||
# 2026-06-12). The Authentik provider `remotedesktop` already exists; the 1P item
|
||||
# `remotedesktop-oidc-client` (vault IAmWorkin) carries issuer_url / client_id /
|
||||
# client_secret, and the 1Password operator mints the same-named K8s Secret that
|
||||
# k8s/web-deployment.yaml (FlowerCore.RemoteDesktop repo) consumes with
|
||||
# optional:true. Gate stays OFF (Q-RD-16) — this is flip-READINESS only.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: remotedesktop-oidc-client
|
||||
namespace: fc-desktop
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/remotedesktop-oidc-client"
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
@@ -65,26 +51,3 @@ spec:
|
||||
port: 8080
|
||||
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).
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# Admin / Helpdesk Console — Infra Finding (Cl-5, ADR-204)
|
||||
|
||||
**Outcome: ZERO new cluster infra required.** The Admin/helpdesk console rides the
|
||||
existing `FlowerCore.DeviceManagement.Web` deploy as routes inside DM.Web (ADR-204).
|
||||
The ingress already in this directory covers every path the admin console serves.
|
||||
|
||||
## What already exists for DM.Web (this directory)
|
||||
|
||||
| Manifest | Resource | Notes |
|
||||
|----------|----------|-------|
|
||||
| `certificate-web.yaml` | cert-manager `Certificate` `fc-devicemgmt-web-tls` | `issuerRef` → `step-ca-acme` `ClusterIssuer`; `dnsNames: [devices.iamworkin.lan]`; `secretName: fc-devicemgmt-web-tls`. DNS preflight gate documented (pfSense A record `devices.iamworkin.lan → 10.0.56.200` required before ACME sync). |
|
||||
| `ingressroute-web.yaml` | Traefik `IngressRoute` `fc-devicemgmt-web` | `entryPoints: [websecure]`, `match: Host(\`devices.iamworkin.lan\`)`, service `fc-devicemgmt-web:80`, `tls.secretName: fc-devicemgmt-web-tls`. |
|
||||
| `service-web.yaml` | `Service` `fc-devicemgmt-web` (ClusterIP, 80→8080) | Owned by the DM.Web deploy. |
|
||||
| `deployment-web.yaml` | `Deployment` `fc-devicemgmt-web` | Currently `replicas: 0` (gated on fc-mysql operator + `flowercore_devicemgmt` DB + 1Password runtime item — see header comment). Not a Cl-5 concern. |
|
||||
| also present | operator RBAC, namespace, network-policy, 1password-item | Full app dir, ArgoCD-managed. |
|
||||
|
||||
## Why the admin console needs nothing new
|
||||
|
||||
The existing IngressRoute matches **`Host(\`devices.iamworkin.lan\`)` with no `PathPrefix`
|
||||
constraint**. Traefik therefore forwards *all* paths on that host to the
|
||||
`fc-devicemgmt-web` service — including any admin/helpdesk routes the DM.Web app exposes
|
||||
under its `FlowerCore:PathBase` (e.g. `/admin`, `/helpdesk`). The same TLS secret
|
||||
(`fc-devicemgmt-web-tls`) and the same step-ca ACME `Certificate` already protect them.
|
||||
|
||||
This matches the established TLS-only-app pattern (e.g. `apps/fc-library/fc-library.yaml`,
|
||||
`apps/fc-retail/fc-retail.yaml`): `Certificate` (issuerRef `step-ca-acme` ClusterIssuer) +
|
||||
host-matched `IngressRoute` sharing the `secretName`. Per ADR-204 the admin console's
|
||||
Deployment/Service stay with the DM.Web deploy — no separate workload is created.
|
||||
|
||||
ArgoCD repo URL convention (for reference, not changed here):
|
||||
`http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git`
|
||||
(internal HTTP — step-ca cert isn't trusted by ArgoCD). Apps in `apps/*` are picked up by
|
||||
the `bluejay-infra` ApplicationSet directory generator; this dir has no `kustomization.yaml`,
|
||||
consistent with that pattern.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Ride DM.Web at a PathBase path → no new Certificate, no new IngressRoute, no new
|
||||
Deployment/Service.** Close the lane. The admin console reaches users at
|
||||
`https://devices.iamworkin.lan/<PathBase>` through the manifests already in this directory.
|
||||
|
||||
## Open question (operator decision — NOT actioned)
|
||||
|
||||
**Q-MP-ADMIN-HOST — Distinct admin hostname vs PathBase path under DM.Web?**
|
||||
If the operator ever wants the admin/helpdesk console on its *own* hostname
|
||||
(e.g. `admin.iamworkin.lan`) rather than a path under `devices.iamworkin.lan`, that is a
|
||||
deliberate routing/auth-surface choice, not a mechanical infra add. It would require:
|
||||
|
||||
1. a pfSense / FlowerCore.DNS A record `admin.iamworkin.lan → 10.0.56.200` (ACME preflight
|
||||
gate — step-ca HTTP-01 can't see the CoreDNS wildcard);
|
||||
2. a second cert-manager `Certificate` (`step-ca-acme` ClusterIssuer, `dnsNames:
|
||||
[admin.iamworkin.lan]`, own `secretName`);
|
||||
3. a second host-matched `IngressRoute` → the same `fc-devicemgmt-web:80` service
|
||||
(still no new Deployment/Service — same app behind a second host).
|
||||
|
||||
**Default taken (do not block): PathBase path under DM.Web = zero new infra.** A separate
|
||||
admin hostname is left UNBUILT pending an explicit operator answer to Q-MP-ADMIN-HOST,
|
||||
because it changes the public/auth surface and conflicts with the ADR-204 "routes inside
|
||||
DM.Web" intent. If the answer is "separate host," author only the `Certificate` +
|
||||
`IngressRoute` above (no Deployment/Service), mirroring `apps/fc-library/fc-library.yaml`.
|
||||
|
||||
## Verification
|
||||
|
||||
- `kubectl apply --dry-run=client` (kubectl v1.34.2, no live cluster): `ingressroute-web.yaml`,
|
||||
`service-web.yaml`, `deployment-web.yaml` validated clean. `certificate-web.yaml` returned
|
||||
"no matches for kind Certificate in cert-manager.io/v1" — expected with no cluster
|
||||
connection (CRD discovery unavailable client-side); the YAML shape is identical to the
|
||||
proven `fc-library` Certificate. Server-side dry-run + live host resolution =
|
||||
**fix-forward** (cluster may be unreachable from this lane).
|
||||
- No manifest authored or changed by this lane — finding note only.
|
||||
@@ -11,7 +11,7 @@ metadata:
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
rules:
|
||||
- apiGroups:
|
||||
- flowercore.io
|
||||
- devices.flowercore.io
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
@@ -23,7 +23,7 @@ rules:
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- flowercore.io
|
||||
- devices.flowercore.io
|
||||
resources:
|
||||
- devices/status
|
||||
- devices/finalizers
|
||||
@@ -33,8 +33,6 @@ rules:
|
||||
- devicepolicies/finalizers
|
||||
- remotecommands/status
|
||||
- remotecommands/finalizers
|
||||
- desiredstatedocuments/status
|
||||
- desiredstatedocuments/finalizers
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# FlowerCore.DeviceManagement CRDs.
|
||||
#
|
||||
# These CRDs match the current operator annotations:
|
||||
# [KubernetesEntity(Group = "flowercore.io", ApiVersion = "v1alpha1", ...)]
|
||||
# Keep the schemas intentionally permissive until the DeviceManagement operator
|
||||
# grows enforced CRD validation.
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: devices.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: devices
|
||||
singular: device
|
||||
kind: Device
|
||||
listKind: DeviceList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: devicegroups.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: devicegroups
|
||||
singular: devicegroup
|
||||
kind: DeviceGroup
|
||||
listKind: DeviceGroupList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: devicepolicies.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: devicepolicies
|
||||
singular: devicepolicy
|
||||
kind: DevicePolicy
|
||||
listKind: DevicePolicyList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: remotecommands.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: remotecommands
|
||||
singular: remotecommand
|
||||
kind: RemoteCommand
|
||||
listKind: RemoteCommandList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: desiredstatedocuments.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: desiredstatedocuments
|
||||
singular: desiredstatedocument
|
||||
kind: DesiredStateDocument
|
||||
listKind: DesiredStateDocumentList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
@@ -5,35 +5,21 @@
|
||||
# exist yet; import localhost/fc-devicemgmt-web:<tag> to all schedulable RKE2
|
||||
# nodes before letting ArgoCD sync a live rollout.
|
||||
#
|
||||
# LIVE — 2026-06-11 DeviceManagement product-host enablement.
|
||||
# The current DeviceManagement Web source is SQLite-backed in Program.cs, so
|
||||
# Phase 1 production uses a Longhorn RWO PVC at /data/devicemgmt.db. The
|
||||
# 1Password runtime item stays mounted through env for future MySQL/API-key
|
||||
# cutover, but MySQL is not required for this first product-host rollout.
|
||||
# Image v20260613-g2-66a43c1 is built from FlowerCore.DeviceManagement master
|
||||
# 66a43c1, carrying edge enrollment network completion and SQLite-safe trust-bundle smoke coverage.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: fc-devicemgmt-web-data
|
||||
namespace: fc-devicemgmt
|
||||
labels:
|
||||
app: fc-devicemgmt-web
|
||||
app.kubernetes.io/name: fc-devicemgmt-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
# 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
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -50,13 +36,8 @@ metadata:
|
||||
annotations:
|
||||
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fc-devicemgmt-web
|
||||
@@ -71,8 +52,6 @@ spec:
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
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"
|
||||
@@ -83,12 +62,11 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-devicemgmt-web:v20260614-regroup-c5b8f82
|
||||
image: localhost/fc-devicemgmt-web:v20260512-cx5
|
||||
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.
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:8080"
|
||||
@@ -96,21 +74,29 @@ spec:
|
||||
value: "Production"
|
||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
||||
value: "false"
|
||||
- name: HOME
|
||||
value: "/data"
|
||||
- name: FlowerCore__Service__Name
|
||||
value: "FlowerCore.DeviceManagement.Web"
|
||||
- name: FlowerCore__DeviceManagement__DefaultTenantId
|
||||
value: "system"
|
||||
- name: FlowerCore__Database__Provider
|
||||
value: "Sqlite"
|
||||
- name: FlowerCore__Database__ConnectionStrings__Sqlite
|
||||
value: "Data Source=/data/devicemgmt.db"
|
||||
value: "MySql"
|
||||
- name: FlowerCore__Database__Host
|
||||
value: "mysql.fc-mysql.svc"
|
||||
- name: FlowerCore__Database__Database
|
||||
value: "flowercore_devicemgmt"
|
||||
- name: FlowerCore__Database__User
|
||||
value: "fc_devicemgmt"
|
||||
- name: FlowerCore__Database__Password
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: fc-devicemgmt-runtime
|
||||
key: DB-Password
|
||||
- name: FlowerCore__DeviceManagement__AgentMtls__CaPath
|
||||
value: "/secrets/devicemgmt-mtls/mtls-ca.pem"
|
||||
- name: FlowerCore__DeviceManagement__AgentMtls__ClientCertificatePath
|
||||
value: "/secrets/devicemgmt-mtls/mtls-client.crt"
|
||||
- name: FlowerCore__DeviceManagement__AgentMtls__ClientKeyPath
|
||||
value: "/secrets/devicemgmt-mtls/mtls-client.key"
|
||||
- name: FlowerCore__EventBus__Redis__Configuration
|
||||
value: "redis.fc-redis.svc:6379"
|
||||
resources:
|
||||
@@ -147,17 +133,19 @@ spec:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: logs
|
||||
mountPath: /app/logs
|
||||
- name: devicemgmt-mtls
|
||||
mountPath: /secrets/devicemgmt-mtls
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: fc-devicemgmt-web-data
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: logs
|
||||
emptyDir: {}
|
||||
- name: devicemgmt-mtls
|
||||
secret:
|
||||
secretName: fc-devicemgmt-runtime
|
||||
defaultMode: 0400
|
||||
|
||||
@@ -109,7 +109,6 @@ spec:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: "/metrics"
|
||||
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||
spec:
|
||||
# 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),
|
||||
|
||||
@@ -30,26 +30,3 @@ spec:
|
||||
port: 80
|
||||
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).
|
||||
|
||||
@@ -48,7 +48,7 @@ data:
|
||||
{
|
||||
"FlowerCore": {
|
||||
"Auth": {
|
||||
"Enabled": false,
|
||||
"Enabled": true,
|
||||
"Oidc": {
|
||||
"Enabled": true,
|
||||
"Audience": "dns",
|
||||
@@ -101,7 +101,6 @@ spec:
|
||||
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:
|
||||
@@ -111,7 +110,7 @@ spec:
|
||||
fsGroup: 1654
|
||||
containers:
|
||||
- name: dns-web
|
||||
image: localhost/fc-dns-web:v20260614-wave5-isolation-6124856
|
||||
image: localhost/fc-dns-web:v20260604-oidc-proper
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
@@ -149,7 +148,7 @@ spec:
|
||||
key: client_secret
|
||||
optional: true
|
||||
- name: FlowerCore__Auth__Enabled
|
||||
value: "false"
|
||||
value: "true"
|
||||
- name: FlowerCore__Auth__Oidc__Enabled
|
||||
value: "true"
|
||||
- name: FlowerCore__Auth__Oidc__Audience
|
||||
@@ -303,7 +302,7 @@ spec:
|
||||
fsGroup: 1654
|
||||
containers:
|
||||
- name: dns-acme-webhook
|
||||
image: localhost/fc-dns-acme-webhook:v20260614-wave5-isolation-6124856
|
||||
image: localhost/fc-dns-acme-webhook:v202604290845
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
@@ -46,8 +46,6 @@ spec:
|
||||
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"
|
||||
@@ -56,11 +54,10 @@ spec:
|
||||
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:v20260614-regroup-f20adc1
|
||||
image: localhost/fc-library-web:v20260602-library-owned-deploy-fix1
|
||||
imagePullPolicy: Never
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -170,26 +167,3 @@ spec:
|
||||
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/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"
|
||||
@@ -118,7 +116,6 @@ spec:
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:8080"
|
||||
@@ -164,33 +161,11 @@ spec:
|
||||
name: fc-llm-bridge-api-keys
|
||||
key: spare-2
|
||||
optional: true
|
||||
# Shared.Chat — GX10 Ollama via the INFRA-VLAN NodePort (10.0.56.14:30976),
|
||||
# NOT the PROD-VLAN MetalLB VIP (10.0.57.201:11434). The cross-VLAN path to
|
||||
# the VIP MTU-black-holes LARGE requests: Agent Zero's full prompt (458-line
|
||||
# system prompt + 108 MCP tool descriptions ~150KB) times out / resets mid-
|
||||
# stream there ("Connection reset by peer" in OllamaClient.ChatStreamAsync),
|
||||
# which made AZ loop on "you have sent the same message again". The NodePort is
|
||||
# same-VLAN as the old cluster (no inter-VLAN hop) and carries 150KB fine.
|
||||
# (Small chat/embed requests still work on the VIP; only big agentic prompts broke.)
|
||||
# Shared.Chat — Ollama (edge1 Pi 5 + AI HAT+, matches bridge default)
|
||||
- name: FlowerCore__Chat__OllamaBaseUrl
|
||||
value: "http://10.0.56.14:30976"
|
||||
value: "http://10.0.57.17:11434"
|
||||
- name: FlowerCore__Chat__HttpTimeout
|
||||
value: "00:05:00"
|
||||
# Tier routing override (Wiring A, 2026-06-14): repoint Agent Zero's
|
||||
# chat (Balanced) + util (Cheap) tiers to the GX10's tool-capable
|
||||
# local qwen2.5. Balanced was Anthropic Sonnet (cloud/cost, and the
|
||||
# Anthropic key is currently 401); Cheap was gemma3:4b which CANNOT
|
||||
# call tools (400 does not support tools) — fatal for an agentic loop.
|
||||
# qwen2.5 instruct supports the tool-calling loop; GX10 has the memory.
|
||||
# OllamaBaseUrl above points at the GX10 NodePort (10.0.56.14:30976).
|
||||
- name: FlowerCore__Chat__ModelRouter__DefaultRoutes__Balanced__Provider
|
||||
value: "Ollama"
|
||||
- name: FlowerCore__Chat__ModelRouter__DefaultRoutes__Balanced__Model
|
||||
value: "qwen2.5:14b"
|
||||
- name: FlowerCore__Chat__ModelRouter__DefaultRoutes__Cheap__Provider
|
||||
value: "Ollama"
|
||||
- name: FlowerCore__Chat__ModelRouter__DefaultRoutes__Cheap__Model
|
||||
value: "qwen2.5:7b"
|
||||
# Shared.Chat — Anthropic
|
||||
- name: FlowerCore__Chat__Anthropic__Enabled
|
||||
value: "true"
|
||||
@@ -306,26 +281,3 @@ spec:
|
||||
port: 8080
|
||||
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).
|
||||
|
||||
@@ -131,7 +131,6 @@ spec:
|
||||
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
|
||||
|
||||
@@ -30,26 +30,3 @@ spec:
|
||||
port: 80
|
||||
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:
|
||||
app: messageboard-web
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/health"
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: "/metrics/prometheus"
|
||||
@@ -54,7 +52,6 @@ spec:
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: messageboard-web-config
|
||||
@@ -144,26 +141,3 @@ spec:
|
||||
port: 80
|
||||
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
|
||||
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).
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Certificate for network.iamworkin.lan.
|
||||
#
|
||||
# Preflight gate: network.iamworkin.lan must resolve to 10.0.56.200 before this
|
||||
# Certificate is synced. step-ca ACME cannot see the CoreDNS wildcard
|
||||
# (*.iamworkin.lan -> 10.0.56.200) — it does an HTTP-01 challenge against the
|
||||
# resolved host. The CoreDNS wildcard template covers network.iamworkin.lan, so
|
||||
# resolution exists fleet-wide; do NOT add a pfSense DNS override (this plane is
|
||||
# read-only and holds no pfSense creds). If ACME backs off, confirm the wildcard
|
||||
# resolves first (feedback_pfsense_dns_required_for_acme).
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: fc-network-web-tls
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
annotations:
|
||||
flowercore.io/dns-preflight: "network.iamworkin.lan must resolve to 10.0.56.200 (CoreDNS wildcard) before ACME sync"
|
||||
spec:
|
||||
secretName: fc-network-web-tls
|
||||
issuerRef:
|
||||
name: step-ca-acme
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- network.iamworkin.lan
|
||||
duration: 720h
|
||||
renewBefore: 240h
|
||||
@@ -1,145 +0,0 @@
|
||||
# FlowerCore.Network.Web — the pfSense automation plane (read-only Phase 0, ADR-189).
|
||||
#
|
||||
# Phase 0 is READ-ONLY: the service holds NO pfSense credentials and has no write
|
||||
# path to pfSense anywhere. The only mutating endpoint is POST /api/v1/snapshots,
|
||||
# which ingests a config.xml the noc1 exporter collected READ-ONLY and stores it
|
||||
# (redacted projection) on the PVC. Auth ships gate-OFF.
|
||||
#
|
||||
# Image localhost/fc-network-web:<tag> is built by FlowerCore.Network
|
||||
# scripts/deploy-k8s.sh and imported to all schedulable RKE2 nodes (rke2-server +
|
||||
# rke2-agent1; agent2 retired). imagePullPolicy: Never — bump the tag here, sync
|
||||
# ArgoCD, then scale 0->1 for the RWO PVC and verify the running pod imageID.
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fc-network-web
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
annotations:
|
||||
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
# RWO PVC: a single replica can't be surged (the new pod can't mount the volume
|
||||
# while the old one holds it). maxSurge 0 / maxUnavailable 1 is the rwo-safe shape;
|
||||
# for image bumps scale 0->1 rather than rollout restart.
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fc-network-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/healthz"
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "5340"
|
||||
prometheus.io/path: "/metrics/prometheus"
|
||||
flowercore.io/audit-trace-id: "runtime-activity-trace"
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1654
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-network-web:v20260612-0b5b049
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 5340
|
||||
# fc-safe-to-expose: read-only plane, auth gate-OFF; X-Forwarded-Proto handled
|
||||
# by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:5340"
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Production"
|
||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
||||
value: "false"
|
||||
- name: HOME
|
||||
value: "/data"
|
||||
- name: FlowerCore__Auth__Enabled
|
||||
value: "false"
|
||||
- name: FlowerCore__Database__Provider
|
||||
value: "Sqlite"
|
||||
- name: FlowerCore__Database__ConnectionStrings__Sqlite
|
||||
value: "Data Source=/data/network.db"
|
||||
# Snapshot store + intended-model paths MUST be absolute on the PVC —
|
||||
# the default is relative to the read-only content root.
|
||||
- name: FlowerCore__Network__SnapshotStore__RootDirectory
|
||||
value: "/data/snapshots"
|
||||
- name: FlowerCore__Network__SnapshotStore__UseGitHistory
|
||||
value: "true"
|
||||
- name: FlowerCore__Network__IntendedModel__FilePath
|
||||
value: "/data/intended.json"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 5340
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 5340
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 5340
|
||||
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: fc-network-web-data
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: logs
|
||||
emptyDir: {}
|
||||
@@ -1,32 +0,0 @@
|
||||
# LAN ingress for FlowerCore.Network Web (network.iamworkin.lan).
|
||||
#
|
||||
# RKE2 Traefik has no built-in ACME resolver; TLS certificate ownership stays in
|
||||
# cert-manager Certificate/fc-network-web-tls. Phase 0 is read-only but the POST
|
||||
# ingest endpoint is genuinely needed by the noc1 exporter, so this route allows
|
||||
# all methods (no GET/HEAD-only restriction like fc-dns) — the service itself has
|
||||
# NO pfSense write path, so allowing POST here only reaches the local snapshot
|
||||
# ingest.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: fc-network-web
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`network.iamworkin.lan`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: fc-network-web
|
||||
port: 80
|
||||
tls:
|
||||
secretName: fc-network-web-tls
|
||||
@@ -1,11 +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:
|
||||
- namespace.yaml
|
||||
- pvc.yaml
|
||||
- deployment-web.yaml
|
||||
- service-web.yaml
|
||||
- certificate-web.yaml
|
||||
- ingressroute-web.yaml
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-network
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
@@ -1,27 +0,0 @@
|
||||
# Persistent store for FlowerCore.Network (read-only pfSense automation plane).
|
||||
#
|
||||
# Holds the SQLite snapshot INDEX db (network.db) AND the on-box snapshot store
|
||||
# (data/snapshots): full-fidelity raw config.xml + redacted inventory sidecars +
|
||||
# an on-box git history. Full-fidelity config is on-box ONLY (this PVC); the
|
||||
# service DB / REST / MCP / UI only ever surface the REDACTED projection.
|
||||
# RWO — single replica, scale 0->1 for updates (never rollout restart).
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: fc-network-web-data
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fc-network-web
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
selector:
|
||||
app: fc-network-web
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 5340
|
||||
type: ClusterIP
|
||||
@@ -30,26 +30,3 @@ spec:
|
||||
port: 5400
|
||||
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
|
||||
tls:
|
||||
secretName: presentations-web-tls
|
||||
# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ----
|
||||
# When the operator decides to expose presentations-web publicly, uncomment + update the host,
|
||||
# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2).
|
||||
#
|
||||
# --- IngressRoute ---
|
||||
# apiVersion: traefik.io/v1alpha1
|
||||
# kind: IngressRoute
|
||||
# metadata:
|
||||
# name: presentations-web-public
|
||||
# namespace: fc-presentations
|
||||
# spec:
|
||||
# entryPoints: [websecure]
|
||||
# routes:
|
||||
# - match: Host(`presentations.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||
# kind: Rule
|
||||
# middlewares:
|
||||
# - name: presentations-web-public-profile-header # injects entitlement profile
|
||||
# services:
|
||||
# - name: presentations-web
|
||||
# port: 80
|
||||
# tls: {}
|
||||
# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface.
|
||||
# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one).
|
||||
|
||||
@@ -46,8 +46,6 @@ spec:
|
||||
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"
|
||||
@@ -57,11 +55,10 @@ spec:
|
||||
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:v20260614-regroup-6d81424
|
||||
image: localhost/fc-retail-web:v20260602-retail-owned-deploy-fix5
|
||||
imagePullPolicy: Never
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -171,26 +168,3 @@ spec:
|
||||
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
|
||||
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
|
||||
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:
|
||||
- name: signage-web
|
||||
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:
|
||||
- name: piper
|
||||
image: rhasspy/wyoming-piper:latest
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: PYTHONHTTPSVERIFY
|
||||
value: "0"
|
||||
@@ -524,8 +523,6 @@ spec:
|
||||
app.kubernetes.io/name: ttsreader-web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/health"
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "5217"
|
||||
prometheus.io/path: "/metrics"
|
||||
@@ -535,7 +532,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-ttsreader-web:v20260614-wave5-help-2f096e3
|
||||
image: localhost/fc-ttsreader-web:v20260603-s54cx14-pr29-schema
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5217
|
||||
@@ -605,7 +602,7 @@ spec:
|
||||
- name: TtsReader__Transcription__TimeoutSeconds
|
||||
value: "300"
|
||||
- name: TtsReader__Ollama__BaseUrl
|
||||
value: "http://10.0.57.201:11434"
|
||||
value: "http://10.0.57.17:11434"
|
||||
- name: TtsReader__Ollama__DefaultModel
|
||||
value: "gemma3:4b"
|
||||
- name: TtsReader__Ollama__TimeoutSeconds
|
||||
@@ -765,26 +762,3 @@ spec:
|
||||
port: 5217
|
||||
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
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/"
|
||||
labels:
|
||||
app: updatecenter-web
|
||||
spec:
|
||||
nodeName: rke2-server
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-updater-web:v20260614-regroup-bdf4a4a
|
||||
image: localhost/fc-updater-web:v202605310029-7974fc4
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: http://+:8080
|
||||
|
||||
@@ -12,8 +12,6 @@ All repo-scoped Linux runners use:
|
||||
- `ACCESS_TOKEN` from the `github-runner-token` Secret
|
||||
- `RUN_AS_ROOT=false`
|
||||
- `EPHEMERAL=true`
|
||||
- `DISABLE_AUTO_UPDATE=true` so the runner does not self-update and exit inside
|
||||
the immutable Kubernetes pod
|
||||
- `LABELS=self-hosted,linux,fc-build-linux`
|
||||
- writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and
|
||||
Actions tool cache
|
||||
@@ -133,7 +131,3 @@ from GitHub Actions and verify it lands on an `rke2-linux-*` runner.
|
||||
value does not change.
|
||||
- `Multi-Attach` volume error: only the Common runner uses a RWO PVC and it must
|
||||
stay single-replica. New multi-replica runners use `emptyDir`.
|
||||
- Runner pods repeatedly registering, downloading a newer Actions runner, then
|
||||
exiting with code 4: verify `DISABLE_AUTO_UPDATE=true` is present. The image
|
||||
translates that into `config.sh --disableupdate`; without it, the Deployment
|
||||
controller sees the expected self-update exit as CrashLoopBackOff.
|
||||
|
||||
@@ -195,11 +195,6 @@ spec:
|
||||
# fresh registration occurs. Prevents stale runner accumulation.
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
# Labels used by workflow files: runs-on: [self-hosted, linux, fc-build-linux]
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
@@ -371,11 +366,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -514,11 +504,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -651,11 +636,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -788,11 +768,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -925,11 +900,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1065,11 +1035,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1202,11 +1167,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1339,11 +1299,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1476,11 +1431,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1615,11 +1565,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1754,11 +1699,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1898,11 +1838,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2037,11 +1972,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2176,11 +2106,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2315,11 +2240,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2453,11 +2373,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2592,11 +2507,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2730,11 +2640,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2868,11 +2773,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3006,11 +2906,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3144,11 +3039,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3282,11 +3172,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3421,11 +3306,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3560,11 +3440,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3699,11 +3574,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3838,11 +3708,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3977,11 +3842,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4115,11 +3975,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4254,11 +4109,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4397,11 +4247,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4541,11 +4386,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4681,11 +4521,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
|
||||
@@ -44,32 +44,9 @@ spec:
|
||||
labels:
|
||||
app: intranet-web
|
||||
spec:
|
||||
# notes-corpus-clone: shallow-clones the Notes docs corpus into an emptyDir so
|
||||
# the IntranetSearch indexer has /srv/flowercore-notes/docs to index. Uses the
|
||||
# trailing-dot FQDN (gitea-clusterip.gitea.svc.cluster.local.) to bypass the
|
||||
# CoreDNS *.iamworkin.lan template that otherwise resolves the in-cluster service
|
||||
# name to the Traefik VIP for musl / ndots:5 pods (search-domain appending).
|
||||
# Cred: gitea-corpus-cred (in-ns secret with the canonical 1P bluejay read cred;
|
||||
# mirrors the imperative gitea-flowercore-notes argocd repo-cred pattern).
|
||||
initContainers:
|
||||
- name: notes-corpus-clone
|
||||
image: alpine/git:2.45.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: gitea-corpus-cred
|
||||
env:
|
||||
- name: GIT_LFS_SKIP_SMUDGE
|
||||
value: "1"
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- 'git clone --depth 1 http://$username:$password@gitea-clusterip.gitea.svc.cluster.local.:3000/bluejay/FlowerCore.Notes.git /srv/flowercore-notes && echo "notes corpus cloned; docs entries:" && ls /srv/flowercore-notes/docs | wc -l'
|
||||
volumeMounts:
|
||||
- name: notes-corpus
|
||||
mountPath: /srv/flowercore-notes
|
||||
containers:
|
||||
- name: intranet-web
|
||||
image: localhost/fc-intranet-web:v20260614-wave5-knowledgefleet-1458b4d
|
||||
image: localhost/fc-intranet-web:v20260531-ttsreader-bridge
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5300
|
||||
@@ -79,32 +56,18 @@ spec:
|
||||
value: Production
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:5300"
|
||||
# Embed backend = edge1 Ollama BY IPv4 (10.0.57.17:11434; has
|
||||
# nomic-embed-text). The hostname edge1.iamworkin.lan is UNUSABLE from
|
||||
# cluster pods: it resolves to an unroutable IPv6 (fdbc:56:*) and the
|
||||
# CoreDNS *.iamworkin.lan template maps the name to the Traefik VIP, so
|
||||
# embeds failed with "No route to host". Use a bare pod-routable IPv4.
|
||||
# Backend is BLUEJAY-AI's GPU node (Ollama / Vulkan Iris Xe, INFRA VLAN
|
||||
# 10.0.56.132) which embeds nomic-embed-text in ~160ms vs the edge1 Pi 5's
|
||||
# ~3.2s for the same ~512-token chunk (~20x faster bulk embed), proven
|
||||
# pod-routable from the intranet namespace 2026-06-13. The prior edge1 Pi 5
|
||||
# backend (10.0.57.17:11434) remains a working fallback if BLUEJAY-AI is
|
||||
# down. Bulk embed runs in the background; /health does not depend on it.
|
||||
# Memory: feedback_pi5_nomic_embed_slow.
|
||||
# Bulk corpus indexing on edge1 Pi 5 takes ~6s/chunk × 5665 chunks
|
||||
# ≈ 9 hours. BLUEJAY-WS GPU (R9700, 32GB VRAM) does the same work
|
||||
# in minutes. Memory: feedback_pi5_nomic_embed_slow.
|
||||
- name: IntranetSearch__OllamaBaseUrl
|
||||
value: "http://10.0.57.201:11434"
|
||||
# Notes docs corpus IS now mounted at /srv/flowercore-notes (see the
|
||||
# notes-corpus-clone initContainer + notes-corpus-sync sidecar), so the
|
||||
# IntranetSearch indexer is ENABLED. First-boot bulk embed of the corpus
|
||||
# runs in the background via the edge1 Ollama backend above (~6s/chunk on
|
||||
# the Pi 5); /health readiness does not depend on it, so the pod stays Ready.
|
||||
- name: IntranetSearch__Enabled
|
||||
value: "true"
|
||||
# Page-reading override SQLite persistence on the writable PVC at
|
||||
# /data. This backs pronunciation, notes, corrections, and
|
||||
# page-profile metadata across pod restarts.
|
||||
- name: PageReadingOverrides__DatabasePath
|
||||
value: "/data/page-reading-overrides.db"
|
||||
value: "http://10.0.56.20:11434"
|
||||
# Sprint E Phase 2α — JSON-file-backed PageReadingOverride persistence
|
||||
# on the writable PVC at /data. Without this env var the
|
||||
# intranet falls back to the in-memory store (loses state on
|
||||
# pod restart). Master's PageReadingOverrideOptions binds
|
||||
# PageReadingOverrides:FilePath.
|
||||
- name: PageReadingOverrides__FilePath
|
||||
value: "/data/page-reading-overrides.json"
|
||||
- name: KnowledgeFleetSearch__BaseUrl
|
||||
value: "https://knowledge.iamworkin.lan"
|
||||
- name: KnowledgeFleetSearch__ApiKey
|
||||
@@ -141,40 +104,10 @@ spec:
|
||||
volumeMounts:
|
||||
- name: vector-store
|
||||
mountPath: /data
|
||||
- name: notes-corpus
|
||||
mountPath: /srv/flowercore-notes
|
||||
readOnly: true
|
||||
# notes-corpus-sync: keeps the mounted corpus fresh between pod restarts by
|
||||
# pulling the Notes repo every 30 min (best-effort; the initContainer guarantees
|
||||
# a fresh clone at pod start). Reuses the clone's origin (trailing-dot host + creds).
|
||||
- name: notes-corpus-sync
|
||||
image: alpine/git:2.45.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: gitea-corpus-cred
|
||||
env:
|
||||
- name: GIT_LFS_SKIP_SMUDGE
|
||||
value: "1"
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- 'while true; do sleep 1800; git -C /srv/flowercore-notes pull --depth 1 2>&1 | sed "s/^/[notes-corpus-sync] /" || true; done'
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "10m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
volumeMounts:
|
||||
- name: notes-corpus
|
||||
mountPath: /srv/flowercore-notes
|
||||
volumes:
|
||||
- name: vector-store
|
||||
persistentVolumeClaim:
|
||||
claimName: intranet-vector-store
|
||||
- name: notes-corpus
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
@@ -90,8 +90,6 @@ spec:
|
||||
app.kubernetes.io/name: knowledge-web
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
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"
|
||||
@@ -119,7 +117,6 @@ spec:
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:8080"
|
||||
@@ -168,7 +165,7 @@ spec:
|
||||
# need a separate ingestion lane that can opt into the
|
||||
# workstation GPU when present.
|
||||
- name: FlowerCore__Ollama__BaseUrl
|
||||
value: "http://10.0.57.201:11434"
|
||||
value: "http://10.0.57.17:11434"
|
||||
- name: FlowerCore__Mcp__ApiKey__Key
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -289,26 +286,3 @@ spec:
|
||||
port: 80
|
||||
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).
|
||||
|
||||
@@ -216,24 +216,19 @@ data:
|
||||
- job_name: "pimanager-app"
|
||||
scrape_interval: 15s
|
||||
metrics_path: /metrics
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: true
|
||||
static_configs:
|
||||
- targets: ["piez.iamworkin.lan"]
|
||||
- targets: ["10.0.58.25:5000"]
|
||||
labels:
|
||||
instance: "piez"
|
||||
service: "signalcontrol"
|
||||
service: "pimanager"
|
||||
vlan: "home"
|
||||
device: "pi4-ezconnect"
|
||||
rig: "signal-b"
|
||||
- targets: ["pirelay.iamworkin.lan"]
|
||||
- targets: ["10.0.58.113:5200"]
|
||||
labels:
|
||||
instance: "pirelay"
|
||||
service: "signalcontrol"
|
||||
service: "pimanager"
|
||||
vlan: "home"
|
||||
device: "pi3-ks0212"
|
||||
rig: "signal-a"
|
||||
|
||||
# Epson ET-3750 EcoTank Printer SNMP
|
||||
- job_name: "snmp-printer"
|
||||
@@ -493,12 +488,6 @@ data:
|
||||
- "https://desktop.iamworkin.lan/"
|
||||
- "https://print.iamworkin.lan/healthz" # root 401 behind API key auth; /healthz anonymous 200
|
||||
- "https://dns.iamworkin.lan/healthz" # root auth-gated by OIDC; /healthz anonymous 200
|
||||
- "https://signalcontrol.iamworkin.lan/health" # FlowerCore.SignalControl Pi control plane
|
||||
- "https://flowercore.iamworkin.lan/healthz" # FlowerCore landing
|
||||
- "https://replay.iamworkin.lan/healthz" # FlowerCore.Signage replay surface
|
||||
- "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
|
||||
@@ -922,13 +911,12 @@ data:
|
||||
# of idle and SNMP times out, so 5m for: would page nightly. A
|
||||
# genuine printer outage (jam, disconnected) lasts well over 30m.
|
||||
- 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
|
||||
labels:
|
||||
severity: info
|
||||
alert_channel: irc
|
||||
severity: warning
|
||||
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
|
||||
expr: hrStorageUsed{job="snmp-nas"} / hrStorageSize{job="snmp-nas"} * 100 > 85
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# FlowerCore.Telephony - Blazor Server + REST API + Twilio IVR
|
||||
# ArgoCD managed - BlueJay Lab
|
||||
# Credentials: 1Password → OnePasswordItem CRD → K8s Secret (twilio-credentials)
|
||||
# TTS: Piper on GX10 (10.0.56.14:30850, en_US-amy-medium) — endpoint /tts with {"text":"..."}
|
||||
# edge1 (10.0.57.17:8500, amy-low) kept as warm fallback (revert PiperUrl to roll back)
|
||||
# TTS: Piper on edge1 (10.0.57.17:8500) — endpoint /tts with {"text":"..."}
|
||||
# Public: telephony.flowercore.io via Cloudflare origin cert
|
||||
---
|
||||
apiVersion: v1
|
||||
@@ -63,8 +62,7 @@ data:
|
||||
"Password": "bluejay-asterisk-ari",
|
||||
"Application": "flowercore-pbx",
|
||||
"ReconnectDelaySeconds": 5,
|
||||
"MaxReconnectDelaySeconds": 60,
|
||||
"WebSocketKeepAliveIntervalSeconds": 30
|
||||
"MaxReconnectDelaySeconds": 60
|
||||
},
|
||||
"Sip": {
|
||||
"Domain": "10.0.56.207",
|
||||
@@ -72,7 +70,7 @@ data:
|
||||
"Transport": "udp"
|
||||
},
|
||||
"Tts": {
|
||||
"PiperUrl": "http://10.0.56.14:30850",
|
||||
"PiperUrl": "http://10.0.57.17:8500",
|
||||
"DefaultEngine": "piper",
|
||||
"SampleRate": 8000
|
||||
},
|
||||
@@ -116,9 +114,6 @@ spec:
|
||||
app: telephony-web
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/health"
|
||||
labels:
|
||||
app: telephony-web
|
||||
spec:
|
||||
@@ -156,7 +151,7 @@ spec:
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- name: telephony-web
|
||||
image: localhost/fc-telephony-web:v20260614-arifix
|
||||
image: localhost/fc-telephony-web:v202604252156
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
@@ -166,7 +161,6 @@ spec:
|
||||
ports:
|
||||
- containerPort: 5100
|
||||
name: http
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: Telephony__Twilio__AccountSid
|
||||
valueFrom:
|
||||
@@ -186,16 +180,6 @@ spec:
|
||||
name: twilio-credentials
|
||||
key: DefaultFromNumber
|
||||
optional: true
|
||||
# Env vars OVERRIDE appsettings.Production.json in ASP.NET Core config.
|
||||
# These were previously applied live-only (kubectl) and drifted from git;
|
||||
# codified here so git is the source of truth. Tts__PiperUrl is the real
|
||||
# TTS cutover lever (the configmap "Tts" block is shadowed by this env).
|
||||
- name: Tts__PiperUrl
|
||||
value: "http://10.0.56.14:30850" # GX10 amy-medium; edge1 10.0.57.17:8500 = rollback
|
||||
- name: Ari__Username
|
||||
value: "flowercore"
|
||||
- name: Ari__Password
|
||||
value: "bluejay-asterisk-ari"
|
||||
volumeMounts:
|
||||
- name: telephony-config
|
||||
mountPath: /app/appsettings.Production.json
|
||||
@@ -332,14 +316,7 @@ spec:
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
# Allow Piper TTS on GX10 (10.0.56.14:30850) — primary
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.0.56.14/32
|
||||
ports:
|
||||
- port: 30850
|
||||
protocol: TCP
|
||||
# Allow Piper TTS on edge1 (10.0.57.17:8500) — warm fallback / rollback target
|
||||
# Allow Piper TTS on edge1 (10.0.57.17:8500)
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.0.57.17/32
|
||||
@@ -410,3 +387,4 @@ spec:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,27 +12,28 @@ Source: `D:\git\FlowerCore\FlowerCore.WorldBuilder` (master)
|
||||
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 Ready RKE2 nodes** — pod can currently schedule to
|
||||
`rke2-server` (10.0.56.11) and `rke2-agent1` (10.0.56.12). Build with:
|
||||
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
|
||||
mkdir -p artifacts/deploy
|
||||
podman save localhost/fc-worldbuilder:v<TAG> -o artifacts/deploy/fc-worldbuilder-v<TAG>.tar
|
||||
for h in 10.0.56.11 10.0.56.12; do
|
||||
ssh fcadmin@$h "mkdir -p /home/fcadmin/.fcv"
|
||||
scp artifacts/deploy/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/home/fcadmin/.fcv/
|
||||
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 /home/fcadmin/.fcv/fc-worldbuilder-v<TAG>.tar"
|
||||
-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** — verify
|
||||
`https://worldbuilder.iamworkin.lan/healthz`, open
|
||||
`https://worldbuilder.iamworkin.lan/settings`, and confirm the image backend
|
||||
reports ComfyUI before running an operator-owned render lane.
|
||||
4. **First production render** — open
|
||||
`https://worldbuilder.iamworkin.lan/studio/c32e0000-0000-4000-8000-000000000004`
|
||||
and confirm the Cyberpunk Blue Jay demo prompt loads with five seeded fake
|
||||
generated images. This Sprint 32 visitor-safe profile uses
|
||||
`ClientMode=fake`; switch the image-generation env vars back to ComfyUI only
|
||||
for an operator-owned GPU render lane.
|
||||
|
||||
## Health probes
|
||||
|
||||
@@ -55,8 +56,13 @@ Source: `D:\git\FlowerCore\FlowerCore.WorldBuilder` (master)
|
||||
|
||||
## Image generation backend
|
||||
|
||||
The live internal profile now uses
|
||||
`FlowerCore:WorldBuilder:ImageGeneration:ClientMode=comfyui` with
|
||||
`BaseUrl=http://10.0.56.20:8188` on BLUEJAY-WS (R9700 / gfx1201 / ROCm 7.2).
|
||||
Keep the public host pre-staging disabled unless the five safe-to-expose gates
|
||||
are rechecked; the live GPU lane is operator-owned and internal-only.
|
||||
Sprint 32 pins the Kubernetes profile to
|
||||
`FlowerCore:WorldBuilder:ImageGeneration:ClientMode=fake` with
|
||||
`BaseUrl=http://127.0.0.1:1`. That keeps the public/internal visitor demo
|
||||
deterministic, avoids GPU exposure, and still exercises the studio/gallery
|
||||
surface with persisted generated-image metadata.
|
||||
|
||||
The previous ComfyUI backend target was `http://10.0.56.20:8188` on
|
||||
BLUEJAY-WS (R9700 / gfx1201 / ROCm 7.2.1). Re-enable it only in an
|
||||
operator-owned follow-up that also verifies workstation reachability and image
|
||||
import freshness.
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
#
|
||||
# Image build (BLUEJAY-WS):
|
||||
# bash deploy/build.sh # in FlowerCore.WorldBuilder repo
|
||||
# podman save localhost/fc-worldbuilder:v<TAG> -o artifacts/deploy/fc-worldbuilder-v<TAG>.tar
|
||||
# for h in 10.0.56.11 10.0.56.12; do
|
||||
# scp artifacts/deploy/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/home/fcadmin/.fcv/
|
||||
# ssh fcadmin@$h "sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /home/fcadmin/.fcv/fc-worldbuilder-v<TAG>.tar"
|
||||
# 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
|
||||
@@ -77,8 +77,6 @@ spec:
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
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"
|
||||
@@ -90,12 +88,11 @@ spec:
|
||||
containers:
|
||||
- name: web
|
||||
# Bump tag for each rebuild. Initial deploy: v202605062048
|
||||
image: localhost/fc-worldbuilder:v20260613-e4-about-edd6efc
|
||||
image: localhost/fc-worldbuilder:v202605062048
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:8080"
|
||||
@@ -117,16 +114,14 @@ spec:
|
||||
value: "/data/gallery"
|
||||
- name: FlowerCore__WorldBuilder__Export__RootPath
|
||||
value: "/data/exports"
|
||||
# Operator-approved live GPU lane. Internal-only host targets
|
||||
# BLUEJAY-WS ComfyUI; keep public host pre-staging disabled below.
|
||||
# Visitor-safe Sprint 32 profile: fake backend keeps public demo
|
||||
# rendering deterministic and avoids exposing BLUEJAY-WS GPU.
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__BaseUrl
|
||||
value: "http://10.0.56.20:8188"
|
||||
value: "http://127.0.0.1:1"
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__ClientMode
|
||||
value: "comfyui"
|
||||
value: "fake"
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__BackendId
|
||||
value: "comfyui"
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__VisitorSafe
|
||||
value: "false"
|
||||
value: "fake"
|
||||
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
|
||||
@@ -259,26 +254,3 @@ spec:
|
||||
port: 80
|
||||
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,129 +0,0 @@
|
||||
# authentik-tenant-mapping-sync — GATED manifest staging
|
||||
|
||||
**Status:** GATED (suspended). **ADR:** ADR-198 §2.A P1 (Au-1 / Au-3 substrate). **Pairs:** Codex **Cx2-7**.
|
||||
|
||||
This directory is a **Notes staging area**, NOT a deploy target. The orchestrator relocates
|
||||
`cronjob.yaml` into a `gated/` path **outside** `bluejay-infra/apps/` so ArgoCD's `apps/*`
|
||||
directory generator never picks it up. Nothing here runs until the activation steps below.
|
||||
|
||||
## What this is
|
||||
|
||||
A nightly Kubernetes `CronJob` that runs
|
||||
[`scripts/authentik/authentik-tenant-mapping-sync.py`](../../../scripts/authentik/authentik-tenant-mapping-sync.py)
|
||||
(Notes repo). The script:
|
||||
|
||||
- reads the 1Password Document **`flowercore-tenant-mapping`** (vault `IAmWorkin`, field
|
||||
`mapping`) via **1Password Connect REST** — never the 1Password CLI/desktop (operator hard rule);
|
||||
- parses + light-validates the mapping JSON (schema: [`authentik-oidc-tenant-mapping-schema.md`](../../standards/authentik-oidc-tenant-mapping-schema.md) — `version==1`, `mappings[]` with `authentikGroup` / `fcTenantId` / `fcRole`);
|
||||
- reconciles each distinct `authentikGroup` into Authentik `/api/v3/core/groups/`:
|
||||
create-if-missing, PATCH-managed-markers-on-drift, **never delete or disable unmanaged groups**;
|
||||
- emits structured (Serilog-shaped JSON) logs and exits 0 on success.
|
||||
|
||||
It is the **slow nightly fix-up path**. The **<1s hot path** stays the MCP tool
|
||||
`authentik_sync_tenant_mapping` (schema doc §6.2 force-broadcast). This CronJob does NOT
|
||||
broadcast SignalR — group reconcile is its only side effect; services pick up mapping changes
|
||||
on their own 5-minute 1P refresh.
|
||||
|
||||
## Why it is GATED (two locks)
|
||||
|
||||
1. **`spec.suspend: true`** in `cronjob.yaml` — belt-and-suspenders so even if applied it never fires.
|
||||
2. **Lives outside `apps/`** — staged here in Notes; ArgoCD does not manage it.
|
||||
|
||||
Both must be cleared to go live. This pairs Codex **Cx2-7**: do not activate ahead of the Au-3
|
||||
public-go for tenant self-registration.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `cronjob.yaml` | The suspended `CronJob` + the script-delivery `ConfigMap` (placeholder body). |
|
||||
| `README.md` | This file. |
|
||||
| `scripts/authentik/authentik-tenant-mapping-sync.py` | The reconcile script (canonical source; NOT in this dir). |
|
||||
|
||||
## Secrets (referenced, not invented)
|
||||
|
||||
No secret **values** appear in `cronjob.yaml` — only `secretKeyRef`s:
|
||||
|
||||
- **`AUTHENTIK_TOKEN`** ← `Secret authentik/authentik-credentials` key `BOOTSTRAP_ADMIN_TOKEN`
|
||||
(already exists; the same token `provision-oidc-client.py` reads). **Au-9 caveat:** this is the
|
||||
never-rotated bootstrap token — when `/rotate-password rotate authentik` (Au-9) lands, this
|
||||
CronJob is one of its fan-out consumers.
|
||||
- **`OP_TOKEN`** ← `Secret authentik/tenant-mapping-sync-op-token` key `token`.
|
||||
|
||||
### OP_TOKEN cross-namespace
|
||||
|
||||
The canonical 1P Connect token Secret is `onepassword-system/onepassword-token`, but this
|
||||
CronJob runs in the `authentik` namespace and K8s Secrets are namespace-scoped. Pick one at
|
||||
activation:
|
||||
|
||||
- **Option A (copy, simplest).** Mint a same-namespace copy right before un-suspending:
|
||||
```sh
|
||||
kubectl get secret onepassword-token -n onepassword-system -o jsonpath='{.data.token}' \
|
||||
| base64 -d \
|
||||
| kubectl create secret generic tenant-mapping-sync-op-token -n authentik \
|
||||
--from-file=token=/dev/stdin --dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
(Re-run whenever the Connect token rotates — add this CronJob to the **Au-10** Connect-token
|
||||
fan-out checklist so the copy can't go stale.)
|
||||
- **Option B (CRD, preferred long-term).** Use an `OnePasswordItem` CRD
|
||||
(`feedback_1password_operator_pattern`) so the 1P operator mints/refreshes
|
||||
`authentik/tenant-mapping-sync-op-token` automatically — no manual copy, rotation-safe.
|
||||
|
||||
> If neither secret exists yet, that's fine **while suspended** — the job never schedules.
|
||||
|
||||
## How to ACTIVATE (at Au-3 public-go)
|
||||
|
||||
1. **Pre-flight (workstation dry-run, writes nothing):**
|
||||
```sh
|
||||
export AUTHENTIK_TOKEN=... # or let it read authentik/authentik-credentials via kubectl
|
||||
export OP_TOKEN=... # or rely on credential-helper.sh get_op_token (fcadmin@noc1)
|
||||
python scripts/authentik/authentik-tenant-mapping-sync.py --dry-run --verbose
|
||||
```
|
||||
Confirm the planned create/update set matches the 1P mapping document.
|
||||
2. **Provide `OP_TOKEN` in-cluster** — Option A or B above.
|
||||
3. **Materialize the script ConfigMap from the canonical file** (do NOT hand-edit a copy into
|
||||
`cronjob.yaml` — the embedded body is a deliberate placeholder):
|
||||
```sh
|
||||
kubectl create configmap authentik-tenant-mapping-sync-script -n authentik \
|
||||
--from-file=authentik-tenant-mapping-sync.py=scripts/authentik/authentik-tenant-mapping-sync.py \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
(Or, in the imaged future per ADR-198 §2.B P3, bake the script into `fc-runtime-base` and
|
||||
drop the ConfigMap volume.)
|
||||
4. **Relocate into bluejay-infra** — move `cronjob.yaml` into a `gated/` (or `apps/`) path in
|
||||
`bluejay-infra` per the orchestrator's placement decision. If under `apps/`, ArgoCD will sync it.
|
||||
5. **Un-suspend** — set `spec.suspend: false` (commit in `bluejay-infra` so ArgoCD selfHeal
|
||||
doesn't revert), or one-off:
|
||||
```sh
|
||||
kubectl patch cronjob authentik-tenant-mapping-sync -n authentik \
|
||||
-p '{"spec":{"suspend":false}}'
|
||||
```
|
||||
6. **Smoke (VG-A1):** trigger an immediate run and check the structured logs:
|
||||
```sh
|
||||
kubectl create job --from=cronjob/authentik-tenant-mapping-sync tms-smoke -n authentik
|
||||
kubectl logs -n authentik job/tms-smoke
|
||||
```
|
||||
Then edit a mapping entry in 1P and confirm the next run reconciles the group; the <1s
|
||||
propagation still comes from the MCP `authentik_sync_tenant_mapping` force-broadcast.
|
||||
|
||||
## Rollback
|
||||
|
||||
Re-suspend (`spec.suspend: true`) or delete the CronJob. The script never deletes Authentik
|
||||
groups, so a bad run can only over-create groups present in the mapping — remove any unwanted
|
||||
group by hand in the Authentik admin UI. No data loss path.
|
||||
|
||||
## Idempotency / safety summary
|
||||
|
||||
- Re-running is a no-op when groups already match (mirrors `provision-oidc-client.py`).
|
||||
- Only the managed attribute block (`fc:managed-by` / `fc:tenant` / `fc:role` / optional
|
||||
`fc:label` / `fc:regulated` / `fc:strict-mode`) is asserted; group parent/users/roles are
|
||||
never touched.
|
||||
- Wildcard SuperAdmin entries (`fcTenantId: "*"`) do not create a per-tenant group.
|
||||
- `--dry-run` prints the plan and writes nothing — always run it first.
|
||||
|
||||
## Cross-links
|
||||
|
||||
- [`docs/standards/auth-acl-unattended-lifecycle-plan.md`](../../standards/auth-acl-unattended-lifecycle-plan.md) — ADR-198; Au-1/Au-3 lanes, VG-A1/A2.
|
||||
- [`docs/standards/authentik-oidc-tenant-mapping-schema.md`](../../standards/authentik-oidc-tenant-mapping-schema.md) — the mapping JSON shape + 1P item layout (§2/§3).
|
||||
- [`scripts/authentik/provision-oidc-client.py`](../../../scripts/authentik/provision-oidc-client.py) — sibling idempotent provisioner (same API + posture).
|
||||
- [`scripts/credential-helper.sh`](../../../scripts/credential-helper.sh) — `get_op_token` 1P Connect bootstrap (fcadmin@noc1).
|
||||
@@ -1,151 +0,0 @@
|
||||
# =====================================================================================
|
||||
# authentik-tenant-mapping-sync — GATED nightly CronJob (Au-3 / ADR-198 §2.A P1)
|
||||
#
|
||||
# STATUS: GATED. spec.suspend: true (belt-and-suspenders). This manifest lives in a Notes
|
||||
# STAGING path (docs/gated-manifests/) and is NOT under bluejay-infra apps/, so ArgoCD
|
||||
# does not deploy it. It does NOTHING until Au-3 public-go (see README.md in this dir).
|
||||
#
|
||||
# WHAT IT RUNS: scripts/authentik/authentik-tenant-mapping-sync.py (Notes repo) — reads the
|
||||
# 1Password Document `flowercore-tenant-mapping` via Connect REST and reconciles its
|
||||
# mappings[].authentikGroup entries into Authentik groups (idempotent; never deletes
|
||||
# unmanaged groups). Pairs Codex Cx2-7.
|
||||
#
|
||||
# SECRETS (referenced, NOT invented — no secret VALUES in this file):
|
||||
# AUTHENTIK_TOKEN <- Secret authentik/authentik-credentials key BOOTSTRAP_ADMIN_TOKEN (exists)
|
||||
# OP_TOKEN <- Secret authentik/tenant-mapping-sync-op-token key token
|
||||
# (a copy of onepassword-system/onepassword-token — see README "OP_TOKEN
|
||||
# cross-namespace" for the one-liner that mints it; OR mint via the
|
||||
# OnePasswordItem CRD per feedback_1password_operator_pattern).
|
||||
#
|
||||
# The script is delivered via the ConfigMap below (same pattern as guacamole guac-k8s-sync).
|
||||
# When this lane is libraryized/imaged later (ADR-198 §2.B P3) this ConfigMap can be replaced
|
||||
# by a baked image; for now ConfigMap-delivery keeps the script the single source of truth.
|
||||
# =====================================================================================
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: authentik-tenant-mapping-sync
|
||||
namespace: authentik
|
||||
labels:
|
||||
app.kubernetes.io/name: authentik-tenant-mapping-sync
|
||||
app.kubernetes.io/component: sync
|
||||
app.kubernetes.io/part-of: flowercore-identity
|
||||
flowercore.io/adr: "198"
|
||||
flowercore.io/gated: "true"
|
||||
annotations:
|
||||
flowercore.io/gate: "Au-3 public-go — suspended until tenant self-registration goes live"
|
||||
flowercore.io/pairs-with: "Codex Cx2-7"
|
||||
spec:
|
||||
# GATE: suspended so it never fires until an operator un-suspends at Au-3 public-go.
|
||||
suspend: true
|
||||
# Nightly at 03:17 (off-peak; jittered minute to avoid colliding with other 03:00 jobs).
|
||||
schedule: "17 3 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
startingDeadlineSeconds: 600
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 2
|
||||
activeDeadlineSeconds: 600
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: authentik-tenant-mapping-sync
|
||||
app.kubernetes.io/component: sync
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: sync
|
||||
# python:3.12-slim is sufficient: the script uses only the stdlib (urllib/json/ssl).
|
||||
# No pip install needed. Pin a digest at activation time for air-gap reproducibility.
|
||||
image: python:3.12-slim
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- python3
|
||||
- /scripts/authentik-tenant-mapping-sync.py
|
||||
# NOTE: no --dry-run here -> this is the real reconcile. Operators wanting a
|
||||
# dry-run first should `kubectl create job --from=cronjob/... ` with the arg
|
||||
# appended, or run the script from a workstation. See README.
|
||||
env:
|
||||
- name: AUTHENTIK_URL
|
||||
value: "https://id.iamworkin.lan"
|
||||
- name: OP_CONNECT_URL
|
||||
value: "http://10.0.56.10:8180/v1" # port 8180, NOT 8443
|
||||
- name: OP_VAULT_ID
|
||||
value: "qaphopopkryhbg353ukzhhuqoq" # IAmWorkin
|
||||
- name: TENANT_MAPPING_ITEM
|
||||
value: "flowercore-tenant-mapping"
|
||||
- name: TENANT_MAPPING_FIELD
|
||||
value: "mapping"
|
||||
- name: AUTHENTIK_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-credentials
|
||||
key: BOOTSTRAP_ADMIN_TOKEN
|
||||
- name: OP_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
# A same-namespace copy of onepassword-system/onepassword-token.
|
||||
# See README "OP_TOKEN cross-namespace". Until Au-3 this Secret need
|
||||
# not exist (the job is suspended).
|
||||
name: tenant-mapping-sync-op-token
|
||||
key: token
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: authentik-tenant-mapping-sync-script
|
||||
defaultMode: 0555
|
||||
---
|
||||
# The reconcile script, delivered as a ConfigMap (single source of truth = the Notes repo
|
||||
# scripts/authentik/authentik-tenant-mapping-sync.py). At activation, regenerate this
|
||||
# ConfigMap from the live script so the two never drift, e.g.:
|
||||
# kubectl create configmap authentik-tenant-mapping-sync-script -n authentik \
|
||||
# --from-file=authentik-tenant-mapping-sync.py=scripts/authentik/authentik-tenant-mapping-sync.py \
|
||||
# --dry-run=client -o yaml > docs/gated-manifests/authentik-tenant-sync/configmap.script.yaml
|
||||
# (kept as a placeholder body here so the manifest set is self-describing; the real body is
|
||||
# the script file — DO NOT hand-edit a divergent copy into this ConfigMap.)
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: authentik-tenant-mapping-sync-script
|
||||
namespace: authentik
|
||||
labels:
|
||||
app.kubernetes.io/name: authentik-tenant-mapping-sync
|
||||
app.kubernetes.io/component: sync
|
||||
flowercore.io/gated: "true"
|
||||
annotations:
|
||||
flowercore.io/source: "scripts/authentik/authentik-tenant-mapping-sync.py (Notes repo) — regenerate at activation, do not hand-edit"
|
||||
data:
|
||||
authentik-tenant-mapping-sync.py: |
|
||||
# PLACEHOLDER — regenerate from the canonical script at activation (see annotation above).
|
||||
# The Notes repo file scripts/authentik/authentik-tenant-mapping-sync.py is the source of
|
||||
# truth; embedding a hand-copy here would drift. The orchestrator (or the activation
|
||||
# runbook) materializes this ConfigMap from the live script via `kubectl create configmap
|
||||
# ... --from-file=...` before un-suspending the CronJob.
|
||||
import sys
|
||||
sys.exit("authentik-tenant-mapping-sync ConfigMap not materialized from the canonical "
|
||||
"script — regenerate with kubectl create configmap --from-file before activation.")
|
||||
@@ -1,39 +0,0 @@
|
||||
# Public-TLS substrate (gated)
|
||||
|
||||
**Lane:** Cl-infra-2 (deep-regroup 2026-06-13). **Status:** authored, **NOT applied** — operator-gated.
|
||||
|
||||
This directory holds the Let's Encrypt + isolation substrate for **public** multi-tenant
|
||||
web hosting. It lives **outside `apps/`** on purpose: the bluejay-infra ApplicationSet only
|
||||
reconciles `apps/*`, so nothing here is auto-applied. Applying a cert-manager ACME
|
||||
`ClusterIssuer` registers an ACME account immediately, so these stay inert until the
|
||||
operator opens the web-hosting public-exposure gate (**R-1**).
|
||||
|
||||
## What's here
|
||||
|
||||
| File | What | Activate when |
|
||||
|---|---|---|
|
||||
| `letsencrypt-issuers.yaml` | `letsencrypt-staging` + `letsencrypt-prod` ClusterIssuers (HTTP-01 via Traefik; DNS-01 stub for wildcards) | Public-go. Move to `apps/cluster-issuers/`, **staging first**. |
|
||||
| `tenant-networkpolicy-template.yaml` | Per-tenant default-deny + allowlist NetworkPolicy (Traefik ingress, CoreDNS, own-DB egress only) | Rendered per tenant at provision time (Wh-C2 isolation). |
|
||||
|
||||
## The gate
|
||||
|
||||
Public exposure is **NO-GO** until the §6 go/no-go checklist in
|
||||
[`docs/standards/web-hosting-production-readiness-plan.md`](../../../FlowerCore.Notes/docs/standards/web-hosting-production-readiness-plan.md)
|
||||
is green (currently 14/14 red) **and** the operator explicitly opens R-1. Internal
|
||||
`*.iamworkin.lan` TLS stays on **step-ca** (`apps/fc-dns/fc-dns.yaml` → `step-ca-dns01`);
|
||||
these LE issuers are **only** for public tenant domains.
|
||||
|
||||
## Pairing
|
||||
|
||||
- **Codex Wh-C1** consumes `letsencrypt-staging`/`-prod` for hybrid public TLS on
|
||||
FlowerCore.PHP/MySQL/DNS.
|
||||
- **Codex Wh-C2** consumes the NetworkPolicy template for cross-tenant isolation suites.
|
||||
|
||||
## Activation checklist (public-go)
|
||||
|
||||
1. Wire a public DNS-01 solver (Cloudflare/Namecheap webhook) **or** confirm public tenant
|
||||
domains route HTTP-01 to the cluster ingress.
|
||||
2. `git mv gated/public-tls/letsencrypt-issuers.yaml apps/cluster-issuers/` — staging only.
|
||||
3. Issue one **staging** cert for a throwaway public domain; verify the chain in a browser.
|
||||
4. Flip that tenant's Certificate `issuerRef` to `letsencrypt-prod`; mind LE rate limits.
|
||||
5. Render `tenant-networkpolicy-template.yaml` per tenant; run the Wh-C2 negative suites.
|
||||
@@ -1,78 +0,0 @@
|
||||
# ============================================================================
|
||||
# Let's Encrypt ClusterIssuers — PUBLIC TLS substrate (Cl-infra-2, deep-regroup 2026-06-13)
|
||||
# ============================================================================
|
||||
# GATED. This file lives OUTSIDE apps/ on purpose, so the bluejay-infra
|
||||
# ApplicationSet does NOT auto-apply it. Applying a cert-manager ACME
|
||||
# ClusterIssuer registers an ACME account immediately, so we keep these inert
|
||||
# until the operator opens the web-hosting public-exposure gate (R-1; the §6
|
||||
# go/no-go checklist in docs/standards/web-hosting-production-readiness-plan.md
|
||||
# is currently 14/14 red).
|
||||
#
|
||||
# Pairs with Codex Wh-C1 (FlowerCore.PHP/MySQL/DNS hybrid public TLS) and
|
||||
# Wh-C2 (isolation). Internal *.iamworkin.lan certs STAY on step-ca
|
||||
# (apps/fc-dns/fc-dns.yaml: ClusterIssuer step-ca-dns01) — these LE issuers are
|
||||
# ONLY for public tenant domains.
|
||||
#
|
||||
# TO ACTIVATE (operator public-go):
|
||||
# 1. Confirm a public DNS-01 solver is wired (Cloudflare/Namecheap webhook) OR
|
||||
# that public tenant domains route HTTP-01 to the cluster's public ingress.
|
||||
# 2. Move this file to apps/cluster-issuers/ (the ApplicationSet will create
|
||||
# infra-cluster-issuers and apply it), staging FIRST.
|
||||
# 3. Issue ONE staging cert for a throwaway public domain, verify the chain,
|
||||
# THEN switch that tenant's Certificate issuerRef to letsencrypt-prod.
|
||||
# 4. Mind LE prod rate limits (50 certs/registered-domain/week, 5 dupes/week).
|
||||
#
|
||||
# Registration email is for expiry notices only — adjust to a role address if
|
||||
# desired (astoltz@iamwork.in is the current operator contact).
|
||||
# ----------------------------------------------------------------------------
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-staging
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
flowercore.io/gate: public-tls
|
||||
spec:
|
||||
acme:
|
||||
# LE STAGING — untrusted certs, generous limits. Use this first, always.
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
email: astoltz@iamwork.in
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-staging-account-key
|
||||
solvers:
|
||||
# HTTP-01 via Traefik. Requires the public tenant domain's :80 traffic to
|
||||
# reach the cluster ingress. For wildcard / apex without inbound :80, swap
|
||||
# to the dns01 solver block below (needs a public DNS provider webhook).
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
# --- DNS-01 alternative for wildcards (uncomment + wire a public DNS webhook) ---
|
||||
# - dns01:
|
||||
# webhook:
|
||||
# groupName: acme.flowercore.io # or the cloudflare/namecheap solver
|
||||
# solverName: <public-dns-solver>
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
flowercore.io/gate: public-tls
|
||||
spec:
|
||||
acme:
|
||||
# LE PRODUCTION — trusted certs, strict rate limits. Only after staging proves out.
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: astoltz@iamwork.in
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-account-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
# - dns01:
|
||||
# webhook:
|
||||
# groupName: acme.flowercore.io
|
||||
# solverName: <public-dns-solver>
|
||||
@@ -1,59 +0,0 @@
|
||||
# ============================================================================
|
||||
# Per-tenant NetworkPolicy TEMPLATE — web-hosting isolation (Cl-infra-2 / Wh-C2)
|
||||
# ============================================================================
|
||||
# GATED substrate (outside apps/, not auto-applied). Modeled on the canonical
|
||||
# default-deny + allowlist shape in apps/fc-devicemgmt/network-policy.yaml.
|
||||
#
|
||||
# Purpose: when a public multi-tenant site is provisioned, each tenant's pods
|
||||
# get a NetworkPolicy that (a) default-denies all ingress/egress, then allows
|
||||
# only Traefik ingress + CoreDNS + that tenant's own DB. This enforces the
|
||||
# cross-tenant isolation Wh-C2 verifies with negative suites.
|
||||
#
|
||||
# Replace the {{TENANT}} placeholders and apply alongside the tenant's workload
|
||||
# (the MySQL/PHP managers should emit this when they create a tenant, or a
|
||||
# templating step in apps/ should render it). Kept here as the reference shape.
|
||||
# ----------------------------------------------------------------------------
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: tenant-{{TENANT}}-isolation
|
||||
namespace: fc-tenant-{{TENANT}}
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/tenant-id: "{{TENANT}}"
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
flowercore.io/gate: public-tls
|
||||
spec:
|
||||
podSelector: {} # all pods in the tenant namespace
|
||||
policyTypes: [Ingress, Egress]
|
||||
ingress:
|
||||
# Only Traefik may reach tenant pods (public traffic terminates at Traefik).
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
ports:
|
||||
- { protocol: TCP, port: 80 }
|
||||
- { protocol: TCP, port: 443 }
|
||||
- { protocol: TCP, port: 8080 }
|
||||
egress:
|
||||
# CoreDNS resolution.
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- { protocol: UDP, port: 53 }
|
||||
- { protocol: TCP, port: 53 }
|
||||
# This tenant's OWN MySQL only (NOT other tenants' DBs — that's the isolation).
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
flowercore.io/tenant-id: "{{TENANT}}"
|
||||
app.kubernetes.io/name: mysql
|
||||
ports:
|
||||
- { protocol: TCP, port: 3306 }
|
||||
# NOTE: deliberately NO blanket egress. Add per-tenant allowances explicitly
|
||||
# (object storage, mail relay, etc.) so a compromised tenant pod cannot reach
|
||||
# the rest of the fleet or other tenants.
|
||||
@@ -1,31 +0,0 @@
|
||||
# GX10 Piper TTS — linux/arm64 (built natively on the GX10 / DGX Spark, aarch64).
|
||||
# Serves the telephony /tts contract: POST {"text"} -> 16 kHz/16-bit/mono WAV.
|
||||
# Voice baked into the image so there is no runtime HuggingFace dependency.
|
||||
FROM python:3.12-slim
|
||||
|
||||
# espeak-ng is the phonemizer backend piper-tts uses at synthesis time.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends espeak-ng ca-certificates curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir piper-tts flask numpy
|
||||
|
||||
# Bake the voice model (en_US-amy-medium, 22.05 kHz native) into the image.
|
||||
ARG PIPER_VOICE=en_US-amy-medium
|
||||
ARG VOICE_BASE=https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/amy/medium
|
||||
RUN mkdir -p /voices \
|
||||
&& curl -sSL -o "/voices/${PIPER_VOICE}.onnx" "${VOICE_BASE}/${PIPER_VOICE}.onnx" \
|
||||
&& curl -sSL -o "/voices/${PIPER_VOICE}.onnx.json" "${VOICE_BASE}/${PIPER_VOICE}.onnx.json" \
|
||||
&& test -s "/voices/${PIPER_VOICE}.onnx" \
|
||||
&& test -s "/voices/${PIPER_VOICE}.onnx.json"
|
||||
|
||||
COPY tts_service.py /app/tts_service.py
|
||||
WORKDIR /app
|
||||
|
||||
ENV TTS_PORT=8500 \
|
||||
PIPER_VOICE=en_US-amy-medium \
|
||||
VOICES_DIR=/voices \
|
||||
TARGET_RATE=16000
|
||||
|
||||
EXPOSE 8500
|
||||
CMD ["python", "tts_service.py"]
|
||||
@@ -1,59 +0,0 @@
|
||||
# GX10 Piper TTS — telephony `/tts` endpoint
|
||||
|
||||
CPU Piper TTS serving the telephony `/tts` contract on the **GX10 RKE2 cluster**
|
||||
(ASUS Ascent GX10 / NVIDIA DGX Spark, ARM64, `10.0.56.14`). This is the
|
||||
telephony-TTS-port-to-GX10 (P1) baseline: edge1 parity at higher quality, zero
|
||||
GPU/aarch64 risk, frees telephony off the slow edge1 Pi 5.
|
||||
|
||||
## What it is
|
||||
- `tts_service.py` — Flask app: `POST /tts {"text"}` → **16 kHz / 16-bit / mono WAV**
|
||||
(canonical 44-byte header) + `GET /health`. Voice `en_US-amy-medium` (22.05 kHz
|
||||
native) is numpy-resampled to 16 kHz so it drops straight onto Asterisk's
|
||||
`.sln16` path (telephony strips the 44-byte header). Same wire contract as the
|
||||
edge1 `speech-pipeline` `/tts`, just the TTS half (no STT/Wyoming).
|
||||
- `Dockerfile` — `linux/arm64`, voice baked in (no runtime HuggingFace dep).
|
||||
- `gx10-tts.yaml` — Namespace `tts` + Deployment (CPU-only, **no GPU request** so it
|
||||
co-resides with the GPU-holding Ollama pod) + NodePort Service.
|
||||
|
||||
## This cluster is NOT under the old-cluster ArgoCD (yet)
|
||||
Apply manually with the GX10's own kubectl:
|
||||
```bash
|
||||
ssh -J noc1 -i ~/.ssh/fcadmin_ed25519 bluejay@10.0.56.14
|
||||
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
|
||||
K=/var/lib/rancher/rke2/bin/kubectl
|
||||
$K apply -f gx10-tts.yaml
|
||||
```
|
||||
|
||||
## Build + import (native arm64 on the GX10)
|
||||
```bash
|
||||
docker build -t localhost/fc-gx10-tts:v20260614 .
|
||||
docker save localhost/fc-gx10-tts:v20260614 -o /tmp/t.tar
|
||||
sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/t.tar
|
||||
# manifest uses imagePullPolicy: Never (image lives in containerd, no registry)
|
||||
```
|
||||
|
||||
## Telephony cutover (reversible)
|
||||
Endpoint telephony hits: **`http://10.0.56.14:30850`** (NodePort, MGMT VLAN 56).
|
||||
In `apps/telephony/telephony.yaml`:
|
||||
1. Deployment env `Tts__PiperUrl=http://10.0.56.14:30850` — **this is the real lever**;
|
||||
env vars override `appsettings.Production.json`, so the configmap `Tts` block alone
|
||||
is inert (it was shadowed by a drifted live env `Tts__PiperUrl=edge1`).
|
||||
2. NetworkPolicy egress to `10.0.56.14/32:30850` (telephony-web is `hostNetwork`, so this
|
||||
only matters for non-hostNetwork pods; harmless either way).
|
||||
3. edge1 (`10.0.57.17:8500`) stays warm — **rollback = set `Tts__PiperUrl` back to it**.
|
||||
The TTS circuit breaker + `MapTextToSound` canned-prompt fallback mean a bad endpoint
|
||||
degrades gracefully, never to silence.
|
||||
|
||||
## Verify (not a manual call)
|
||||
```bash
|
||||
FLOWERCORE_SIP_TEST_MODE=required dotnet.exe test \
|
||||
FlowerCore.Telephony/tests/FlowerCore.Telephony.SipTests/FlowerCore.Telephony.SipTests.csproj \
|
||||
--filter FullyQualifiedName~Call_Star100_ReceivesAudibleAudioStream
|
||||
```
|
||||
A passing audible test alone is NOT sufficient (edge1 also produces audible audio) —
|
||||
confirm the **GX10 TTS pod's own access log** (`kubectl -n tts logs deploy/gx10-tts`)
|
||||
shows `POST /tts 200` during the call, and telephony-web logs target `10.0.56.14:30850`.
|
||||
|
||||
## Voice upgrade (follow-on)
|
||||
Operator's pick is **Kokoro**; needs GPU time-slicing (Ollama holds the GB10 GPU; MPS is
|
||||
refuted on GB10) OR Kokoro-CPU behind a `/tts` shim. This Piper baseline stays as the floor.
|
||||
@@ -1,81 +0,0 @@
|
||||
# GX10 Piper TTS — telephony /tts endpoint on the GX10 RKE2 cluster.
|
||||
# Applied DIRECTLY via the GX10's own kubectl (KUBECONFIG=/etc/rancher/rke2/rke2.yaml);
|
||||
# the GX10 cluster is NOT yet under the old-cluster ArgoCD. CPU-only (no GPU request)
|
||||
# so it co-resides with the GPU-holding Ollama pod without contending for the GB10.
|
||||
# Image is imported into RKE2 containerd (imagePullPolicy: Never).
|
||||
# Telephony reaches it at http://10.0.56.14:30850 (NodePort, MGMT VLAN 56).
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: tts
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gx10-tts
|
||||
namespace: tts
|
||||
labels:
|
||||
app: gx10-tts
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gx10-tts
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gx10-tts
|
||||
spec:
|
||||
containers:
|
||||
- name: tts
|
||||
image: localhost/fc-gx10-tts:v20260614
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8500
|
||||
name: http
|
||||
env:
|
||||
- name: TTS_PORT
|
||||
value: "8500"
|
||||
- name: PIPER_VOICE
|
||||
value: "en_US-amy-medium"
|
||||
- name: TARGET_RATE
|
||||
value: "16000"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8500
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8500
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
limits:
|
||||
cpu: "4"
|
||||
memory: "2Gi"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gx10-tts
|
||||
namespace: tts
|
||||
labels:
|
||||
app: gx10-tts
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: gx10-tts
|
||||
ports:
|
||||
- name: http
|
||||
port: 8500
|
||||
targetPort: 8500
|
||||
nodePort: 30850
|
||||
protocol: TCP
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""GX10 Piper TTS microservice — telephony /tts contract.
|
||||
|
||||
POST /tts {"text": "..."} -> 16 kHz / 16-bit / mono WAV (canonical 44-byte header)
|
||||
GET /health -> JSON status
|
||||
|
||||
The telephony AsteriskProvider strips the 44-byte WAV header and writes the
|
||||
remainder as a `.sln16` (signed-linear 16 kHz) file that Asterisk transcodes to
|
||||
any codec. So the response MUST be 16 kHz / 16-bit / mono. The en_US-amy-medium
|
||||
voice is 22.05 kHz native, so we resample to 16 kHz (a 22.05 kHz stream treated
|
||||
as 16 kHz plays ~1.38x too fast). This is a drop-in upgrade over edge1's
|
||||
en_US-amy-low (16 kHz native, lower quality), keeping the exact wire contract.
|
||||
"""
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import wave
|
||||
|
||||
import numpy as np
|
||||
from flask import Flask, Response, jsonify, request
|
||||
|
||||
API_PORT = int(os.environ.get("TTS_PORT", "8500"))
|
||||
PIPER_VOICE = os.environ.get("PIPER_VOICE", "en_US-amy-medium")
|
||||
VOICES_DIR = os.environ.get("VOICES_DIR", "/voices")
|
||||
TARGET_RATE = int(os.environ.get("TARGET_RATE", "16000"))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
stream=sys.stdout,
|
||||
)
|
||||
log = logging.getLogger("gx10-tts")
|
||||
|
||||
piper_voice_obj = None
|
||||
piper_loaded = False
|
||||
piper_lock = threading.Lock()
|
||||
native_rate = None
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def load_piper():
|
||||
"""Load the Piper voice model once at startup (shared, lock-guarded)."""
|
||||
global piper_voice_obj, piper_loaded
|
||||
try:
|
||||
from piper import PiperVoice
|
||||
model_path = os.path.join(VOICES_DIR, f"{PIPER_VOICE}.onnx")
|
||||
if not os.path.isfile(model_path):
|
||||
log.error("Piper voice model not found at %s — TTS disabled", model_path)
|
||||
piper_loaded = False
|
||||
return
|
||||
log.info("Loading Piper voice %s from %s", PIPER_VOICE, model_path)
|
||||
piper_voice_obj = PiperVoice.load(model_path)
|
||||
piper_loaded = True
|
||||
log.info("Piper voice loaded")
|
||||
except Exception as exc: # noqa: BLE001 — fail-soft, /health reports it
|
||||
log.error("Failed to load Piper: %s", exc)
|
||||
piper_loaded = False
|
||||
|
||||
|
||||
def synthesize_chunks(text):
|
||||
"""Run Piper synthesis under a lock because the loaded voice is shared."""
|
||||
with piper_lock:
|
||||
return list(piper_voice_obj.synthesize(text))
|
||||
|
||||
|
||||
def resample_i16(pcm_i16, src_rate, dst_rate):
|
||||
"""Linear-interpolation resample of int16 PCM (matches edge1's STT resample)."""
|
||||
if src_rate == dst_rate or len(pcm_i16) == 0:
|
||||
return pcm_i16
|
||||
audio = pcm_i16.astype(np.float32)
|
||||
target_len = int(round(len(audio) * dst_rate / src_rate))
|
||||
if target_len <= 0:
|
||||
return np.zeros(0, dtype=np.int16)
|
||||
idx = np.linspace(0, len(audio) - 1, target_len)
|
||||
res = np.interp(idx, np.arange(len(audio)), audio)
|
||||
return np.clip(np.round(res), -32768, 32767).astype(np.int16)
|
||||
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"voice": PIPER_VOICE,
|
||||
"loaded": piper_loaded,
|
||||
"target_rate": TARGET_RATE,
|
||||
"native_rate": native_rate,
|
||||
})
|
||||
|
||||
|
||||
@app.route("/tts", methods=["POST"])
|
||||
def tts():
|
||||
"""Text -> 16 kHz/16-bit/mono WAV. Mirrors the edge1 speech-pipeline contract."""
|
||||
if not piper_loaded:
|
||||
return jsonify({"error": "Piper TTS model not loaded"}), 503
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "text" not in data:
|
||||
return jsonify({"error": "Missing required field: text"}), 400
|
||||
|
||||
text = data["text"].strip()
|
||||
if not text:
|
||||
return jsonify({"error": "Text field is empty"}), 400
|
||||
if len(text) > 10000:
|
||||
return jsonify({"error": "Text too long (max 10000 characters)"}), 400
|
||||
|
||||
try:
|
||||
chunks = synthesize_chunks(text)
|
||||
if not chunks:
|
||||
return jsonify({"error": "No audio produced"}), 500
|
||||
|
||||
global native_rate
|
||||
first = chunks[0]
|
||||
native_rate = first.sample_rate
|
||||
|
||||
if first.sample_width != 2 or first.sample_channels != 1:
|
||||
return jsonify({
|
||||
"error": f"Unexpected PCM format: width={first.sample_width} "
|
||||
f"channels={first.sample_channels} (need 16-bit mono)"
|
||||
}), 500
|
||||
|
||||
pcm = np.frombuffer(
|
||||
b"".join(c.audio_int16_bytes for c in chunks), dtype=np.int16
|
||||
)
|
||||
out = resample_i16(pcm, native_rate, TARGET_RATE)
|
||||
|
||||
wav_buffer = io.BytesIO()
|
||||
with wave.open(wav_buffer, "wb") as wav_file:
|
||||
wav_file.setnchannels(1)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(TARGET_RATE)
|
||||
wav_file.writeframes(out.tobytes())
|
||||
wav_buffer.seek(0)
|
||||
|
||||
return Response(
|
||||
wav_buffer.read(),
|
||||
mimetype="audio/wav",
|
||||
headers={"Content-Disposition": 'inline; filename="speech.wav"'},
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.error("TTS synthesis failed: %s", exc)
|
||||
return jsonify({"error": f"Synthesis failed: {exc}"}), 500
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.info(
|
||||
"GX10 TTS starting on port %d (voice=%s -> %d Hz)",
|
||||
API_PORT, PIPER_VOICE, TARGET_RATE,
|
||||
)
|
||||
load_piper()
|
||||
app.run(host="0.0.0.0", port=API_PORT, threaded=True)
|
||||
@@ -17,17 +17,21 @@ public sealed class FleetManifestLintTests
|
||||
"dist.flowercore.io",
|
||||
};
|
||||
|
||||
// Hosts that allow a tightly bounded write surface in addition to GET/HEAD.
|
||||
// updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id}
|
||||
// Public hosts that allow a tightly bounded write surface in addition to
|
||||
// GET/HEAD. updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id}
|
||||
// (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS — but
|
||||
// PUT/PATCH/DELETE must still 404 at the route. Public
|
||||
// update.flowercore.io remains a GET/HEAD download surface in the
|
||||
// FlowerCore.Updater sibling manifest and is covered by the general
|
||||
// public-method allowlist lint instead of this write-surface rule.
|
||||
// PUT/PATCH/DELETE must still 404 at the route. Anything wider than this
|
||||
// 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)
|
||||
{
|
||||
"updatecenter.iamworkin.lan",
|
||||
"updates.iamworkin.lan",
|
||||
"update.flowercore.io",
|
||||
"updates.flowercore.io",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> ApiKeyProtectedDeployments = new(StringComparer.Ordinal)
|
||||
@@ -65,7 +69,7 @@ public sealed class FleetManifestLintTests
|
||||
["github-runner-updater"] = "https://github.com/astoltz/FlowerCore.Updater",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> RepoScopedLinuxRunnerDeployments = new(StringComparer.Ordinal)
|
||||
private static readonly HashSet<string> ScaledLinuxRunnerDeployments = new(StringComparer.Ordinal)
|
||||
{
|
||||
"github-runner-sharedpos",
|
||||
"github-runner-puppet",
|
||||
@@ -79,44 +83,6 @@ public sealed class FleetManifestLintTests
|
||||
"github-runner-updater",
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, (string Deployment, string ProbePath)> BroaderHardeningDeployments =
|
||||
new Dictionary<string, (string Deployment, string ProbePath)>(StringComparer.Ordinal)
|
||||
{
|
||||
["fc-aistation"] = ("aistation-web", "/healthz"),
|
||||
["fc-chat"] = ("chat-web", "/healthz"),
|
||||
["fc-devicemgmt"] = ("fc-devicemgmt-web", "/healthz"),
|
||||
["fc-library"] = ("library-web", "/health"),
|
||||
["fc-llm-bridge"] = ("fc-llm-bridge", "/healthz"),
|
||||
["fc-messageboard"] = ("messageboard-web", "/health"),
|
||||
["fc-retail"] = ("retail-web", "/healthz"),
|
||||
["fc-ttsreader"] = ("ttsreader-web", "/health"),
|
||||
["fc-updater"] = ("updatecenter-web", "/"),
|
||||
["knowledge"] = ("knowledge-web", "/healthz"),
|
||||
["telephony"] = ("telephony-web", "/health"),
|
||||
["worldbuilder"] = ("worldbuilder-web", "/healthz"),
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> BroaderHardeningInternalPrestageApps = new(StringComparer.Ordinal)
|
||||
{
|
||||
"fc-aistation",
|
||||
"fc-desktop",
|
||||
"fc-dms",
|
||||
"fc-library",
|
||||
"fc-llm-bridge",
|
||||
"fc-menuboard",
|
||||
"fc-messageboard",
|
||||
"fc-mysql",
|
||||
"fc-php",
|
||||
"fc-presentations",
|
||||
"fc-retail",
|
||||
"fc-scoreboard",
|
||||
"fc-segmentdisplay",
|
||||
"fc-signage",
|
||||
"fc-ttsreader",
|
||||
"knowledge",
|
||||
"worldbuilder",
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> WritableRunnerEnv = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["HOME"] = "/home/runner",
|
||||
@@ -272,7 +238,6 @@ public sealed class FleetManifestLintTests
|
||||
var container = deployments[expectedRunner.Key].MainContainerMappings().Should().ContainSingle().Subject;
|
||||
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
|
||||
EnvValue(container, "EPHEMERAL").Should().Be("true");
|
||||
EnvValue(container, "DISABLE_AUTO_UPDATE").Should().Be("true", $"{expectedRunner.Key} must not self-update inside immutable Kubernetes runner pods");
|
||||
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
|
||||
EnvValue(container, "RUN_AS_ROOT").Should().Be("false");
|
||||
EnvValue(container, "ACCESS_TOKEN").Should().BeNull("ACCESS_TOKEN must come from github-runner-token Secret, not a literal");
|
||||
@@ -306,17 +271,17 @@ public sealed class FleetManifestLintTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForRepoScopedDeployments()
|
||||
public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForScaledDeployments()
|
||||
{
|
||||
var deployments = GitHubRunnerDeployments();
|
||||
|
||||
foreach (var deploymentName in RepoScopedLinuxRunnerDeployments)
|
||||
foreach (var deploymentName in ScaledLinuxRunnerDeployments)
|
||||
{
|
||||
var deployment = deployments[deploymentName];
|
||||
// Sprint 34 ops trimmed runner load while the cluster was degraded
|
||||
// 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");
|
||||
// Scaled runners must have >= 2 replicas (avoid single-pod bottleneck).
|
||||
// Individual deployments may be tuned upward per CI activity — see
|
||||
// "runners: right-size replica counts per 14d CI activity (#24)".
|
||||
ReplicaCount(deployment).Should().BeGreaterOrEqualTo(2, $"{deploymentName} is in the scaled set and must run with at least 2 replicas");
|
||||
|
||||
var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes");
|
||||
var claimNames = volumes
|
||||
@@ -324,7 +289,7 @@ public sealed class FleetManifestLintTests
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.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 =>
|
||||
string.Equals(ManifestNodeExtensions.Scalar(volume, "name"), "nuget-cache", StringComparison.Ordinal)
|
||||
&& ManifestNodeExtensions.Mapping(volume, "emptyDir") != null);
|
||||
@@ -522,16 +487,16 @@ public sealed class FleetManifestLintTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Distribution_OidcEnforcement_MustKeepHealthzAnonymousContractVisibleInManifest()
|
||||
public void Distribution_OidcEnforcement_MustStayOffUntilHealthzAllowAnonymousProofLands()
|
||||
{
|
||||
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");
|
||||
EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be("false");
|
||||
ProbeHttpGetPath(container, "readinessProbe").Should().Be("/healthz");
|
||||
PodAnnotation(distribution, "flowercore.io/healthz-auth-policy").Should().Be("allow-anonymous");
|
||||
PodAnnotation(distribution, "flowercore.io/healthz-auth-policy").Should().NotBe("allow-anonymous");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -647,10 +612,10 @@ public sealed class FleetManifestLintTests
|
||||
var expectedFiles = new[]
|
||||
{
|
||||
"1password-item.yaml",
|
||||
"argocd-application.yaml",
|
||||
"certificate-web.yaml",
|
||||
"clusterrole-operator.yaml",
|
||||
"clusterrolebinding-operator.yaml",
|
||||
"crds.yaml",
|
||||
"deployment-operator.yaml",
|
||||
"deployment-web.yaml",
|
||||
"ingressroute-web.yaml",
|
||||
@@ -740,8 +705,7 @@ public sealed class FleetManifestLintTests
|
||||
.Single(document => document.Kind == "ClusterRole" && document.Name == "fc-devicemgmt-operator");
|
||||
var allScalars = clusterRole.AllScalars().ToList();
|
||||
|
||||
allScalars.Should().Contain("flowercore.io");
|
||||
allScalars.Should().NotContain("devices.flowercore.io");
|
||||
allScalars.Should().Contain("devices.flowercore.io");
|
||||
allScalars.Should().Contain("*");
|
||||
allScalars.Should().Contain("deployments");
|
||||
allScalars.Should().Contain("get");
|
||||
@@ -770,7 +734,7 @@ public sealed class FleetManifestLintTests
|
||||
|
||||
FcDeviceManagementDocuments().Should().NotContain(document => document.Kind == "Secret");
|
||||
appText.Should().Contain("secretKeyRef:");
|
||||
appText.Should().Contain("name: fc-devicemgmt-runtime");
|
||||
appText.Should().Contain("secretName: fc-devicemgmt-runtime");
|
||||
appText.Should().NotContain("stringData:");
|
||||
appText.Should().NotContain("from-literal");
|
||||
appText.Should().NotContain("tls.key:");
|
||||
@@ -804,62 +768,17 @@ public sealed class FleetManifestLintTests
|
||||
}
|
||||
|
||||
[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");
|
||||
|
||||
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)
|
||||
application.Namespace.Should().Be("argocd");
|
||||
application.Scalar("spec", "source", "repoURL")
|
||||
.Should()
|
||||
.NotContain(match => match.Contains(".flowercore.io", StringComparison.Ordinal),
|
||||
"Sprint 61 broader hardening only pre-stages commented public hosts for internal-only apps");
|
||||
}
|
||||
.Be("http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git");
|
||||
application.Scalar("spec", "source", "path").Should().Be("apps/fc-devicemgmt");
|
||||
application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -867,9 +786,9 @@ public sealed class FleetManifestLintTests
|
||||
{
|
||||
var deployments = new[]
|
||||
{
|
||||
(App: "fc-dns", Name: "dns-web", Slug: "dns", Secret: "dns-oidc-client", AuthEnabled: "false"),
|
||||
(App: "fc-media", Name: "fc-media-web", Slug: "media", Secret: "media-oidc-client", AuthEnabled: "true"),
|
||||
(App: "fc-distribution", Name: "fc-distribution", Slug: "distribution", Secret: "distribution-oidc-client", AuthEnabled: "true"),
|
||||
(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)
|
||||
@@ -878,7 +797,7 @@ public sealed class FleetManifestLintTests
|
||||
.Single(document => document.Kind == "Deployment" && document.Name == expected.Name);
|
||||
var container = deployment.MainContainerMappings().Should().ContainSingle().Subject;
|
||||
|
||||
EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be(expected.AuthEnabled);
|
||||
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()
|
||||
@@ -927,7 +846,7 @@ public sealed class FleetManifestLintTests
|
||||
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:v20260613-g5-quota-aa99bd1");
|
||||
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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user