Files
Hanzo Dev cb6a7d86fa fix(zoo-explorer): the explorer takes -config, and it needs an RPC that resolves
zoo-explorer-explorer had restarted 600+ times in both zoo-system and
zoo-devnet. The operator (<=0.1.3) rendered the ZooExplorer CR as
container args --rpc/--chain/--port, but ghcr.io/luxfi/explorer is a Go
binary whose stdlib `flag` set defines only -config/-data/-http/-mdns/
-version. Every start therefore ended in:

    flag provided but not defined: -rpc
    Usage of explorer: ...

i.e. exit 2 before the listener ever bound. operator 0.1.4
(src/controller.rs::reconcile_explorer) renders the CR into a chains.yaml
ConfigMap and passes --config=/etc/explorer/chains.yaml instead. 62864bd
already pinned 0.1.4 for the readiness fix, so only the rationale is
added here — but that pin is what stops the crash, and the comment should
say so or the next person re-floats the tag.

The crash was hiding two further defects. Both pods now come up Running
and are still useless without these:

* base rpcEndpoint pointed at zoo-chain-rpc.zoo.svc.cluster.local. There
  is no `zoo` namespace on do-sfo3-zoo-k8s (`kubectl -n zoo get svc` ->
  "No resources found"), so the indexer logs `no such host` every 30s and
  has indexed zero blocks since it stopped crashing. `zoo` is not a
  blockchain alias either — the EVM answers on /ext/bc/C/rpc. The CR is
  applied into zoo-<env>, so the address is namespace-relative;
  zoo-testnet/zoo-mainnet keep their own tracked-blockchainID overrides.

* the devnet validator ran without --http-allowed-hosts, so luxd's
  localhost-only default 403s every in-cluster caller — the same
  eth_chainId POST returns 0x17872 with `Host: localhost` and 403
  Forbidden without it. spec.config renders as --kebab-case flags
  (operator 0.1.3+), so HTTP_ALLOWED_HOSTS there is the one source of
  truth; the unreferenced zoo-devnet/statefulset-patch.yaml is not.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 22:42:35 -07:00

157 lines
5.6 KiB
YAML

# zoo-operator — reconciles ZooNetwork / ZooChain / ZooExplorer / ZooGateway.
#
# Topology: ONE operator runs in zoo-system, watches every namespace
# (--namespace ""). zoo-mainnet/zoo-testnet/zoo-devnet overlays scale
# this Deployment to 0 so only zoo-system holds the lease.
#
# OPERATOR_NAMESPACE comes from the pod's own namespace (fieldRef) so
# the lease lives in zoo-system — see src/main.rs line 106 + src/leader.rs.
# That fieldRef is load-bearing: main.rs falls back to the literal
# "zoo-system", so a replica running WITHOUT this env writes zoo-system's
# lease from whatever namespace it is in. That is exactly how a stale
# zoo-testnet pod (no env, floating :main) held zoo-system's lease and
# starved the real operator. Scaling the overlays to 0 is the invariant,
# not a tidiness preference — apply them or a stray replica survives.
apiVersion: apps/v1
kind: Deployment
metadata:
name: zoo-operator
labels:
app: zoo-operator
app.kubernetes.io/name: zoo-operator
app.kubernetes.io/part-of: zoo
spec:
replicas: 1
selector:
matchLabels:
app: zoo-operator
template:
metadata:
labels:
app: zoo-operator
spec:
serviceAccountName: zoo-operator
imagePullSecrets:
- name: ghcr-secret
- name: hanzo
containers:
- name: operator
# Pinned to the immutable semver — the floating :main this used to
# track is what let a stale replica outlive its manifest (see above).
# 0.1.3 renders the zoo-chain StatefulSet with luxd CLI flags derived
# from the ZooNetwork CR (earlier images emitted only ZOO_* env, which
# luxd's viper EnvPrefix ignores). 0.1.4 fixes two more: it unbraids
# readiness from leadership (/readyz used to 503 on every non-leader,
# so a healthy standby was NotReady forever and zoo-system sat 0/1 for
# two days), and it renders the ZooExplorer Deployment with
# --config=/etc/explorer/chains.yaml. Earlier images passed
# --rpc/--chain/--port, which luxfi/explorer's stdlib `flag` set does
# not define (only -config/-data/-http/-mdns/-version), so the binary
# printed usage and exited 2 on every start — 600+ restarts of
# zoo-explorer-explorer in zoo-system and zoo-devnet.
# Bump x.y.z+ on every operator change; never float.
image: ghcr.io/zooai/operator:0.1.4
imagePullPolicy: IfNotPresent
args: ["--namespace", ""]
env:
- name: OPERATOR_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- containerPort: 8081
name: health
- containerPort: 8443
name: metrics
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: zoo-operator
labels:
app: zoo-operator
app.kubernetes.io/part-of: zoo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: zoo-operator
rules:
- apiGroups: ["zoo.network"]
resources: ["zoonetworks", "zoonetworks/status", "zoonetworks/finalizers", "zoochains", "zoochains/status", "zooexplorers", "zooexplorers/status", "zoogateways", "zoogateways/status"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets", "events", "persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
# ClusterRoleBinding is cluster-scoped, so a single object of this name is
# shared by every overlay. The operator runs with --namespace "" (all
# namespaces) behind ONE cluster-global leader-election lease, so whichever
# replica wins the lease — in ANY of these namespaces — must be able to
# list/watch the zoo.network CRs cluster-wide. Therefore bind EVERY per-env
# operator ServiceAccount here (one binding, many subjects). Overlays must NOT
# rewrite a single subject's namespace: that turned this into a
# last-apply-wins collision that left only one SA bound (the leader's SA was
# unbound → 403 on every reconcile → /readyz 503).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: zoo-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: zoo-operator
subjects:
- kind: ServiceAccount
name: zoo-operator
namespace: zoo-mainnet
- kind: ServiceAccount
name: zoo-operator
namespace: zoo-testnet
- kind: ServiceAccount
name: zoo-operator
namespace: zoo-devnet
- kind: ServiceAccount
name: zoo-operator
namespace: zoo-system
---
apiVersion: v1
kind: Service
metadata:
name: zoo-operator
labels:
app: zoo-operator
app.kubernetes.io/part-of: zoo
spec:
selector:
app: zoo-operator
ports:
- port: 8443
targetPort: 8443
name: metrics