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
88 changes: 88 additions & 0 deletions api/v1alpha1/seinode_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ import (
// requests.memory, and the controller derives the limit from the request, so the
// footprint is frozen by freezing requests.
// +kubebuilder:validation:XValidation:rule="(!has(self.resources) && !has(oldSelf.resources)) || (has(self.resources) && has(oldSelf.resources) && (has(self.resources.requests) == has(oldSelf.resources.requests)) && (!has(self.resources.requests) || ((('cpu' in self.resources.requests) == ('cpu' in oldSelf.resources.requests)) && (('memory' in self.resources.requests) == ('memory' in oldSelf.resources.requests)) && (!('cpu' in self.resources.requests) || !('cpu' in oldSelf.resources.requests) || quantity(string(self.resources.requests['cpu'])).compareTo(quantity(string(oldSelf.resources.requests['cpu']))) == 0) && (!('memory' in self.resources.requests) || !('memory' in oldSelf.resources.requests) || quantity(string(self.resources.requests['memory'])).compareTo(quantity(string(oldSelf.resources.requests['memory']))) == 0))))",message="spec.resources is create-only: the footprint is fixed at creation (a change is not rolled onto a running pod — the StatefulSet is OnDelete and drift detection is image-only), so replace the node to resize"
// A node with spec.nodeConfig runs no task that writes config.toml or
// app.toml, so every field whose only route to seid was one of those tasks is
// rejected beside it. Accepting one would report success on an edit that never
// reached the node — and for peers, status.resolvedPeers would keep updating
// and keep looking correct while config.toml stayed as the operator wrote it.
// +kubebuilder:validation:XValidation:rule="!has(self.nodeConfig) || !has(self.configValues)",message="spec.nodeConfig and spec.configValues cannot both be set: a node reading its config from ConfigMaps runs no config-patch task, so the values would never reach seid; put them in the ConfigMap"
// +kubebuilder:validation:XValidation:rule="!has(self.nodeConfig) || !has(self.overrides)",message="spec.nodeConfig and spec.overrides cannot both be set: a node reading its config from ConfigMaps runs no config-apply task, so the overrides would never reach seid; put them in the ConfigMap"
// +kubebuilder:validation:XValidation:rule="!has(self.nodeConfig) || !has(self.peers)",message="spec.nodeConfig and spec.peers cannot both be set: nothing carries a resolved peer set into config.toml on a node reading its config from ConfigMaps; write p2p.persistent-peers in the ConfigMap"
// +kubebuilder:validation:XValidation:rule="!has(self.nodeConfig) || !has(self.externalAddress)",message="spec.nodeConfig and spec.externalAddress cannot both be set: nothing carries it into config.toml on a node reading its config from ConfigMaps; write p2p.external-address in the ConfigMap"
type SeiNodeSpec struct {
// ChainID of the chain this node belongs to.
// Constrained to DNS-1123 label characters because the controller composes
Expand Down Expand Up @@ -122,6 +131,41 @@ type SeiNodeSpec struct {
// +listMapKey=key
ConfigValues []ConfigValue `json:"configValues,omitempty"`

// NodeConfig supplies this node's seid config files from existing
// ConfigMaps. The files mount read-only over the seid config directory, so
// they replace whatever the data volume already holds.
//
// A node with this field set takes the static-config plan: no task in its
// plan writes either file on the production pod. That is what keeps the
// mount attached. A rename onto a mounted path from another container
// detaches the mount, and seid then reads the writer's file.
//
// The operator owns both files verbatim. The controller supplies nothing:
// not the mode's base configuration, not persistent-peers, not
// external-address, not the freeze height, not the snapshot-generation
// keys. It does not validate them either — replace-pod parses both files
// before it deletes a pod, and that is the only check.
//
// The fields whose only route to seid was a config task are rejected
// beside this one: configValues, overrides, peers, externalAddress.
//
// The references are the unit of change. Kubelet pins a subPath mount at
// pod start, so editing a ConfigMap in place does not reach a running pod.
// Publish under a new name and the node rolls.
//
// Not supported with a bootstrap Job, a state-sync snapshot source, a
// genesis ceremony, or consensus engine Autobahn. Each of those writes
// config.toml at run time, and the plan is refused.
//
// The StatefulSet carries the references as soon as they are set, before
// any plan runs. A reference that does not resolve leaves the template
// unmountable: the controller will not replace the pod itself, but a
// drain, an eviction, or a manual delete recreates it into
// ContainerCreating, and StatefulSets are OnDelete so nothing rolls it
// back. Create the ConfigMaps first.
// +optional
NodeConfig *NodeConfig `json:"nodeConfig,omitempty"`

// Scheduling configures worker-node isolation.
// +optional
Scheduling *SchedulingConfig `json:"scheduling,omitempty"`
Expand Down Expand Up @@ -347,6 +391,32 @@ func (s *SeiNodeSpec) SnapshotSource() *SnapshotSource {
}
}

// NodeConfig supplies a node's seid config files from existing ConfigMaps.
// Both references are required: a node that takes its config.toml from a
// ConfigMap and its app.toml from the controller would have two owners of one
// directory, and the controller's writer would detach the mount delivering the
// other file.
type NodeConfig struct {
// ConfigRef holds config.toml.
ConfigRef ConfigFileRef `json:"configRef"`

// AppRef holds app.toml.
AppRef ConfigFileRef `json:"appRef"`
}

// ConfigFileRef names the ConfigMap holding one seid config file. Both
// references may name the same ConfigMap.
type ConfigFileRef struct {
// Name of an existing ConfigMap in the SeiNode's namespace. The file is
// read from the key matching its own name, config.toml or app.toml.
// Kubelet refuses the mount when that key is absent, so the pod stays in
// ContainerCreating and `kubectl describe pod` names the missing key.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
Name string `json:"name"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion — Name carries only length bounds, so a value that is not a DNS subdomain passes admission and then renders a StatefulSet the API server rejects, leaving apply-statefulset failing every reconcile. The sibling object-name fields in this file (DataVolumeImport.PVCName, volumeAttributesClassName) both pin the DNS pattern; adding it here rejects the typo at kubectl apply. (Raised by codex and confirmed.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion — Name carries only length bounds, so a value that is not a DNS subdomain passes admission and then renders a StatefulSet the API server rejects, leaving apply-statefulset failing every reconcile. The sibling object-name fields in this file (DataVolumeImport.PVCName, volumeAttributesClassName) both pin the DNS pattern; adding it here rejects the typo at kubectl apply. (Raised by codex and confirmed.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 707e8d1 — pinned to the same DNS-subdomain pattern the sibling object-name fields use. The field carried a pattern before the rename to configRef/appRef and I dropped it in the move.

}

// NodeKeySecret returns the Secret supplying this node's P2P identity
// (node_key.json), or nil when the node has none and `seid init` generates one
// onto the data volume.
Expand Down Expand Up @@ -486,6 +556,14 @@ type TaskPlan struct {
// +optional
ConfigValuesHash string `json:"configValuesHash,omitempty"`

// ClearsNodeConfig marks a plan that takes the operator's ConfigMaps away
// from a node. On successful completion Status.CurrentNodeConfig is
// cleared. It is not cleared earlier: the plan writes the
// controller-managed base after the pod is replaced, and until that write
// lands the stamp is what keeps the node on the planner that will retry.
// +optional
ClearsNodeConfig bool `json:"clearsNodeConfig,omitempty"`

// FailedPhase is the SeiNodePhase the executor sets on the owning
// resource when the plan fails terminally. When empty, the executor
// does not perform a phase transition on failure.
Expand Down Expand Up @@ -781,6 +859,16 @@ type SeiNodeStatus struct {
// +optional
CurrentNodeIsolation NodeIsolation `json:"currentNodeIsolation,omitempty"`

// CurrentNodeConfig is the spec.nodeConfig the owned StatefulSet's pod was
// last rolled with, stamped jointly with CurrentImage on rollout
// completion. Unset means the pod mounts no operator-supplied config.
//
// Unset means the pod mounts no operator-supplied config. Unlike the fields
// above, unset is a real observation and not "not yet observed". It records
// the references, never the ConfigMaps' contents.
// +optional
CurrentNodeConfig *NodeConfig `json:"currentNodeConfig,omitempty"`

// +listType=map
// +listMapKey=type
// +optional
Expand Down
42 changes: 42 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.

8 changes: 8 additions & 0 deletions config/crd/sei.io_seinetworks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,14 @@ spec:
Plan tracks the active network-level task plan (genesis assembly,
deployment, etc.). Nil when no plan is in progress.
properties:
clearsNodeConfig:
description: |-
ClearsNodeConfig marks a plan that takes the operator's ConfigMaps away
from a node. On successful completion Status.CurrentNodeConfig is
cleared. It is not cleared earlier: the plan writes the
controller-managed base after the pod is replaced, and until that write
lands the stamp is what keeps the node on the planner that will retry.
type: boolean
configValuesHash:
description: |-
ConfigValuesHash identifies the configValues captured by this materialization
Expand Down
Loading
Loading