@@ -640,3 +640,191 @@ spec:
640640 g .Expect (cancelEvent .Message ).To (ContainSubstring ("Health checks canceled" ))
641641 g .Expect (cancelEvent .Message ).To (ContainSubstring ("GitRepository" ))
642642}
643+
644+ func TestKustomizationReconciler_HealthCheckExprs_GroupOnly (t * testing.T ) {
645+ g := NewWithT (t )
646+ id := "cel-grp-" + randStringRunes (5 )
647+ revision := "v1.0.0"
648+ resultK := & kustomizev1.Kustomization {}
649+ timeout := 60 * time .Second
650+
651+ err := createNamespace (id )
652+ g .Expect (err ).NotTo (HaveOccurred (), "failed to create test namespace" )
653+
654+ err = createKubeConfigSecret (id )
655+ g .Expect (err ).NotTo (HaveOccurred (), "failed to create kubeconfig secret" )
656+
657+ // Unique group per test run to avoid CRD name collisions within the shared envtest.
658+ group := fmt .Sprintf ("%s.flux-test.io" , id )
659+ fooCRName := "foo-" + randStringRunes (5 )
660+ barCRName := "bar-" + randStringRunes (5 )
661+
662+ // Two cluster-scoped CRDs in the same group, plus one CR of each kind.
663+ // The CEL expression reads `spec.ready`, applied via SSA from the artifact.
664+ buildFiles := func (barReady bool ) []testserver.File {
665+ return []testserver.File {
666+ {
667+ Name : "crd-foo.yaml" ,
668+ Body : fmt .Sprintf (`---
669+ apiVersion: apiextensions.k8s.io/v1
670+ kind: CustomResourceDefinition
671+ metadata:
672+ name: foos.%[1]s
673+ spec:
674+ group: %[1]s
675+ names:
676+ kind: Foo
677+ listKind: FooList
678+ plural: foos
679+ singular: foo
680+ scope: Cluster
681+ versions:
682+ - name: v1
683+ served: true
684+ storage: true
685+ schema:
686+ openAPIV3Schema:
687+ type: object
688+ properties:
689+ spec:
690+ type: object
691+ properties:
692+ ready:
693+ type: boolean
694+ ` , group ),
695+ },
696+ {
697+ Name : "crd-bar.yaml" ,
698+ Body : fmt .Sprintf (`---
699+ apiVersion: apiextensions.k8s.io/v1
700+ kind: CustomResourceDefinition
701+ metadata:
702+ name: bars.%[1]s
703+ spec:
704+ group: %[1]s
705+ names:
706+ kind: Bar
707+ listKind: BarList
708+ plural: bars
709+ singular: bar
710+ scope: Cluster
711+ versions:
712+ - name: v1
713+ served: true
714+ storage: true
715+ schema:
716+ openAPIV3Schema:
717+ type: object
718+ properties:
719+ spec:
720+ type: object
721+ properties:
722+ ready:
723+ type: boolean
724+ ` , group ),
725+ },
726+ {
727+ Name : "foo.yaml" ,
728+ Body : fmt .Sprintf (`---
729+ apiVersion: %[1]s/v1
730+ kind: Foo
731+ metadata:
732+ name: %[2]s
733+ spec:
734+ ready: true
735+ ` , group , fooCRName ),
736+ },
737+ {
738+ Name : "bar.yaml" ,
739+ Body : fmt .Sprintf (`---
740+ apiVersion: %[1]s/v1
741+ kind: Bar
742+ metadata:
743+ name: %[2]s
744+ spec:
745+ ready: %[3]t
746+ ` , group , barCRName , barReady ),
747+ },
748+ }
749+ }
750+
751+ artifact , err := testServer .ArtifactFromFiles (buildFiles (true ))
752+ g .Expect (err ).NotTo (HaveOccurred ())
753+
754+ repositoryName := types.NamespacedName {
755+ Name : fmt .Sprintf ("grp-%s" , randStringRunes (5 )),
756+ Namespace : id ,
757+ }
758+
759+ err = applyGitRepository (repositoryName , artifact , revision )
760+ g .Expect (err ).NotTo (HaveOccurred ())
761+
762+ kustomization := & kustomizev1.Kustomization {
763+ ObjectMeta : metav1.ObjectMeta {
764+ Name : fmt .Sprintf ("grp-%s" , randStringRunes (5 )),
765+ Namespace : id ,
766+ },
767+ Spec : kustomizev1.KustomizationSpec {
768+ Interval : metav1.Duration {Duration : 2 * time .Minute },
769+ Path : "./" ,
770+ KubeConfig : & meta.KubeConfigReference {
771+ SecretRef : & meta.SecretKeyReference {
772+ Name : "kubeconfig" ,
773+ },
774+ },
775+ SourceRef : kustomizev1.CrossNamespaceSourceReference {
776+ Name : repositoryName .Name ,
777+ Namespace : repositoryName .Namespace ,
778+ Kind : sourcev1 .GitRepositoryKind ,
779+ },
780+ Prune : true ,
781+ Wait : true ,
782+ // Single group-only healthcheck (empty Kind) that must be applied
783+ // to both Foo and Bar custom resources.
784+ HealthCheckExprs : []kustomize.CustomHealthCheck {{
785+ APIVersion : group + "/v1" ,
786+ HealthCheckExpressions : kustomize.HealthCheckExpressions {
787+ Current : "has(spec.ready) && spec.ready == true" ,
788+ },
789+ }},
790+ },
791+ }
792+
793+ g .Expect (k8sClient .Create (context .Background (), kustomization )).To (Succeed ())
794+
795+ t .Run ("group-only healthcheck succeeds for both kinds" , func (t * testing.T ) {
796+ g .Eventually (func () bool {
797+ _ = k8sClient .Get (context .Background (), client .ObjectKeyFromObject (kustomization ), resultK )
798+ return isReconcileSuccess (resultK )
799+ }, timeout , time .Second ).Should (BeTrue ())
800+ logStatus (t , resultK )
801+
802+ g .Expect (conditions .IsTrue (resultK , meta .HealthyCondition )).To (BeTrue ())
803+ g .Expect (conditions .GetReason (resultK , meta .HealthyCondition )).To (BeIdenticalTo (meta .SucceededReason ))
804+ })
805+
806+ t .Run ("reports unhealthy when one kind stops satisfying the group expression" , func (t * testing.T ) {
807+ badArtifact , err := testServer .ArtifactFromFiles (buildFiles (false ))
808+ g .Expect (err ).NotTo (HaveOccurred ())
809+
810+ err = applyGitRepository (repositoryName , badArtifact , "v1.0.1" )
811+ g .Expect (err ).NotTo (HaveOccurred ())
812+
813+ // Shorten healthcheck timeout so the failure surfaces quickly.
814+ g .Eventually (func () error {
815+ _ = k8sClient .Get (context .Background (), client .ObjectKeyFromObject (kustomization ), resultK )
816+ resultK .Spec .Timeout = & metav1.Duration {Duration : 5 * time .Second }
817+ return k8sClient .Update (context .Background (), resultK )
818+ }, timeout , time .Second ).Should (BeNil ())
819+
820+ g .Eventually (func () bool {
821+ _ = k8sClient .Get (context .Background (), client .ObjectKeyFromObject (kustomization ), resultK )
822+ return conditions .IsFalse (resultK , meta .HealthyCondition ) &&
823+ conditions .GetReason (resultK , meta .HealthyCondition ) == meta .HealthCheckFailedReason
824+ }, timeout , time .Second ).Should (BeTrue ())
825+
826+ msg := conditions .GetMessage (resultK , meta .HealthyCondition )
827+ g .Expect (msg ).To (ContainSubstring ("Bar" ))
828+ g .Expect (msg ).To (ContainSubstring (barCRName ))
829+ })
830+ }
0 commit comments