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
53 changes: 53 additions & 0 deletions api/v1alpha1/seinode_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ type SeiNodeSpec struct {
// +optional
PodLabels map[string]string `json:"podLabels,omitempty"`

// DataVolume configures the data PersistentVolumeClaim for this node.
// When omitted, the controller creates a PVC using the node's mode-default
// storage class and size (see noderesource.DefaultStorageForMode).
// +optional
DataVolume *DataVolumeSpec `json:"dataVolume,omitempty"`

// --- Mode-specific sub-specs (exactly one must be set) ---

// FullNode configures a chain-following full node (absorbs the "rpc" role).
Expand All @@ -66,6 +72,31 @@ type SeiNodeSpec struct {
Validator *ValidatorSpec `json:"validator,omitempty"`
}

// DataVolumeSpec configures how the data PVC is sourced.
type DataVolumeSpec struct {
// Import references a pre-existing PersistentVolumeClaim in the same
// namespace as the SeiNode, instead of creating a new one. The
// controller validates the referenced PVC but never mutates it.
//
// When Import is set, the controller never deletes the referenced PVC
// on SeiNode deletion — storage lifecycle is the operator's responsibility.
// +optional
Import *DataVolumeImport `json:"import,omitempty"`
}

// DataVolumeImport names a pre-existing PVC to adopt as this node's data volume.
type DataVolumeImport struct {
// PVCName is the name of a PersistentVolumeClaim in the SeiNode's
// namespace. The PVC must be Bound, ReadWriteOnce, and sized at or above
// the node mode's default storage size. Immutable after creation.
//
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="pvcName is immutable"
PVCName string `json:"pvcName"`
}

// SnapshotSource returns the SnapshotSource from whichever mode sub-spec is
// populated, or nil if no snapshot is configured. Archive nodes always return
// nil because they use state sync (configured internally by the planner).
Expand Down Expand Up @@ -209,6 +240,28 @@ const (
const (
// ConditionNodeUpdateInProgress indicates an image update is being rolled out.
ConditionNodeUpdateInProgress = "NodeUpdateInProgress"

// ConditionImportPVCReady indicates whether an imported data PVC passes all
// validation requirements. Only set on SeiNodes with spec.dataVolume.import.
ConditionImportPVCReady = "ImportPVCReady"
)

// Reasons for the ImportPVCReady condition. These strings form a public
// alerting contract: Prometheus alerts, audit tools, and operator scripts
// may key on these exact values. Renaming or removing a reason is a breaking
// change and requires a deprecation window. Adding a new reason is additive
// and backward compatible.
const (
ReasonImportValidated = "PVCValidated"
ReasonImportPVCNotFound = "PVCNotFound"
ReasonImportPVCTerminating = "PVCTerminating"
ReasonImportPVCNotBound = "PVCNotBound" // Pending/Released
ReasonImportPVCLost = "PVCLost" // terminal
ReasonImportAccessModeInvalid = "AccessModeInvalid"
ReasonImportCapacityTooSmall = "CapacityTooSmall"
ReasonImportPVMissing = "UnderlyingPVMissing"
ReasonImportPVCapacityMismatch = "UnderlyingPVCapacityMismatch"
ReasonImportPVFailed = "UnderlyingPVFailed"
)

// SeiNodeStatus defines the observed state of a SeiNode.
Expand Down
40 changes: 40 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.

31 changes: 31 additions & 0 deletions config/crd/sei.io_seinodedeployments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,37 @@ spec:
description: ChainID of the chain this node belongs to.
minLength: 1
type: string
dataVolume:
description: |-
DataVolume configures the data PersistentVolumeClaim for this node.
When omitted, the controller creates a PVC using the node's mode-default
storage class and size (see noderesource.DefaultStorageForMode).
properties:
import:
description: |-
Import references a pre-existing PersistentVolumeClaim in the same
namespace as the SeiNode, instead of creating a new one. The
controller validates the referenced PVC but never mutates it.

When Import is set, the controller never deletes the referenced PVC
on SeiNode deletion — storage lifecycle is the operator's responsibility.
properties:
pvcName:
description: |-
PVCName is the name of a PersistentVolumeClaim in the SeiNode's
namespace. The PVC must be Bound, ReadWriteOnce, and sized at or above
the node mode's default storage size. Immutable after creation.
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
x-kubernetes-validations:
- message: pvcName is immutable
rule: self == oldSelf
required:
- pvcName
type: object
type: object
entrypoint:
description: Entrypoint overrides the image command for the
running node process.
Expand Down
31 changes: 31 additions & 0 deletions config/crd/sei.io_seinodes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,37 @@ spec:
description: ChainID of the chain this node belongs to.
minLength: 1
type: string
dataVolume:
description: |-
DataVolume configures the data PersistentVolumeClaim for this node.
When omitted, the controller creates a PVC using the node's mode-default
storage class and size (see noderesource.DefaultStorageForMode).
properties:
import:
description: |-
Import references a pre-existing PersistentVolumeClaim in the same
namespace as the SeiNode, instead of creating a new one. The
controller validates the referenced PVC but never mutates it.

When Import is set, the controller never deletes the referenced PVC
on SeiNode deletion — storage lifecycle is the operator's responsibility.
properties:
pvcName:
description: |-
PVCName is the name of a PersistentVolumeClaim in the SeiNode's
namespace. The PVC must be Bound, ReadWriteOnce, and sized at or above
the node mode's default storage size. Immutable after creation.
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
x-kubernetes-validations:
- message: pvcName is immutable
rule: self == oldSelf
required:
- pvcName
type: object
type: object
entrypoint:
description: Entrypoint overrides the image command for the running
node process.
Expand Down
1 change: 1 addition & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ rules:
- apiGroups:
- ""
resources:
- persistentvolumes
- pods
verbs:
- get
Expand Down
53 changes: 53 additions & 0 deletions internal/controller/node/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type SeiNodeReconciler struct {
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=persistentvolumes,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

Expand Down Expand Up @@ -122,6 +123,16 @@ func (r *SeiNodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
statusDirty = true
}

// Sync the ImportPVCReady condition from the current ensure-data-pvc
// task state, before flushing status. No-op when import is not configured.
// If the condition list mutates and no other status changes occurred, the
// patch still covers it because the condition is part of status.
condBefore := conditionsSnapshot(node)
planner.ReconcileImportPVCCondition(node)
if !statusDirty && !conditionsEqual(condBefore, conditionsSnapshot(node)) {
statusDirty = true
}

if statusDirty {
if err := r.Status().Patch(ctx, node, statusBase); err != nil {
if execErr != nil {
Expand Down Expand Up @@ -216,6 +227,14 @@ func (r *SeiNodeReconciler) handleNodeDeletion(ctx context.Context, node *seiv1a
}

func (r *SeiNodeReconciler) deleteNodeDataPVC(ctx context.Context, node *seiv1alpha1.SeiNode) error {
// Imported PVCs are managed externally — never delete them.
if node.Spec.DataVolume != nil && node.Spec.DataVolume.Import != nil &&
node.Spec.DataVolume.Import.PVCName != "" {
log.FromContext(ctx).Info("skipping data PVC delete for imported volume",
"pvc", node.Spec.DataVolume.Import.PVCName)
return nil
}

pvc := &corev1.PersistentVolumeClaim{}
err := r.Get(ctx, types.NamespacedName{Name: noderesource.DataPVCName(node), Namespace: node.Namespace}, pvc)
if apierrors.IsNotFound(err) {
Expand All @@ -226,3 +245,37 @@ func (r *SeiNodeReconciler) deleteNodeDataPVC(ctx context.Context, node *seiv1al
}
return r.Delete(ctx, pvc)
}

// conditionsSnapshot returns a compact comparable representation of the
// node's current conditions for change detection prior to status flush.
type conditionKey struct {
Type string
Status string
Reason string
Message string
}

func conditionsSnapshot(node *seiv1alpha1.SeiNode) []conditionKey {
out := make([]conditionKey, len(node.Status.Conditions))
for i, c := range node.Status.Conditions {
out[i] = conditionKey{
Type: c.Type,
Status: string(c.Status),
Reason: c.Reason,
Message: c.Message,
}
}
return out
}

func conditionsEqual(a, b []conditionKey) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
Loading
Loading