Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions api/v1alpha1/seinodetask_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

// SeiNodeTaskKind discriminates the SeiNodeTask spec union. Exactly one of
// the matching payload sub-structs in SeiNodeTaskSpec must be set.
// +kubebuilder:validation:Enum=GovSoftwareUpgrade;GovVote;GovParamChange;AwaitCondition;UpdateNodeImage;AwaitNodesAtHeight;RestartSeid;MarkReady
// +kubebuilder:validation:Enum=GovSoftwareUpgrade;GovVote;GovParamChange;AwaitCondition;UpdateNodeImage;AwaitNodesAtHeight;RestartSeid;MarkReady;ConfigPatch
type SeiNodeTaskKind string

const (
Expand Down Expand Up @@ -68,6 +68,32 @@ const (
// accepts the request, a beat before /v0/healthz serves 200. Gate on the node
// actually serving with a following AwaitCondition/AwaitNodesAtHeight step.
SeiNodeTaskKindMarkReady SeiNodeTaskKind = "MarkReady"

// SeiNodeTaskKindConfigPatch applies an incremental config patch to the target
// node's on-disk config via the sidecar's config-apply task. The payload uses
// the SAME operator-facing dotted sei-config key schema as SeiNodeSpec.Overrides
// (e.g. "giga_executor.enabled"); the sidecar's sei-config resolver reads the
// current on-disk config, applies the overrides (registry-aware, covering every
// registered key), and re-renders config.toml / app.toml — so rendering is
// correct-by-construction, not a controller-side render table.
//
// PATCHING ONLY — this kind does NOT restart seid or reload config. seid
// re-reads config.toml/app.toml only on (re)start, so a caller composes a
// following RestartSeid (in-place seid bounce) or StateSync/re-bootstrap step
// to make the change take effect. Completion = the sidecar acks the patch
// written to disk (idempotent: re-running rewrites the same keys).
//
// Scope is a fail-closed ALLOWLIST (see ConfigPatchPayload.Overrides): only
// runtime-safe toggles are permitted, and keys the controller re-asserts on
// its own update plans (p2p peers/external-address, genesis/state-sync-managed
// keys) are rejected — a patch to those would be silently clobbered by the
// next SeiNode update plan.
//
// Wiring note: this kind maps to sidecar.TaskTypeConfigApply (incremental dotted
// intent), NOT sidecar.TaskTypeConfigPatch. The latter drives the separate
// ConfigPatchTask{Files} file-tree task used by the StateSync migration path; do
// not rewire this kind to it.
SeiNodeTaskKindConfigPatch SeiNodeTaskKind = "ConfigPatch"
)

// SeiNodeTaskPhase is the high-level lifecycle state of a SeiNodeTask.
Expand Down Expand Up @@ -112,7 +138,7 @@ const (
// Field names locked at v1alpha1 — see https://github.com/sei-protocol/bdchatham-designs/blob/main/designs/seinode-task/seinode-task-lld.md
// (PR sei-protocol/sei-k8s-controller#277).
//
// +kubebuilder:validation:XValidation:rule="(has(self.govSoftwareUpgrade) ? 1 : 0) + (has(self.govVote) ? 1 : 0) + (has(self.govParamChange) ? 1 : 0) + (has(self.awaitCondition) ? 1 : 0) + (has(self.updateNodeImage) ? 1 : 0) + (has(self.awaitNodesAtHeight) ? 1 : 0) + (has(self.restartSeid) ? 1 : 0) + (has(self.markReady) ? 1 : 0) == 1",message="exactly one of govSoftwareUpgrade, govVote, govParamChange, awaitCondition, updateNodeImage, awaitNodesAtHeight, restartSeid, or markReady must be set"
// +kubebuilder:validation:XValidation:rule="(has(self.govSoftwareUpgrade) ? 1 : 0) + (has(self.govVote) ? 1 : 0) + (has(self.govParamChange) ? 1 : 0) + (has(self.awaitCondition) ? 1 : 0) + (has(self.updateNodeImage) ? 1 : 0) + (has(self.awaitNodesAtHeight) ? 1 : 0) + (has(self.restartSeid) ? 1 : 0) + (has(self.markReady) ? 1 : 0) + (has(self.configPatch) ? 1 : 0) == 1",message="exactly one of govSoftwareUpgrade, govVote, govParamChange, awaitCondition, updateNodeImage, awaitNodesAtHeight, restartSeid, markReady, or configPatch must be set"
// +kubebuilder:validation:XValidation:rule="self.kind != 'GovSoftwareUpgrade' || has(self.govSoftwareUpgrade)",message="spec.govSoftwareUpgrade is required when kind=GovSoftwareUpgrade"
// +kubebuilder:validation:XValidation:rule="self.kind != 'GovVote' || has(self.govVote)",message="spec.govVote is required when kind=GovVote"
// +kubebuilder:validation:XValidation:rule="self.kind != 'GovParamChange' || has(self.govParamChange)",message="spec.govParamChange is required when kind=GovParamChange"
Expand All @@ -121,6 +147,8 @@ const (
// +kubebuilder:validation:XValidation:rule="self.kind != 'AwaitNodesAtHeight' || has(self.awaitNodesAtHeight)",message="spec.awaitNodesAtHeight is required when kind=AwaitNodesAtHeight"
// +kubebuilder:validation:XValidation:rule="self.kind != 'RestartSeid' || has(self.restartSeid)",message="spec.restartSeid is required when kind=RestartSeid"
// +kubebuilder:validation:XValidation:rule="self.kind != 'MarkReady' || has(self.markReady)",message="spec.markReady is required when kind=MarkReady"
// +kubebuilder:validation:XValidation:rule="self.kind != 'ConfigPatch' || has(self.configPatch)",message="spec.configPatch is required when kind=ConfigPatch"
// +kubebuilder:validation:XValidation:rule="!has(self.configPatch) || self.configPatch.overrides.all(k, k == 'chain.occ_enabled' || k.startsWith('giga_executor.') || k.startsWith('mempool.'))",message="spec.configPatch.overrides only admits allowlisted runtime-safe keys (chain.occ_enabled, giga_executor.*, mempool.*)"
// +kubebuilder:validation:XValidation:rule="self.kind == oldSelf.kind",message="spec.kind is immutable"
type SeiNodeTaskSpec struct {
// Kind selects the task implementation. Immutable after creation.
Expand Down Expand Up @@ -175,6 +203,10 @@ type SeiNodeTaskSpec struct {
// MarkReady is the payload for kind=MarkReady.
// +optional
MarkReady *MarkReadyPayload `json:"markReady,omitempty"`

// ConfigPatch is the payload for kind=ConfigPatch.
// +optional
ConfigPatch *ConfigPatchPayload `json:"configPatch,omitempty"`
}

// SeiNodeTaskTarget identifies the single SeiNode this task operates on.
Expand Down Expand Up @@ -445,6 +477,41 @@ type RestartSeidPayload struct{}
// SeiNodeTaskKindMarkReady doc comment for the use case.
type MarkReadyPayload struct{}

// ConfigPatchPayload is the payload for kind=ConfigPatch — a set of dotted
// sei-config key overrides to merge-patch into the target node's on-disk config.
//
// VALUE CONTRACT — THE ALLOWLIST (fail-closed; keep in sync across the three
// enforcement points below). Only these runtime-safe toggle families are
// admitted; everything else is rejected:
//
// chain.occ_enabled — OCC concurrency toggle
// giga_executor.* — v2 (giga) executor toggles, incl. giga_executor.enabled
// mempool.* — mempool sizing/TTL toggles
//
// Deliberately EXCLUDED (the controller re-asserts these on its own SeiNode
// update plans, so a patch here would be silently clobbered): p2p peers
// (network.p2p.persistent_peers), p2p.external-address, and genesis /
// state-sync-managed keys.
//
// The allowlist is pure allow/deny POLICY on key prefixes, not a renderer:
// sei-config resolves and renders every registered key on the node, but only
// these families are safe to patch out-of-band. Enforcement is layered:
// (1) a coarse family-level CEL rule on SeiNodeTaskSpec rejects out-of-family
// keys at admission; (2) the controller re-enforces the same allowlist policy
// when it synthesizes the config-apply task, and the sidecar's sei-config
// resolver additionally rejects unknown fields and type-mismatched values;
// (3) the SDK re-checks family membership client-side for a fast clean error.
// Widening this allowlist is a value-contract change — review the three points
// together.
type ConfigPatchPayload struct {
// Overrides maps dotted sei-config keys (the same operator-facing schema as
// SeiNodeSpec.Overrides, e.g. "giga_executor.enabled": "false") to their
// desired string values. Values are coerced to each field's registered type
// (bool/int/…) when rendered to TOML. At least one entry is required.
// +kubebuilder:validation:MinProperties=1
Overrides map[string]string `json:"overrides"`
}

// ---------------------------------------------------------------------------
// Status
// ---------------------------------------------------------------------------
Expand Down
27 changes: 27 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 25 additions & 2 deletions config/crd/sei.io_seinodetasks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ spec:
required:
- targetHeight
type: object
configPatch:
description: ConfigPatch is the payload for kind=ConfigPatch.
properties:
overrides:
additionalProperties:
type: string
description: |-
Overrides maps dotted sei-config keys (the same operator-facing schema as
SeiNodeSpec.Overrides, e.g. "giga_executor.enabled": "false") to their
desired string values. Values are coerced to each field's registered type
(bool/int/…) when rendered to TOML. At least one entry is required.
minProperties: 1
type: object
required:
- overrides
type: object
govParamChange:
description: GovParamChange is the payload for kind=GovParamChange.
properties:
Expand Down Expand Up @@ -338,6 +354,7 @@ spec:
- AwaitNodesAtHeight
- RestartSeid
- MarkReady
- ConfigPatch
type: string
markReady:
description: MarkReady is the payload for kind=MarkReady.
Expand Down Expand Up @@ -416,12 +433,12 @@ spec:
x-kubernetes-validations:
- message: exactly one of govSoftwareUpgrade, govVote, govParamChange,
awaitCondition, updateNodeImage, awaitNodesAtHeight, restartSeid,
or markReady must be set
markReady, or configPatch must be set
rule: '(has(self.govSoftwareUpgrade) ? 1 : 0) + (has(self.govVote) ?
1 : 0) + (has(self.govParamChange) ? 1 : 0) + (has(self.awaitCondition)
? 1 : 0) + (has(self.updateNodeImage) ? 1 : 0) + (has(self.awaitNodesAtHeight)
? 1 : 0) + (has(self.restartSeid) ? 1 : 0) + (has(self.markReady)
? 1 : 0) == 1'
? 1 : 0) + (has(self.configPatch) ? 1 : 0) == 1'
- message: spec.govSoftwareUpgrade is required when kind=GovSoftwareUpgrade
rule: self.kind != 'GovSoftwareUpgrade' || has(self.govSoftwareUpgrade)
- message: spec.govVote is required when kind=GovVote
Expand All @@ -438,6 +455,12 @@ spec:
rule: self.kind != 'RestartSeid' || has(self.restartSeid)
- message: spec.markReady is required when kind=MarkReady
rule: self.kind != 'MarkReady' || has(self.markReady)
- message: spec.configPatch is required when kind=ConfigPatch
rule: self.kind != 'ConfigPatch' || has(self.configPatch)
- message: spec.configPatch.overrides only admits allowlisted runtime-safe
keys (chain.occ_enabled, giga_executor.*, mempool.*)
rule: '!has(self.configPatch) || self.configPatch.overrides.all(k, k
== ''chain.occ_enabled'' || k.startsWith(''giga_executor.'') || k.startsWith(''mempool.''))'
- message: spec.kind is immutable
rule: self.kind == oldSelf.kind
status:
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/sei-protocol/sei-k8s-controller
go 1.26.0

require (
github.com/BurntSushi/toml v1.5.0
github.com/aws/aws-sdk-go-v2 v1.41.6
github.com/aws/aws-sdk-go-v2/config v1.32.12
github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0
Expand Down Expand Up @@ -35,7 +36,6 @@ require (

require (
cel.dev/expr v0.25.1 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
Expand Down Expand Up @@ -74,14 +74,17 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/oapi-codegen/runtime v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg=
github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
Expand Down Expand Up @@ -117,6 +119,8 @@ github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/v
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand All @@ -138,6 +142,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -146,6 +152,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4=
github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0=
github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc=
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/nodetask/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ const (
// sidecar SIGTERMs seid (up to ~90s graceful), then polls seid's RPC back up.
defaultRestartSeidTimeout = 10 * time.Minute
defaultMarkReadyTimeout = 2 * time.Minute
// defaultConfigPatchTimeout bounds a config-patch: a quick TOML merge-and-
// write on the sidecar. A generous 2m guards against a wedged sidecar
// pinning the task unbounded, while leaving ample room for the write + poll.
defaultConfigPatchTimeout = 2 * time.Minute
)

// resultRequeueImmediate mirrors planner.ResultRequeueImmediate without
Expand Down Expand Up @@ -376,6 +380,8 @@ func effectiveTimeout(cr *seiv1alpha1.SeiNodeTask) time.Duration {
return defaultRestartSeidTimeout
case seiv1alpha1.SeiNodeTaskKindMarkReady:
return defaultMarkReadyTimeout
case seiv1alpha1.SeiNodeTaskKindConfigPatch:
return defaultConfigPatchTimeout
default:
return 0
}
Expand Down
Loading
Loading