Skip to content

Commit 822e075

Browse files
Add pipeline error metrics to ResourceGroup controller tests (#1914)
* Add pipeline error metrics to ResourceGroup controller tests - Add PipelineErrorView to metric tests - Tests metrics validation for empty ResourceGroup transitioning to one with resources, verifying all metric types with proper tag values. * cleanup - simplify metric reset - use computed keyName as production would use
1 parent ac3478c commit 822e075

File tree

1 file changed

+305
-0
lines changed

1 file changed

+305
-0
lines changed

pkg/resourcegroup/controllers/resourcegroup/resourcegroup_controller_test.go

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
614919
func 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

Comments
 (0)