@@ -322,6 +322,7 @@ func TestReconcile_Metrics(t *testing.T) {
322322 metrics .ClusterScopedResourceCountView ,
323323 metrics .CRDCountView ,
324324 metrics .KCCResourceCountView ,
325+ metrics .PipelineErrorView ,
325326 )
326327
327328 // Configure controller-manager to log to the test logger
@@ -600,6 +601,17 @@ func TestReconcile_Metrics(t *testing.T) {
600601 {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics" },
601602 }},
602603 },
604+ metrics .PipelineErrorView : {
605+ // Metrics test ResourceGroup (default/test-rg-metrics) - no pipeline errors
606+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
607+ {Key : metrics .KeyComponent , Value : "readiness" },
608+ {Key : metrics .KeyName , Value : func () string {
609+ reconcilerName , _ := metrics .ComputeReconcilerNameType (types.NamespacedName {Name : "test-rg-metrics" , Namespace : "default" })
610+ return reconcilerName
611+ }()},
612+ {Key : metrics .KeyType , Value : "repo-sync" },
613+ }},
614+ },
603615 }
604616
605617 // Validate metrics
@@ -610,6 +622,299 @@ func TestReconcile_Metrics(t *testing.T) {
610622 }
611623}
612624
625+ func TestReconcile_Metrics_EmptyThenAddResources (t * testing.T ) {
626+ // Register metrics views with test exporter at the beginning
627+ exporter := testmetrics .RegisterMetrics (
628+ metrics .ResourceCountView ,
629+ metrics .ReadyResourceCountView ,
630+ metrics .NamespaceCountView ,
631+ metrics .ClusterScopedResourceCountView ,
632+ metrics .CRDCountView ,
633+ metrics .KCCResourceCountView ,
634+ metrics .PipelineErrorView ,
635+ )
636+
637+ var channelKpt chan event.GenericEvent
638+
639+ // Configure controller-manager to log to the test logger
640+ testLogger := testcontroller .NewTestLogger (t )
641+ controllerruntime .SetLogger (testLogger )
642+
643+ // Setup the Manager with metrics enabled for testing
644+ mgr , err := manager .New (cfg , manager.Options {
645+ // Enable metrics for this test
646+ Metrics : metricsserver.Options {BindAddress : "127.0.0.1:0" },
647+ Logger : testLogger .WithName ("controller-manager" ),
648+ // Use a client.WithWatch, instead of just a client.Client
649+ NewClient : func (cfg * rest.Config , opts client.Options ) (client.Client , error ) {
650+ return client .NewWithWatch (cfg , opts )
651+ },
652+ // Skip name validation to allow multiple controllers with the same name
653+ Controller : config.Controller {
654+ SkipNameValidation : func () * bool { b := true ; return & b }(),
655+ },
656+ })
657+ require .NoError (t , err )
658+ // Get the watch client built by the manager
659+ c := mgr .GetClient ().(client.WithWatch )
660+
661+ ctx := t .Context ()
662+
663+ // Setup the controllers
664+ logger := testLogger .WithName ("controllers-metrics-empty-add" )
665+ channelKpt = make (chan event.GenericEvent )
666+ resolver , err := typeresolver .ForManager (mgr , logger .WithName ("typeresolver-metrics-empty-add" ))
667+ require .NoError (t , err )
668+ resMap := resourcemap .NewResourceMap ()
669+ err = NewRGController (mgr , channelKpt , logger .WithName ("resourcegroup-metrics-empty-add" ), resolver , resMap , 0 )
670+ require .NoError (t , err )
671+
672+ // Start the manager
673+ stopTestManager := testcontroller .StartTestManager (t , mgr )
674+ // Block test cleanup until manager is fully stopped
675+ defer stopTestManager ()
676+
677+ resources := []v1alpha1.ObjMetadata {}
678+
679+ // Create a ResourceGroup object which does not include any resources
680+ rgKey := client.ObjectKey {
681+ Name : "test-rg-metrics-empty-add" ,
682+ Namespace : rgNamespace ,
683+ }
684+ resgroupKpt := & v1alpha1.ResourceGroup {
685+ ObjectMeta : metav1.ObjectMeta {
686+ Name : "test-rg-metrics-empty-add" ,
687+ Namespace : rgNamespace ,
688+ Labels : map [string ]string {
689+ common .InventoryLabel : "test-inventory-empty-add" ,
690+ },
691+ },
692+ Spec : v1alpha1.ResourceGroupSpec {
693+ Resources : resources ,
694+ },
695+ }
696+ expectedStatus := v1alpha1.ResourceGroupStatus {
697+ ObservedGeneration : 0 ,
698+ }
699+
700+ // Create the ResourceGroup spec (simulating InventoryResourceGroup.Apply)
701+ err = c .Create (ctx , resgroupKpt , client .FieldOwner (fake .FieldManager ))
702+ require .NoError (t , err )
703+ resgroupKpt = waitForResourceGroupStatus (t , ctx , c , rgKey , 1 , 0 , expectedStatus )
704+
705+ // Update the ResourceGroup status (simulating InventoryResourceGroup.Apply)
706+ resgroupKpt .Status .ObservedGeneration = resgroupKpt .Generation
707+ err = c .Status ().Update (ctx , resgroupKpt , client .FieldOwner (fake .FieldManager ))
708+ require .NoError (t , err )
709+ expectedStatus .ObservedGeneration = 1
710+ resgroupKpt = waitForResourceGroupStatus (t , ctx , c , rgKey , 1 , 0 , expectedStatus )
711+
712+ // Push an event to the channel, which will cause trigger a reconciliation for resgroup
713+ t .Log ("Sending event to controller for empty ResourceGroup" )
714+ channelKpt <- event.GenericEvent {Object : resgroupKpt }
715+
716+ // Verify that the reconciliation modifies the ResourceGroupStatus field correctly
717+ expectedStatus .ObservedGeneration = 1
718+ expectedStatus .Conditions = []v1alpha1.Condition {
719+ newReconcilingCondition (v1alpha1 .FalseConditionStatus , FinishReconciling , finishReconcilingMsg ),
720+ newStalledCondition (v1alpha1 .FalseConditionStatus , FinishReconciling , finishReconcilingMsg ),
721+ }
722+ resgroupKpt = waitForResourceGroupStatus (t , ctx , c , rgKey , 1 , 0 , expectedStatus )
723+
724+ // Verify metrics for empty ResourceGroup - all should be 0
725+ expectedEmptyMetrics := map [* view.View ][]* view.Row {
726+ metrics .ResourceCountView : {
727+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
728+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
729+ }},
730+ },
731+ metrics .ReadyResourceCountView : {
732+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
733+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
734+ }},
735+ },
736+ metrics .NamespaceCountView : {
737+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
738+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
739+ }},
740+ },
741+ metrics .ClusterScopedResourceCountView : {
742+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
743+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
744+ }},
745+ },
746+ metrics .CRDCountView : {
747+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
748+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
749+ }},
750+ },
751+ metrics .KCCResourceCountView : {
752+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
753+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
754+ }},
755+ },
756+ metrics .PipelineErrorView : {
757+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
758+ {Key : metrics .KeyComponent , Value : "readiness" },
759+ {Key : metrics .KeyName , Value : func () string {
760+ reconcilerName , _ := metrics .ComputeReconcilerNameType (types.NamespacedName {Name : "test-rg-metrics-empty-add" , Namespace : "default" })
761+ return reconcilerName
762+ }()},
763+ {Key : metrics .KeyType , Value : "repo-sync" },
764+ }},
765+ },
766+ }
767+
768+ // Validate empty metrics
769+ for view , rows := range expectedEmptyMetrics {
770+ if diff := exporter .ValidateMetrics (view , rows ); diff != "" {
771+ t .Errorf ("Unexpected empty metrics recorded (%s): %v" , view .Name , diff )
772+ }
773+ }
774+
775+ // Reset the exporter to clear accumulated metrics before testing with resources
776+ exporter = testmetrics .RegisterMetrics (
777+ metrics .ResourceCountView ,
778+ metrics .ReadyResourceCountView ,
779+ metrics .NamespaceCountView ,
780+ metrics .ClusterScopedResourceCountView ,
781+ metrics .CRDCountView ,
782+ metrics .KCCResourceCountView ,
783+ metrics .PipelineErrorView ,
784+ )
785+
786+ // Now add test resources to the ResourceGroup (without creating the actual objects)
787+ // Use unique names to avoid conflicts with existing resources from other tests
788+ namespaceRes := v1alpha1.ObjMetadata {
789+ Name : "test-namespace-empty-add" ,
790+ Namespace : "" ,
791+ GroupKind : v1alpha1.GroupKind {
792+ Group : "" ,
793+ Kind : "Namespace" ,
794+ },
795+ }
796+ crdRes := v1alpha1.ObjMetadata {
797+ Name : "testresources-empty-add.test.example.com" ,
798+ Namespace : "" ,
799+ GroupKind : v1alpha1.GroupKind {
800+ Group : "apiextensions.k8s.io" ,
801+ Kind : "CustomResourceDefinition" ,
802+ },
803+ }
804+ pubsubRes := v1alpha1.ObjMetadata {
805+ Name : "test-pubsub-topic-empty-add" ,
806+ Namespace : "default" ,
807+ GroupKind : v1alpha1.GroupKind {
808+ Group : "pubsub.cnrm.cloud.google.com" ,
809+ Kind : "PubSubTopic" ,
810+ },
811+ }
812+ podRes := v1alpha1.ObjMetadata {
813+ Name : "test-pod-empty-add" ,
814+ Namespace : "default" ,
815+ GroupKind : v1alpha1.GroupKind {
816+ Group : "" ,
817+ Kind : "Pod" ,
818+ },
819+ }
820+ resources = []v1alpha1.ObjMetadata {namespaceRes , crdRes , pubsubRes , podRes }
821+ resgroupKpt .Spec = v1alpha1.ResourceGroupSpec {
822+ Resources : resources ,
823+ }
824+
825+ // Update the ResourceGroup spec (simulating InventoryResourceGroup.Apply)
826+ err = c .Update (ctx , resgroupKpt , client .FieldOwner (fake .FieldManager ))
827+ require .NoError (t , err )
828+ resgroupKpt = waitForResourceGroupStatus (t , ctx , c , rgKey , 2 , 4 , expectedStatus )
829+
830+ // Update the ResourceGroup status (simulating InventoryResourceGroup.Apply)
831+ resgroupKpt .Status .ObservedGeneration = resgroupKpt .Generation
832+ err = c .Status ().Update (ctx , resgroupKpt , client .FieldOwner (fake .FieldManager ))
833+ require .NoError (t , err )
834+ expectedStatus .ObservedGeneration = 2
835+ resgroupKpt = waitForResourceGroupStatus (t , ctx , c , rgKey , 2 , 4 , expectedStatus )
836+
837+ t .Log ("Sending event to controller for ResourceGroup with resources" )
838+ channelKpt <- event.GenericEvent {Object : resgroupKpt }
839+
840+ // Verify that the reconciliation modifies the ResourceGroupStatus field correctly
841+ expectedStatus .ResourceStatuses = []v1alpha1.ResourceStatus {
842+ {
843+ ObjMetadata : namespaceRes ,
844+ Status : v1alpha1 .NotFound ,
845+ },
846+ {
847+ ObjMetadata : crdRes ,
848+ Status : v1alpha1 .NotFound ,
849+ },
850+ {
851+ ObjMetadata : pubsubRes ,
852+ Status : v1alpha1 .NotFound ,
853+ },
854+ {
855+ ObjMetadata : podRes ,
856+ Status : v1alpha1 .NotFound ,
857+ },
858+ }
859+ expectedStatus .ObservedGeneration = 2
860+ expectedStatus .Conditions = []v1alpha1.Condition {
861+ newReconcilingCondition (v1alpha1 .FalseConditionStatus , FinishReconciling , finishReconcilingMsg ),
862+ newStalledCondition (v1alpha1 .FalseConditionStatus , FinishReconciling , finishReconcilingMsg ),
863+ }
864+ _ = waitForResourceGroupStatus (t , ctx , c , rgKey , 2 , 4 , expectedStatus )
865+
866+ // Verify metrics for ResourceGroup with resources (all NotFound, so 0 ready)
867+ expectedResourcesMetrics := map [* view.View ][]* view.Row {
868+ metrics .ResourceCountView : {
869+ {Data : & view.LastValueData {Value : 4 }, Tags : []tag.Tag {
870+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
871+ }},
872+ },
873+ metrics .ReadyResourceCountView : {
874+ {Data : & view.LastValueData {Value : 0 }, Tags : []tag.Tag {
875+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
876+ }},
877+ },
878+ metrics .NamespaceCountView : {
879+ {Data : & view.LastValueData {Value : 2 }, Tags : []tag.Tag {
880+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
881+ }},
882+ },
883+ metrics .ClusterScopedResourceCountView : {
884+ {Data : & view.LastValueData {Value : 2 }, Tags : []tag.Tag {
885+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
886+ }},
887+ },
888+ metrics .CRDCountView : {
889+ {Data : & view.LastValueData {Value : 1 }, Tags : []tag.Tag {
890+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
891+ }},
892+ },
893+ metrics .KCCResourceCountView : {
894+ {Data : & view.LastValueData {Value : 1 }, Tags : []tag.Tag {
895+ {Key : metrics .KeyResourceGroup , Value : "default/test-rg-metrics-empty-add" },
896+ }},
897+ },
898+ metrics .PipelineErrorView : {
899+ {Data : & view.LastValueData {Value : 1 }, Tags : []tag.Tag {
900+ {Key : metrics .KeyComponent , Value : "readiness" },
901+ {Key : metrics .KeyName , Value : func () string {
902+ reconcilerName , _ := metrics .ComputeReconcilerNameType (types.NamespacedName {Name : "test-rg-metrics-empty-add" , Namespace : "default" })
903+ return reconcilerName
904+ }()},
905+ {Key : metrics .KeyType , Value : "repo-sync" },
906+ }},
907+ },
908+ }
909+
910+ // Validate metrics with resources
911+ for view , rows := range expectedResourcesMetrics {
912+ if diff := exporter .ValidateMetrics (view , rows ); diff != "" {
913+ t .Errorf ("Unexpected resources metrics recorded (%s): %v" , view .Name , diff )
914+ }
915+ }
916+ }
917+
613918//nolint:revive // testing.T before context.Context
614919func waitForResourceGroupStatus (t * testing.T , ctx context.Context , c client.WithWatch , key client.ObjectKey , expectedGeneration , expectedResourceCount int , expectedStatus v1alpha1.ResourceGroupStatus ) * v1alpha1.ResourceGroup {
615920 watcher , err := testwatch .WatchObject (ctx , c , & v1alpha1.ResourceGroupList {})
0 commit comments