Kubernetes operator for managing Sei blockchain nodes. Single binary, two controllers: SeiNodeDeployment (fleet orchestration, genesis ceremonies, deployments) and SeiNode (individual node lifecycle).
- API group:
sei.io/v1alpha1 - CRD types:
SeiNodeDeployment,SeiNode(defined inapi/v1alpha1/) - Controllers:
internal/controller/nodedeployment/,internal/controller/node/ - Entry point:
cmd/main.go— thin binary that creates amanager.Managerand registers both controllers - Framework: controller-runtime v0.23.1 / kubebuilder v4.12.0
Always use the available subagents for relevant work:
- kubernetes-specialist: Use for all Kubernetes design, deployment, troubleshooting, and operational decisions. Consult before making changes to CRDs, RBAC, kustomize configs, StatefulSet specs, or any cluster-facing resource definitions.
- platform-engineer: Use for architectural decisions about platform composability, developer experience, CI/CD workflows, and infrastructure abstraction patterns.
- Follow idiomatic Go. No unnecessary abstractions — three similar lines are better than a premature helper.
- Use
controller-runtimepatterns: every controller exports a reconciler struct withSetupWithManager(mgr). - All code must pass
golangci-lint(config in.golangci.yml). Fix lint issues, don't suppress them. - Imports must be grouped: stdlib, external, then
github.com/sei-protocol/sei-k8s-controller(enforced by goimports). - No
panicin controller code. Return errors and let the reconciler retry. - Keep reconcile loops idempotent — every reconcile should converge toward desired state regardless of current state.
- Tests use
testing+gomegafor assertions. - Test fixtures for platform config live in
internal/platform/platformtest/. - Run tests with
make testbefore submitting changes.
- Edit types in
api/v1alpha1/(e.g.,seinode_types.go,seinodedeployment_types.go,validator_types.go). - After any type change, run
make manifests generateto regenerate CRD YAML and DeepCopy methods. - Never hand-edit files in
manifests/orzz_generated.deepcopy.go.
- RBAC is generated from
// +kubebuilder:rbac:markers on controller files. - After changing markers, run
make manifeststo regeneratemanifests/role.yaml.
make build # Build the manager binary
make test # Run unit tests
make lint # Run golangci-lint
make manifests generate # Regenerate CRDs, RBAC, DeepCopy after type changes
make docker-build IMG=<image> # Build container image
make docker-push IMG=<image> # Push container image- SeiNodeDeployment creates and owns SeiNode resources. Groups orchestrate genesis ceremonies, manage deployments, and coordinate networking/monitoring.
- SeiNode creates StatefulSets (replicas=1), headless Services, and PVCs via server-side apply (fieldOwner:
seinode-controller). - Plan-driven reconciliation — Both controllers use ordered task plans (stored in
.status.plan) to drive lifecycle. Plans are built byinternal/planner/(ResolvePlanfor nodes,ForGroupfor deployments), executed byplanner.Executor, with individual tasks ininternal/task/. The reconcile loop is:ResolvePlan → persist plan → ExecutePlan. Seeinternal/planner/doc.gofor the full plan lifecycle. - Init plans transition nodes from Pending → Running. They include infrastructure tasks (
ensure-data-pvc,apply-statefulset,apply-service) followed by sidecar tasks (configure-genesis,config-apply, etc.). - NodeUpdate plans roll out image changes on Running nodes. Built when
spec.image != status.currentImage. Tasks:apply-statefulset,apply-service,observe-image(polls StatefulSet rollout, stampscurrentImage),mark-ready(sidecar re-init). The planner setsNodeUpdateInProgresscondition on creation and clears it on completion/failure. When no drift is detected, no plan is built — the node sits in steady state. - Atomic plan creation — New plans are persisted before any tasks execute. The reconciler flushes the plan, then requeues. Execution starts on the next reconcile. This guarantees external observers see the plan before side effects occur.
- Condition ownership — The planner owns all condition management on the owning resource. It sets conditions when creating plans (e.g.,
NodeUpdateInProgress=True) and when observing terminal plans (e.g.,NodeUpdateInProgress=False). The executor does not set conditions — it only mutates plan/task state and phase transitions. - Single-patch model — All status mutations (plan state, conditions, phase, currentImage) accumulate in-memory during a reconcile and are flushed in a single
Status().Patch()at the end. Tasks mutate owned resources (StatefulSets, Services, PVCs); the executor mutates plan state in-memory; the reconciler flushes once. - Resource generators live in
internal/noderesource/— pure functions that produce StatefulSets, Services, and PVCs from a SeiNode spec. Used by both the controller and plan tasks. - Platform config is fully environment-driven — all fields in
platform.Configmust be set via env vars (no defaults). Seeinternal/platform/platform.gofor the full list. - Genesis resolution is handled by the sidecar autonomously: embedded sei-config for well-known chains, S3 fallback at
{SEI_GENESIS_BUCKET}/{chainID}/genesis.jsonfor custom chains. - Config keys in seid's
config.tomluse hyphens (e.g.,persistent-peers,trust-height), not underscores.