diff --git a/apis/v1/cocoonset_types.go b/apis/v1/cocoonset_types.go index 27277cd..ec44f89 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,11 @@ type CocoonSetSpec struct { // +kubebuilder:default=default NodePool string `json:"nodePool,omitempty"` + // +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..3d10796 100644 --- a/meta/keys.go +++ b/meta/keys.go @@ -18,6 +18,8 @@ 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 + LabelSnapshotCompatibilityClass = "cocoonstack.io/snapshot-cpu-class" // AnnotationMode declares the VM provisioning mode (clone / run / static). AnnotationMode = "cocoonset.cocoonstack.io/mode"