Compare commits
2 Commits
codex/cx2-
...
codex/s67-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13d8ca8c1a | ||
|
|
b0a3ef7448 |
16
README.md
16
README.md
@@ -2,6 +2,22 @@
|
||||
|
||||
Infrastructure manifests for ArgoCD. An `ApplicationSet` in `argocd` namespace watches the `apps/*` directories in this repo and creates one `Application` per subdir (prefixed `infra-<name>`).
|
||||
|
||||
## Root GitOps ApplicationSet
|
||||
|
||||
`argocd/applicationset-bluejay-infra.yaml` is the root of this GitOps tree, but
|
||||
it is **NOT self-managed** by ArgoCD. Apply it manually when the root generator
|
||||
or sync policy changes:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd apply -f argocd/applicationset-bluejay-infra.yaml
|
||||
```
|
||||
|
||||
Keep the per-StatefulSet `ignoreDifferences` entries in that file synced with
|
||||
the live ApplicationSet. They intentionally cover `zabbix-postgres`,
|
||||
`guac-mysql`, `matrix-postgres`, and `authentik-postgres` so ArgoCD does not
|
||||
loop forever on server-side-apply `volumeClaimTemplates` status drift. Every new
|
||||
StatefulSet with `volumeClaimTemplates` needs its own entry appended.
|
||||
|
||||
## Adding a new service to the cluster
|
||||
|
||||
Follow these steps in order. **Step 1 must run before step 3** — if you skip it, cert-manager HTTP-01 will silently fail for ~2h per cert (exponential backoff) until someone diagnoses the DNS.
|
||||
|
||||
@@ -30,15 +30,18 @@ data:
|
||||
FlowerCore__Auth__Oidc__Audience: "chat"
|
||||
FlowerCore__Auth__Oidc__ClientId: "chat"
|
||||
FlowerCore__Database__ConnectionStrings__Sqlite: "Data Source=/data/chat.db"
|
||||
# Ollama target. BLUEJAY-WS remains faster from the workstation, but this lane
|
||||
# proved Chat pods time out reaching 10.0.56.20:11434. Keep generation and
|
||||
# behavior-rule checks on the cluster-routable edge1 endpoint until that route
|
||||
# is fixed; choose models that edge1 actually hosts.
|
||||
FlowerCore__AI__OllamaBaseUrl: "http://10.0.57.17:11434"
|
||||
FlowerCore__AI__DefaultModelName: "qwen2.5-coder:7b"
|
||||
ChatOptions__BehaviorRuleEngine__OllamaBaseUrl: "http://10.0.57.17:11434"
|
||||
# Ollama target. Switched 2026-04-25 from edge1 Pi5 (10.0.57.17) to BLUEJAY-WS
|
||||
# workstation (10.0.56.20, RX 9070 XT 16GB, OLLAMA_HOST=0.0.0.0:11434, Vulkan
|
||||
# backend per feedback_rdna4_vulkan_broken). The Pi5 was timing out every team-
|
||||
# round speaker at the 300s per-turn cap (live-proven 2026-04-25 03:53 UTC,
|
||||
# see feedback_chat_team_round_edge1_too_slow). Workstation has gemma3:4b for
|
||||
# the Cheap tier, plus gemma3:27b/phi4:14b/qwen3:14b for Default/Balanced/Deep.
|
||||
# Piper TTS stays on edge1 below (different service, Pi handles TTS fine).
|
||||
FlowerCore__AI__OllamaBaseUrl: "http://10.0.56.20:11434"
|
||||
FlowerCore__AI__DefaultModelName: "phi4:14b"
|
||||
ChatOptions__BehaviorRuleEngine__OllamaBaseUrl: "http://10.0.56.20:11434"
|
||||
ChatOptions__BehaviorRuleEngine__FallbackOllamaBaseUrl: "http://10.0.57.17:11434"
|
||||
ChatOptions__BehaviorRuleEngine__ModelName: "gemma3:4b"
|
||||
ChatOptions__BehaviorRuleEngine__ModelName: "gemma3:12b"
|
||||
FlowerCore__AI__Memory__UseSharedIndexingAdapter: "true"
|
||||
FlowerCore__AI__Memory__UseOllamaEmbeddings: "true"
|
||||
FlowerCore__AI__Memory__EmbeddingModel: "nomic-embed-text"
|
||||
@@ -48,7 +51,6 @@ data:
|
||||
FlowerCore__AI__Skills__Retail__RetailApiUrl: "http://retail-web.fc-retail.svc.cluster.local"
|
||||
FlowerCore__AI__Skills__Intranet__IntranetBaseUrl: "http://intranet-web.intranet.svc.cluster.local"
|
||||
FlowerCore__AI__Skills__Print__PrintMcpBaseUrl: "http://10.0.57.16:5200"
|
||||
FlowerCore__AI__Helpdesk__SentimentEscalation__Enabled: "true"
|
||||
FlowerCore__AI__IrcBridge__Enabled: "true"
|
||||
FlowerCore__AI__IrcBridge__DefaultProfileSlug: "it-helpdesk"
|
||||
FlowerCore__AI__IrcBridge__MentionProfileSlug: "it-helpdesk"
|
||||
@@ -123,7 +125,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: chat-web
|
||||
image: localhost/fc-chat-web:v20260614-wave5-sentiment-685f62c
|
||||
image: localhost/fc-chat-web:v20260603-oidc-authentik
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -14,20 +14,6 @@
|
||||
# cluster-rebuild repeatability. See
|
||||
# feedback_networkpolicies_belong_in_bluejay_infra.md.
|
||||
---
|
||||
# OIDC client secret for the RemoteDesktop end-user sign-in (fleet regroup L9,
|
||||
# 2026-06-12). The Authentik provider `remotedesktop` already exists; the 1P item
|
||||
# `remotedesktop-oidc-client` (vault IAmWorkin) carries issuer_url / client_id /
|
||||
# client_secret, and the 1Password operator mints the same-named K8s Secret that
|
||||
# k8s/web-deployment.yaml (FlowerCore.RemoteDesktop repo) consumes with
|
||||
# optional:true. Gate stays OFF (Q-RD-16) — this is flip-READINESS only.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: remotedesktop-oidc-client
|
||||
namespace: fc-desktop
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/remotedesktop-oidc-client"
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
|
||||
@@ -11,7 +11,7 @@ metadata:
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
rules:
|
||||
- apiGroups:
|
||||
- flowercore.io
|
||||
- devices.flowercore.io
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
@@ -23,7 +23,7 @@ rules:
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- flowercore.io
|
||||
- devices.flowercore.io
|
||||
resources:
|
||||
- devices/status
|
||||
- devices/finalizers
|
||||
@@ -33,8 +33,6 @@ rules:
|
||||
- devicepolicies/finalizers
|
||||
- remotecommands/status
|
||||
- remotecommands/finalizers
|
||||
- desiredstatedocuments/status
|
||||
- desiredstatedocuments/finalizers
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# FlowerCore.DeviceManagement CRDs.
|
||||
#
|
||||
# These CRDs match the current operator annotations:
|
||||
# [KubernetesEntity(Group = "flowercore.io", ApiVersion = "v1alpha1", ...)]
|
||||
# Keep the schemas intentionally permissive until the DeviceManagement operator
|
||||
# grows enforced CRD validation.
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: devices.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: devices
|
||||
singular: device
|
||||
kind: Device
|
||||
listKind: DeviceList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: devicegroups.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: devicegroups
|
||||
singular: devicegroup
|
||||
kind: DeviceGroup
|
||||
listKind: DeviceGroupList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: devicepolicies.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: devicepolicies
|
||||
singular: devicepolicy
|
||||
kind: DevicePolicy
|
||||
listKind: DevicePolicyList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: remotecommands.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: remotecommands
|
||||
singular: remotecommand
|
||||
kind: RemoteCommand
|
||||
listKind: RemoteCommandList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: desiredstatedocuments.flowercore.io
|
||||
labels:
|
||||
app.kubernetes.io/name: fc-devicemgmt-operator
|
||||
app.kubernetes.io/component: operator
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
group: flowercore.io
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: desiredstatedocuments
|
||||
singular: desiredstatedocument
|
||||
kind: DesiredStateDocument
|
||||
listKind: DesiredStateDocumentList
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
status:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
@@ -5,35 +5,21 @@
|
||||
# exist yet; import localhost/fc-devicemgmt-web:<tag> to all schedulable RKE2
|
||||
# nodes before letting ArgoCD sync a live rollout.
|
||||
#
|
||||
# LIVE — 2026-06-11 DeviceManagement product-host enablement.
|
||||
# The current DeviceManagement Web source is SQLite-backed in Program.cs, so
|
||||
# Phase 1 production uses a Longhorn RWO PVC at /data/devicemgmt.db. The
|
||||
# 1Password runtime item stays mounted through env for future MySQL/API-key
|
||||
# cutover, but MySQL is not required for this first product-host rollout.
|
||||
# Image v20260613-g2-66a43c1 is built from FlowerCore.DeviceManagement master
|
||||
# 66a43c1, carrying edge enrollment network completion and SQLite-safe trust-bundle smoke coverage.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: fc-devicemgmt-web-data
|
||||
namespace: fc-devicemgmt
|
||||
labels:
|
||||
app: fc-devicemgmt-web
|
||||
app.kubernetes.io/name: fc-devicemgmt-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
# SCALED TO 0 — 2026-05-19 morning-routine cleanup.
|
||||
# The Web pod cannot start until TWO upstream gaps close:
|
||||
# 1. MySQL DB instance `flowercore_devicemgmt` (user `fc_devicemgmt`) is
|
||||
# provisioned via fc-mysql Manager. The cluster currently has ZERO
|
||||
# MySqlInstanceCrds and no `mysql.fc-mysql.svc:3306` Service, so the
|
||||
# deployment-web container env `FlowerCore__Database__Host=mysql.fc-mysql.svc`
|
||||
# points at nothing. Provision via the fc-mysql Manager UI/REST/MCP.
|
||||
# 2. 1Password vault item `IAmWorkin/FlowerCore DeviceManagement Runtime`
|
||||
# with 5 fields (DB-Password, mtls-ca.pem, mtls-client.crt, mtls-client.key,
|
||||
# mtls-chain.pem) — see apps/fc-devicemgmt/1password-item.yaml. Mint mTLS
|
||||
# from step-ca-agent ClusterIssuer per ADR-126; DB-Password must match the
|
||||
# password configured for the MySQL user.
|
||||
# Re-enable: change replicas back to 2 after both gaps close. The image tag
|
||||
# in this file (v20260512-cx5) MAY also need a refresh — it predates the
|
||||
# Sprint 34 Cl-3 operator fix; Web may have an analogous bug.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -50,13 +36,8 @@ metadata:
|
||||
annotations:
|
||||
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fc-devicemgmt-web
|
||||
@@ -83,7 +64,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-devicemgmt-web:v20260613-g3-6555c0d
|
||||
image: localhost/fc-devicemgmt-web:v20260512-cx5
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
@@ -96,21 +77,29 @@ spec:
|
||||
value: "Production"
|
||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
||||
value: "false"
|
||||
- name: HOME
|
||||
value: "/data"
|
||||
- name: FlowerCore__Service__Name
|
||||
value: "FlowerCore.DeviceManagement.Web"
|
||||
- name: FlowerCore__DeviceManagement__DefaultTenantId
|
||||
value: "system"
|
||||
- name: FlowerCore__Database__Provider
|
||||
value: "Sqlite"
|
||||
- name: FlowerCore__Database__ConnectionStrings__Sqlite
|
||||
value: "Data Source=/data/devicemgmt.db"
|
||||
value: "MySql"
|
||||
- name: FlowerCore__Database__Host
|
||||
value: "mysql.fc-mysql.svc"
|
||||
- name: FlowerCore__Database__Database
|
||||
value: "flowercore_devicemgmt"
|
||||
- name: FlowerCore__Database__User
|
||||
value: "fc_devicemgmt"
|
||||
- name: FlowerCore__Database__Password
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: fc-devicemgmt-runtime
|
||||
key: DB-Password
|
||||
- name: FlowerCore__DeviceManagement__AgentMtls__CaPath
|
||||
value: "/secrets/devicemgmt-mtls/mtls-ca.pem"
|
||||
- name: FlowerCore__DeviceManagement__AgentMtls__ClientCertificatePath
|
||||
value: "/secrets/devicemgmt-mtls/mtls-client.crt"
|
||||
- name: FlowerCore__DeviceManagement__AgentMtls__ClientKeyPath
|
||||
value: "/secrets/devicemgmt-mtls/mtls-client.key"
|
||||
- name: FlowerCore__EventBus__Redis__Configuration
|
||||
value: "redis.fc-redis.svc:6379"
|
||||
resources:
|
||||
@@ -147,17 +136,19 @@ spec:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: logs
|
||||
mountPath: /app/logs
|
||||
- name: devicemgmt-mtls
|
||||
mountPath: /secrets/devicemgmt-mtls
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: fc-devicemgmt-web-data
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: logs
|
||||
emptyDir: {}
|
||||
- name: devicemgmt-mtls
|
||||
secret:
|
||||
secretName: fc-devicemgmt-runtime
|
||||
defaultMode: 0400
|
||||
|
||||
@@ -26,20 +26,6 @@ metadata:
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/dns-oidc-client"
|
||||
---
|
||||
# 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:
|
||||
name: dns-api-keys
|
||||
namespace: fc-dns
|
||||
spec:
|
||||
itemPath: "vaults/IAmWorkin/items/FlowerCore DNS API Keys"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
@@ -62,7 +48,7 @@ data:
|
||||
{
|
||||
"FlowerCore": {
|
||||
"Auth": {
|
||||
"Enabled": false,
|
||||
"Enabled": true,
|
||||
"Oidc": {
|
||||
"Enabled": true,
|
||||
"Audience": "dns",
|
||||
@@ -125,7 +111,7 @@ spec:
|
||||
fsGroup: 1654
|
||||
containers:
|
||||
- name: dns-web
|
||||
image: localhost/fc-dns-web:v20260615-phase0-hybrid-f77fb94
|
||||
image: localhost/fc-dns-web:v20260604-oidc-proper
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
@@ -162,18 +148,8 @@ 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__Auth__Enabled
|
||||
value: "false"
|
||||
value: "true"
|
||||
- name: FlowerCore__Auth__Oidc__Enabled
|
||||
value: "true"
|
||||
- name: FlowerCore__Auth__Oidc__Audience
|
||||
@@ -233,54 +209,6 @@ 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:
|
||||
name: dns-web-ingress-isolation
|
||||
namespace: fc-dns
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: dns-web
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: fc-dns
|
||||
- ipBlock:
|
||||
cidr: 10.42.0.0/16
|
||||
- ipBlock:
|
||||
cidr: 10.0.56.0/24
|
||||
- ipBlock:
|
||||
cidr: 10.0.57.0/24
|
||||
- ipBlock:
|
||||
cidr: 10.0.58.0/24
|
||||
- ipBlock:
|
||||
cidr: 10.0.68.0/27
|
||||
ports:
|
||||
- port: 5320
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
@@ -375,7 +303,7 @@ spec:
|
||||
fsGroup: 1654
|
||||
containers:
|
||||
- name: dns-acme-webhook
|
||||
image: localhost/fc-dns-acme-webhook:v20260615-phase0-hybrid-f77fb94
|
||||
image: localhost/fc-dns-acme-webhook:v202604290845
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
@@ -394,16 +322,6 @@ 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:
|
||||
name: dns-api-keys
|
||||
key: api_key
|
||||
optional: true
|
||||
- name: FlowerCore__Dns__AcmeWebhook__GroupName
|
||||
value: acme.flowercore.io
|
||||
- name: FlowerCore__Dns__AcmeWebhook__SolverName
|
||||
|
||||
@@ -9,7 +9,7 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: bluejay-infra
|
||||
---
|
||||
# Landing page HTML (public-safe - no internal LAN references, no CDN)
|
||||
# Landing page HTML (public-safe - no internal LAN references)
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
@@ -20,357 +20,144 @@ data:
|
||||
<!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>
|
||||
<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>
|
||||
</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 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>
|
||||
</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>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,33 +0,0 @@
|
||||
# Certificate for network.iamworkin.lan.
|
||||
#
|
||||
# Preflight gate: network.iamworkin.lan must resolve to 10.0.56.200 before this
|
||||
# Certificate is synced. step-ca ACME cannot see the CoreDNS wildcard
|
||||
# (*.iamworkin.lan -> 10.0.56.200) — it does an HTTP-01 challenge against the
|
||||
# resolved host. The CoreDNS wildcard template covers network.iamworkin.lan, so
|
||||
# resolution exists fleet-wide; do NOT add a pfSense DNS override (this plane is
|
||||
# read-only and holds no pfSense creds). If ACME backs off, confirm the wildcard
|
||||
# resolves first (feedback_pfsense_dns_required_for_acme).
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: fc-network-web-tls
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
annotations:
|
||||
flowercore.io/dns-preflight: "network.iamworkin.lan must resolve to 10.0.56.200 (CoreDNS wildcard) before ACME sync"
|
||||
spec:
|
||||
secretName: fc-network-web-tls
|
||||
issuerRef:
|
||||
name: step-ca-acme
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- network.iamworkin.lan
|
||||
duration: 720h
|
||||
renewBefore: 240h
|
||||
@@ -1,145 +0,0 @@
|
||||
# FlowerCore.Network.Web — the pfSense automation plane (read-only Phase 0, ADR-189).
|
||||
#
|
||||
# Phase 0 is READ-ONLY: the service holds NO pfSense credentials and has no write
|
||||
# path to pfSense anywhere. The only mutating endpoint is POST /api/v1/snapshots,
|
||||
# which ingests a config.xml the noc1 exporter collected READ-ONLY and stores it
|
||||
# (redacted projection) on the PVC. Auth ships gate-OFF.
|
||||
#
|
||||
# Image localhost/fc-network-web:<tag> is built by FlowerCore.Network
|
||||
# scripts/deploy-k8s.sh and imported to all schedulable RKE2 nodes (rke2-server +
|
||||
# rke2-agent1; agent2 retired). imagePullPolicy: Never — bump the tag here, sync
|
||||
# ArgoCD, then scale 0->1 for the RWO PVC and verify the running pod imageID.
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fc-network-web
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
annotations:
|
||||
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
# RWO PVC: a single replica can't be surged (the new pod can't mount the volume
|
||||
# while the old one holds it). maxSurge 0 / maxUnavailable 1 is the rwo-safe shape;
|
||||
# for image bumps scale 0->1 rather than rollout restart.
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fc-network-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
annotations:
|
||||
fc.flowercore.io/healthz-anon: "true"
|
||||
fc.flowercore.io/probe-path: "/healthz"
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "5340"
|
||||
prometheus.io/path: "/metrics/prometheus"
|
||||
flowercore.io/audit-trace-id: "runtime-activity-trace"
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1654
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-network-web:v20260612-0b5b049
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 5340
|
||||
# fc-safe-to-expose: read-only plane, auth gate-OFF; X-Forwarded-Proto handled
|
||||
# by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip.
|
||||
env:
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:5340"
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Production"
|
||||
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
||||
value: "false"
|
||||
- name: HOME
|
||||
value: "/data"
|
||||
- name: FlowerCore__Auth__Enabled
|
||||
value: "false"
|
||||
- name: FlowerCore__Database__Provider
|
||||
value: "Sqlite"
|
||||
- name: FlowerCore__Database__ConnectionStrings__Sqlite
|
||||
value: "Data Source=/data/network.db"
|
||||
# Snapshot store + intended-model paths MUST be absolute on the PVC —
|
||||
# the default is relative to the read-only content root.
|
||||
- name: FlowerCore__Network__SnapshotStore__RootDirectory
|
||||
value: "/data/snapshots"
|
||||
- name: FlowerCore__Network__SnapshotStore__UseGitHistory
|
||||
value: "true"
|
||||
- name: FlowerCore__Network__IntendedModel__FilePath
|
||||
value: "/data/intended.json"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 5340
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 5340
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 5340
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1654
|
||||
runAsGroup: 1654
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: logs
|
||||
mountPath: /app/logs
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: fc-network-web-data
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: logs
|
||||
emptyDir: {}
|
||||
@@ -1,32 +0,0 @@
|
||||
# LAN ingress for FlowerCore.Network Web (network.iamworkin.lan).
|
||||
#
|
||||
# RKE2 Traefik has no built-in ACME resolver; TLS certificate ownership stays in
|
||||
# cert-manager Certificate/fc-network-web-tls. Phase 0 is read-only but the POST
|
||||
# ingest endpoint is genuinely needed by the noc1 exporter, so this route allows
|
||||
# all methods (no GET/HEAD-only restriction like fc-dns) — the service itself has
|
||||
# NO pfSense write path, so allowing POST here only reaches the local snapshot
|
||||
# ingest.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: fc-network-web
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`network.iamworkin.lan`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: fc-network-web
|
||||
port: 80
|
||||
tls:
|
||||
secretName: fc-network-web-tls
|
||||
@@ -1,11 +0,0 @@
|
||||
# ArgoCD's bluejay-infra ApplicationSet discovers apps/* directories on main.
|
||||
# The kustomization is included for local previews and single-app validation.
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- pvc.yaml
|
||||
- deployment-web.yaml
|
||||
- service-web.yaml
|
||||
- certificate-web.yaml
|
||||
- ingressroute-web.yaml
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: fc-network
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
@@ -1,27 +0,0 @@
|
||||
# Persistent store for FlowerCore.Network (read-only pfSense automation plane).
|
||||
#
|
||||
# Holds the SQLite snapshot INDEX db (network.db) AND the on-box snapshot store
|
||||
# (data/snapshots): full-fidelity raw config.xml + redacted inventory sidecars +
|
||||
# an on-box git history. Full-fidelity config is on-box ONLY (this PVC); the
|
||||
# service DB / REST / MCP / UI only ever surface the REDACTED projection.
|
||||
# RWO — single replica, scale 0->1 for updates (never rollout restart).
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: fc-network-web-data
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fc-network-web
|
||||
namespace: fc-network
|
||||
labels:
|
||||
app: fc-network-web
|
||||
app.kubernetes.io/name: fc-network-web
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
flowercore.io/tenant-id: system
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
spec:
|
||||
selector:
|
||||
app: fc-network-web
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 5340
|
||||
type: ClusterIP
|
||||
@@ -535,7 +535,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: web
|
||||
image: localhost/fc-ttsreader-web:v20260614-wave5-help-2f096e3
|
||||
image: localhost/fc-ttsreader-web:v20260603-s54cx14-pr29-schema
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5217
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
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
|
||||
@@ -12,8 +12,6 @@ All repo-scoped Linux runners use:
|
||||
- `ACCESS_TOKEN` from the `github-runner-token` Secret
|
||||
- `RUN_AS_ROOT=false`
|
||||
- `EPHEMERAL=true`
|
||||
- `DISABLE_AUTO_UPDATE=true` so the runner does not self-update and exit inside
|
||||
the immutable Kubernetes pod
|
||||
- `LABELS=self-hosted,linux,fc-build-linux`
|
||||
- writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and
|
||||
Actions tool cache
|
||||
@@ -26,6 +24,12 @@ original Longhorn ReadWriteOnce NuGet PVC. Every other repo-scoped runner uses
|
||||
two replicas with per-pod `emptyDir` caches. That is the safe backlog-drain
|
||||
strategy: no two pods share one RWO PVC.
|
||||
|
||||
Ephemeral runner pods are expected to register, run one job, deregister, and
|
||||
exit so the Deployment starts a fresh pod for the next registration token. A
|
||||
small amount of exit-1/restart churn from token-expiry or no-work windows is
|
||||
accepted operational noise as long as jobs are not stuck queued and the
|
||||
repo-scoped runner-offline alerts stay quiet.
|
||||
|
||||
Sprint 32 final long-tail wave adds 16 two-replica Deployments:
|
||||
`FlowerCore.Knowledge`, `FlowerCore.LlmBridge`, `FlowerCore.Media`,
|
||||
`FlowerCore.Presentations`, `FlowerCore.RemoteDesktop`, `FlowerCore.DNS`,
|
||||
@@ -133,7 +137,3 @@ from GitHub Actions and verify it lands on an `rke2-linux-*` runner.
|
||||
value does not change.
|
||||
- `Multi-Attach` volume error: only the Common runner uses a RWO PVC and it must
|
||||
stay single-replica. New multi-replica runners use `emptyDir`.
|
||||
- Runner pods repeatedly registering, downloading a newer Actions runner, then
|
||||
exiting with code 4: verify `DISABLE_AUTO_UPDATE=true` is present. The image
|
||||
translates that into `config.sh --disableupdate`; without it, the Deployment
|
||||
controller sees the expected self-update exit as CrashLoopBackOff.
|
||||
|
||||
@@ -195,11 +195,6 @@ spec:
|
||||
# fresh registration occurs. Prevents stale runner accumulation.
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
# Labels used by workflow files: runs-on: [self-hosted, linux, fc-build-linux]
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
@@ -371,11 +366,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -514,11 +504,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -651,11 +636,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -788,11 +768,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -925,11 +900,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1065,11 +1035,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1202,11 +1167,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1339,11 +1299,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1476,11 +1431,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1615,11 +1565,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1754,11 +1699,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -1898,11 +1838,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2037,11 +1972,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2176,11 +2106,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2315,11 +2240,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2453,11 +2373,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2592,11 +2507,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2730,11 +2640,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -2868,11 +2773,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3006,11 +2906,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3144,11 +3039,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3282,11 +3172,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3421,11 +3306,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3560,11 +3440,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3699,11 +3574,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3838,11 +3708,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -3977,11 +3842,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4115,11 +3975,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4254,11 +4109,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4397,11 +4247,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4541,11 +4386,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
@@ -4681,11 +4521,6 @@ spec:
|
||||
value: "/tmp/runner/work"
|
||||
- name: EPHEMERAL
|
||||
value: "true"
|
||||
# The runner image must not self-update inside an immutable
|
||||
# Kubernetes pod. Without this, GitHub runner auto-update exits
|
||||
# with code 4 and the Deployment falls into CrashLoopBackOff.
|
||||
- name: DISABLE_AUTO_UPDATE
|
||||
value: "true"
|
||||
- name: LABELS
|
||||
value: "self-hosted,linux,fc-build-linux"
|
||||
- name: HOME
|
||||
|
||||
@@ -6,16 +6,12 @@ RUBY_MINOR="${RUBY_MINOR:-3.3}"
|
||||
TOOLCACHE_ROOT="${TOOLCACHE_ROOT:-/opt/runner-toolcache}"
|
||||
RUNNER_UID="${RUNNER_UID:-1001}"
|
||||
RUNNER_GID="${RUNNER_GID:-1001}"
|
||||
# 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}"
|
||||
RUBY_PREFIX="${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64"
|
||||
|
||||
mkdir -p "${TOOLCACHE_ROOT}/Ruby"
|
||||
RUBY_CONFIGURE_OPTS="${RUBY_CONFIGURE_OPTS:---disable-install-doc --disable-yjit}" ruby-build "${RUBY_VERSION}" "${RUBY_PREFIX}"
|
||||
|
||||
touch "${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/${TOOLCACHE_ARCH}.complete"
|
||||
touch "${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64.complete"
|
||||
ln -sfn "${RUBY_VERSION}" "${TOOLCACHE_ROOT}/Ruby/${RUBY_MINOR}"
|
||||
|
||||
"${RUBY_PREFIX}/bin/ruby" -v
|
||||
|
||||
@@ -44,32 +44,9 @@ spec:
|
||||
labels:
|
||||
app: intranet-web
|
||||
spec:
|
||||
# notes-corpus-clone: shallow-clones the Notes docs corpus into an emptyDir so
|
||||
# the IntranetSearch indexer has /srv/flowercore-notes/docs to index. Uses the
|
||||
# trailing-dot FQDN (gitea-clusterip.gitea.svc.cluster.local.) to bypass the
|
||||
# CoreDNS *.iamworkin.lan template that otherwise resolves the in-cluster service
|
||||
# name to the Traefik VIP for musl / ndots:5 pods (search-domain appending).
|
||||
# Cred: gitea-corpus-cred (in-ns secret with the canonical 1P bluejay read cred;
|
||||
# mirrors the imperative gitea-flowercore-notes argocd repo-cred pattern).
|
||||
initContainers:
|
||||
- name: notes-corpus-clone
|
||||
image: alpine/git:2.45.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: gitea-corpus-cred
|
||||
env:
|
||||
- name: GIT_LFS_SKIP_SMUDGE
|
||||
value: "1"
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- 'git clone --depth 1 http://$username:$password@gitea-clusterip.gitea.svc.cluster.local.:3000/bluejay/FlowerCore.Notes.git /srv/flowercore-notes && echo "notes corpus cloned; docs entries:" && ls /srv/flowercore-notes/docs | wc -l'
|
||||
volumeMounts:
|
||||
- name: notes-corpus
|
||||
mountPath: /srv/flowercore-notes
|
||||
containers:
|
||||
- name: intranet-web
|
||||
image: localhost/fc-intranet-web:v20260614-wave5-knowledgefleet-1458b4d
|
||||
image: localhost/fc-intranet-web:v20260531-ttsreader-bridge
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5300
|
||||
@@ -79,32 +56,18 @@ spec:
|
||||
value: Production
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:5300"
|
||||
# Embed backend = edge1 Ollama BY IPv4 (10.0.57.17:11434; has
|
||||
# nomic-embed-text). The hostname edge1.iamworkin.lan is UNUSABLE from
|
||||
# cluster pods: it resolves to an unroutable IPv6 (fdbc:56:*) and the
|
||||
# CoreDNS *.iamworkin.lan template maps the name to the Traefik VIP, so
|
||||
# embeds failed with "No route to host". Use a bare pod-routable IPv4.
|
||||
# Backend is BLUEJAY-AI's GPU node (Ollama / Vulkan Iris Xe, INFRA VLAN
|
||||
# 10.0.56.132) which embeds nomic-embed-text in ~160ms vs the edge1 Pi 5's
|
||||
# ~3.2s for the same ~512-token chunk (~20x faster bulk embed), proven
|
||||
# pod-routable from the intranet namespace 2026-06-13. The prior edge1 Pi 5
|
||||
# backend (10.0.57.17:11434) remains a working fallback if BLUEJAY-AI is
|
||||
# down. Bulk embed runs in the background; /health does not depend on it.
|
||||
# Memory: feedback_pi5_nomic_embed_slow.
|
||||
# Bulk corpus indexing on edge1 Pi 5 takes ~6s/chunk × 5665 chunks
|
||||
# ≈ 9 hours. BLUEJAY-WS GPU (R9700, 32GB VRAM) does the same work
|
||||
# in minutes. Memory: feedback_pi5_nomic_embed_slow.
|
||||
- name: IntranetSearch__OllamaBaseUrl
|
||||
value: "http://10.0.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
|
||||
# runs in the background via the edge1 Ollama backend above (~6s/chunk on
|
||||
# the Pi 5); /health readiness does not depend on it, so the pod stays Ready.
|
||||
- name: IntranetSearch__Enabled
|
||||
value: "true"
|
||||
# Page-reading override SQLite persistence on the writable PVC at
|
||||
# /data. This backs pronunciation, notes, corrections, and
|
||||
# page-profile metadata across pod restarts.
|
||||
- name: PageReadingOverrides__DatabasePath
|
||||
value: "/data/page-reading-overrides.db"
|
||||
value: "http://10.0.56.20:11434"
|
||||
# Sprint E Phase 2α — JSON-file-backed PageReadingOverride persistence
|
||||
# on the writable PVC at /data. Without this env var the
|
||||
# intranet falls back to the in-memory store (loses state on
|
||||
# pod restart). Master's PageReadingOverrideOptions binds
|
||||
# PageReadingOverrides:FilePath.
|
||||
- name: PageReadingOverrides__FilePath
|
||||
value: "/data/page-reading-overrides.json"
|
||||
- name: KnowledgeFleetSearch__BaseUrl
|
||||
value: "https://knowledge.iamworkin.lan"
|
||||
- name: KnowledgeFleetSearch__ApiKey
|
||||
@@ -141,40 +104,10 @@ spec:
|
||||
volumeMounts:
|
||||
- name: vector-store
|
||||
mountPath: /data
|
||||
- name: notes-corpus
|
||||
mountPath: /srv/flowercore-notes
|
||||
readOnly: true
|
||||
# notes-corpus-sync: keeps the mounted corpus fresh between pod restarts by
|
||||
# pulling the Notes repo every 30 min (best-effort; the initContainer guarantees
|
||||
# a fresh clone at pod start). Reuses the clone's origin (trailing-dot host + creds).
|
||||
- name: notes-corpus-sync
|
||||
image: alpine/git:2.45.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: gitea-corpus-cred
|
||||
env:
|
||||
- name: GIT_LFS_SKIP_SMUDGE
|
||||
value: "1"
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- 'while true; do sleep 1800; git -C /srv/flowercore-notes pull --depth 1 2>&1 | sed "s/^/[notes-corpus-sync] /" || true; done'
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "10m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
volumeMounts:
|
||||
- name: notes-corpus
|
||||
mountPath: /srv/flowercore-notes
|
||||
volumes:
|
||||
- name: vector-store
|
||||
persistentVolumeClaim:
|
||||
claimName: intranet-vector-store
|
||||
- name: notes-corpus
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
@@ -843,7 +843,9 @@ data:
|
||||
rules:
|
||||
- alert: PiManagerDown
|
||||
expr: up{job="pimanager-app"} == 0
|
||||
for: 3m
|
||||
# Sprint 67: delayed behind NodeDown's critical page so a powered-off
|
||||
# Pi does not create the first duplicate page for the same host.
|
||||
for: 8m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
@@ -1242,6 +1244,58 @@ data:
|
||||
summary: "Marquee animation duration drifting > 10% on {{ $labels.renderer }} ({{ $labels.phase }})"
|
||||
description: "Median observed cycle duration deviates from target DurationMs by >10%. Could indicate browser tab throttling, GPU pressure, or phase-advancement bug."
|
||||
|
||||
# ============================================================
|
||||
# Update Center public-edge probes
|
||||
# Live-mirrored from FlowerCore.Notes/scripts/monitoring/alerts.yml.
|
||||
# This K8s ConfigMap is the future migration target; live Prometheus
|
||||
# still reads the canonical Notes file from noc1 Podman.
|
||||
# ============================================================
|
||||
- name: update_center
|
||||
rules:
|
||||
# Critical only when the edge is genuinely unreachable. A Cloudflare
|
||||
# HTTP 429 means the prober hit a rate-limit, not that real clients
|
||||
# are down, so the warning rule below owns that signal.
|
||||
- alert: UpdateCenterPublicEdgeDown
|
||||
expr: |
|
||||
(probe_success{job="probe-update-center-public-edge"} == 0)
|
||||
unless on(instance)
|
||||
(probe_http_status_code{job="probe-update-center-public-edge"} == 429)
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
service: update-center
|
||||
alert_channel: irc
|
||||
annotations:
|
||||
summary: "Update Center public edge probe failed for {{ $labels.instance }}"
|
||||
description: >-
|
||||
The external probe for {{ $labels.instance }} failed for 10 minutes with a
|
||||
non-2xx status that is not a rate-limit. Public Update Center clients may be
|
||||
unable to fetch manifest schema metadata through Cloudflare.
|
||||
runbook: >-
|
||||
1. curl -sk https://{{ $labels.instance }}/api/v1/manifests/_schema
|
||||
2. Verify Cloudflare DNS record is proxied and targets the current public edge IP
|
||||
3. kubectl -n fc-updater get ingressroute updatecenter-web-public secret cf-origin-flowercore-io
|
||||
4. Check Traefik logs for Method() or TLS secret errors
|
||||
|
||||
- alert: UpdateCenterPublicEdgeRateLimited
|
||||
expr: probe_http_status_code{job="probe-update-center-public-edge"} == 429
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
service: update-center
|
||||
alert_channel: irc
|
||||
annotations:
|
||||
summary: "Cloudflare is rate-limiting (HTTP 429) the public-edge probe for {{ $labels.instance }}"
|
||||
description: >-
|
||||
The blackbox prober receives HTTP 429 from Cloudflare for {{ $labels.instance }}
|
||||
while the origin is healthy. This is a Cloudflare rate-limit / WAF condition on
|
||||
the public hostname, not an outage.
|
||||
runbook: >-
|
||||
1. curl -sk https://{{ $labels.instance }}/api/v1/manifests/_schema (expect 200 from a normal client)
|
||||
2. Review Cloudflare rate-limit / WAF rules for the hostname; the 5m-cadence prober is tripping a 429
|
||||
3. Add a Cloudflare rate-limit exception for the prober source IP or the /api/v1/manifests/_schema path
|
||||
4. Confirm whether the singular host update.flowercore.io is still required, or only updates.flowercore.io
|
||||
|
||||
# =============================================================================
|
||||
# ConfigMap: Blackbox Exporter Configuration
|
||||
# =============================================================================
|
||||
|
||||
@@ -12,27 +12,28 @@ Source: `D:\git\FlowerCore\FlowerCore.WorldBuilder` (master)
|
||||
in pfSense Unbound before this manifest is applied, or cert-manager
|
||||
HTTP-01 silently exponential-backs-off ~2h.
|
||||
Memory: `feedback_pfsense_dns_required_for_acme`.
|
||||
2. **Image import to ALL Ready RKE2 nodes** — pod can currently schedule to
|
||||
`rke2-server` (10.0.56.11) and `rke2-agent1` (10.0.56.12). Build with:
|
||||
2. **Image import to ALL RKE2 nodes** — pod can schedule to any of
|
||||
`rke2-server` (10.0.56.11), `rke2-agent1` (10.0.56.12),
|
||||
`rke2-agent2` (10.0.56.13). Build with:
|
||||
```bash
|
||||
bash deploy/build.sh # in FlowerCore.WorldBuilder repo
|
||||
mkdir -p artifacts/deploy
|
||||
podman save localhost/fc-worldbuilder:v<TAG> -o artifacts/deploy/fc-worldbuilder-v<TAG>.tar
|
||||
for h in 10.0.56.11 10.0.56.12; do
|
||||
ssh fcadmin@$h "mkdir -p /home/fcadmin/.fcv"
|
||||
scp artifacts/deploy/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/home/fcadmin/.fcv/
|
||||
podman save localhost/fc-worldbuilder:v<TAG> -o /tmp/fc-worldbuilder-v<TAG>.tar
|
||||
for h in 10.0.56.11 10.0.56.12 10.0.56.13; do
|
||||
scp /tmp/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/tmp/
|
||||
ssh fcadmin@$h \
|
||||
"sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock \
|
||||
-n k8s.io images import /home/fcadmin/.fcv/fc-worldbuilder-v<TAG>.tar"
|
||||
-n k8s.io images import /tmp/fc-worldbuilder-v<TAG>.tar"
|
||||
done
|
||||
```
|
||||
Memory: `feedback_rke2_image_import_per_node_scp`.
|
||||
3. **Bump image tag** in `worldbuilder.yaml` and git push.
|
||||
ArgoCD ApplicationSet picks up within ~3 minutes.
|
||||
4. **First production render** — verify
|
||||
`https://worldbuilder.iamworkin.lan/healthz`, open
|
||||
`https://worldbuilder.iamworkin.lan/settings`, and confirm the image backend
|
||||
reports ComfyUI before running an operator-owned render lane.
|
||||
4. **First production render** — open
|
||||
`https://worldbuilder.iamworkin.lan/studio/c32e0000-0000-4000-8000-000000000004`
|
||||
and confirm the Cyberpunk Blue Jay demo prompt loads with five seeded fake
|
||||
generated images. This Sprint 32 visitor-safe profile uses
|
||||
`ClientMode=fake`; switch the image-generation env vars back to ComfyUI only
|
||||
for an operator-owned GPU render lane.
|
||||
|
||||
## Health probes
|
||||
|
||||
@@ -55,8 +56,13 @@ Source: `D:\git\FlowerCore\FlowerCore.WorldBuilder` (master)
|
||||
|
||||
## Image generation backend
|
||||
|
||||
The live internal profile now uses
|
||||
`FlowerCore:WorldBuilder:ImageGeneration:ClientMode=comfyui` with
|
||||
`BaseUrl=http://10.0.56.20:8188` on BLUEJAY-WS (R9700 / gfx1201 / ROCm 7.2).
|
||||
Keep the public host pre-staging disabled unless the five safe-to-expose gates
|
||||
are rechecked; the live GPU lane is operator-owned and internal-only.
|
||||
Sprint 32 pins the Kubernetes profile to
|
||||
`FlowerCore:WorldBuilder:ImageGeneration:ClientMode=fake` with
|
||||
`BaseUrl=http://127.0.0.1:1`. That keeps the public/internal visitor demo
|
||||
deterministic, avoids GPU exposure, and still exercises the studio/gallery
|
||||
surface with persisted generated-image metadata.
|
||||
|
||||
The previous ComfyUI backend target was `http://10.0.56.20:8188` on
|
||||
BLUEJAY-WS (R9700 / gfx1201 / ROCm 7.2.1). Re-enable it only in an
|
||||
operator-owned follow-up that also verifies workstation reachability and image
|
||||
import freshness.
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
#
|
||||
# Image build (BLUEJAY-WS):
|
||||
# bash deploy/build.sh # in FlowerCore.WorldBuilder repo
|
||||
# podman save localhost/fc-worldbuilder:v<TAG> -o artifacts/deploy/fc-worldbuilder-v<TAG>.tar
|
||||
# for h in 10.0.56.11 10.0.56.12; do
|
||||
# scp artifacts/deploy/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/home/fcadmin/.fcv/
|
||||
# ssh fcadmin@$h "sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /home/fcadmin/.fcv/fc-worldbuilder-v<TAG>.tar"
|
||||
# podman save localhost/fc-worldbuilder:v<TAG> -o /tmp/fc-worldbuilder-v<TAG>.tar
|
||||
# for h in 10.0.56.11 10.0.56.12 10.0.56.13; do
|
||||
# scp /tmp/fc-worldbuilder-v<TAG>.tar fcadmin@$h:/tmp/
|
||||
# ssh fcadmin@$h "sudo /var/lib/rancher/rke2/bin/ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/fc-worldbuilder-v<TAG>.tar"
|
||||
# done
|
||||
---
|
||||
apiVersion: v1
|
||||
@@ -90,7 +90,7 @@ spec:
|
||||
containers:
|
||||
- name: web
|
||||
# Bump tag for each rebuild. Initial deploy: v202605062048
|
||||
image: localhost/fc-worldbuilder:v20260613-e4-about-edd6efc
|
||||
image: localhost/fc-worldbuilder:v202605062048
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -117,16 +117,14 @@ spec:
|
||||
value: "/data/gallery"
|
||||
- name: FlowerCore__WorldBuilder__Export__RootPath
|
||||
value: "/data/exports"
|
||||
# Operator-approved live GPU lane. Internal-only host targets
|
||||
# BLUEJAY-WS ComfyUI; keep public host pre-staging disabled below.
|
||||
# Visitor-safe Sprint 32 profile: fake backend keeps public demo
|
||||
# rendering deterministic and avoids exposing BLUEJAY-WS GPU.
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__BaseUrl
|
||||
value: "http://10.0.56.20:8188"
|
||||
value: "http://127.0.0.1:1"
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__ClientMode
|
||||
value: "comfyui"
|
||||
value: "fake"
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__BackendId
|
||||
value: "comfyui"
|
||||
- name: FlowerCore__WorldBuilder__ImageGeneration__VisitorSafe
|
||||
value: "false"
|
||||
value: "fake"
|
||||
resources:
|
||||
# Cluster CPU-request budget runs hot (99% on all 3 nodes at deploy
|
||||
# time) while actual CPU usage is well below capacity. Idle Blazor
|
||||
|
||||
74
argocd/applicationset-bluejay-infra.yaml
Normal file
74
argocd/applicationset-bluejay-infra.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ApplicationSet
|
||||
metadata:
|
||||
annotations:
|
||||
argocd.argoproj.io/refresh: "true"
|
||||
name: bluejay-infra
|
||||
namespace: argocd
|
||||
spec:
|
||||
generators:
|
||||
- git:
|
||||
directories:
|
||||
- path: apps/*
|
||||
repoURL: http://gitea-clusterip.gitea.svc:3000/bluejay/bluejay-infra.git
|
||||
revision: main
|
||||
template:
|
||||
metadata: {}
|
||||
spec:
|
||||
destination: {}
|
||||
project: ""
|
||||
goTemplate: true
|
||||
goTemplateOptions:
|
||||
- missingkey=error
|
||||
template:
|
||||
metadata:
|
||||
name: infra-{{.path.basename}}
|
||||
spec:
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
ignoreDifferences:
|
||||
- group: apps
|
||||
jqPathExpressions:
|
||||
- .spec.volumeClaimTemplates[]?.status
|
||||
jsonPointers:
|
||||
- /spec/volumeClaimTemplates
|
||||
kind: StatefulSet
|
||||
name: zabbix-postgres
|
||||
namespace: zabbix
|
||||
- group: apps
|
||||
jqPathExpressions:
|
||||
- .spec.volumeClaimTemplates[]?.status
|
||||
jsonPointers:
|
||||
- /spec/volumeClaimTemplates
|
||||
kind: StatefulSet
|
||||
name: guac-mysql
|
||||
namespace: guacamole
|
||||
- group: apps
|
||||
jqPathExpressions:
|
||||
- .spec.volumeClaimTemplates[]?.status
|
||||
jsonPointers:
|
||||
- /spec/volumeClaimTemplates
|
||||
kind: StatefulSet
|
||||
name: matrix-postgres
|
||||
namespace: matrix
|
||||
- group: apps
|
||||
jqPathExpressions:
|
||||
- .spec.volumeClaimTemplates[]?.status
|
||||
jsonPointers:
|
||||
- /spec/volumeClaimTemplates
|
||||
kind: StatefulSet
|
||||
name: authentik-postgres
|
||||
namespace: authentik
|
||||
project: default
|
||||
source:
|
||||
path: '{{.path.path}}'
|
||||
repoURL: http://gitea-clusterip.gitea.svc:3000/bluejay/bluejay-infra.git
|
||||
targetRevision: main
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- ServerSideApply=true
|
||||
- RespectIgnoreDifferences=true
|
||||
@@ -1,83 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,225 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,208 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,225 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,225 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,208 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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,129 +0,0 @@
|
||||
# authentik-tenant-mapping-sync — GATED manifest staging
|
||||
|
||||
**Status:** GATED (suspended). **ADR:** ADR-198 §2.A P1 (Au-1 / Au-3 substrate). **Pairs:** Codex **Cx2-7**.
|
||||
|
||||
This directory is a **Notes staging area**, NOT a deploy target. The orchestrator relocates
|
||||
`cronjob.yaml` into a `gated/` path **outside** `bluejay-infra/apps/` so ArgoCD's `apps/*`
|
||||
directory generator never picks it up. Nothing here runs until the activation steps below.
|
||||
|
||||
## What this is
|
||||
|
||||
A nightly Kubernetes `CronJob` that runs
|
||||
[`scripts/authentik/authentik-tenant-mapping-sync.py`](../../../scripts/authentik/authentik-tenant-mapping-sync.py)
|
||||
(Notes repo). The script:
|
||||
|
||||
- reads the 1Password Document **`flowercore-tenant-mapping`** (vault `IAmWorkin`, field
|
||||
`mapping`) via **1Password Connect REST** — never the 1Password CLI/desktop (operator hard rule);
|
||||
- parses + light-validates the mapping JSON (schema: [`authentik-oidc-tenant-mapping-schema.md`](../../standards/authentik-oidc-tenant-mapping-schema.md) — `version==1`, `mappings[]` with `authentikGroup` / `fcTenantId` / `fcRole`);
|
||||
- reconciles each distinct `authentikGroup` into Authentik `/api/v3/core/groups/`:
|
||||
create-if-missing, PATCH-managed-markers-on-drift, **never delete or disable unmanaged groups**;
|
||||
- emits structured (Serilog-shaped JSON) logs and exits 0 on success.
|
||||
|
||||
It is the **slow nightly fix-up path**. The **<1s hot path** stays the MCP tool
|
||||
`authentik_sync_tenant_mapping` (schema doc §6.2 force-broadcast). This CronJob does NOT
|
||||
broadcast SignalR — group reconcile is its only side effect; services pick up mapping changes
|
||||
on their own 5-minute 1P refresh.
|
||||
|
||||
## Why it is GATED (two locks)
|
||||
|
||||
1. **`spec.suspend: true`** in `cronjob.yaml` — belt-and-suspenders so even if applied it never fires.
|
||||
2. **Lives outside `apps/`** — staged here in Notes; ArgoCD does not manage it.
|
||||
|
||||
Both must be cleared to go live. This pairs Codex **Cx2-7**: do not activate ahead of the Au-3
|
||||
public-go for tenant self-registration.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `cronjob.yaml` | The suspended `CronJob` + the script-delivery `ConfigMap` (placeholder body). |
|
||||
| `README.md` | This file. |
|
||||
| `scripts/authentik/authentik-tenant-mapping-sync.py` | The reconcile script (canonical source; NOT in this dir). |
|
||||
|
||||
## Secrets (referenced, not invented)
|
||||
|
||||
No secret **values** appear in `cronjob.yaml` — only `secretKeyRef`s:
|
||||
|
||||
- **`AUTHENTIK_TOKEN`** ← `Secret authentik/authentik-credentials` key `BOOTSTRAP_ADMIN_TOKEN`
|
||||
(already exists; the same token `provision-oidc-client.py` reads). **Au-9 caveat:** this is the
|
||||
never-rotated bootstrap token — when `/rotate-password rotate authentik` (Au-9) lands, this
|
||||
CronJob is one of its fan-out consumers.
|
||||
- **`OP_TOKEN`** ← `Secret authentik/tenant-mapping-sync-op-token` key `token`.
|
||||
|
||||
### OP_TOKEN cross-namespace
|
||||
|
||||
The canonical 1P Connect token Secret is `onepassword-system/onepassword-token`, but this
|
||||
CronJob runs in the `authentik` namespace and K8s Secrets are namespace-scoped. Pick one at
|
||||
activation:
|
||||
|
||||
- **Option A (copy, simplest).** Mint a same-namespace copy right before un-suspending:
|
||||
```sh
|
||||
kubectl get secret onepassword-token -n onepassword-system -o jsonpath='{.data.token}' \
|
||||
| base64 -d \
|
||||
| kubectl create secret generic tenant-mapping-sync-op-token -n authentik \
|
||||
--from-file=token=/dev/stdin --dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
(Re-run whenever the Connect token rotates — add this CronJob to the **Au-10** Connect-token
|
||||
fan-out checklist so the copy can't go stale.)
|
||||
- **Option B (CRD, preferred long-term).** Use an `OnePasswordItem` CRD
|
||||
(`feedback_1password_operator_pattern`) so the 1P operator mints/refreshes
|
||||
`authentik/tenant-mapping-sync-op-token` automatically — no manual copy, rotation-safe.
|
||||
|
||||
> If neither secret exists yet, that's fine **while suspended** — the job never schedules.
|
||||
|
||||
## How to ACTIVATE (at Au-3 public-go)
|
||||
|
||||
1. **Pre-flight (workstation dry-run, writes nothing):**
|
||||
```sh
|
||||
export AUTHENTIK_TOKEN=... # or let it read authentik/authentik-credentials via kubectl
|
||||
export OP_TOKEN=... # or rely on credential-helper.sh get_op_token (fcadmin@noc1)
|
||||
python scripts/authentik/authentik-tenant-mapping-sync.py --dry-run --verbose
|
||||
```
|
||||
Confirm the planned create/update set matches the 1P mapping document.
|
||||
2. **Provide `OP_TOKEN` in-cluster** — Option A or B above.
|
||||
3. **Materialize the script ConfigMap from the canonical file** (do NOT hand-edit a copy into
|
||||
`cronjob.yaml` — the embedded body is a deliberate placeholder):
|
||||
```sh
|
||||
kubectl create configmap authentik-tenant-mapping-sync-script -n authentik \
|
||||
--from-file=authentik-tenant-mapping-sync.py=scripts/authentik/authentik-tenant-mapping-sync.py \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
(Or, in the imaged future per ADR-198 §2.B P3, bake the script into `fc-runtime-base` and
|
||||
drop the ConfigMap volume.)
|
||||
4. **Relocate into bluejay-infra** — move `cronjob.yaml` into a `gated/` (or `apps/`) path in
|
||||
`bluejay-infra` per the orchestrator's placement decision. If under `apps/`, ArgoCD will sync it.
|
||||
5. **Un-suspend** — set `spec.suspend: false` (commit in `bluejay-infra` so ArgoCD selfHeal
|
||||
doesn't revert), or one-off:
|
||||
```sh
|
||||
kubectl patch cronjob authentik-tenant-mapping-sync -n authentik \
|
||||
-p '{"spec":{"suspend":false}}'
|
||||
```
|
||||
6. **Smoke (VG-A1):** trigger an immediate run and check the structured logs:
|
||||
```sh
|
||||
kubectl create job --from=cronjob/authentik-tenant-mapping-sync tms-smoke -n authentik
|
||||
kubectl logs -n authentik job/tms-smoke
|
||||
```
|
||||
Then edit a mapping entry in 1P and confirm the next run reconciles the group; the <1s
|
||||
propagation still comes from the MCP `authentik_sync_tenant_mapping` force-broadcast.
|
||||
|
||||
## Rollback
|
||||
|
||||
Re-suspend (`spec.suspend: true`) or delete the CronJob. The script never deletes Authentik
|
||||
groups, so a bad run can only over-create groups present in the mapping — remove any unwanted
|
||||
group by hand in the Authentik admin UI. No data loss path.
|
||||
|
||||
## Idempotency / safety summary
|
||||
|
||||
- Re-running is a no-op when groups already match (mirrors `provision-oidc-client.py`).
|
||||
- Only the managed attribute block (`fc:managed-by` / `fc:tenant` / `fc:role` / optional
|
||||
`fc:label` / `fc:regulated` / `fc:strict-mode`) is asserted; group parent/users/roles are
|
||||
never touched.
|
||||
- Wildcard SuperAdmin entries (`fcTenantId: "*"`) do not create a per-tenant group.
|
||||
- `--dry-run` prints the plan and writes nothing — always run it first.
|
||||
|
||||
## Cross-links
|
||||
|
||||
- [`docs/standards/auth-acl-unattended-lifecycle-plan.md`](../../standards/auth-acl-unattended-lifecycle-plan.md) — ADR-198; Au-1/Au-3 lanes, VG-A1/A2.
|
||||
- [`docs/standards/authentik-oidc-tenant-mapping-schema.md`](../../standards/authentik-oidc-tenant-mapping-schema.md) — the mapping JSON shape + 1P item layout (§2/§3).
|
||||
- [`scripts/authentik/provision-oidc-client.py`](../../../scripts/authentik/provision-oidc-client.py) — sibling idempotent provisioner (same API + posture).
|
||||
- [`scripts/credential-helper.sh`](../../../scripts/credential-helper.sh) — `get_op_token` 1P Connect bootstrap (fcadmin@noc1).
|
||||
@@ -1,151 +0,0 @@
|
||||
# =====================================================================================
|
||||
# authentik-tenant-mapping-sync — GATED nightly CronJob (Au-3 / ADR-198 §2.A P1)
|
||||
#
|
||||
# STATUS: GATED. spec.suspend: true (belt-and-suspenders). This manifest lives in a Notes
|
||||
# STAGING path (docs/gated-manifests/) and is NOT under bluejay-infra apps/, so ArgoCD
|
||||
# does not deploy it. It does NOTHING until Au-3 public-go (see README.md in this dir).
|
||||
#
|
||||
# WHAT IT RUNS: scripts/authentik/authentik-tenant-mapping-sync.py (Notes repo) — reads the
|
||||
# 1Password Document `flowercore-tenant-mapping` via Connect REST and reconciles its
|
||||
# mappings[].authentikGroup entries into Authentik groups (idempotent; never deletes
|
||||
# unmanaged groups). Pairs Codex Cx2-7.
|
||||
#
|
||||
# SECRETS (referenced, NOT invented — no secret VALUES in this file):
|
||||
# AUTHENTIK_TOKEN <- Secret authentik/authentik-credentials key BOOTSTRAP_ADMIN_TOKEN (exists)
|
||||
# OP_TOKEN <- Secret authentik/tenant-mapping-sync-op-token key token
|
||||
# (a copy of onepassword-system/onepassword-token — see README "OP_TOKEN
|
||||
# cross-namespace" for the one-liner that mints it; OR mint via the
|
||||
# OnePasswordItem CRD per feedback_1password_operator_pattern).
|
||||
#
|
||||
# The script is delivered via the ConfigMap below (same pattern as guacamole guac-k8s-sync).
|
||||
# When this lane is libraryized/imaged later (ADR-198 §2.B P3) this ConfigMap can be replaced
|
||||
# by a baked image; for now ConfigMap-delivery keeps the script the single source of truth.
|
||||
# =====================================================================================
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: authentik-tenant-mapping-sync
|
||||
namespace: authentik
|
||||
labels:
|
||||
app.kubernetes.io/name: authentik-tenant-mapping-sync
|
||||
app.kubernetes.io/component: sync
|
||||
app.kubernetes.io/part-of: flowercore-identity
|
||||
flowercore.io/adr: "198"
|
||||
flowercore.io/gated: "true"
|
||||
annotations:
|
||||
flowercore.io/gate: "Au-3 public-go — suspended until tenant self-registration goes live"
|
||||
flowercore.io/pairs-with: "Codex Cx2-7"
|
||||
spec:
|
||||
# GATE: suspended so it never fires until an operator un-suspends at Au-3 public-go.
|
||||
suspend: true
|
||||
# Nightly at 03:17 (off-peak; jittered minute to avoid colliding with other 03:00 jobs).
|
||||
schedule: "17 3 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
startingDeadlineSeconds: 600
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 2
|
||||
activeDeadlineSeconds: 600
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: authentik-tenant-mapping-sync
|
||||
app.kubernetes.io/component: sync
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: sync
|
||||
# python:3.12-slim is sufficient: the script uses only the stdlib (urllib/json/ssl).
|
||||
# No pip install needed. Pin a digest at activation time for air-gap reproducibility.
|
||||
image: python:3.12-slim
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- python3
|
||||
- /scripts/authentik-tenant-mapping-sync.py
|
||||
# NOTE: no --dry-run here -> this is the real reconcile. Operators wanting a
|
||||
# dry-run first should `kubectl create job --from=cronjob/... ` with the arg
|
||||
# appended, or run the script from a workstation. See README.
|
||||
env:
|
||||
- name: AUTHENTIK_URL
|
||||
value: "https://id.iamworkin.lan"
|
||||
- name: OP_CONNECT_URL
|
||||
value: "http://10.0.56.10:8180/v1" # port 8180, NOT 8443
|
||||
- name: OP_VAULT_ID
|
||||
value: "qaphopopkryhbg353ukzhhuqoq" # IAmWorkin
|
||||
- name: TENANT_MAPPING_ITEM
|
||||
value: "flowercore-tenant-mapping"
|
||||
- name: TENANT_MAPPING_FIELD
|
||||
value: "mapping"
|
||||
- name: AUTHENTIK_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-credentials
|
||||
key: BOOTSTRAP_ADMIN_TOKEN
|
||||
- name: OP_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
# A same-namespace copy of onepassword-system/onepassword-token.
|
||||
# See README "OP_TOKEN cross-namespace". Until Au-3 this Secret need
|
||||
# not exist (the job is suspended).
|
||||
name: tenant-mapping-sync-op-token
|
||||
key: token
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: authentik-tenant-mapping-sync-script
|
||||
defaultMode: 0555
|
||||
---
|
||||
# The reconcile script, delivered as a ConfigMap (single source of truth = the Notes repo
|
||||
# scripts/authentik/authentik-tenant-mapping-sync.py). At activation, regenerate this
|
||||
# ConfigMap from the live script so the two never drift, e.g.:
|
||||
# kubectl create configmap authentik-tenant-mapping-sync-script -n authentik \
|
||||
# --from-file=authentik-tenant-mapping-sync.py=scripts/authentik/authentik-tenant-mapping-sync.py \
|
||||
# --dry-run=client -o yaml > docs/gated-manifests/authentik-tenant-sync/configmap.script.yaml
|
||||
# (kept as a placeholder body here so the manifest set is self-describing; the real body is
|
||||
# the script file — DO NOT hand-edit a divergent copy into this ConfigMap.)
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: authentik-tenant-mapping-sync-script
|
||||
namespace: authentik
|
||||
labels:
|
||||
app.kubernetes.io/name: authentik-tenant-mapping-sync
|
||||
app.kubernetes.io/component: sync
|
||||
flowercore.io/gated: "true"
|
||||
annotations:
|
||||
flowercore.io/source: "scripts/authentik/authentik-tenant-mapping-sync.py (Notes repo) — regenerate at activation, do not hand-edit"
|
||||
data:
|
||||
authentik-tenant-mapping-sync.py: |
|
||||
# PLACEHOLDER — regenerate from the canonical script at activation (see annotation above).
|
||||
# The Notes repo file scripts/authentik/authentik-tenant-mapping-sync.py is the source of
|
||||
# truth; embedding a hand-copy here would drift. The orchestrator (or the activation
|
||||
# runbook) materializes this ConfigMap from the live script via `kubectl create configmap
|
||||
# ... --from-file=...` before un-suspending the CronJob.
|
||||
import sys
|
||||
sys.exit("authentik-tenant-mapping-sync ConfigMap not materialized from the canonical "
|
||||
"script — regenerate with kubectl create configmap --from-file before activation.")
|
||||
@@ -1,39 +0,0 @@
|
||||
# Public-TLS substrate (gated)
|
||||
|
||||
**Lane:** Cl-infra-2 (deep-regroup 2026-06-13). **Status:** authored, **NOT applied** — operator-gated.
|
||||
|
||||
This directory holds the Let's Encrypt + isolation substrate for **public** multi-tenant
|
||||
web hosting. It lives **outside `apps/`** on purpose: the bluejay-infra ApplicationSet only
|
||||
reconciles `apps/*`, so nothing here is auto-applied. Applying a cert-manager ACME
|
||||
`ClusterIssuer` registers an ACME account immediately, so these stay inert until the
|
||||
operator opens the web-hosting public-exposure gate (**R-1**).
|
||||
|
||||
## What's here
|
||||
|
||||
| File | What | Activate when |
|
||||
|---|---|---|
|
||||
| `letsencrypt-issuers.yaml` | `letsencrypt-staging` + `letsencrypt-prod` ClusterIssuers (HTTP-01 via Traefik; DNS-01 stub for wildcards) | Public-go. Move to `apps/cluster-issuers/`, **staging first**. |
|
||||
| `tenant-networkpolicy-template.yaml` | Per-tenant default-deny + allowlist NetworkPolicy (Traefik ingress, CoreDNS, own-DB egress only) | Rendered per tenant at provision time (Wh-C2 isolation). |
|
||||
|
||||
## The gate
|
||||
|
||||
Public exposure is **NO-GO** until the §6 go/no-go checklist in
|
||||
[`docs/standards/web-hosting-production-readiness-plan.md`](../../../FlowerCore.Notes/docs/standards/web-hosting-production-readiness-plan.md)
|
||||
is green (currently 14/14 red) **and** the operator explicitly opens R-1. Internal
|
||||
`*.iamworkin.lan` TLS stays on **step-ca** (`apps/fc-dns/fc-dns.yaml` → `step-ca-dns01`);
|
||||
these LE issuers are **only** for public tenant domains.
|
||||
|
||||
## Pairing
|
||||
|
||||
- **Codex Wh-C1** consumes `letsencrypt-staging`/`-prod` for hybrid public TLS on
|
||||
FlowerCore.PHP/MySQL/DNS.
|
||||
- **Codex Wh-C2** consumes the NetworkPolicy template for cross-tenant isolation suites.
|
||||
|
||||
## Activation checklist (public-go)
|
||||
|
||||
1. Wire a public DNS-01 solver (Cloudflare/Namecheap webhook) **or** confirm public tenant
|
||||
domains route HTTP-01 to the cluster ingress.
|
||||
2. `git mv gated/public-tls/letsencrypt-issuers.yaml apps/cluster-issuers/` — staging only.
|
||||
3. Issue one **staging** cert for a throwaway public domain; verify the chain in a browser.
|
||||
4. Flip that tenant's Certificate `issuerRef` to `letsencrypt-prod`; mind LE rate limits.
|
||||
5. Render `tenant-networkpolicy-template.yaml` per tenant; run the Wh-C2 negative suites.
|
||||
@@ -1,78 +0,0 @@
|
||||
# ============================================================================
|
||||
# Let's Encrypt ClusterIssuers — PUBLIC TLS substrate (Cl-infra-2, deep-regroup 2026-06-13)
|
||||
# ============================================================================
|
||||
# GATED. This file lives OUTSIDE apps/ on purpose, so the bluejay-infra
|
||||
# ApplicationSet does NOT auto-apply it. Applying a cert-manager ACME
|
||||
# ClusterIssuer registers an ACME account immediately, so we keep these inert
|
||||
# until the operator opens the web-hosting public-exposure gate (R-1; the §6
|
||||
# go/no-go checklist in docs/standards/web-hosting-production-readiness-plan.md
|
||||
# is currently 14/14 red).
|
||||
#
|
||||
# Pairs with Codex Wh-C1 (FlowerCore.PHP/MySQL/DNS hybrid public TLS) and
|
||||
# Wh-C2 (isolation). Internal *.iamworkin.lan certs STAY on step-ca
|
||||
# (apps/fc-dns/fc-dns.yaml: ClusterIssuer step-ca-dns01) — these LE issuers are
|
||||
# ONLY for public tenant domains.
|
||||
#
|
||||
# TO ACTIVATE (operator public-go):
|
||||
# 1. Confirm a public DNS-01 solver is wired (Cloudflare/Namecheap webhook) OR
|
||||
# that public tenant domains route HTTP-01 to the cluster's public ingress.
|
||||
# 2. Move this file to apps/cluster-issuers/ (the ApplicationSet will create
|
||||
# infra-cluster-issuers and apply it), staging FIRST.
|
||||
# 3. Issue ONE staging cert for a throwaway public domain, verify the chain,
|
||||
# THEN switch that tenant's Certificate issuerRef to letsencrypt-prod.
|
||||
# 4. Mind LE prod rate limits (50 certs/registered-domain/week, 5 dupes/week).
|
||||
#
|
||||
# Registration email is for expiry notices only — adjust to a role address if
|
||||
# desired (astoltz@iamwork.in is the current operator contact).
|
||||
# ----------------------------------------------------------------------------
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-staging
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
flowercore.io/gate: public-tls
|
||||
spec:
|
||||
acme:
|
||||
# LE STAGING — untrusted certs, generous limits. Use this first, always.
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
email: astoltz@iamwork.in
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-staging-account-key
|
||||
solvers:
|
||||
# HTTP-01 via Traefik. Requires the public tenant domain's :80 traffic to
|
||||
# reach the cluster ingress. For wildcard / apex without inbound :80, swap
|
||||
# to the dns01 solver block below (needs a public DNS provider webhook).
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
# --- DNS-01 alternative for wildcards (uncomment + wire a public DNS webhook) ---
|
||||
# - dns01:
|
||||
# webhook:
|
||||
# groupName: acme.flowercore.io # or the cloudflare/namecheap solver
|
||||
# solverName: <public-dns-solver>
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
flowercore.io/gate: public-tls
|
||||
spec:
|
||||
acme:
|
||||
# LE PRODUCTION — trusted certs, strict rate limits. Only after staging proves out.
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: astoltz@iamwork.in
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-account-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
# - dns01:
|
||||
# webhook:
|
||||
# groupName: acme.flowercore.io
|
||||
# solverName: <public-dns-solver>
|
||||
@@ -1,59 +0,0 @@
|
||||
# ============================================================================
|
||||
# Per-tenant NetworkPolicy TEMPLATE — web-hosting isolation (Cl-infra-2 / Wh-C2)
|
||||
# ============================================================================
|
||||
# GATED substrate (outside apps/, not auto-applied). Modeled on the canonical
|
||||
# default-deny + allowlist shape in apps/fc-devicemgmt/network-policy.yaml.
|
||||
#
|
||||
# Purpose: when a public multi-tenant site is provisioned, each tenant's pods
|
||||
# get a NetworkPolicy that (a) default-denies all ingress/egress, then allows
|
||||
# only Traefik ingress + CoreDNS + that tenant's own DB. This enforces the
|
||||
# cross-tenant isolation Wh-C2 verifies with negative suites.
|
||||
#
|
||||
# Replace the {{TENANT}} placeholders and apply alongside the tenant's workload
|
||||
# (the MySQL/PHP managers should emit this when they create a tenant, or a
|
||||
# templating step in apps/ should render it). Kept here as the reference shape.
|
||||
# ----------------------------------------------------------------------------
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: tenant-{{TENANT}}-isolation
|
||||
namespace: fc-tenant-{{TENANT}}
|
||||
labels:
|
||||
app.kubernetes.io/part-of: flowercore
|
||||
flowercore.io/tenant-id: "{{TENANT}}"
|
||||
flowercore.io/created-by: bluejay-infra
|
||||
flowercore.io/gate: public-tls
|
||||
spec:
|
||||
podSelector: {} # all pods in the tenant namespace
|
||||
policyTypes: [Ingress, Egress]
|
||||
ingress:
|
||||
# Only Traefik may reach tenant pods (public traffic terminates at Traefik).
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik-system
|
||||
ports:
|
||||
- { protocol: TCP, port: 80 }
|
||||
- { protocol: TCP, port: 443 }
|
||||
- { protocol: TCP, port: 8080 }
|
||||
egress:
|
||||
# CoreDNS resolution.
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- { protocol: UDP, port: 53 }
|
||||
- { protocol: TCP, port: 53 }
|
||||
# This tenant's OWN MySQL only (NOT other tenants' DBs — that's the isolation).
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
flowercore.io/tenant-id: "{{TENANT}}"
|
||||
app.kubernetes.io/name: mysql
|
||||
ports:
|
||||
- { protocol: TCP, port: 3306 }
|
||||
# NOTE: deliberately NO blanket egress. Add per-tenant allowances explicitly
|
||||
# (object storage, mail relay, etc.) so a compromised tenant pod cannot reach
|
||||
# the rest of the fleet or other tenants.
|
||||
@@ -272,7 +272,6 @@ public sealed class FleetManifestLintTests
|
||||
var container = deployments[expectedRunner.Key].MainContainerMappings().Should().ContainSingle().Subject;
|
||||
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
|
||||
EnvValue(container, "EPHEMERAL").Should().Be("true");
|
||||
EnvValue(container, "DISABLE_AUTO_UPDATE").Should().Be("true", $"{expectedRunner.Key} must not self-update inside immutable Kubernetes runner pods");
|
||||
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
|
||||
EnvValue(container, "RUN_AS_ROOT").Should().Be("false");
|
||||
EnvValue(container, "ACCESS_TOKEN").Should().BeNull("ACCESS_TOKEN must come from github-runner-token Secret, not a literal");
|
||||
@@ -469,6 +468,99 @@ public sealed class FleetManifestLintTests
|
||||
monitoring.Should().Contain("dedicated LinuxRunnerOffline/MacMiniRunnerOffline alerts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GithubRunnerReadme_DocumentsAcceptedEphemeralExitChurn()
|
||||
{
|
||||
var readme = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "github-runner", "README.md"));
|
||||
|
||||
readme.Should().Contain("Ephemeral runner pods");
|
||||
readme.Should().Contain("exit-1/restart churn");
|
||||
readme.Should().Contain("accepted operational noise");
|
||||
readme.Should().Contain("repo-scoped runner-offline alerts stay quiet");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Monitoring_PiManagerDownDelayAndUpdateCenterRateLimit_MatchCanonicalAlerts()
|
||||
{
|
||||
var notesAlerts = File.ReadAllText(Path.Combine(
|
||||
Inventory.WorkspaceRoot,
|
||||
"FlowerCore.Notes",
|
||||
"scripts",
|
||||
"monitoring",
|
||||
"alerts.yml"));
|
||||
var monitoring = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "apps", "monitoring", "noc-monitoring.yaml"));
|
||||
|
||||
notesAlerts.Should().Contain("# Sprint 67: keep this warning behind NodeDown's 5m critical page");
|
||||
notesAlerts.Should().Contain("- alert: PiManagerDown");
|
||||
notesAlerts.Should().Contain("for: 8m");
|
||||
monitoring.Should().Contain("# Sprint 67: delayed behind NodeDown's critical page");
|
||||
monitoring.Should().Contain("- alert: PiManagerDown");
|
||||
monitoring.Should().Contain("for: 8m");
|
||||
|
||||
notesAlerts.Should().Contain("- alert: UpdateCenterPublicEdgeRateLimited");
|
||||
notesAlerts.Should().Contain("expr: probe_http_status_code{job=\"probe-update-center-public-edge\"} == 429");
|
||||
notesAlerts.Should().Contain("for: 15m");
|
||||
monitoring.Should().Contain("- alert: UpdateCenterPublicEdgeRateLimited");
|
||||
monitoring.Should().Contain("expr: probe_http_status_code{job=\"probe-update-center-public-edge\"} == 429");
|
||||
monitoring.Should().Contain("for: 15m");
|
||||
monitoring.Should().Contain("severity: warning");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplicationSetExport_MustRemainManualRootOfGitOpsTree()
|
||||
{
|
||||
var readme = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "README.md"));
|
||||
var appsetPath = Path.Combine(Inventory.BluejayRoot, "argocd", "applicationset-bluejay-infra.yaml");
|
||||
|
||||
File.Exists(appsetPath).Should().BeTrue();
|
||||
var appset = File.ReadAllText(appsetPath);
|
||||
|
||||
appset.Should().Contain("kind: ApplicationSet");
|
||||
appset.Should().Contain("name: bluejay-infra");
|
||||
appset.Should().NotContain("\nstatus:");
|
||||
appset.Should().NotContain("managedFields:");
|
||||
readme.Should().Contain("root of this GitOps tree");
|
||||
readme.Should().Contain("NOT self-managed");
|
||||
readme.Should().Contain("kubectl -n argocd apply -f argocd/applicationset-bluejay-infra.yaml");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplicationSetExport_MustDiscoverAppsDirectoryOnMain()
|
||||
{
|
||||
var appset = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "argocd", "applicationset-bluejay-infra.yaml"));
|
||||
|
||||
appset.Should().Contain("path: apps/*");
|
||||
appset.Should().Contain("revision: main");
|
||||
appset.Should().Contain("repoURL: http://gitea-clusterip.gitea.svc:3000/bluejay/bluejay-infra.git");
|
||||
appset.Should().Contain("path: '{{.path.path}}'");
|
||||
appset.Should().Contain("targetRevision: main");
|
||||
appset.Should().Contain("ServerSideApply=true");
|
||||
appset.Should().Contain("RespectIgnoreDifferences=true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplicationSetExport_MustPreserveStatefulSetIgnoreDifferences()
|
||||
{
|
||||
var appset = File.ReadAllText(Path.Combine(Inventory.BluejayRoot, "argocd", "applicationset-bluejay-infra.yaml"));
|
||||
|
||||
appset.Should().Contain("jsonPointers:");
|
||||
appset.Should().Contain("- /spec/volumeClaimTemplates");
|
||||
appset.Should().Contain(".spec.volumeClaimTemplates[]?.status");
|
||||
Regex.Matches(appset, "kind: StatefulSet").Should().HaveCount(4);
|
||||
|
||||
foreach (var (name, ns) in new[]
|
||||
{
|
||||
("zabbix-postgres", "zabbix"),
|
||||
("guac-mysql", "guacamole"),
|
||||
("matrix-postgres", "matrix"),
|
||||
("authentik-postgres", "authentik"),
|
||||
})
|
||||
{
|
||||
appset.Should().Contain($"name: {name}");
|
||||
appset.Should().Contain($"namespace: {ns}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Monitoring_BlackboxTargetsForOidcSensitiveServices_MustUseAnonymousHealthRoutesWhenAvailable()
|
||||
{
|
||||
@@ -650,7 +742,6 @@ public sealed class FleetManifestLintTests
|
||||
"certificate-web.yaml",
|
||||
"clusterrole-operator.yaml",
|
||||
"clusterrolebinding-operator.yaml",
|
||||
"crds.yaml",
|
||||
"deployment-operator.yaml",
|
||||
"deployment-web.yaml",
|
||||
"ingressroute-web.yaml",
|
||||
@@ -740,8 +831,7 @@ public sealed class FleetManifestLintTests
|
||||
.Single(document => document.Kind == "ClusterRole" && document.Name == "fc-devicemgmt-operator");
|
||||
var allScalars = clusterRole.AllScalars().ToList();
|
||||
|
||||
allScalars.Should().Contain("flowercore.io");
|
||||
allScalars.Should().NotContain("devices.flowercore.io");
|
||||
allScalars.Should().Contain("devices.flowercore.io");
|
||||
allScalars.Should().Contain("*");
|
||||
allScalars.Should().Contain("deployments");
|
||||
allScalars.Should().Contain("get");
|
||||
@@ -770,7 +860,7 @@ public sealed class FleetManifestLintTests
|
||||
|
||||
FcDeviceManagementDocuments().Should().NotContain(document => document.Kind == "Secret");
|
||||
appText.Should().Contain("secretKeyRef:");
|
||||
appText.Should().Contain("name: fc-devicemgmt-runtime");
|
||||
appText.Should().Contain("secretName: fc-devicemgmt-runtime");
|
||||
appText.Should().NotContain("stringData:");
|
||||
appText.Should().NotContain("from-literal");
|
||||
appText.Should().NotContain("tls.key:");
|
||||
@@ -867,9 +957,9 @@ public sealed class FleetManifestLintTests
|
||||
{
|
||||
var deployments = new[]
|
||||
{
|
||||
(App: "fc-dns", Name: "dns-web", Slug: "dns", Secret: "dns-oidc-client", AuthEnabled: "false"),
|
||||
(App: "fc-media", Name: "fc-media-web", Slug: "media", Secret: "media-oidc-client", AuthEnabled: "true"),
|
||||
(App: "fc-distribution", Name: "fc-distribution", Slug: "distribution", Secret: "distribution-oidc-client", AuthEnabled: "true"),
|
||||
(App: "fc-dns", Name: "dns-web", Slug: "dns", Secret: "dns-oidc-client"),
|
||||
(App: "fc-media", Name: "fc-media-web", Slug: "media", Secret: "media-oidc-client"),
|
||||
(App: "fc-distribution", Name: "fc-distribution", Slug: "distribution", Secret: "distribution-oidc-client"),
|
||||
};
|
||||
|
||||
foreach (var expected in deployments)
|
||||
@@ -878,7 +968,7 @@ public sealed class FleetManifestLintTests
|
||||
.Single(document => document.Kind == "Deployment" && document.Name == expected.Name);
|
||||
var container = deployment.MainContainerMappings().Should().ContainSingle().Subject;
|
||||
|
||||
EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be(expected.AuthEnabled);
|
||||
EnvValue(container, "FlowerCore__Auth__Enabled").Should().Be("true");
|
||||
EnvValue(container, "FlowerCore__Auth__Oidc__Enabled").Should().Be("true");
|
||||
(EnvValue(container, "FlowerCore__Auth__Oidc__Audience") ?? EnvValue(container, "FlowerCore__Auth__Oidc__ClientId"))
|
||||
.Should()
|
||||
@@ -927,7 +1017,7 @@ public sealed class FleetManifestLintTests
|
||||
var dnsPvc = AppDocuments("fc-dns")
|
||||
.Single(document => document.Kind == "PersistentVolumeClaim" && document.Name == "dns-web-data");
|
||||
|
||||
ManifestNodeExtensions.Scalar(dnsContainer, "image").Should().Be("localhost/fc-dns-web:v20260613-g5-quota-aa99bd1");
|
||||
ManifestNodeExtensions.Scalar(dnsContainer, "image").Should().Be("localhost/fc-dns-web:v20260604-oidc-proper");
|
||||
dnsPvc.Scalar("spec", "storageClassName").Should().Be("longhorn");
|
||||
dnsPvc.Scalar("spec", "resources", "requests", "storage").Should().Be("1Gi");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user