@@ -18,6 +18,8 @@ limitations under the License.
1818package v1
1919
2020import (
21+ "fmt"
22+
2123 . "github.com/onsi/ginkgo/v2"
2224 . "github.com/onsi/gomega"
2325 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -396,3 +398,254 @@ var _ = Describe("MaintenanceReason CEL Validation", func() {
396398 })
397399 })
398400})
401+
402+ // TestGroupsCELValidation tests the CEL validation rules for spec.groups:
403+ // 1. Exactly one group type must be set per entry (union rule on Group)
404+ // 2. Field-level validation (minLength) on trait name, aggregate name, and aggregate UUID
405+ var _ = Describe ("Groups CEL Validation" , func () {
406+ var (
407+ hypervisor * Hypervisor
408+ hypervisorName types.NamespacedName
409+ counter int
410+ )
411+
412+ BeforeEach (func (ctx SpecContext ) {
413+ counter ++
414+ hypervisorName = types.NamespacedName {
415+ Name : fmt .Sprintf ("test-groups-hv-%d" , counter ),
416+ }
417+ })
418+
419+ AfterEach (func (ctx SpecContext ) {
420+ if hypervisor != nil {
421+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , hypervisor ))).To (Succeed ())
422+ hypervisor = nil
423+ }
424+ })
425+
426+ Context ("Union rule: exactly one group type per entry" , func () {
427+ It ("should accept a group with only trait set" , func (ctx SpecContext ) {
428+ hypervisor = & Hypervisor {
429+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
430+ Spec : HypervisorSpec {
431+ Groups : []Group {
432+ {Trait : & TraitGroup {Name : "HW_CPU_X86_AVX2" }},
433+ },
434+ },
435+ }
436+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
437+ })
438+
439+ It ("should accept a group with only aggregate set" , func (ctx SpecContext ) {
440+ hypervisor = & Hypervisor {
441+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
442+ Spec : HypervisorSpec {
443+ Groups : []Group {
444+ {Aggregate : & AggregateGroup {Name : "fast-storage" , UUID : "abc-123" }},
445+ },
446+ },
447+ }
448+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
449+ })
450+
451+ It ("should accept mixed trait and aggregate entries" , func (ctx SpecContext ) {
452+ hypervisor = & Hypervisor {
453+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
454+ Spec : HypervisorSpec {
455+ Groups : []Group {
456+ {Trait : & TraitGroup {Name : "HW_CPU_X86_AVX2" }},
457+ {Aggregate : & AggregateGroup {Name : "fast-storage" , UUID : "abc-123" }},
458+ {Trait : & TraitGroup {Name : "COMPUTE_STATUS_DISABLED" }},
459+ },
460+ },
461+ }
462+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
463+ })
464+
465+ It ("should reject a group with both trait and aggregate set" , func (ctx SpecContext ) {
466+ hypervisor = & Hypervisor {
467+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
468+ Spec : HypervisorSpec {
469+ Groups : []Group {
470+ {
471+ Trait : & TraitGroup {Name : "HW_CPU_X86_AVX2" },
472+ Aggregate : & AggregateGroup {Name : "fast-storage" , UUID : "abc-123" },
473+ },
474+ },
475+ },
476+ }
477+ err := k8sClient .Create (ctx , hypervisor )
478+ Expect (err ).To (HaveOccurred ())
479+ Expect (err .Error ()).To (ContainSubstring ("exactly one group type must be set" ))
480+ })
481+
482+ It ("should reject a group with neither trait nor aggregate set" , func (ctx SpecContext ) {
483+ hypervisor = & Hypervisor {
484+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
485+ Spec : HypervisorSpec {
486+ Groups : []Group {
487+ {},
488+ },
489+ },
490+ }
491+ err := k8sClient .Create (ctx , hypervisor )
492+ Expect (err ).To (HaveOccurred ())
493+ Expect (err .Error ()).To (ContainSubstring ("exactly one group type must be set" ))
494+ })
495+ })
496+
497+ Context ("Field validation" , func () {
498+ It ("should reject a trait with empty name" , func (ctx SpecContext ) {
499+ hypervisor = & Hypervisor {
500+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
501+ Spec : HypervisorSpec {
502+ Groups : []Group {
503+ {Trait : & TraitGroup {Name : "" }},
504+ },
505+ },
506+ }
507+ err := k8sClient .Create (ctx , hypervisor )
508+ Expect (err ).To (HaveOccurred ())
509+ })
510+
511+ It ("should reject an aggregate with empty name" , func (ctx SpecContext ) {
512+ hypervisor = & Hypervisor {
513+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
514+ Spec : HypervisorSpec {
515+ Groups : []Group {
516+ {Aggregate : & AggregateGroup {Name : "" , UUID : "uuid-1" }},
517+ },
518+ },
519+ }
520+ err := k8sClient .Create (ctx , hypervisor )
521+ Expect (err ).To (HaveOccurred ())
522+ })
523+
524+ It ("should reject an aggregate with empty UUID" , func (ctx SpecContext ) {
525+ hypervisor = & Hypervisor {
526+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
527+ Spec : HypervisorSpec {
528+ Groups : []Group {
529+ {Aggregate : & AggregateGroup {Name : "fast-storage" , UUID : "" }},
530+ },
531+ },
532+ }
533+ err := k8sClient .Create (ctx , hypervisor )
534+ Expect (err ).To (HaveOccurred ())
535+ })
536+
537+ It ("should accept an aggregate without metadata" , func (ctx SpecContext ) {
538+ hypervisor = & Hypervisor {
539+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
540+ Spec : HypervisorSpec {
541+ Groups : []Group {
542+ {Aggregate : & AggregateGroup {Name : "fast-storage" , UUID : "uuid-1" }},
543+ },
544+ },
545+ }
546+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
547+ })
548+
549+ It ("should accept an aggregate with metadata" , func (ctx SpecContext ) {
550+ hypervisor = & Hypervisor {
551+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
552+ Spec : HypervisorSpec {
553+ Groups : []Group {
554+ {Aggregate : & AggregateGroup {
555+ Name : "fast-storage" ,
556+ UUID : "uuid-1" ,
557+ Metadata : map [string ]string {"ssd" : "true" },
558+ }},
559+ },
560+ },
561+ }
562+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
563+ })
564+
565+ It ("should accept an empty groups list" , func (ctx SpecContext ) {
566+ hypervisor = & Hypervisor {
567+ ObjectMeta : metav1.ObjectMeta {Name : hypervisorName .Name },
568+ Spec : HypervisorSpec {
569+ Groups : []Group {},
570+ },
571+ }
572+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
573+ })
574+ })
575+ })
576+
577+ var _ = Describe ("Group Helper Functions" , func () {
578+ groups := []Group {
579+ {Trait : & TraitGroup {Name : "HW_CPU_X86_AVX2" }},
580+ {Trait : & TraitGroup {Name : "COMPUTE_STATUS_DISABLED" }},
581+ {Aggregate : & AggregateGroup {Name : "fast-storage" , UUID : "uuid-1" , Metadata : map [string ]string {"ssd" : "true" }}},
582+ {Aggregate : & AggregateGroup {Name : "slow-storage" , UUID : "uuid-2" }},
583+ }
584+
585+ Context ("HasTrait" , func () {
586+ It ("should return true for an existing trait" , func () {
587+ Expect (HasTrait (groups , "HW_CPU_X86_AVX2" )).To (BeTrue ())
588+ })
589+
590+ It ("should return false for a missing trait" , func () {
591+ Expect (HasTrait (groups , "NONEXISTENT" )).To (BeFalse ())
592+ })
593+
594+ It ("should return false for an empty list" , func () {
595+ Expect (HasTrait (nil , "HW_CPU_X86_AVX2" )).To (BeFalse ())
596+ })
597+ })
598+
599+ Context ("GetTraits" , func () {
600+ It ("should return all trait entries" , func () {
601+ traits := GetTraits (groups )
602+ Expect (traits ).To (HaveLen (2 ))
603+ Expect (traits [0 ].Name ).To (Equal ("HW_CPU_X86_AVX2" ))
604+ Expect (traits [1 ].Name ).To (Equal ("COMPUTE_STATUS_DISABLED" ))
605+ })
606+
607+ It ("should return empty for a list with no traits" , func () {
608+ aggs := []Group {{Aggregate : & AggregateGroup {Name : "a" , UUID : "u" }}}
609+ Expect (GetTraits (aggs )).To (BeEmpty ())
610+ })
611+
612+ It ("should return nil for an empty list" , func () {
613+ Expect (GetTraits (nil )).To (BeNil ())
614+ })
615+ })
616+
617+ Context ("HasAggregate" , func () {
618+ It ("should return true for an existing aggregate UUID" , func () {
619+ Expect (HasAggregate (groups , "uuid-1" )).To (BeTrue ())
620+ })
621+
622+ It ("should return false for a missing aggregate UUID" , func () {
623+ Expect (HasAggregate (groups , "uuid-999" )).To (BeFalse ())
624+ })
625+
626+ It ("should return false for an empty list" , func () {
627+ Expect (HasAggregate (nil , "uuid-1" )).To (BeFalse ())
628+ })
629+ })
630+
631+ Context ("GetAggregates" , func () {
632+ It ("should return all aggregate entries" , func () {
633+ aggs := GetAggregates (groups )
634+ Expect (aggs ).To (HaveLen (2 ))
635+ Expect (aggs [0 ].Name ).To (Equal ("fast-storage" ))
636+ Expect (aggs [0 ].UUID ).To (Equal ("uuid-1" ))
637+ Expect (aggs [0 ].Metadata ).To (HaveKeyWithValue ("ssd" , "true" ))
638+ Expect (aggs [1 ].Name ).To (Equal ("slow-storage" ))
639+ Expect (aggs [1 ].UUID ).To (Equal ("uuid-2" ))
640+ })
641+
642+ It ("should return empty for a list with no aggregates" , func () {
643+ traits := []Group {{Trait : & TraitGroup {Name : "T" }}}
644+ Expect (GetAggregates (traits )).To (BeEmpty ())
645+ })
646+
647+ It ("should return nil for an empty list" , func () {
648+ Expect (GetAggregates (nil )).To (BeNil ())
649+ })
650+ })
651+ })
0 commit comments