Compare commits
1 Commits
codex/mdm-
...
codex/cx2-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eae7b4ed7a |
@@ -139,31 +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"
|
||||
|
||||
---
|
||||
# FlowerCore MCP Gateway key. Agent Zero advertises only fc_gateway so product
|
||||
# tool schemas are discovered on demand instead of dumped into the prompt.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: gateway-mcp-keys
|
||||
namespace: agent-zero
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore Gateway MCP Keys"
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -273,15 +248,35 @@ 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
|
||||
# Agent Zero does not interpolate env vars inside
|
||||
# A0_SET_mcp_servers JSON, so build the final JSON here from the
|
||||
# secret-backed gateway key before initialize.sh.
|
||||
# Phase 0 Chat MCP pilot: Agent Zero does not interpolate env vars
|
||||
# inside A0_SET_mcp_servers JSON, so build the final JSON here from
|
||||
# the secret-backed env vars before initialize.sh. Keep the local
|
||||
# corpus_search.py tool mounted either way so outage fallback
|
||||
# remains available even when fc_knowledge is not advertised.
|
||||
export KNOWLEDGE_MCP_ENABLED=false
|
||||
if [ -n "${KNOWLEDGE_MCP_BEARER_TOKEN:-}" ]; then
|
||||
if curl -sf --connect-timeout 3 "${KNOWLEDGE_MCP_HEALTH_URL}" > /dev/null && \
|
||||
curl -sf --connect-timeout 5 \
|
||||
-H "Authorization: Bearer ${KNOWLEDGE_MCP_BEARER_TOKEN}" \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":"fc-knowledge-bootstrap","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"agent-zero-bootstrap","version":"1.0"}}}' \
|
||||
"${KNOWLEDGE_MCP_URL}" > /dev/null; then
|
||||
export KNOWLEDGE_MCP_ENABLED=true
|
||||
echo "fc_knowledge enabled from ${KNOWLEDGE_MCP_URL}."
|
||||
else
|
||||
echo "fc_knowledge unavailable or unauthorized; keeping local corpus_search.py as the fallback path."
|
||||
fi
|
||||
else
|
||||
echo "fc_knowledge token missing; keeping local corpus_search.py as the fallback path."
|
||||
fi
|
||||
|
||||
export A0_SET_mcp_servers="$(
|
||||
python3 -c 'import json, os; key=os.getenv("GATEWAY_MCP_API_KEY"); url=os.getenv("GATEWAY_MCP_URL", "http://fc-gateway.fc-gateway.svc/mcp"); servers={"fc_gateway":{"type":"streamable-http","url":url,"headers":{"X-Api-Key":key}}} if key else {}; 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
|
||||
@@ -290,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.
|
||||
@@ -349,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
|
||||
@@ -358,19 +353,11 @@ 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
|
||||
value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080"
|
||||
# FlowerCore.Mcp.Gateway — single MCP fan-in for product tools.
|
||||
- name: GATEWAY_MCP_URL
|
||||
value: "http://fc-gateway.fc-gateway.svc/mcp"
|
||||
- name: GATEWAY_MCP_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gateway-mcp-keys
|
||||
key: credential
|
||||
# Agent profile — Blue Jay personality, tools, and system prompt
|
||||
- name: A0_SET_agent_profile
|
||||
value: "bluejay"
|
||||
@@ -406,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).
|
||||
@@ -664,30 +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
|
||||
# FlowerCore.Mcp.Gateway — Agent Zero advertises this single MCP server;
|
||||
# the gateway performs backend search and invokes product MCP tools.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-gateway
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
# Allow internet (for kubectl image pull, etc)
|
||||
- to:
|
||||
- ipBlock:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
# Runtime secret placeholder for the self-hosted Apple MDM substrate.
|
||||
#
|
||||
# OnePasswordItem operator syncs this item into a Kubernetes Secret with the
|
||||
# same name. Expected fields for MDM-N1:
|
||||
# NANOHUB_API_KEY
|
||||
#
|
||||
# Optional fields for later lanes:
|
||||
# NANOHUB_WEBHOOK_URL
|
||||
# APNS_MDM_CERT_PEM
|
||||
# APNS_MDM_KEY_PEM
|
||||
# APNS_MDM_TOPIC
|
||||
# SCEP_CA_CERT_PEM
|
||||
# SCEP_CA_KEY_PEM
|
||||
# PROFILE_SIGNING_CERT_PEM
|
||||
# PROFILE_SIGNING_KEY_PEM
|
||||
#
|
||||
# Do not commit APNs, SCEP, profile-signing, webhook, or API key material to
|
||||
# Git. MDM-N1 only consumes NANOHUB_API_KEY and optional NANOHUB_WEBHOOK_URL.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: fc-apple-mdm-runtime
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: secrets
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore Apple MDM Runtime"
|
||||
@@ -1,65 +0,0 @@
|
||||
# FlowerCore Apple MDM Infra
|
||||
|
||||
This app hosts the private NanoHUB bootstrap service for FlowerCore iPad
|
||||
management at `https://mdm.iamworkin.lan`.
|
||||
|
||||
## Runtime Shape
|
||||
|
||||
- Namespace: `fc-apple-mdm`
|
||||
- Host: `mdm.iamworkin.lan`
|
||||
- Image: `localhost/fc-apple-mdm-nanohub:v0.2.0-20260617`
|
||||
- Upstream baseline: NanoHUB `v0.2.0`, published 2025-12-25
|
||||
- Persistent data: `fc-apple-mdm-data` mounted at `/var/lib/nanohub`
|
||||
- NanoHUB file backend root: `/var/lib/nanohub/db`
|
||||
- Runtime secret: `OnePasswordItem/fc-apple-mdm-runtime`
|
||||
- Required secret field: `NANOHUB_API_KEY`
|
||||
- Optional secret field: `NANOHUB_WEBHOOK_URL`
|
||||
|
||||
NanoHUB listens on HTTP `:9004` inside the pod; Traefik owns TLS using
|
||||
`Certificate/fc-apple-mdm-tls`. The public route intentionally exposes only
|
||||
`/mdm`, `/checkin`, and `/version`. The NanoHUB APIs under `/api/v1/*` stay
|
||||
cluster-internal for MDM-N1 and are intended for the FlowerCore
|
||||
DeviceManagement bridge.
|
||||
|
||||
## NanoHUB Endpoints
|
||||
|
||||
- Device command/report and default check-in endpoint: `/mdm`
|
||||
- Separate check-in endpoint enabled by `NANOHUB_CHECKIN=true`: `/checkin`
|
||||
- Health/version endpoint: `/version`
|
||||
- Internal NanoMDM API: `/api/v1/nanomdm/`
|
||||
- Internal NanoCMD API: `/api/v1/nanocmd/`
|
||||
- Internal KMFDDM API: `/api/v1/ddm/`
|
||||
|
||||
NanoHUB API authentication is HTTP Basic with username `nanohub` and password
|
||||
from `NANOHUB_API_KEY`.
|
||||
|
||||
## Operator Gates
|
||||
|
||||
1. Create `FlowerCore Apple MDM Runtime` in the `IAmWorkin` 1Password vault with
|
||||
field `NANOHUB_API_KEY`. Add `NANOHUB_WEBHOOK_URL` only after the
|
||||
DeviceManagement Nano bridge endpoint is live.
|
||||
2. Add or confirm `mdm.iamworkin.lan -> 10.0.56.200` in FlowerCore.DNS/pfSense
|
||||
before cert-manager syncs the certificate.
|
||||
3. Mirror or build the pinned NanoHUB image, then import it on every schedulable
|
||||
RKE2 node:
|
||||
|
||||
```bash
|
||||
podman pull --arch arm64 ghcr.io/micromdm/nanohub:latest@sha256:e36a50db2dc3d2bf736645e58712f622c04b05b28487390981905ef4d0be5fbd
|
||||
podman tag ghcr.io/micromdm/nanohub@sha256:e36a50db2dc3d2bf736645e58712f622c04b05b28487390981905ef4d0be5fbd localhost/fc-apple-mdm-nanohub:v0.2.0-20260617
|
||||
podman save localhost/fc-apple-mdm-nanohub:v0.2.0-20260617 -o fc-apple-mdm-nanohub-v0.2.0-20260617.tar
|
||||
# copy to each RKE2 node, then:
|
||||
sudo ctr -n k8s.io images import fc-apple-mdm-nanohub-v0.2.0-20260617.tar
|
||||
```
|
||||
|
||||
If GHCR changes or becomes unavailable, rebuild/import from
|
||||
`nanohub-linux-arm64-v0.2.0.zip` with SHA-256
|
||||
`b05968322a9bc34e79169ebee28d16554046f981eaee48a12cf80899f51a9dbd`.
|
||||
|
||||
4. Sync the ArgoCD app and prove `https://mdm.iamworkin.lan/version`.
|
||||
|
||||
## Support Boundary
|
||||
|
||||
This MDM-N1 lane deploys the protocol substrate only. It does not create an APNs
|
||||
MDM push certificate, enrollment profile, SCEP/device identity service, managed
|
||||
Wi-Fi payload, managed app install, or supervised iPad enrollment. Those stay in
|
||||
MDM-N2 through MDM-N8.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Certificate for mdm.iamworkin.lan.
|
||||
#
|
||||
# Preflight gate: FlowerCore.DNS / pfSense must contain an explicit A record:
|
||||
# mdm.iamworkin.lan -> 10.0.56.200
|
||||
# before this Certificate is synced. step-ca ACME cannot see the CoreDNS
|
||||
# wildcard, so missing pfSense DNS produces cert-manager HTTP-01 backoff.
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: fc-apple-mdm-tls
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
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: "mdm.iamworkin.lan must resolve to 10.0.56.200 before ACME sync"
|
||||
spec:
|
||||
secretName: fc-apple-mdm-tls
|
||||
issuerRef:
|
||||
name: step-ca-acme
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- mdm.iamworkin.lan
|
||||
duration: 720h
|
||||
renewBefore: 240h
|
||||
@@ -1,127 +0,0 @@
|
||||
# Self-hosted NanoHUB lane for FlowerCore Apple device management.
|
||||
#
|
||||
# Image contract:
|
||||
# Mirror/import localhost/fc-apple-mdm-nanohub:v0.2.0-20260617 from
|
||||
# ghcr.io/micromdm/nanohub:latest@sha256:e36a50db2dc3d2bf736645e58712f622c04b05b28487390981905ef4d0be5fbd
|
||||
# or rebuild from nanohub-linux-arm64-v0.2.0.zip with SHA-256
|
||||
# b05968322a9bc34e79169ebee28d16554046f981eaee48a12cf80899f51a9dbd.
|
||||
# Keep imagePullPolicy: Never so the RKE2 nodes do not depend on GHCR at
|
||||
# runtime.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
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
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fc-apple-mdm
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
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: "/version"
|
||||
prometheus.io/scrape: "false"
|
||||
flowercore.io/audit-trace-id: "apple-mdm-nanohub-runtime-trace"
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1654
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: nanohub
|
||||
image: localhost/fc-apple-mdm-nanohub:v0.2.0-20260617
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 9004
|
||||
env:
|
||||
- name: HOME
|
||||
value: "/var/lib/nanohub"
|
||||
- name: NANOHUB_LISTEN
|
||||
value: ":9004"
|
||||
- name: NANOHUB_STORAGE
|
||||
value: "file"
|
||||
- name: NANOHUB_STORAGE_DSN
|
||||
value: "/var/lib/nanohub/db"
|
||||
- name: NANOHUB_CHECKIN
|
||||
value: "true"
|
||||
- name: NANOHUB_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: fc-apple-mdm-runtime
|
||||
key: NANOHUB_API_KEY
|
||||
- name: NANOHUB_WEBHOOK_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: fc-apple-mdm-runtime
|
||||
key: NANOHUB_WEBHOOK_URL
|
||||
optional: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /version
|
||||
port: 9004
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /version
|
||||
port: 9004
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /version
|
||||
port: 9004
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1654
|
||||
runAsGroup: 1654
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/nanohub
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: fc-apple-mdm-data
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
@@ -1,29 +0,0 @@
|
||||
# LAN ingress for NanoHUB.
|
||||
#
|
||||
# Traefik terminates step-ca TLS; NanoHUB listens on HTTP :9004 and serves the
|
||||
# Apple MDM protocol endpoints. The NanoHUB API stays cluster-internal for
|
||||
# MDM-N1; do not route /api/v1 through Traefik until the operator approves an
|
||||
# API exposure model.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
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(`mdm.iamworkin.lan`) && (PathPrefix(`/mdm`) || PathPrefix(`/checkin`) || PathPrefix(`/version`))
|
||||
kind: Rule
|
||||
services:
|
||||
- name: fc-apple-mdm
|
||||
port: 80
|
||||
tls:
|
||||
secretName: fc-apple-mdm-tls
|
||||
@@ -1,13 +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
|
||||
- 1password-item.yaml
|
||||
- pvc.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- certificate.yaml
|
||||
- ingressroute.yaml
|
||||
- network-policy.yaml
|
||||
@@ -1,13 +0,0 @@
|
||||
# FlowerCore Apple MDM namespace.
|
||||
#
|
||||
# ArgoCD discovers this directory as Application `infra-fc-apple-mdm`.
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
@@ -1,94 +0,0 @@
|
||||
# FlowerCore Apple MDM network isolation.
|
||||
#
|
||||
# Public/LAN device traffic enters through Traefik. NanoHUB API access is kept
|
||||
# cluster-internal for MDM-N1 and is reachable by the DeviceManagement bridge.
|
||||
# Egress 443 is required for Apple APNs/ADE/VPP endpoints once APNs and Apple
|
||||
# enrollment material are configured in later lanes.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: fc-apple-mdm-isolation
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: fc-apple-mdm
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
ports:
|
||||
- port: 9004
|
||||
protocol: TCP
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-devicemgmt
|
||||
ports:
|
||||
- port: 9004
|
||||
protocol: TCP
|
||||
egress:
|
||||
# CoreDNS.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- port: 53
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
# Apple APNs/ADE/VPP endpoints and upstream certificate checks.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
# Traefik VIP / in-cluster Traefik for public URL self-checks. Include
|
||||
# post-DNAT backend ports 8443 + 8080.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.0.56.200/32
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
- port: 8443
|
||||
protocol: TCP
|
||||
# DeviceManagement bridge webhook/API target.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-devicemgmt
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
@@ -1,25 +0,0 @@
|
||||
# Persistent NanoHUB file backend state.
|
||||
#
|
||||
# NanoHUB stores NanoMDM, NanoCMD, and KMFDDM data under the file backend root.
|
||||
# RWO: keep a single replica and use Recreate for disruptive image/runtime
|
||||
# changes.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: fc-apple-mdm-data
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
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,22 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fc-apple-mdm
|
||||
namespace: fc-apple-mdm
|
||||
labels:
|
||||
app: fc-apple-mdm
|
||||
app.kubernetes.io/name: fc-apple-mdm
|
||||
app.kubernetes.io/component: mdm
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: fc-apple-mdm
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 9004
|
||||
protocol: TCP
|
||||
@@ -34,10 +34,10 @@ data:
|
||||
# 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"
|
||||
FlowerCore__AI__OllamaBaseUrl: "http://10.0.57.17:11434"
|
||||
FlowerCore__AI__DefaultModelName: "qwen2.5-coder:7b"
|
||||
ChatOptions__BehaviorRuleEngine__OllamaBaseUrl: "http://10.0.57.17:11434"
|
||||
ChatOptions__BehaviorRuleEngine__FallbackOllamaBaseUrl: "http://10.0.57.17:11434"
|
||||
ChatOptions__BehaviorRuleEngine__ModelName: "gemma3:4b"
|
||||
FlowerCore__AI__Memory__UseSharedIndexingAdapter: "true"
|
||||
FlowerCore__AI__Memory__UseOllamaEmbeddings: "true"
|
||||
@@ -47,8 +47,6 @@ data:
|
||||
FlowerCore__AI__Skills__Library__LibraryApiUrl: "http://library-web.fc-library.svc.cluster.local"
|
||||
FlowerCore__AI__Skills__Retail__RetailApiUrl: "http://retail-web.fc-retail.svc.cluster.local"
|
||||
FlowerCore__AI__Skills__Intranet__IntranetBaseUrl: "http://intranet-web.intranet.svc.cluster.local"
|
||||
FlowerCore__AI__Skills__Intranet__SearchBaseUrl: "https://intranet.iamworkin.lan"
|
||||
FlowerCore__AI__Skills__Intranet__PublicBaseUrl: "https://intranet.iamworkin.lan"
|
||||
FlowerCore__AI__Skills__Print__PrintMcpBaseUrl: "http://10.0.57.16:5200"
|
||||
FlowerCore__AI__Helpdesk__SentimentEscalation__Enabled: "true"
|
||||
FlowerCore__AI__IrcBridge__Enabled: "true"
|
||||
@@ -125,7 +123,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: chat-web
|
||||
image: localhost/fc-chat-web:v20260616-circuit-mood-5711f2d
|
||||
image: localhost/fc-chat-web:v20260614-wave5-sentiment-685f62c
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -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.
|
||||
@@ -83,7 +83,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-devicemgmt-web:v20260614-regroup-c5b8f82
|
||||
image: localhost/fc-devicemgmt-web:v20260613-g3-6555c0d
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -26,9 +26,12 @@ metadata:
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/dns-oidc-client"
|
||||
---
|
||||
# Service X-Api-Key for the cert-manager ACME webhook -> dns-web call path.
|
||||
# The 1Password operator resolves this item into Secret/dns-api-keys; field
|
||||
# `api_key` becomes Secret key `api_key`.
|
||||
# Service X-Api-Key for the cert-manager ACME webhook -> dns-web call path
|
||||
# (Phase 0 auth-flip). The 1Password operator resolves this item into a K8s
|
||||
# Secret of the same name; the `api_key` field becomes Secret key `api_key`.
|
||||
# dns-web reads it as FlowerCore__Auth__ApiKey (FcApiKey scheme, Operator
|
||||
# principal); dns-acme-webhook sends it as the X-Api-Key header. Dormant while
|
||||
# FlowerCore__Auth__Enabled=false (all policies allow-all).
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
@@ -59,7 +62,7 @@ data:
|
||||
{
|
||||
"FlowerCore": {
|
||||
"Auth": {
|
||||
"Enabled": true,
|
||||
"Enabled": false,
|
||||
"Oidc": {
|
||||
"Enabled": true,
|
||||
"Audience": "dns",
|
||||
@@ -74,7 +77,7 @@ data:
|
||||
},
|
||||
"Tenant": {
|
||||
"DefaultTenantId": "default",
|
||||
"JwtClaimsEnabled": true,
|
||||
"JwtClaimsEnabled": false,
|
||||
"DefaultTenantHosts": [
|
||||
"dns.iamworkin.lan"
|
||||
]
|
||||
@@ -85,13 +88,6 @@ data:
|
||||
"Distribution": "Warn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Dns": {
|
||||
"RateLimits": {
|
||||
"PermitLimit": 60,
|
||||
"WindowSeconds": 60,
|
||||
"QueueLimit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +125,7 @@ spec:
|
||||
fsGroup: 1654
|
||||
containers:
|
||||
- name: dns-web
|
||||
image: localhost/fc-dns-web:v20260616-dn-current-3626f04
|
||||
image: localhost/fc-dns-web:v20260615-phase0-hybrid-f77fb94
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
@@ -166,22 +162,18 @@ spec:
|
||||
name: dns-oidc-client
|
||||
key: client_secret
|
||||
optional: true
|
||||
# Service X-Api-Key accepted by the FcApiKey scheme. The standard
|
||||
# key maps to an Operator principal (satisfies OperatorPolicy on the
|
||||
# ACME present/cleanup endpoints). optional:true keeps the pod
|
||||
# starting if the 1P operator has not yet produced the secret.
|
||||
- name: FlowerCore__Auth__ApiKey
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dns-api-keys
|
||||
key: api_key
|
||||
optional: true
|
||||
- name: FlowerCore__Mcp__ApiKey__Key
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dns-api-keys
|
||||
key: api_key
|
||||
optional: true
|
||||
- name: FlowerCore__Mcp__ServiceName
|
||||
value: flowercore.dns
|
||||
- name: FlowerCore__Auth__Enabled
|
||||
value: "true"
|
||||
value: "false"
|
||||
- name: FlowerCore__Auth__Oidc__Enabled
|
||||
value: "true"
|
||||
- name: FlowerCore__Auth__Oidc__Audience
|
||||
@@ -241,6 +233,18 @@ spec:
|
||||
targetPort: 5320
|
||||
type: ClusterIP
|
||||
---
|
||||
# Defense-in-depth ingress isolation for dns-web (Phase 0). NetworkPolicy is
|
||||
# L3/L4 and cannot path-scope, so it CANNOT restrict only present/cleanup — the
|
||||
# real control on those endpoints is the X-Api-Key + OperatorPolicy. This policy
|
||||
# simply confines who may reach dns-web:5320 to known network zones without
|
||||
# breaking any live path:
|
||||
# * Traefik pods -> UI/API on dns.iamworkin.lan
|
||||
# * same fc-dns namespace -> dns-acme-webhook -> present/cleanup
|
||||
# * cluster pod CIDR (10.42/16) -> in-cluster Prometheus scrape, etc.
|
||||
# * node + LAN CIDRs -> kubelet probes, noc1 host-net Prometheus
|
||||
# Egress is intentionally left unrestricted: dns-web must reach pfSense
|
||||
# (diag_command.php / HTTPS), the K8s API, Authentik OIDC discovery, step-ca,
|
||||
# and DNS — over-tight egress would break the provider + auth paths.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
@@ -371,7 +375,7 @@ spec:
|
||||
fsGroup: 1654
|
||||
containers:
|
||||
- name: dns-acme-webhook
|
||||
image: localhost/fc-dns-acme-webhook:v20260616-dn-current-3626f04
|
||||
image: localhost/fc-dns-acme-webhook:v20260615-phase0-hybrid-f77fb94
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
@@ -390,6 +394,10 @@ spec:
|
||||
value: /tls/tls.key
|
||||
- name: FlowerCore__Dns__AcmeWebhook__ServiceBaseUrl
|
||||
value: http://dns-web:5320
|
||||
# X-Api-Key sent to dns-web on present/cleanup so the webhook
|
||||
# authenticates as an Operator once dns-web auth is enabled.
|
||||
# optional:true keeps the webhook starting before the 1P secret
|
||||
# exists; the header is simply omitted when the value is empty.
|
||||
- name: FlowerCore__Dns__AcmeWebhook__ApiKey
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
# FlowerCore.Mcp.Gateway — Agent Zero MCP tool-router.
|
||||
# Exposes one AZ-facing MCP endpoint with three meta-tools:
|
||||
# list_capabilities, search_tools, invoke_tool.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-gateway
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
---
|
||||
# Inbound key Agent Zero sends to the gateway as X-Api-Key.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: gateway-mcp-keys
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore Gateway MCP Keys"
|
||||
---
|
||||
# Embedding API key for fc:embedding via fc-llm-bridge. Optional at runtime:
|
||||
# the gateway falls back to deterministic keyword ranking if embeddings are down.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: fc-llm-bridge-api-keys
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FC LLM Bridge API Keys"
|
||||
---
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: mysql-mcp-keys
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore MySQL MCP Keys"
|
||||
---
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: telephony-mcp-keys
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/Twilio IVR MCP Token (Agent Zero)"
|
||||
---
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: chat-mcp-keys
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore Chat MCP Keys"
|
||||
---
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: dms-mcp-keys
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore DMS MCP Keys"
|
||||
---
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: knowledge-mcp-tokens
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore Knowledge MCP Tokens"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fc-gateway
|
||||
namespace: fc-gateway
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-gateway
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: fc-gateway
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-gateway
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/healthz"
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: "/metrics/prometheus"
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
fsGroup: 1654
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-gateway:v20260617-az-gateway
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:8080"
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Production"
|
||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
||||
value: "false"
|
||||
- name: FlowerCore__Mcp__ApiKey__Key
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gateway-mcp-keys
|
||||
key: credential
|
||||
- name: FlowerCore__Mcp__Gateway__Embedding__BaseUrl
|
||||
value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1"
|
||||
- name: FlowerCore__Mcp__Gateway__Embedding__Model
|
||||
value: "fc:embedding"
|
||||
- name: FlowerCore__Mcp__Gateway__Embedding__Mode
|
||||
value: "openai"
|
||||
- name: FlowerCore__Mcp__Gateway__Embedding__ApiKey
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: fc-llm-bridge-api-keys
|
||||
key: agent-zero-k8s
|
||||
optional: true
|
||||
- name: GW_BACKEND_fc_mysql_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-mcp-keys
|
||||
key: credential
|
||||
optional: true
|
||||
- name: GW_BACKEND_fc_telephony_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: telephony-mcp-keys
|
||||
key: credential
|
||||
optional: true
|
||||
- name: GW_BACKEND_fc_chat_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: chat-mcp-keys
|
||||
key: credential
|
||||
optional: true
|
||||
- name: GW_BACKEND_fc_dms_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dms-mcp-keys
|
||||
key: credential
|
||||
optional: true
|
||||
- name: GW_BACKEND_fc_knowledge_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: knowledge-mcp-tokens
|
||||
key: password
|
||||
optional: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 384Mi
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: logs
|
||||
mountPath: /home/app/logs
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1654
|
||||
runAsGroup: 1654
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: logs
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fc-gateway
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: fc-gateway
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: fc-gateway-tls
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
secretName: fc-gateway-tls
|
||||
issuerRef:
|
||||
name: step-ca-acme
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- gateway.iamworkin.lan
|
||||
duration: 720h
|
||||
renewBefore: 240h
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: fc-gateway
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`gateway.iamworkin.lan`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: fc-gateway
|
||||
port: 80
|
||||
tls:
|
||||
secretName: fc-gateway-tls
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: fc-gateway-netpol
|
||||
namespace: fc-gateway
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: fc-gateway
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: agent-zero
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: monitoring
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- port: 53
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-llm-bridge
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-mysql
|
||||
ports:
|
||||
- port: 5300
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: telephony
|
||||
ports:
|
||||
- port: 5100
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-chat
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-dms
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: knowledge
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
@@ -9,7 +9,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
---
|
||||
# Landing page HTML (public-safe - no internal LAN references)
|
||||
# Landing page HTML (public-safe - no internal LAN references, no CDN)
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
@@ -20,144 +20,357 @@ data:
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FlowerCore</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #1a2744 50%, #0d1f3c 100%);
|
||||
color: #e0e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
max-width: 800px;
|
||||
}
|
||||
.logo {
|
||||
font-size: 5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
filter: drop-shadow(0 0 20px rgba(74, 158, 255, 0.3));
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
background: linear-gradient(135deg, #4a9eff, #7ab3ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 1.3rem;
|
||||
color: #7ab3ff;
|
||||
font-weight: 300;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.description {
|
||||
font-size: 1rem;
|
||||
color: #8aa8c4;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 3rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
.services {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.service {
|
||||
background: rgba(74, 158, 255, 0.08);
|
||||
border: 1px solid rgba(74, 158, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 1.2rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.service:hover {
|
||||
background: rgba(74, 158, 255, 0.15);
|
||||
border-color: rgba(74, 158, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.service h3 { color: #4a9eff; font-size: 0.95rem; margin-bottom: 0.3rem; }
|
||||
.service p { color: #8aa8c4; font-size: 0.8rem; }
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem 2rem;
|
||||
background: rgba(74, 158, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
.status-item { text-align: center; }
|
||||
.status-item .value { color: #4a9eff; font-size: 1.5rem; font-weight: 700; }
|
||||
.status-item .label { color: #6a8ca4; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.footer {
|
||||
margin-top: 3rem;
|
||||
color: #4a6580;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.footer a { color: #4a6580; text-decoration: none; }
|
||||
.footer a:hover { color: #7ab3ff; }
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FlowerCore</title>
|
||||
<!--
|
||||
FlowerCore public landing page.
|
||||
Air-gap safe: zero CDN, zero external fonts, zero external scripts.
|
||||
All assets are inline SVG / CSS / JS only.
|
||||
Update Center link: https://update.flowercore.io/
|
||||
Animation: bloom mark (8-petal flower) rotates slowly with a floating
|
||||
seed-core pulse. Respects prefers-reduced-motion (animation disabled).
|
||||
-->
|
||||
<style>
|
||||
/* ── Blue Jay palette (matches decisions-waiting.html tokens) ── */
|
||||
:root {
|
||||
--bg: #0A1628;
|
||||
--bg2: #0E1E36;
|
||||
--surface: #111D33;
|
||||
--border: #1E3A5F;
|
||||
--text: #E8EDF5;
|
||||
--text-muted: #8899B3;
|
||||
--gold: #FFB300;
|
||||
--gold-light: #FFCA40;
|
||||
--gold-dim: #CC8F00;
|
||||
--ice: #82BBFF;
|
||||
--steel: #2E5FA3;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
background: radial-gradient(ellipse at 60% 30%, #13233E 0%, #0C1A30 55%, #081222 100%);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Skip link (keyboard accessibility) ── */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -999px;
|
||||
top: 0;
|
||||
background: var(--gold);
|
||||
color: #0A1628;
|
||||
padding: 8px 16px;
|
||||
border-radius: 0 0 8px 0;
|
||||
font-weight: 700;
|
||||
z-index: 100;
|
||||
text-decoration: none;
|
||||
}
|
||||
.skip-link:focus { left: 0; }
|
||||
|
||||
/* ── Focus ring ── */
|
||||
a:focus-visible, button:focus-visible {
|
||||
outline: 3px solid var(--ice);
|
||||
outline-offset: 3px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ── Background star field (CSS only, no JS needed) ── */
|
||||
.starfield {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
.star {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
opacity: 0;
|
||||
animation: twinkle var(--d, 4s) var(--delay, 0s) ease-in-out infinite;
|
||||
}
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0; transform: scale(0.5); }
|
||||
50% { opacity: var(--op, 0.5); transform: scale(1); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.star { animation: none; opacity: 0.2; }
|
||||
}
|
||||
|
||||
/* ── Main content ── */
|
||||
main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Bloom mark container ── */
|
||||
.bloom-wrap {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Outer glow ring */
|
||||
.bloom-wrap::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -24px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255,179,0,0.12) 0%, transparent 70%);
|
||||
animation: pulse-glow 3s ease-in-out infinite;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-wrap::before { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.12); opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* The bloom SVG itself */
|
||||
.bloom-svg {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
animation: bloom-spin 24s linear infinite;
|
||||
transform-origin: center;
|
||||
filter: drop-shadow(0 0 18px rgba(255,179,0,0.35));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-svg { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes bloom-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Seed core counter-rotates so it stays upright */
|
||||
.bloom-core {
|
||||
animation: bloom-counter 24s linear infinite;
|
||||
transform-origin: 80px 80px; /* center of 160px viewBox */
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-core { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes bloom-counter {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(-360deg); }
|
||||
}
|
||||
|
||||
/* Core pulse */
|
||||
.bloom-core-circle {
|
||||
animation: core-pulse 2.4s ease-in-out infinite;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-core-circle { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes core-pulse {
|
||||
0%, 100% { r: 13; }
|
||||
50% { r: 17; }
|
||||
}
|
||||
|
||||
/* ── Wordmark ── */
|
||||
.wordmark {
|
||||
letter-spacing: 0.22em;
|
||||
font-size: 1.9rem;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.35rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.12em;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* ── Update Center link (primary action) ── */
|
||||
.update-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
background: var(--gold);
|
||||
color: #0A1628;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.75rem 1.75rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
border: 2px solid var(--gold);
|
||||
transition: background 0.18s, border-color 0.18s, transform 0.15s;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.update-link:hover {
|
||||
background: var(--gold-light);
|
||||
border-color: var(--gold-light);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.update-link:active {
|
||||
background: var(--gold-dim);
|
||||
border-color: var(--gold-dim);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Arrow icon inside the button */
|
||||
.update-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── "More soon" note ── */
|
||||
.more-soon {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 3rem;
|
||||
color: #2E5FA3;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.05em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 400px) {
|
||||
.bloom-wrap { width: 140px; height: 140px; }
|
||||
.bloom-svg { width: 124px; height: 124px; }
|
||||
.bloom-core { transform-origin: 62px 62px; }
|
||||
.wordmark { font-size: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<div class="logo">🌻</div>
|
||||
<h1>FlowerCore</h1>
|
||||
<p class="subtitle">Blue Jay Lab</p>
|
||||
<p class="description">
|
||||
Multi-tenant service management platform built on .NET 10,
|
||||
Kubernetes, and GitOps. Digital signage, telephony IVR,
|
||||
MySQL/PHP hosting, and infrastructure automation.
|
||||
</p>
|
||||
<a href="#main" class="skip-link">Skip to content</a>
|
||||
|
||||
<!-- CSS-only star field: 20 stars, varied positions/sizes/speeds -->
|
||||
<div class="starfield" aria-hidden="true">
|
||||
<div class="star" style="width:2px;height:2px;top:8%;left:12%;--d:5s;--delay:0.3s;--op:0.6"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:15%;left:75%;--d:7s;--delay:1.1s;--op:0.4"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:22%;left:45%;--d:4s;--delay:0.7s;--op:0.5"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:30%;left:88%;--d:6s;--delay:2.0s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:40%;left:5%;--d:8s;--delay:0.4s;--op:0.4"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:55%;left:30%;--d:5s;--delay:1.5s;--op:0.35"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:65%;left:62%;--d:7s;--delay:0.9s;--op:0.45"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:72%;left:20%;--d:4s;--delay:2.3s;--op:0.5"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:80%;left:82%;--d:6s;--delay:0.1s;--op:0.3"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:88%;left:50%;--d:9s;--delay:1.7s;--op:0.4"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:5%;left:55%;--d:6s;--delay:3.1s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:18%;left:92%;--d:5s;--delay:0.5s;--op:0.5"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:35%;left:68%;--d:7s;--delay:1.9s;--op:0.35"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:48%;left:18%;--d:4s;--delay:2.7s;--op:0.45"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:60%;left:95%;--d:8s;--delay:0.6s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:75%;left:40%;--d:6s;--delay:1.3s;--op:0.4"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:85%;left:70%;--d:5s;--delay:2.9s;--op:0.35"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:92%;left:8%;--d:7s;--delay:0.2s;--op:0.5"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:10%;left:33%;--d:4s;--delay:1.8s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:93%;left:88%;--d:9s;--delay:3.5s;--op:0.4"></div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
<!-- Animated bloom mark -->
|
||||
<div class="bloom-wrap" role="img" aria-label="FlowerCore bloom mark">
|
||||
<!--
|
||||
Bloom mark: 8 elliptical petals + outer ring + pulsing seed core.
|
||||
Source of truth: scripts/reformat/branding/oem-logo-bluejay.svg
|
||||
Petals rotate slowly (24s/rev). Core counter-rotates to stay upright.
|
||||
Outer glow added via CSS. Respects prefers-reduced-motion.
|
||||
-->
|
||||
<svg class="bloom-svg" viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<radialGradient id="petal-grad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#FFCA40" stop-opacity="0.95"/>
|
||||
<stop offset="100%" stop-color="#FFB300" stop-opacity="0.7"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="core-grad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#FFCA40"/>
|
||||
<stop offset="100%" stop-color="#CC8F00"/>
|
||||
</radialGradient>
|
||||
<!-- Petal shape: thin tall ellipse offset upward from origin -->
|
||||
<ellipse id="petal" cx="0" cy="-38" rx="6.5" ry="26"
|
||||
fill="none" stroke="url(#petal-grad)" stroke-width="1.8" opacity="0.85"/>
|
||||
</defs>
|
||||
|
||||
<!-- 8 petals, each 45° apart, centred at 80,80 -->
|
||||
<g transform="translate(80 80)">
|
||||
<use href="#petal" transform="rotate(0)"/>
|
||||
<use href="#petal" transform="rotate(45)"/>
|
||||
<use href="#petal" transform="rotate(90)"/>
|
||||
<use href="#petal" transform="rotate(135)"/>
|
||||
<use href="#petal" transform="rotate(180)"/>
|
||||
<use href="#petal" transform="rotate(225)"/>
|
||||
<use href="#petal" transform="rotate(270)"/>
|
||||
<use href="#petal" transform="rotate(315)"/>
|
||||
<!-- Outer ring -->
|
||||
<circle r="44" fill="none" stroke="#FFB300" stroke-width="1.5" opacity="0.6"/>
|
||||
<!-- Seed core: counter-rotates via CSS so it stays upright -->
|
||||
<g class="bloom-core">
|
||||
<circle class="bloom-core-circle" r="13" fill="url(#core-grad)"/>
|
||||
<circle r="13" fill="none" stroke="#0A1628" stroke-width="1.5" opacity="0.45"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="services">
|
||||
<a class="service" href="https://gitea.flowercore.io">
|
||||
<h3>Source</h3>
|
||||
<p>Gitea repositories</p>
|
||||
</a>
|
||||
<a class="service" href="https://webmail.flowercore.io">
|
||||
<h3>Mail</h3>
|
||||
<p>Webmail access</p>
|
||||
</a>
|
||||
<a class="service" href="https://element.flowercore.io">
|
||||
<h3>Chat</h3>
|
||||
<p>Matrix messaging</p>
|
||||
</a>
|
||||
<a class="service" href="https://github.com/FlowerCoreIO">
|
||||
<h3>GitHub</h3>
|
||||
<p>Open source</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<div class="value">17</div>
|
||||
<div class="label">Services</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="value">13</div>
|
||||
<div class="label">VLANs</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="value">12k+</div>
|
||||
<div class="label">Tests</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer">
|
||||
FlowerCore · Bare-metal RKE2 · ArgoCD managed
|
||||
· <a href="mailto:admin@flowercore.io">Contact</a>
|
||||
</p>
|
||||
|
||||
<!-- Wordmark -->
|
||||
<p class="wordmark">FlowerCore</p>
|
||||
<p class="tagline">More to come.</p>
|
||||
|
||||
<!-- Primary CTA: Update Center -->
|
||||
<a class="update-link" href="https://update.flowercore.io/" target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<!-- Inline download/arrow SVG — no external ref -->
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true" focusable="false">
|
||||
<path d="M8 2v8M4 7l4 4 4-4" stroke="#0A1628" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 13h12" stroke="#0A1628" stroke-width="2"
|
||||
stroke-linecap="round"/>
|
||||
</svg>
|
||||
Update Center
|
||||
</a>
|
||||
|
||||
<p class="more-soon">Software updates & releases</p>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
© FlowerCore
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
|
||||
356
apps/fc-landing/preview.html
Normal file
356
apps/fc-landing/preview.html
Normal file
@@ -0,0 +1,356 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FlowerCore</title>
|
||||
<!--
|
||||
FlowerCore public landing page.
|
||||
Air-gap safe: zero CDN, zero external fonts, zero external scripts.
|
||||
All assets are inline SVG / CSS / JS only.
|
||||
Update Center link: https://update.flowercore.io/
|
||||
Animation: bloom mark (8-petal flower) rotates slowly with a floating
|
||||
seed-core pulse. Respects prefers-reduced-motion (animation disabled).
|
||||
-->
|
||||
<style>
|
||||
/* ── Blue Jay palette (matches decisions-waiting.html tokens) ── */
|
||||
:root {
|
||||
--bg: #0A1628;
|
||||
--bg2: #0E1E36;
|
||||
--surface: #111D33;
|
||||
--border: #1E3A5F;
|
||||
--text: #E8EDF5;
|
||||
--text-muted: #8899B3;
|
||||
--gold: #FFB300;
|
||||
--gold-light: #FFCA40;
|
||||
--gold-dim: #CC8F00;
|
||||
--ice: #82BBFF;
|
||||
--steel: #2E5FA3;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
background: radial-gradient(ellipse at 60% 30%, #13233E 0%, #0C1A30 55%, #081222 100%);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Skip link (keyboard accessibility) ── */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -999px;
|
||||
top: 0;
|
||||
background: var(--gold);
|
||||
color: #0A1628;
|
||||
padding: 8px 16px;
|
||||
border-radius: 0 0 8px 0;
|
||||
font-weight: 700;
|
||||
z-index: 100;
|
||||
text-decoration: none;
|
||||
}
|
||||
.skip-link:focus { left: 0; }
|
||||
|
||||
/* ── Focus ring ── */
|
||||
a:focus-visible, button:focus-visible {
|
||||
outline: 3px solid var(--ice);
|
||||
outline-offset: 3px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ── Background star field (CSS only, no JS needed) ── */
|
||||
.starfield {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
.star {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
opacity: 0;
|
||||
animation: twinkle var(--d, 4s) var(--delay, 0s) ease-in-out infinite;
|
||||
}
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0; transform: scale(0.5); }
|
||||
50% { opacity: var(--op, 0.5); transform: scale(1); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.star { animation: none; opacity: 0.2; }
|
||||
}
|
||||
|
||||
/* ── Main content ── */
|
||||
main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Bloom mark container ── */
|
||||
.bloom-wrap {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Outer glow ring */
|
||||
.bloom-wrap::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -24px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255,179,0,0.12) 0%, transparent 70%);
|
||||
animation: pulse-glow 3s ease-in-out infinite;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-wrap::before { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.12); opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* The bloom SVG itself */
|
||||
.bloom-svg {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
animation: bloom-spin 24s linear infinite;
|
||||
transform-origin: center;
|
||||
filter: drop-shadow(0 0 18px rgba(255,179,0,0.35));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-svg { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes bloom-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Seed core counter-rotates so it stays upright */
|
||||
.bloom-core {
|
||||
animation: bloom-counter 24s linear infinite;
|
||||
transform-origin: 80px 80px; /* center of 160px viewBox */
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-core { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes bloom-counter {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(-360deg); }
|
||||
}
|
||||
|
||||
/* Core pulse */
|
||||
.bloom-core-circle {
|
||||
animation: core-pulse 2.4s ease-in-out infinite;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bloom-core-circle { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes core-pulse {
|
||||
0%, 100% { r: 13; }
|
||||
50% { r: 17; }
|
||||
}
|
||||
|
||||
/* ── Wordmark ── */
|
||||
.wordmark {
|
||||
letter-spacing: 0.22em;
|
||||
font-size: 1.9rem;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.35rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.12em;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* ── Update Center link (primary action) ── */
|
||||
.update-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
background: var(--gold);
|
||||
color: #0A1628;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.75rem 1.75rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
border: 2px solid var(--gold);
|
||||
transition: background 0.18s, border-color 0.18s, transform 0.15s;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.update-link:hover {
|
||||
background: var(--gold-light);
|
||||
border-color: var(--gold-light);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.update-link:active {
|
||||
background: var(--gold-dim);
|
||||
border-color: var(--gold-dim);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Arrow icon inside the button */
|
||||
.update-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── "More soon" note ── */
|
||||
.more-soon {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 3rem;
|
||||
color: #2E5FA3;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.05em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 400px) {
|
||||
.bloom-wrap { width: 140px; height: 140px; }
|
||||
.bloom-svg { width: 124px; height: 124px; }
|
||||
.bloom-core { transform-origin: 62px 62px; }
|
||||
.wordmark { font-size: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main" class="skip-link">Skip to content</a>
|
||||
|
||||
<!-- CSS-only star field: 20 stars, varied positions/sizes/speeds -->
|
||||
<div class="starfield" aria-hidden="true">
|
||||
<div class="star" style="width:2px;height:2px;top:8%;left:12%;--d:5s;--delay:0.3s;--op:0.6"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:15%;left:75%;--d:7s;--delay:1.1s;--op:0.4"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:22%;left:45%;--d:4s;--delay:0.7s;--op:0.5"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:30%;left:88%;--d:6s;--delay:2.0s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:40%;left:5%;--d:8s;--delay:0.4s;--op:0.4"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:55%;left:30%;--d:5s;--delay:1.5s;--op:0.35"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:65%;left:62%;--d:7s;--delay:0.9s;--op:0.45"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:72%;left:20%;--d:4s;--delay:2.3s;--op:0.5"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:80%;left:82%;--d:6s;--delay:0.1s;--op:0.3"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:88%;left:50%;--d:9s;--delay:1.7s;--op:0.4"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:5%;left:55%;--d:6s;--delay:3.1s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:18%;left:92%;--d:5s;--delay:0.5s;--op:0.5"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:35%;left:68%;--d:7s;--delay:1.9s;--op:0.35"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:48%;left:18%;--d:4s;--delay:2.7s;--op:0.45"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:60%;left:95%;--d:8s;--delay:0.6s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:75%;left:40%;--d:6s;--delay:1.3s;--op:0.4"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:85%;left:70%;--d:5s;--delay:2.9s;--op:0.35"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:92%;left:8%;--d:7s;--delay:0.2s;--op:0.5"></div>
|
||||
<div class="star" style="width:1px;height:1px;top:10%;left:33%;--d:4s;--delay:1.8s;--op:0.3"></div>
|
||||
<div class="star" style="width:2px;height:2px;top:93%;left:88%;--d:9s;--delay:3.5s;--op:0.4"></div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
<!-- Animated bloom mark -->
|
||||
<div class="bloom-wrap" role="img" aria-label="FlowerCore bloom mark">
|
||||
<!--
|
||||
Bloom mark: 8 elliptical petals + outer ring + pulsing seed core.
|
||||
Source of truth: scripts/reformat/branding/oem-logo-bluejay.svg
|
||||
Petals rotate slowly (24s/rev). Core counter-rotates to stay upright.
|
||||
Outer glow added via CSS. Respects prefers-reduced-motion.
|
||||
-->
|
||||
<svg class="bloom-svg" viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<radialGradient id="petal-grad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#FFCA40" stop-opacity="0.95"/>
|
||||
<stop offset="100%" stop-color="#FFB300" stop-opacity="0.7"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="core-grad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#FFCA40"/>
|
||||
<stop offset="100%" stop-color="#CC8F00"/>
|
||||
</radialGradient>
|
||||
<!-- Petal shape: thin tall ellipse offset upward from origin -->
|
||||
<ellipse id="petal" cx="0" cy="-38" rx="6.5" ry="26"
|
||||
fill="none" stroke="url(#petal-grad)" stroke-width="1.8" opacity="0.85"/>
|
||||
</defs>
|
||||
|
||||
<!-- 8 petals, each 45° apart, centred at 80,80 -->
|
||||
<g transform="translate(80 80)">
|
||||
<use href="#petal" transform="rotate(0)"/>
|
||||
<use href="#petal" transform="rotate(45)"/>
|
||||
<use href="#petal" transform="rotate(90)"/>
|
||||
<use href="#petal" transform="rotate(135)"/>
|
||||
<use href="#petal" transform="rotate(180)"/>
|
||||
<use href="#petal" transform="rotate(225)"/>
|
||||
<use href="#petal" transform="rotate(270)"/>
|
||||
<use href="#petal" transform="rotate(315)"/>
|
||||
<!-- Outer ring -->
|
||||
<circle r="44" fill="none" stroke="#FFB300" stroke-width="1.5" opacity="0.6"/>
|
||||
<!-- Seed core: counter-rotates via CSS so it stays upright -->
|
||||
<g class="bloom-core">
|
||||
<circle class="bloom-core-circle" r="13" fill="url(#core-grad)"/>
|
||||
<circle r="13" fill="none" stroke="#0A1628" stroke-width="1.5" opacity="0.45"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Wordmark -->
|
||||
<p class="wordmark">FlowerCore</p>
|
||||
<p class="tagline">More to come.</p>
|
||||
|
||||
<!-- Primary CTA: Update Center -->
|
||||
<a class="update-link" href="https://update.flowercore.io/" target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<!-- Inline download/arrow SVG — no external ref -->
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true" focusable="false">
|
||||
<path d="M8 2v8M4 7l4 4 4-4" stroke="#0A1628" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 13h12" stroke="#0A1628" stroke-width="2"
|
||||
stroke-linecap="round"/>
|
||||
</svg>
|
||||
Update Center
|
||||
</a>
|
||||
|
||||
<p class="more-soon">Software updates & releases</p>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
© FlowerCore
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -60,7 +60,7 @@ spec:
|
||||
- envFrom:
|
||||
- configMapRef:
|
||||
name: library-web-config
|
||||
image: localhost/fc-library-web:v20260615-rl5-fineaction-80981c3
|
||||
image: localhost/fc-library-web:v20260602-library-owned-deploy-fix1
|
||||
imagePullPolicy: Never
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
|
||||
@@ -164,33 +164,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"
|
||||
|
||||
@@ -61,7 +61,7 @@ spec:
|
||||
- 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
|
||||
|
||||
@@ -535,7 +535,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-ttsreader-web:v20260616-tt3-plane-3fe50f0
|
||||
image: localhost/fc-ttsreader-web:v20260614-wave5-help-2f096e3
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5217
|
||||
@@ -605,7 +605,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
|
||||
|
||||
@@ -61,7 +61,7 @@ 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
|
||||
|
||||
63
apps/github-runner/Dockerfile.arm64
Normal file
63
apps/github-runner/Dockerfile.arm64
Normal file
@@ -0,0 +1,63 @@
|
||||
FROM myoung34/github-runner:latest
|
||||
|
||||
# arm64 variant of the FlowerCore self-hosted runner image.
|
||||
# Built/maintained alongside the amd64 Dockerfile. The ONLY substantive
|
||||
# difference from the amd64 image is that the Actions/Ruby tool-cache arch
|
||||
# label is "arm64" instead of the amd64 "x64". Ruby itself is still compiled
|
||||
# from source via ruby-build, so it is naturally arm64 on an aarch64 host.
|
||||
|
||||
ARG RUBY_VERSION=3.3.11
|
||||
ARG RUBY_MINOR=3.3
|
||||
ARG RUBY_BUILD_VERSION=v20260326
|
||||
ARG RUNNER_UID=1001
|
||||
ARG RUNNER_GID=1001
|
||||
# arm64 tool-cache arch label (was x64 on amd64). ruby/setup-ruby@v1 on a
|
||||
# self-hosted aarch64 Linux runner discovers Ruby under _tool/Ruby/<ver>/arm64.
|
||||
ARG TOOLCACHE_ARCH=arm64
|
||||
|
||||
ENV RUNNER_TOOL_CACHE=/home/runner/_tool
|
||||
ENV RUNNER_RUBY_TOOLCACHE=/opt/runner-toolcache
|
||||
ENV PATH="/home/runner/_tool/Ruby/${RUBY_MINOR}/${TOOLCACHE_ARCH}/bin:/opt/runner-toolcache/Ruby/${RUBY_MINOR}/${TOOLCACHE_ARCH}/bin:${PATH}"
|
||||
|
||||
USER root
|
||||
|
||||
# Bake the IAmWorkin step-ca root CA into the system trust store. Without
|
||||
# this, .NET HttpClient calls from CI tests against *.iamworkin.lan
|
||||
# (e.g. https://selenium.iamworkin.lan/session) fail with `PartialChain`
|
||||
# because the runner image's default Ubuntu trust bundle doesn't include
|
||||
# our internal Root CA. update-ca-certificates regenerates
|
||||
# /etc/ssl/certs/ca-certificates.crt, which OpenSSL + .NET on Linux read
|
||||
# automatically — no SSL_CERT_FILE env var needed.
|
||||
COPY step-ca-root.crt /usr/local/share/ca-certificates/iamworkin-step-ca-root.crt
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
bison \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libdb-dev \
|
||||
libffi-dev \
|
||||
libgdbm-dev \
|
||||
libgmp-dev \
|
||||
libncurses-dev \
|
||||
libreadline-dev \
|
||||
libssl-dev \
|
||||
libyaml-dev \
|
||||
patch \
|
||||
pkg-config \
|
||||
uuid-dev \
|
||||
zlib1g-dev \
|
||||
&& update-ca-certificates \
|
||||
&& curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/${RUBY_BUILD_VERSION}.tar.gz" -o /tmp/ruby-build.tar.gz \
|
||||
&& mkdir -p /tmp/ruby-build \
|
||||
&& tar -xzf /tmp/ruby-build.tar.gz --strip-components=1 -C /tmp/ruby-build \
|
||||
&& /tmp/ruby-build/install.sh \
|
||||
&& rm -rf /tmp/ruby-build /tmp/ruby-build.tar.gz /var/lib/apt/lists/*
|
||||
|
||||
COPY install-ruby-toolcache.sh /usr/local/bin/install-ruby-toolcache.sh
|
||||
|
||||
RUN chmod +x /usr/local/bin/install-ruby-toolcache.sh \
|
||||
&& RUBY_VERSION="${RUBY_VERSION}" RUBY_MINOR="${RUBY_MINOR}" TOOLCACHE_ARCH="${TOOLCACHE_ARCH}" TOOLCACHE_ROOT="${RUNNER_RUBY_TOOLCACHE}" RUNNER_UID="${RUNNER_UID}" RUNNER_GID="${RUNNER_GID}" /usr/local/bin/install-ruby-toolcache.sh \
|
||||
&& ruby -v
|
||||
@@ -1698,12 +1698,7 @@ metadata:
|
||||
flowercore.io/runner-repo: tts-reader
|
||||
flowercore.io/github-repo: FlowerCore.TtsReader
|
||||
spec:
|
||||
# Scaled to 0 2026-06-16: this runner was crash-looping (329 restarts) and
|
||||
# consuming memory on the over-pressured old cluster (agent1 ~81%), which was
|
||||
# contributing to Blazor circuit drops on ttsreader/chat. It provides no
|
||||
# working CI in this state. Restore to 1 once the runner is fixed (or the repo
|
||||
# CI moves to the GX10 / a working runner).
|
||||
replicas: 0
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: github-runner-tts-reader
|
||||
|
||||
@@ -6,12 +6,16 @@ RUBY_MINOR="${RUBY_MINOR:-3.3}"
|
||||
TOOLCACHE_ROOT="${TOOLCACHE_ROOT:-/opt/runner-toolcache}"
|
||||
RUNNER_UID="${RUNNER_UID:-1001}"
|
||||
RUNNER_GID="${RUNNER_GID:-1001}"
|
||||
RUBY_PREFIX="${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64"
|
||||
# Tool-cache arch label. Defaults to x64 (amd64 image, unchanged). The arm64
|
||||
# Dockerfile passes TOOLCACHE_ARCH=arm64 so ruby/setup-ruby@v1 finds Ruby on
|
||||
# aarch64 self-hosted runners.
|
||||
TOOLCACHE_ARCH="${TOOLCACHE_ARCH:-x64}"
|
||||
RUBY_PREFIX="${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/${TOOLCACHE_ARCH}"
|
||||
|
||||
mkdir -p "${TOOLCACHE_ROOT}/Ruby"
|
||||
RUBY_CONFIGURE_OPTS="${RUBY_CONFIGURE_OPTS:---disable-install-doc --disable-yjit}" ruby-build "${RUBY_VERSION}" "${RUBY_PREFIX}"
|
||||
|
||||
touch "${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64.complete"
|
||||
touch "${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/${TOOLCACHE_ARCH}.complete"
|
||||
ln -sfn "${RUBY_VERSION}" "${TOOLCACHE_ROOT}/Ruby/${RUBY_MINOR}"
|
||||
|
||||
"${RUBY_PREFIX}/bin/ruby" -v
|
||||
|
||||
@@ -92,7 +92,7 @@ spec:
|
||||
# down. Bulk embed runs in the background; /health does not depend on it.
|
||||
# Memory: feedback_pi5_nomic_embed_slow.
|
||||
- name: IntranetSearch__OllamaBaseUrl
|
||||
value: "http://10.0.57.201:11434"
|
||||
value: "http://10.0.56.132: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
|
||||
|
||||
@@ -168,7 +168,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:
|
||||
|
||||
@@ -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
|
||||
@@ -25,25 +24,17 @@ data:
|
||||
---
|
||||
# 1Password → K8s Secret sync for Twilio credentials
|
||||
# Creates secret "twilio-credentials" with fields: AccountSid, AuthToken, DefaultFromNumber
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: twilio-credentials
|
||||
namespace: telephony
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/Twilio Account"
|
||||
---
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: telephony-mcp-keys
|
||||
namespace: telephony
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/Twilio IVR MCP Token (Agent Zero)"
|
||||
---
|
||||
# Application configuration overlay
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: twilio-credentials
|
||||
namespace: telephony
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/Twilio Account"
|
||||
---
|
||||
# Application configuration overlay
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: telephony-config
|
||||
namespace: telephony
|
||||
@@ -71,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",
|
||||
@@ -80,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
|
||||
},
|
||||
@@ -124,9 +114,9 @@ spec:
|
||||
app: telephony-web
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/health"
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/health"
|
||||
labels:
|
||||
app: telephony-web
|
||||
spec:
|
||||
@@ -164,7 +154,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
|
||||
@@ -174,7 +164,7 @@ spec:
|
||||
ports:
|
||||
- containerPort: 5100
|
||||
name: http
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
# fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: Telephony__Twilio__AccountSid
|
||||
valueFrom:
|
||||
@@ -188,28 +178,12 @@ spec:
|
||||
name: twilio-credentials
|
||||
key: AuthToken
|
||||
optional: true
|
||||
- name: Telephony__Twilio__DefaultFromNumber
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: twilio-credentials
|
||||
key: DefaultFromNumber
|
||||
optional: true
|
||||
- name: FlowerCore__Mcp__ApiKey__Key
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: telephony-mcp-keys
|
||||
key: credential
|
||||
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"
|
||||
- name: Telephony__Twilio__DefaultFromNumber
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: twilio-credentials
|
||||
key: DefaultFromNumber
|
||||
optional: true
|
||||
volumeMounts:
|
||||
- name: telephony-config
|
||||
mountPath: /app/appsettings.Production.json
|
||||
@@ -315,25 +289,17 @@ spec:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
# Allow Selenium Grid for automated UI testing
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: selenium
|
||||
ports:
|
||||
- port: 5100
|
||||
protocol: TCP
|
||||
# Allow FlowerCore.Mcp.Gateway to reach Telephony /mcp on the destination pod port.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-gateway
|
||||
ports:
|
||||
- port: 5100
|
||||
protocol: TCP
|
||||
# Allow SIP/RTP from external sources (Yealink phones, Twilio SIP trunk)
|
||||
- from:
|
||||
- ipBlock:
|
||||
# Allow Selenium Grid for automated UI testing
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: selenium
|
||||
ports:
|
||||
- port: 5100
|
||||
protocol: TCP
|
||||
# Allow SIP/RTP from external sources (Yealink phones, Twilio SIP trunk)
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
- port: 5060
|
||||
@@ -354,14 +320,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
|
||||
|
||||
83
docs/gx10-tenant-landing/CUTOVER-RUNBOOK.md
Normal file
83
docs/gx10-tenant-landing/CUTOVER-RUNBOOK.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# GX10 Tenant Landing-Site Migration — Cutover Runbook
|
||||
|
||||
Date: 2026-06-16. Migrates the 5 per-tenant public landing sites from the OLD RKE2
|
||||
cluster (`10.0.56.200` Traefik) to the GX10 ARM64 cluster (`10.0.57.202` VIP /
|
||||
NodePort `10.0.56.14:32491`).
|
||||
|
||||
## Deployed on GX10 (DONE — staged-verified, NOT yet receiving public traffic)
|
||||
|
||||
| Domain(s) | GX10 ns | Workload | TLS secret (in ns + traefik-system) | Live content replicated |
|
||||
|-----------------------------------|--------------------|---------------|-------------------------------------|-------------------------|
|
||||
| bluejay.dev, www.bluejay.dev | `fc-tenant-andrew` | nginx:alpine | `cf-origin-bluejay-dev` | "Blue Jay" (custom) |
|
||||
| timeforta.co, www.timeforta.co | `fc-tenant-dustin` | nginx:alpine | `cf-origin-timeforta-co` | "Coming Soon" (generic) |
|
||||
| erckak.dev, www.erckak.dev | `fc-tenant-erik` | nginx:alpine | `cf-origin-erckak-dev` | "Erckak" (custom) |
|
||||
| flowerinsider.xyz, www.* | `fc-tenant-fit` | nginx:alpine | `cf-origin-flowerinsider-xyz` | "Flower Insider" (custom)|
|
||||
| matt.flowercore.io | `fc-tenant-matt` | nginx:alpine | `cf-origin-flowercore-io` | "Coming Soon" (generic) |
|
||||
|
||||
All nginx pods 1/1 Running, IngressRoutes priority 100 (override the GX10
|
||||
`public-catchall`). Each site replicates EXACTLY what was live on OLD at migration
|
||||
time, so cutover is content-invisible.
|
||||
|
||||
Staged verification (all HTTP 200, correct content, SNI-correct cert):
|
||||
```
|
||||
curl -sk --resolve <host>:32491:10.0.56.14 https://<host>:32491/
|
||||
```
|
||||
|
||||
## Public routing reality (why NO automatic cutover happened)
|
||||
|
||||
Every tenant domain enters the network through Cloudflare (proxied) → a dedicated
|
||||
pfSense WAN IP in 74.40.140.16/28 → pfSense port-forward. ALL FIVE currently forward
|
||||
to OLD Traefik `10.0.56.200:443`:
|
||||
|
||||
| Domain | CF origin WAN IP | pfSense rdr today |
|
||||
|-------------------|------------------|--------------------|
|
||||
| bluejay.dev | 74.40.140.17 | → 10.0.56.200:443 |
|
||||
| matt.flowercore.io| 74.40.140.19 | → 10.0.56.200:443 |
|
||||
| timeforta.co | 74.40.140.21 | → 10.0.56.200:443 |
|
||||
| erckak.dev | 74.40.140.23 | → 10.0.56.200:443 |
|
||||
| flowerinsider.xyz | 74.40.140.25 | → 10.0.56.200:443 |
|
||||
|
||||
(Contrast: main flowercore.io = WAN `.24` → already GX10 `10.0.56.14:32491`.)
|
||||
NOTE: matt.flowercore.io is bound to WAN `.19` (the MATT VPN IP), NOT `.24`, so the
|
||||
"*.flowercore.io already NATs to GX10" assumption does NOT cover matt.
|
||||
|
||||
Because none of these NAT to GX10 yet, no cutover was performed (live sites untouched).
|
||||
|
||||
## OPERATOR ACTION — cutover = repoint the pfSense port-forward target
|
||||
|
||||
For each domain, change the HTTPS (and HTTP) port-forward TARGET from
|
||||
`10.0.56.200` to `10.0.56.14:32491` (HTTPS) / `10.0.56.14:30776` (HTTP). pfSense
|
||||
port-forwards (Firewall → NAT → Port Forward), edit these rule descriptions:
|
||||
|
||||
- `ANDREW: HTTPS to Traefik` 74.40.140.17:443 → change target `10.0.56.200:443` to `10.0.56.14:32491`
|
||||
- `MATT: HTTPS to Traefik` 74.40.140.19:443 → change target `10.0.56.200:443` to `10.0.56.14:32491`
|
||||
- `DUSTIN: HTTPS to Traefik` 74.40.140.21:443 → change target `10.0.56.200:443` to `10.0.56.14:32491`
|
||||
- `ERIK: HTTPS to Traefik` 74.40.140.23:443 → change target `10.0.56.200:443` to `10.0.56.14:32491`
|
||||
- `FIT: HTTPS to Traefik` 74.40.140.25:443 → change target `10.0.56.200:443` to `10.0.56.14:32491`
|
||||
- (corresponding `:80 → 10.0.56.14:30776` HTTP rules likewise, optional — sites are HTTPS-only)
|
||||
|
||||
No Cloudflare DNS change is required: the WAN IPs stay the same, only the internal
|
||||
NAT target moves. Each can be flipped independently (per-tenant blast radius).
|
||||
|
||||
Post-flip verify (external):
|
||||
```
|
||||
curl -sI https://<host>/ # expect HTTP 200, Server: cloudflare, unchanged content
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
OLD cluster left fully intact (ArgoCD apps infra-andrew/dustin/erik/fit Synced+Healthy,
|
||||
pods Running). To roll back any domain: revert that pfSense port-forward target to
|
||||
`10.0.56.200`.
|
||||
|
||||
## Notes
|
||||
- The OLD cluster has DUPLICATE namespaces per tenant (`tenant-X` custom page +
|
||||
`fc-tenant-X` generic landing), both with IngressRoutes claiming the same host.
|
||||
Traefik non-deterministically picked a winner; live content was: andrew/erik/fit =
|
||||
custom (`tenant-X`), dustin/matt = generic (`fc-tenant-X`). GX10 consolidates to ONE
|
||||
namespace per tenant (`fc-tenant-X`) serving the content that was actually live.
|
||||
- `infra-worldbuilder` (worldbuilder.iamworkin.lan, internal .NET app) was ALREADY
|
||||
migrated to GX10 (`fc-worldbuilder`, 1/1 Running) — no action.
|
||||
- `infra-flowercore` (tenant-flowercore/flowercore-web demo) has NO public route and is
|
||||
superseded by the production `fc-system/fc-landing-public` (flowercore.io root) already
|
||||
live on GX10 — intentionally NOT migrated.
|
||||
225
docs/gx10-tenant-landing/andrew-deploy.yaml
Normal file
225
docs/gx10-tenant-landing/andrew-deploy.yaml
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-tenant-andrew
|
||||
labels:
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
flowercore.io/tenant: andrew
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: andrew-web-html
|
||||
namespace: fc-tenant-andrew
|
||||
data:
|
||||
index.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Blue Jay — bluejay.dev</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0A1628;
|
||||
color: #e0e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% 0%, rgba(43,138,255,0.15) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 60% 50% at 80% 100%, rgba(43,138,255,0.08) 0%, transparent 60%),
|
||||
linear-gradient(135deg, #0A1628 0%, #111E36 50%, #0A1628 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: rgba(43,138,255,0.12);
|
||||
border: 1px solid rgba(43,138,255,0.25);
|
||||
border-radius: 20px;
|
||||
padding: 0.4rem 1.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: #2B8AFF;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
filter: drop-shadow(0 0 30px rgba(43,138,255,0.3));
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.8rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #2B8AFF 0%, #6BB3FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.domain {
|
||||
font-size: 1.1rem;
|
||||
color: #6BB3FF;
|
||||
font-weight: 300;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(43,138,255,0.08);
|
||||
border: 1px solid rgba(43,138,255,0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.status .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #2B8AFF;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 1; box-shadow: 0 0 8px rgba(43,138,255,0.6); }
|
||||
}
|
||||
.status p {
|
||||
font-size: 0.95rem;
|
||||
color: #8aa8c4;
|
||||
}
|
||||
.divider {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(43,138,255,0.4), transparent);
|
||||
margin: 2rem auto;
|
||||
}
|
||||
.footer {
|
||||
color: #3a5570;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.footer a {
|
||||
color: #4a7a9e;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
h1 { font-size: 2rem; }
|
||||
.icon { font-size: 3rem; }
|
||||
.container { padding: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div>
|
||||
<div class="container">
|
||||
<div class="badge">Andrew's Space</div>
|
||||
<div class="icon">🐦</div>
|
||||
<h1>Blue Jay</h1>
|
||||
<p class="domain">bluejay.dev</p>
|
||||
<div class="status">
|
||||
<span class="dot"></span>
|
||||
<p>Coming Soon</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<p class="footer">
|
||||
Powered by <a href="https://flowercore.io">FlowerCore</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: andrew-web-nginx-conf
|
||||
namespace: fc-tenant-andrew
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
location /healthz { access_log off; return 200 "ok"; add_header Content-Type text/plain; }
|
||||
}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: andrew-web
|
||||
namespace: fc-tenant-andrew
|
||||
labels:
|
||||
app: andrew-web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: andrew-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: andrew-web
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: nginx-conf
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
- name: html
|
||||
mountPath: /usr/share/nginx/html
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi, cpu: 50m }
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: nginx-conf
|
||||
configMap: { name: andrew-web-nginx-conf }
|
||||
- name: html
|
||||
configMap: { name: andrew-web-html }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: andrew-web
|
||||
namespace: fc-tenant-andrew
|
||||
spec:
|
||||
selector:
|
||||
app: andrew-web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
18
docs/gx10-tenant-landing/andrew-ingressroute.yaml
Normal file
18
docs/gx10-tenant-landing/andrew-ingressroute.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: andrew-web
|
||||
namespace: fc-tenant-andrew
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`bluejay.dev`) || Host(`www.bluejay.dev`)
|
||||
kind: Rule
|
||||
priority: 100
|
||||
services:
|
||||
- name: andrew-web
|
||||
port: 80
|
||||
tls:
|
||||
secretName: cf-origin-bluejay-dev
|
||||
208
docs/gx10-tenant-landing/dustin-deploy.yaml
Normal file
208
docs/gx10-tenant-landing/dustin-deploy.yaml
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-tenant-dustin
|
||||
labels:
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
flowercore.io/tenant: dustin
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dustin-web-html
|
||||
namespace: fc-tenant-dustin
|
||||
data:
|
||||
index.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>timeforta.co — Coming Soon</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
filter: drop-shadow(0 0 20px #4aff9e40);
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #4aff9e;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.domain {
|
||||
font-size: 1.1rem;
|
||||
color: #888;
|
||||
margin-bottom: 2rem;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
}
|
||||
.divider {
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: #4aff9e;
|
||||
margin: 0 auto 2rem;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.tagline {
|
||||
font-size: 1.3rem;
|
||||
color: #ccc;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 1.2rem;
|
||||
background: #4aff9e15;
|
||||
border: 1px solid #4aff9e40;
|
||||
border-radius: 20px;
|
||||
color: #4aff9e;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.pulse {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #4aff9e;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
color: #555;
|
||||
font-size: 0.8rem;
|
||||
border-top: 1px solid #ffffff08;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
footer a {
|
||||
color: #4a9eff;
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">🌮</div>
|
||||
<h1>timeforta.co</h1>
|
||||
<div class="domain">Dustin</div>
|
||||
<div class="divider"></div>
|
||||
<p class="tagline">It's always time for tacos.</p>
|
||||
<div class="status">
|
||||
<span class="pulse"></span>
|
||||
Under Construction
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
Powered by <a href="https://flowercore.io">FlowerCore</a> • Hosted on Blue Jay Infrastructure
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dustin-web-nginx-conf
|
||||
namespace: fc-tenant-dustin
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
location /healthz { access_log off; return 200 "ok"; add_header Content-Type text/plain; }
|
||||
}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dustin-web
|
||||
namespace: fc-tenant-dustin
|
||||
labels:
|
||||
app: dustin-web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dustin-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dustin-web
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: nginx-conf
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
- name: html
|
||||
mountPath: /usr/share/nginx/html
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi, cpu: 50m }
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: nginx-conf
|
||||
configMap: { name: dustin-web-nginx-conf }
|
||||
- name: html
|
||||
configMap: { name: dustin-web-html }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dustin-web
|
||||
namespace: fc-tenant-dustin
|
||||
spec:
|
||||
selector:
|
||||
app: dustin-web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
18
docs/gx10-tenant-landing/dustin-ingressroute.yaml
Normal file
18
docs/gx10-tenant-landing/dustin-ingressroute.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: dustin-web
|
||||
namespace: fc-tenant-dustin
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`timeforta.co`) || Host(`www.timeforta.co`)
|
||||
kind: Rule
|
||||
priority: 100
|
||||
services:
|
||||
- name: dustin-web
|
||||
port: 80
|
||||
tls:
|
||||
secretName: cf-origin-timeforta-co
|
||||
225
docs/gx10-tenant-landing/erik-deploy.yaml
Normal file
225
docs/gx10-tenant-landing/erik-deploy.yaml
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-tenant-erik
|
||||
labels:
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
flowercore.io/tenant: erik
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: erik-web-html
|
||||
namespace: fc-tenant-erik
|
||||
data:
|
||||
index.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Erckak — erckak.dev</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0A1628;
|
||||
color: #e0e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% 0%, rgba(43,138,255,0.15) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 60% 50% at 80% 100%, rgba(43,138,255,0.08) 0%, transparent 60%),
|
||||
linear-gradient(135deg, #0A1628 0%, #111E36 50%, #0A1628 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: rgba(43,138,255,0.12);
|
||||
border: 1px solid rgba(43,138,255,0.25);
|
||||
border-radius: 20px;
|
||||
padding: 0.4rem 1.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: #2B8AFF;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
filter: drop-shadow(0 0 30px rgba(43,138,255,0.3));
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.8rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #2B8AFF 0%, #6BB3FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.domain {
|
||||
font-size: 1.1rem;
|
||||
color: #6BB3FF;
|
||||
font-weight: 300;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(43,138,255,0.08);
|
||||
border: 1px solid rgba(43,138,255,0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.status .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #2B8AFF;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 1; box-shadow: 0 0 8px rgba(43,138,255,0.6); }
|
||||
}
|
||||
.status p {
|
||||
font-size: 0.95rem;
|
||||
color: #8aa8c4;
|
||||
}
|
||||
.divider {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(43,138,255,0.4), transparent);
|
||||
margin: 2rem auto;
|
||||
}
|
||||
.footer {
|
||||
color: #3a5570;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.footer a {
|
||||
color: #4a7a9e;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
h1 { font-size: 2rem; }
|
||||
.icon { font-size: 3rem; }
|
||||
.container { padding: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div>
|
||||
<div class="container">
|
||||
<div class="badge">Erik's Space</div>
|
||||
<div class="icon">🚀</div>
|
||||
<h1>Erckak</h1>
|
||||
<p class="domain">erckak.dev</p>
|
||||
<div class="status">
|
||||
<span class="dot"></span>
|
||||
<p>Coming Soon</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<p class="footer">
|
||||
Powered by <a href="https://flowercore.io">FlowerCore</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: erik-web-nginx-conf
|
||||
namespace: fc-tenant-erik
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
location /healthz { access_log off; return 200 "ok"; add_header Content-Type text/plain; }
|
||||
}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: erik-web
|
||||
namespace: fc-tenant-erik
|
||||
labels:
|
||||
app: erik-web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: erik-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: erik-web
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: nginx-conf
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
- name: html
|
||||
mountPath: /usr/share/nginx/html
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi, cpu: 50m }
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: nginx-conf
|
||||
configMap: { name: erik-web-nginx-conf }
|
||||
- name: html
|
||||
configMap: { name: erik-web-html }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: erik-web
|
||||
namespace: fc-tenant-erik
|
||||
spec:
|
||||
selector:
|
||||
app: erik-web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
18
docs/gx10-tenant-landing/erik-ingressroute.yaml
Normal file
18
docs/gx10-tenant-landing/erik-ingressroute.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: erik-web
|
||||
namespace: fc-tenant-erik
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`erckak.dev`) || Host(`www.erckak.dev`)
|
||||
kind: Rule
|
||||
priority: 100
|
||||
services:
|
||||
- name: erik-web
|
||||
port: 80
|
||||
tls:
|
||||
secretName: cf-origin-erckak-dev
|
||||
225
docs/gx10-tenant-landing/fit-deploy.yaml
Normal file
225
docs/gx10-tenant-landing/fit-deploy.yaml
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-tenant-fit
|
||||
labels:
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
flowercore.io/tenant: fit
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: fit-web-html
|
||||
namespace: fc-tenant-fit
|
||||
data:
|
||||
index.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Flower Insider — flowerinsider.xyz</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0A1628;
|
||||
color: #e0e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% 0%, rgba(43,138,255,0.15) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 60% 50% at 80% 100%, rgba(43,138,255,0.08) 0%, transparent 60%),
|
||||
linear-gradient(135deg, #0A1628 0%, #111E36 50%, #0A1628 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: rgba(43,138,255,0.12);
|
||||
border: 1px solid rgba(43,138,255,0.25);
|
||||
border-radius: 20px;
|
||||
padding: 0.4rem 1.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: #2B8AFF;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
filter: drop-shadow(0 0 30px rgba(43,138,255,0.3));
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.8rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #2B8AFF 0%, #6BB3FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.domain {
|
||||
font-size: 1.1rem;
|
||||
color: #6BB3FF;
|
||||
font-weight: 300;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(43,138,255,0.08);
|
||||
border: 1px solid rgba(43,138,255,0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.status .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #2B8AFF;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 1; box-shadow: 0 0 8px rgba(43,138,255,0.6); }
|
||||
}
|
||||
.status p {
|
||||
font-size: 0.95rem;
|
||||
color: #8aa8c4;
|
||||
}
|
||||
.divider {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(43,138,255,0.4), transparent);
|
||||
margin: 2rem auto;
|
||||
}
|
||||
.footer {
|
||||
color: #3a5570;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.footer a {
|
||||
color: #4a7a9e;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
h1 { font-size: 2rem; }
|
||||
.icon { font-size: 3rem; }
|
||||
.container { padding: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div>
|
||||
<div class="container">
|
||||
<div class="badge">Flower Insider Team</div>
|
||||
<div class="icon">🌸</div>
|
||||
<h1>Flower Insider</h1>
|
||||
<p class="domain">flowerinsider.xyz</p>
|
||||
<div class="status">
|
||||
<span class="dot"></span>
|
||||
<p>Coming Soon</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<p class="footer">
|
||||
Powered by <a href="https://flowercore.io">FlowerCore</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: fit-web-nginx-conf
|
||||
namespace: fc-tenant-fit
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
location /healthz { access_log off; return 200 "ok"; add_header Content-Type text/plain; }
|
||||
}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fit-web
|
||||
namespace: fc-tenant-fit
|
||||
labels:
|
||||
app: fit-web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fit-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fit-web
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: nginx-conf
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
- name: html
|
||||
mountPath: /usr/share/nginx/html
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi, cpu: 50m }
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: nginx-conf
|
||||
configMap: { name: fit-web-nginx-conf }
|
||||
- name: html
|
||||
configMap: { name: fit-web-html }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fit-web
|
||||
namespace: fc-tenant-fit
|
||||
spec:
|
||||
selector:
|
||||
app: fit-web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
18
docs/gx10-tenant-landing/fit-ingressroute.yaml
Normal file
18
docs/gx10-tenant-landing/fit-ingressroute.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: fit-web
|
||||
namespace: fc-tenant-fit
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`flowerinsider.xyz`) || Host(`www.flowerinsider.xyz`)
|
||||
kind: Rule
|
||||
priority: 100
|
||||
services:
|
||||
- name: fit-web
|
||||
port: 80
|
||||
tls:
|
||||
secretName: cf-origin-flowerinsider-xyz
|
||||
208
docs/gx10-tenant-landing/matt-deploy.yaml
Normal file
208
docs/gx10-tenant-landing/matt-deploy.yaml
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-tenant-matt
|
||||
labels:
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
flowercore.io/tenant: matt
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: matt-web-html
|
||||
namespace: fc-tenant-matt
|
||||
data:
|
||||
index.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>matt.flowercore.io — Coming Soon</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
filter: drop-shadow(0 0 20px #ff6b4a40);
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #ff6b4a;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.domain {
|
||||
font-size: 1.1rem;
|
||||
color: #888;
|
||||
margin-bottom: 2rem;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
}
|
||||
.divider {
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: #ff6b4a;
|
||||
margin: 0 auto 2rem;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.tagline {
|
||||
font-size: 1.3rem;
|
||||
color: #ccc;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 1.2rem;
|
||||
background: #ff6b4a15;
|
||||
border: 1px solid #ff6b4a40;
|
||||
border-radius: 20px;
|
||||
color: #ff6b4a;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.pulse {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ff6b4a;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
color: #555;
|
||||
font-size: 0.8rem;
|
||||
border-top: 1px solid #ffffff08;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
footer a {
|
||||
color: #4a9eff;
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">🔥</div>
|
||||
<h1>matt.flowercore.io</h1>
|
||||
<div class="domain">Matt</div>
|
||||
<div class="divider"></div>
|
||||
<p class="tagline">Building something extraordinary.</p>
|
||||
<div class="status">
|
||||
<span class="pulse"></span>
|
||||
Under Construction
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
Powered by <a href="https://flowercore.io">FlowerCore</a> • Hosted on Blue Jay Infrastructure
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: matt-web-nginx-conf
|
||||
namespace: fc-tenant-matt
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
location /healthz { access_log off; return 200 "ok"; add_header Content-Type text/plain; }
|
||||
}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: matt-web
|
||||
namespace: fc-tenant-matt
|
||||
labels:
|
||||
app: matt-web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: matt-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: matt-web
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: nginx-conf
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
- name: html
|
||||
mountPath: /usr/share/nginx/html
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi, cpu: 50m }
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: 80 }
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: nginx-conf
|
||||
configMap: { name: matt-web-nginx-conf }
|
||||
- name: html
|
||||
configMap: { name: matt-web-html }
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: matt-web
|
||||
namespace: fc-tenant-matt
|
||||
spec:
|
||||
selector:
|
||||
app: matt-web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
18
docs/gx10-tenant-landing/matt-ingressroute.yaml
Normal file
18
docs/gx10-tenant-landing/matt-ingressroute.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: matt-web
|
||||
namespace: fc-tenant-matt
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`matt.flowercore.io`)
|
||||
kind: Rule
|
||||
priority: 100
|
||||
services:
|
||||
- name: matt-web
|
||||
port: 80
|
||||
tls:
|
||||
secretName: cf-origin-flowercore-io
|
||||
@@ -1,15 +0,0 @@
|
||||
# GX10 cluster platform layer (NOT old-cluster ArgoCD)
|
||||
|
||||
These manifests bootstrap the GX10 RKE2 cluster's platform layer for the NUC→GX10
|
||||
migration. They are **direct-applied** to the GX10 (its own kubectl) during
|
||||
bootstrap, and live under `gx10/` (NOT `apps/`) so the OLD cluster's bluejay-infra
|
||||
ApplicationSet (whose `apps/*` generator targets the OLD cluster) does NOT
|
||||
auto-deploy them there. Once ArgoCD is stood up on the GX10, a GX10-only
|
||||
ApplicationSet (`apps-gx10/*`) will own these.
|
||||
|
||||
- `step-ca-acme.yaml` — cert-manager ClusterIssuer (ACME → noc1 step-ca, in-spec caBundle). APPLIED + Ready.
|
||||
- `traefik-helmchart.yaml` — Traefik v3.6.10 (chart 39.0.5) via the RKE2 HelmChart CRD, LoadBalancer VIP 10.0.57.202 (prod-pool; temp parallel-run VIP — canonical .200 reclaimed at cutover). APPLIED.
|
||||
|
||||
cert-manager v1.17.2 was installed separately (upstream static manifest). See
|
||||
`docs/ai-agents/gx10-migration-continuation-2026-06-14.md` + memory
|
||||
`project_gx10_ai_node_2026_06_13`.
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: step-ca-acme
|
||||
spec:
|
||||
acme:
|
||||
server: https://10.0.56.10:9443/acme/acme/directory
|
||||
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ4RENDQVdxZ0F3SUJBZ0lSQVBZMzU3RzZvdzZ6TUFMNSs0YlMya2t3Q2dZSUtvWkl6ajBFQXdJd1FERWEKTUJnR0ExVUVDaE1SU1VGdFYyOXlhMmx1SUVGRFRVVWdRMEV4SWpBZ0JnTlZCQU1UR1VsQmJWZHZjbXRwYmlCQgpRMDFGSUVOQklGSnZiM1FnUTBFd0hoY05Nall3TXpBNE1UZ3dOekV4V2hjTk16WXdNekExTVRnd056RXhXakJBCk1Sb3dHQVlEVlFRS0V4RkpRVzFYYjNKcmFXNGdRVU5OUlNCRFFURWlNQ0FHQTFVRUF4TVpTVUZ0VjI5eWEybHUKSUVGRFRVVWdRMEVnVW05dmRDQkRRVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSjJuMDRYMQpKWm81WmRxL2kxSWR2OCtmcXdaeUF6Qmg3d2hicWowU1dzSkw4VVdSYWJDTXFZQ3M3K2RYTzB4UlN6cWt3RkRMCngrdm9vT2FpOFJnUk5oYWpSVEJETUE0R0ExVWREd0VCL3dRRUF3SUJCakFTQmdOVkhSTUJBZjhFQ0RBR0FRSC8KQWdFQk1CMEdBMVVkRGdRV0JCUm51UFBRUjZpTS9INnZPbHVpVTNTeWdheXo4akFLQmdncWhrak9QUVFEQWdOSQpBREJGQWlFQXJRSzlkWVBHbUFac2RZbmp6aXVGVlZFNU5LWlVjY2VZdkdmR0MrdExYVXNDSUF1ZEYyekpyQ1JxCjNtSzUwWlpFVC9md1RrSndpRUY0ODI0bWpQOHAxQ0tNCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
||||
privateKeySecretRef:
|
||||
name: step-ca-acme-account-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
ingressClassName: traefik
|
||||
@@ -1,81 +0,0 @@
|
||||
apiVersion: helm.cattle.io/v1
|
||||
kind: HelmChart
|
||||
metadata:
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
spec:
|
||||
chart: traefik
|
||||
repo: https://traefik.github.io/charts
|
||||
version: "39.0.5"
|
||||
targetNamespace: traefik-system
|
||||
createNamespace: true
|
||||
valuesContent: |
|
||||
deployment:
|
||||
replicas: 1
|
||||
additionalArguments:
|
||||
- "--api.dashboard=true"
|
||||
- "--log.level=INFO"
|
||||
- "--providers.kubernetescrd"
|
||||
- "--providers.kubernetesingress"
|
||||
- "--providers.kubernetescrd.allowEmptyServices=true"
|
||||
- "--providers.kubernetesingress.allowEmptyServices=true"
|
||||
- "--providers.kubernetesingress.ingressendpoint.publishedservice=traefik-system/traefik"
|
||||
ingressRoute:
|
||||
dashboard:
|
||||
enabled: false
|
||||
rbac:
|
||||
enabled: true
|
||||
service:
|
||||
type: LoadBalancer
|
||||
annotations:
|
||||
metallb.io/loadBalancerIPs: "10.0.57.202"
|
||||
metallb.io/address-pool: "prod-pool"
|
||||
ports:
|
||||
web:
|
||||
port: 8000
|
||||
exposedPort: 80
|
||||
protocol: TCP
|
||||
websecure:
|
||||
port: 8443
|
||||
exposedPort: 443
|
||||
protocol: TCP
|
||||
tls:
|
||||
enabled: true
|
||||
irc:
|
||||
port: 6667
|
||||
exposedPort: 6667
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: true
|
||||
irctls:
|
||||
port: 6697
|
||||
exposedPort: 6697
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: true
|
||||
traefik:
|
||||
port: 8080
|
||||
exposedPort: 8080
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: false
|
||||
metrics:
|
||||
port: 9100
|
||||
exposedPort: 9100
|
||||
protocol: TCP
|
||||
expose:
|
||||
default: false
|
||||
metrics:
|
||||
prometheus:
|
||||
entryPoint: metrics
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
tolerations:
|
||||
- key: "node-role.kubernetes.io/control-plane"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
@@ -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)
|
||||
@@ -814,78 +814,6 @@ public sealed class FleetManifestLintTests
|
||||
ns.FileText.Should().Contain("ArgoCD discovers this directory as Application `infra-fc-devicemgmt`.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FcAppleMdm_NanoHubWorkloadMustStayPinnedAndInternalApiOnly()
|
||||
{
|
||||
var documents = AppDocuments("fc-apple-mdm");
|
||||
|
||||
documents.Should().Contain(document => document.Kind == "Namespace" && document.Name == "fc-apple-mdm");
|
||||
documents.Should().Contain(document => document.Kind == "OnePasswordItem" && document.Name == "fc-apple-mdm-runtime");
|
||||
documents.Should().NotContain(document => document.Kind == "Secret");
|
||||
|
||||
var item = documents.Single(document => document.Kind == "OnePasswordItem" && document.Name == "fc-apple-mdm-runtime");
|
||||
item.Scalar("spec", "itemPath").Should().Be("vaults/IAmWorkin/items/FlowerCore Apple MDM Runtime");
|
||||
|
||||
var deployment = documents.Single(document => document.Kind == "Deployment" && document.Name == "fc-apple-mdm");
|
||||
deployment.Scalar("spec", "strategy", "type").Should().Be("Recreate");
|
||||
PodAnnotation(deployment, "fc.flowercore.io/healthz-anon").Should().Be("true");
|
||||
PodAnnotation(deployment, "fc.flowercore.io/probe-path").Should().Be("/version");
|
||||
PodAnnotation(deployment, "flowercore.io/audit-trace-id").Should().Be("apple-mdm-nanohub-runtime-trace");
|
||||
|
||||
var container = deployment.MainContainerMappings().Should().ContainSingle().Subject;
|
||||
ManifestNodeExtensions.Scalar(container, "name").Should().Be("nanohub");
|
||||
ManifestNodeExtensions.Scalar(container, "image").Should().Be("localhost/fc-apple-mdm-nanohub:v0.2.0-20260617");
|
||||
ManifestNodeExtensions.Scalar(container, "imagePullPolicy").Should().Be("Never");
|
||||
EnvValue(container, "NANOHUB_LISTEN").Should().Be(":9004");
|
||||
EnvValue(container, "NANOHUB_STORAGE").Should().Be("file");
|
||||
EnvValue(container, "NANOHUB_STORAGE_DSN").Should().Be("/var/lib/nanohub/db");
|
||||
EnvValue(container, "NANOHUB_CHECKIN").Should().Be("true");
|
||||
EnvSecretName(container, "NANOHUB_API_KEY").Should().Be("fc-apple-mdm-runtime");
|
||||
EnvSecretKey(container, "NANOHUB_API_KEY").Should().Be("NANOHUB_API_KEY");
|
||||
EnvSecretName(container, "NANOHUB_WEBHOOK_URL").Should().Be("fc-apple-mdm-runtime");
|
||||
EnvSecretKey(container, "NANOHUB_WEBHOOK_URL").Should().Be("NANOHUB_WEBHOOK_URL");
|
||||
EnvSecretOptional(container, "NANOHUB_WEBHOOK_URL").Should().Be("true");
|
||||
ProbePath(container, "readinessProbe").Should().Be("/version");
|
||||
ProbePath(container, "startupProbe").Should().Be("/version");
|
||||
ProbePath(container, "livenessProbe").Should().Be("/version");
|
||||
|
||||
var certificate = documents.Single(document => document.Kind == "Certificate" && document.Name == "fc-apple-mdm-tls");
|
||||
certificate.Scalar("spec", "issuerRef", "name").Should().Be("step-ca-acme");
|
||||
certificate.Scalar("spec", "issuerRef", "kind").Should().Be("ClusterIssuer");
|
||||
ManifestNodeExtensions.ScalarSequence(certificate.Root, "spec", "dnsNames")
|
||||
.Should()
|
||||
.ContainSingle("mdm.iamworkin.lan");
|
||||
|
||||
var ingress = documents.Single(document => document.Kind == "IngressRoute" && document.Name == "fc-apple-mdm");
|
||||
var match = ingress.MappingSequence("spec", "routes")
|
||||
.Select(route => ManifestNodeExtensions.Scalar(route, "match") ?? string.Empty)
|
||||
.Should()
|
||||
.ContainSingle()
|
||||
.Subject;
|
||||
|
||||
match.Should().Contain("Host(`mdm.iamworkin.lan`)");
|
||||
match.Should().Contain("PathPrefix(`/mdm`)");
|
||||
match.Should().Contain("PathPrefix(`/checkin`)");
|
||||
match.Should().Contain("PathPrefix(`/version`)");
|
||||
match.Should().NotContain("/api/v1", "NanoHUB API access is cluster-internal for MDM-N1");
|
||||
|
||||
var service = documents.Single(document => document.Kind == "Service" && document.Name == "fc-apple-mdm");
|
||||
service.AllScalars().Should().Contain("9004");
|
||||
|
||||
var policy = documents.Single(document => document.Kind == "NetworkPolicy" && document.Name == "fc-apple-mdm-isolation");
|
||||
policy.AllScalars().Should().Contain(new[]
|
||||
{
|
||||
"traefik-system",
|
||||
"fc-devicemgmt",
|
||||
"10.0.56.200/32",
|
||||
});
|
||||
policy.EgressPorts().Should().Contain(new[] { "53", "80", "443", "8080", "8443" });
|
||||
|
||||
documents.Should().NotContain(document => document.AllScalars().Any(value =>
|
||||
value.Contains("micromdm", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Contains("MICROMDM", StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BroaderHardeningDeployments_MustAnnotateAnonymousHealthProbeIntent()
|
||||
{
|
||||
@@ -939,7 +867,7 @@ public sealed class FleetManifestLintTests
|
||||
{
|
||||
var deployments = new[]
|
||||
{
|
||||
(App: "fc-dns", Name: "dns-web", Slug: "dns", Secret: "dns-oidc-client", AuthEnabled: "true"),
|
||||
(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"),
|
||||
};
|
||||
@@ -990,37 +918,6 @@ public sealed class FleetManifestLintTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DnsPhase0_UsesOnePasswordBackedAcmeApiKey()
|
||||
{
|
||||
var item = AppDocuments("fc-dns")
|
||||
.Single(document => document.Kind == "OnePasswordItem" && document.Name == "dns-api-keys");
|
||||
item.Scalar("spec", "itemPath").Should().Be("vaults/IAmWorkin/items/FlowerCore DNS API Keys");
|
||||
|
||||
var dnsWeb = AppDocuments("fc-dns")
|
||||
.Single(document => document.Kind == "Deployment" && document.Name == "dns-web")
|
||||
.MainContainerMappings()
|
||||
.Should()
|
||||
.ContainSingle()
|
||||
.Subject;
|
||||
EnvSecretName(dnsWeb, "FlowerCore__Auth__ApiKey").Should().Be("dns-api-keys");
|
||||
EnvSecretKey(dnsWeb, "FlowerCore__Auth__ApiKey").Should().Be("api_key");
|
||||
EnvSecretOptional(dnsWeb, "FlowerCore__Auth__ApiKey").Should().Be("true");
|
||||
EnvSecretName(dnsWeb, "FlowerCore__Mcp__ApiKey__Key").Should().Be("dns-api-keys");
|
||||
EnvSecretKey(dnsWeb, "FlowerCore__Mcp__ApiKey__Key").Should().Be("api_key");
|
||||
EnvSecretOptional(dnsWeb, "FlowerCore__Mcp__ApiKey__Key").Should().Be("true");
|
||||
|
||||
var webhook = AppDocuments("fc-dns")
|
||||
.Single(document => document.Kind == "Deployment" && document.Name == "dns-acme-webhook")
|
||||
.MainContainerMappings()
|
||||
.Should()
|
||||
.ContainSingle()
|
||||
.Subject;
|
||||
EnvSecretName(webhook, "FlowerCore__Dns__AcmeWebhook__ApiKey").Should().Be("dns-api-keys");
|
||||
EnvSecretKey(webhook, "FlowerCore__Dns__AcmeWebhook__ApiKey").Should().Be("api_key");
|
||||
EnvSecretOptional(webhook, "FlowerCore__Dns__AcmeWebhook__ApiKey").Should().Be("true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DnsAndMediaGitOpsAdoption_PreservesLiveStorageAndImageShape()
|
||||
{
|
||||
@@ -1030,7 +927,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:v20260616-dn-current-3626f04");
|
||||
ManifestNodeExtensions.Scalar(dnsContainer, "image").Should().Be("localhost/fc-dns-web:v20260613-g5-quota-aa99bd1");
|
||||
dnsPvc.Scalar("spec", "storageClassName").Should().Be("longhorn");
|
||||
dnsPvc.Scalar("spec", "resources", "requests", "storage").Should().Be("1Gi");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user