From db20baca6ee5fca028b7eeb0aefa438690df8144 Mon Sep 17 00:00:00 2001 From: tonic Date: Tue, 11 Aug 2026 13:46:00 +0800 Subject: [PATCH 1/3] feat: add snapshot CPU compatibility contract Expose an immutable CocoonSet compatibility class and shared node label so scheduling can constrain memory snapshots independently from workload pools. --- apis/v1/cocoonset_types.go | 9 +++++++ apis/v1/cocoonset_types_test.go | 26 +++++++++++++++++++ .../cocoonset.cocoonstack.io_cocoonsets.yaml | 11 ++++++++ meta/keys.go | 3 +++ 4 files changed, 49 insertions(+) create mode 100644 apis/v1/cocoonset_types_test.go diff --git a/apis/v1/cocoonset_types.go b/apis/v1/cocoonset_types.go index 27277cd..8c8893b 100644 --- a/apis/v1/cocoonset_types.go +++ b/apis/v1/cocoonset_types.go @@ -8,6 +8,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" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.snapshotCompatibilityClass) || (has(self.snapshotCompatibilityClass) && self.snapshotCompatibilityClass == oldSelf.snapshotCompatibilityClass)",message="snapshotCompatibilityClass is immutable once set" type CocoonSetSpec struct { // +optional Suspend bool `json:"suspend,omitempty"` @@ -27,6 +28,14 @@ type CocoonSetSpec struct { // +kubebuilder:default=default NodePool string `json:"nodePool,omitempty"` + // SnapshotCompatibilityClass selects nodes that expose a guest-visible CPU + // ABI compatible with this set's memory snapshots. It is independent from + // NodePool so multiple workload pools can share one snapshot class. + // +optional + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$` + SnapshotCompatibilityClass string `json:"snapshotCompatibilityClass,omitempty"` + // NodeName pins the VM to a node (cross-node migrate). Empty = let the // scheduler place it within NodePool and leave it alone; a value adds a // hostname nodeAffinity so the pod lands there or stays Pending if it won't fit. diff --git a/apis/v1/cocoonset_types_test.go b/apis/v1/cocoonset_types_test.go new file mode 100644 index 0000000..487d9d1 --- /dev/null +++ b/apis/v1/cocoonset_types_test.go @@ -0,0 +1,26 @@ +package v1 + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestCocoonSetSpecSnapshotCompatibilityClassJSON(t *testing.T) { + spec := CocoonSetSpec{SnapshotCompatibilityClass: "n2-cascade-lake-v1"} + raw, err := json.Marshal(spec) + if err != nil { + t.Fatalf("marshal CocoonSetSpec: %v", err) + } + if !strings.Contains(string(raw), `"snapshotCompatibilityClass":"n2-cascade-lake-v1"`) { + t.Fatalf("snapshot compatibility class missing from JSON: %s", raw) + } + + raw, err = json.Marshal(CocoonSetSpec{}) + if err != nil { + t.Fatalf("marshal empty CocoonSetSpec: %v", err) + } + if strings.Contains(string(raw), "snapshotCompatibilityClass") { + t.Fatalf("empty snapshot compatibility class must be omitted: %s", raw) + } +} diff --git a/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml b/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml index 68c738d..c365e2c 100644 --- a/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml +++ b/apis/v1/crds/cocoonset.cocoonstack.io_cocoonsets.yaml @@ -267,6 +267,14 @@ spec: nodePool: default: default type: string + snapshotCompatibilityClass: + description: |- + SnapshotCompatibilityClass selects nodes that expose a guest-visible CPU + ABI compatible with this set's memory snapshots. It is independent from + NodePool so multiple workload pools can share one snapshot class. + maxLength: 63 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string snapshotPolicy: default: always description: SnapshotPolicy defines when VM snapshots are taken. @@ -436,6 +444,9 @@ spec: rule: '!has(self.hibernatePolicy) || self.hibernatePolicy != ''release'' || ((!has(self.agent.replicas) || self.agent.replicas == 0) && (!has(self.toolboxes) || size(self.toolboxes) == 0))' + - message: snapshotCompatibilityClass is immutable once set + rule: '!has(oldSelf.snapshotCompatibilityClass) || (has(self.snapshotCompatibilityClass) + && self.snapshotCompatibilityClass == oldSelf.snapshotCompatibilityClass)' status: description: CocoonSetStatus represents the observed state of a CocoonSet. properties: diff --git a/meta/keys.go b/meta/keys.go index aa18939..4c6cad5 100644 --- a/meta/keys.go +++ b/meta/keys.go @@ -18,6 +18,9 @@ const ( LabelNodePool = "cocoonstack.io/pool" // DefaultNodePool is the pool name used when LabelNodePool is unset. DefaultNodePool = "default" + // LabelSnapshotCompatibilityClass selects nodes whose guest-visible CPU ABI + // can restore the pod's memory snapshots. + LabelSnapshotCompatibilityClass = "cocoonstack.io/snapshot-cpu-class" // AnnotationMode declares the VM provisioning mode (clone / run / static). AnnotationMode = "cocoonset.cocoonstack.io/mode" From 6246dd56a677e5eb4e2d484f1b4f2fc96213afbf Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 11 Aug 2026 15:09:42 +0800 Subject: [PATCH 2/3] clean --- apis/v1/cocoonset_types.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/apis/v1/cocoonset_types.go b/apis/v1/cocoonset_types.go index 8c8893b..ec44f89 100644 --- a/apis/v1/cocoonset_types.go +++ b/apis/v1/cocoonset_types.go @@ -28,9 +28,6 @@ type CocoonSetSpec struct { // +kubebuilder:default=default NodePool string `json:"nodePool,omitempty"` - // SnapshotCompatibilityClass selects nodes that expose a guest-visible CPU - // ABI compatible with this set's memory snapshots. It is independent from - // NodePool so multiple workload pools can share one snapshot class. // +optional // +kubebuilder:validation:MaxLength=63 // +kubebuilder:validation:Pattern=`^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$` From 316ce2765be62e374ed25250147931cae9bfd723 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 11 Aug 2026 15:10:27 +0800 Subject: [PATCH 3/3] clean --- meta/keys.go | 1 - 1 file changed, 1 deletion(-) diff --git a/meta/keys.go b/meta/keys.go index 4c6cad5..3d10796 100644 --- a/meta/keys.go +++ b/meta/keys.go @@ -19,7 +19,6 @@ const ( // DefaultNodePool is the pool name used when LabelNodePool is unset. DefaultNodePool = "default" // LabelSnapshotCompatibilityClass selects nodes whose guest-visible CPU ABI - // can restore the pod's memory snapshots. LabelSnapshotCompatibilityClass = "cocoonstack.io/snapshot-cpu-class" // AnnotationMode declares the VM provisioning mode (clone / run / static).