From 2aee89a5b1e1a40fdc781e657faa19e51fe4feaf Mon Sep 17 00:00:00 2001 From: tonic Date: Thu, 23 Jul 2026 13:28:24 +0800 Subject: [PATCH] feat: add spec.hibernatePolicy, Waking phase, hibernated-on-node annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hibernatePolicy selects the scheduling-seat semantics of a suspended CocoonSet: retain (default, today's behavior) keeps the placeholder pod bound to its node; release lets the operator delete the pods once the hibernate snapshot is registry-verified, freeing the seat — wake then reschedules anywhere in the pool. CEL validation restricts release to main-only sets (replicas=0, no toolboxes) until sub-agent/toolbox restore intent is extended. New Waking phase marks a released-seat restore in flight; the hibernated-on-node annotation carries the wake placement hint. --- apis/v1/cocoonset_types.go | 8 +++++ .../cocoonset.cocoonstack.io_cocoonsets.yaml | 16 ++++++++++ apis/v1/enums.go | 31 ++++++++++++++----- apis/v1/enums_test.go | 25 +++++++++++++++ meta/keys.go | 3 ++ 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/apis/v1/cocoonset_types.go b/apis/v1/cocoonset_types.go index d29d8bb..27277cd 100644 --- a/apis/v1/cocoonset_types.go +++ b/apis/v1/cocoonset_types.go @@ -7,6 +7,7 @@ import ( ) // CocoonSetSpec defines the desired state of a CocoonSet. +// +kubebuilder:validation:XValidation:rule="!has(self.hibernatePolicy) || self.hibernatePolicy != 'release' || ((!has(self.agent.replicas) || self.agent.replicas == 0) && (!has(self.toolboxes) || size(self.toolboxes) == 0))",message="hibernatePolicy=release requires agent.replicas=0 and no toolboxes" type CocoonSetSpec struct { // +optional Suspend bool `json:"suspend,omitempty"` @@ -15,6 +16,13 @@ type CocoonSetSpec struct { // +kubebuilder:default=always SnapshotPolicy SnapshotPolicy `json:"snapshotPolicy,omitempty"` + // HibernatePolicy keeps (retain) or frees (release) the VM's scheduling + // seat while suspended; release requires a main-only set until sub-agent/ + // toolbox restore intent is extended. + // +optional + // +kubebuilder:default=retain + HibernatePolicy HibernatePolicy `json:"hibernatePolicy,omitempty"` + // +optional // +kubebuilder:default=default NodePool string `json:"nodePool,omitempty"` diff --git a/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml b/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml index 2c1f6a5..68c738d 100644 --- a/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml +++ b/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml @@ -248,6 +248,16 @@ spec: required: - image type: object + hibernatePolicy: + default: retain + description: |- + HibernatePolicy keeps (retain) or frees (release) the VM's scheduling + seat while suspended; release requires a main-only set until sub-agent/ + toolbox restore intent is extended. + enum: + - retain + - release + type: string nodeName: description: |- NodeName pins the VM to a node (cross-node migrate). Empty = let the @@ -421,6 +431,11 @@ spec: required: - agent type: object + x-kubernetes-validations: + - message: hibernatePolicy=release requires agent.replicas=0 and no toolboxes + rule: '!has(self.hibernatePolicy) || self.hibernatePolicy != ''release'' + || ((!has(self.agent.replicas) || self.agent.replicas == 0) && (!has(self.toolboxes) + || size(self.toolboxes) == 0))' status: description: CocoonSetStatus represents the observed state of a CocoonSet. properties: @@ -530,6 +545,7 @@ spec: - Scaling - Suspending - Suspended + - Waking - Migrating - Failed type: string diff --git a/apis/v1/enums.go b/apis/v1/enums.go index c7895ee..ab66b77 100644 --- a/apis/v1/enums.go +++ b/apis/v1/enums.go @@ -22,11 +22,15 @@ const ( SnapshotPolicyMainOnly SnapshotPolicy = "main-only" SnapshotPolicyNever SnapshotPolicy = "never" + HibernatePolicyRetain HibernatePolicy = "retain" + HibernatePolicyRelease HibernatePolicy = "release" + CocoonSetPhasePending CocoonSetPhase = "Pending" CocoonSetPhaseRunning CocoonSetPhase = "Running" CocoonSetPhaseScaling CocoonSetPhase = "Scaling" CocoonSetPhaseSuspending CocoonSetPhase = "Suspending" CocoonSetPhaseSuspended CocoonSetPhase = "Suspended" + CocoonSetPhaseWaking CocoonSetPhase = "Waking" CocoonSetPhaseMigrating CocoonSetPhase = "Migrating" CocoonSetPhaseFailed CocoonSetPhase = "Failed" @@ -40,12 +44,13 @@ const ( ) var ( - agentModeValid = []AgentMode{AgentModeClone, AgentModeRun} - toolboxModeValid = []ToolboxMode{ToolboxModeRun, ToolboxModeClone, ToolboxModeStatic} - osTypeValid = []OSType{OSLinux, OSWindows, OSAndroid, OSMacos} - snapshotPolicyValid = []SnapshotPolicy{SnapshotPolicyAlways, SnapshotPolicyMainOnly, SnapshotPolicyNever} - connTypeValid = []ConnType{ConnTypeSSH, ConnTypeRDP, ConnTypeVNC, ConnTypeADB} - backendValid = []Backend{BackendCloudHypervisor, BackendFirecracker} + agentModeValid = []AgentMode{AgentModeClone, AgentModeRun} + toolboxModeValid = []ToolboxMode{ToolboxModeRun, ToolboxModeClone, ToolboxModeStatic} + osTypeValid = []OSType{OSLinux, OSWindows, OSAndroid, OSMacos} + snapshotPolicyValid = []SnapshotPolicy{SnapshotPolicyAlways, SnapshotPolicyMainOnly, SnapshotPolicyNever} + hibernatePolicyValid = []HibernatePolicy{HibernatePolicyRetain, HibernatePolicyRelease} + connTypeValid = []ConnType{ConnTypeSSH, ConnTypeRDP, ConnTypeVNC, ConnTypeADB} + backendValid = []Backend{BackendCloudHypervisor, BackendFirecracker} ) // AgentMode defines the mode of an agent VM. @@ -88,8 +93,20 @@ func (p SnapshotPolicy) IsValid() bool { return slices.Contains(snapshotPolicyVa // Default returns p when set, otherwise SnapshotPolicyAlways. func (p SnapshotPolicy) Default() SnapshotPolicy { return cmp.Or(p, SnapshotPolicyAlways) } +// HibernatePolicy selects the scheduling-seat semantics of a suspended +// CocoonSet: retain keeps the placeholder pod on its node, release frees the +// seat once the hibernate snapshot is registry-verified (wake reschedules). +// +kubebuilder:validation:Enum=retain;release +type HibernatePolicy string + +// IsValid reports whether p is a recognized HibernatePolicy value. +func (p HibernatePolicy) IsValid() bool { return slices.Contains(hibernatePolicyValid, p) } + +// Default returns p when set, otherwise HibernatePolicyRetain. +func (p HibernatePolicy) Default() HibernatePolicy { return cmp.Or(p, HibernatePolicyRetain) } + // CocoonSetPhase represents the lifecycle phase of a CocoonSet. -// +kubebuilder:validation:Enum=Pending;Running;Scaling;Suspending;Suspended;Migrating;Failed +// +kubebuilder:validation:Enum=Pending;Running;Scaling;Suspending;Suspended;Waking;Migrating;Failed type CocoonSetPhase string // ConnType is the connection protocol advertised for a VM. Empty diff --git a/apis/v1/enums_test.go b/apis/v1/enums_test.go index bd4453b..984ee1d 100644 --- a/apis/v1/enums_test.go +++ b/apis/v1/enums_test.go @@ -73,6 +73,23 @@ func TestSnapshotPolicyIsValid(t *testing.T) { } } +func TestHibernatePolicyIsValid(t *testing.T) { + cases := []struct { + in HibernatePolicy + want bool + }{ + {HibernatePolicyRetain, true}, + {HibernatePolicyRelease, true}, + {"", false}, + {"relase", false}, + } + for _, c := range cases { + if got := c.in.IsValid(); got != c.want { + t.Errorf("HibernatePolicy(%q).IsValid() = %v, want %v", c.in, got, c.want) + } + } +} + func TestConnTypeIsValid(t *testing.T) { cases := []struct { in ConnType @@ -142,6 +159,14 @@ func TestEnumDefaults(t *testing.T) { t.Errorf("set value should pass through, got %q", got) } }) + t.Run("HibernatePolicy", func(t *testing.T) { + if got := HibernatePolicy("").Default(); got != HibernatePolicyRetain { + t.Errorf("empty default = %q, want %q", got, HibernatePolicyRetain) + } + if got := HibernatePolicyRelease.Default(); got != HibernatePolicyRelease { + t.Errorf("set value should pass through, got %q", got) + } + }) t.Run("Backend", func(t *testing.T) { if got := Backend("").Default(); got != BackendCloudHypervisor { t.Errorf("empty default = %q, want %q", got, BackendCloudHypervisor) diff --git a/meta/keys.go b/meta/keys.go index 6adba01..51db498 100644 --- a/meta/keys.go +++ b/meta/keys.go @@ -37,6 +37,9 @@ const ( AnnotationForcePull = "cocoonset.cocoonstack.io/force-pull" // AnnotationCocoonSetGeneration carries the CocoonSet generation stamped at scheduling time. AnnotationCocoonSetGeneration = "cocoonset.cocoonstack.io/generation" + // AnnotationHibernatedOnNode records the main agent's node at release-policy + // suspend; wake uses it as a preferred-affinity hint for a warm restore. + AnnotationHibernatedOnNode = "cocoonset.cocoonstack.io/hibernated-on-node" // AnnotationVMID carries the runtime VM identifier vk-cocoon assigns after creation. AnnotationVMID = "vm.cocoonstack.io/id"