k8s: draft luxfi-explorer manifest for app/explorer/ replacement (issue #387)

Lands a side-by-side draft of the new single-binary Go explorer at
`ghcr.io/luxfi/explorer` — the canonical replacement for the legacy
Next.js `app/explorer/` (`@luxbridge/explorer`).

Includes:
  - ConfigMap `luxfi-explorer-chains` — initial chain registry (lux +
    ethereum), hot-reloadable via the binary's /v1/explorer/admin/chains
    API once running.
  - Deployment — replicas=1, strategy: Recreate (single PVC writer per
    the SQLite+ZapDB embedded index docs), readiness + liveness probes
    against /healthz, resources sized small (100m/256Mi req → 2c/2Gi limit).
  - PVC 20Gi for the embedded chain index.
  - Service ClusterIP :80 → :3000.
  - Ingress on `bridge-explorer-v2.lux.network` for the soak window —
    legacy keeps `bridge-explorer.lux.network`. Cut over after 1-week QA.

Marked DRAFT in the file header. New bridge dev confirms the image tag
(pin off `:latest` before the legacy delete step) and the RPC config
before `kubectl apply`.

Status update on #387 will go in the issue once this lands on main.
This commit is contained in:
2026-05-22 00:09:01 -07:00
parent 5ced993961
commit 5ff4b6a86f
+199
View File
@@ -0,0 +1,199 @@
# Lux Explorer — single Go binary at ~/work/lux/explorer.
#
# Replaces the legacy Next.js app/explorer/ (@luxbridge/explorer). The new
# binary embeds three Lux libraries via go:embed:
# - luxfi/indexer → EVM chain indexer
# - luxfi/graph → per-chain GraphQL engine
# - luxfi/explore → Next.js SPA frontend
#
# Routes (served from one process, see ~/work/lux/explorer/README.md):
# / SPA (embedded, SPA-routing fallback)
# /envs.js Runtime window.ENV = {...}
# /healthz Service health
# /v1/indexer/{slug}/* Per-chain explorer REST API
# /v1/graph/{slug}/{subgraph}/graphql Per-chain, per-subgraph GraphQL
# /v1/explorer/realtime WebSocket realtime hub
# /v1/explorer/admin/chains[/{slug}] Runtime registry CRUD
#
# Migration plan (issue #387):
# 1. Deploy this manifest alongside the legacy app/explorer/ (different host).
# 2. One-week QA soak — compare GraphQL responses against legacy.
# 3. Flip bridge-explorer.lux.network ingress from legacy → this.
# 4. Scale legacy to 0, hold for one-week rollback window.
# 5. Delete app/explorer/ and this comment's "luxfi-explorer-v2" hostname.
#
# Status as of v1.1.12: DRAFT — not yet applied. New bridge dev confirms
# image tag + chain config, applies via the standard kubectl flow.
apiVersion: v1
kind: ConfigMap
metadata:
name: luxfi-explorer-chains
namespace: lux-bridge
labels:
app: luxfi-explorer
data:
# Maps explorer slug → indexer config. The Go binary reads this at
# startup. See ~/work/lux/explorer/chains.example.yaml for the schema.
chains.yaml: |
chains:
- slug: lux
name: Lux Network
chain_id: 96369
rpc_url: http://lux-c-chain:9650/ext/bc/C/rpc
subgraphs:
- name: bridge
schema: bridge
- slug: ethereum
name: Ethereum Mainnet
chain_id: 1
rpc_url: ${ETHEREUM_RPC_URL}
subgraphs:
- name: bridge
schema: bridge
# Add chains here — the registry is hot-reloadable via the
# /v1/explorer/admin/chains API; this ConfigMap is for the
# cold-start set.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: luxfi-explorer
namespace: lux-bridge
labels:
app: luxfi-explorer
spec:
# Single-writer at the start. Scale to 2+ replicas after confirming
# the embedded index store (SQLite + ZapDB per docs) handles read
# replication cleanly; the bridge backend doesn't depend on it being
# multi-writer.
replicas: 1
strategy:
# Recreate because the embedded chain index sits on a single PVC.
type: Recreate
selector:
matchLabels:
app: luxfi-explorer
template:
metadata:
labels:
app: luxfi-explorer
spec:
containers:
- name: explorer
# TODO(#387): pin to a specific semver once the luxfi/explorer
# release cadence stabilises. `:latest` is fine for the soak
# window but switch to `:vX.Y.Z` before the legacy delete step.
image: ghcr.io/luxfi/explorer:latest
imagePullPolicy: Always
ports:
- containerPort: 3000
name: http
env:
- name: EXPLORER_BIND
value: ":3000"
- name: EXPLORER_CHAINS_CONFIG
value: /etc/luxfi-explorer/chains.yaml
- name: EXPLORER_DATA_DIR
value: /var/lib/luxfi-explorer
# Per-chain RPC URLs injected via env so secrets stay out of
# the ConfigMap. The chains.yaml above references ${ENV}.
- name: ETHEREUM_RPC_URL
valueFrom:
secretKeyRef:
name: luxfi-explorer-secrets
key: ethereum_rpc_url
optional: true
volumeMounts:
- name: chains-config
mountPath: /etc/luxfi-explorer
readOnly: true
- name: data
mountPath: /var/lib/luxfi-explorer
readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "2000m"
memory: "2Gi"
volumes:
- name: chains-config
configMap:
name: luxfi-explorer-chains
- name: data
persistentVolumeClaim:
claimName: luxfi-explorer-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: luxfi-explorer-data
namespace: lux-bridge
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: luxfi-explorer
namespace: lux-bridge
labels:
app: luxfi-explorer
spec:
type: ClusterIP
selector:
app: luxfi-explorer
ports:
- name: http
port: 80
targetPort: 3000
---
# Side-by-side host during the soak. Cut over bridge-explorer.lux.network
# to this Service after one-week QA against the legacy app/explorer/.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: luxfi-explorer
namespace: lux-bridge
labels:
app: luxfi-explorer
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- bridge-explorer-v2.lux.network
secretName: luxfi-explorer-tls
rules:
- host: bridge-explorer-v2.lux.network
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: luxfi-explorer
port:
number: 80