@@ -24,6 +24,7 @@ import (
2424 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2525 "k8s.io/apimachinery/pkg/runtime"
2626 "k8s.io/apimachinery/pkg/runtime/schema"
27+ "k8s.io/apimachinery/pkg/types"
2728 "k8s.io/apimachinery/pkg/watch"
2829 "k8s.io/client-go/dynamic/fake"
2930 "k8s.io/client-go/kubernetes/scheme"
@@ -1157,6 +1158,102 @@ func TestIterateHierachyV2(t *testing.T) {
11571158 })
11581159}
11591160
1161+ // TestIterateHierarchyV2_CrossNamespaceOwnerReference tests that cross-namespace
1162+ // owner references work correctly, specifically cluster-scoped resources with
1163+ // namespaced children
1164+ func TestIterateHierarchyV2_CrossNamespaceOwnerReference (t * testing.T ) {
1165+ cluster := newCluster (t )
1166+
1167+ // Create cluster-scoped parent resource (ProviderRevision)
1168+ parentUID := types .UID ("parent-uid-123" )
1169+ clusterScopedParent := & Resource {
1170+ Ref : corev1.ObjectReference {
1171+ APIVersion : "pkg.crossplane.io/v1" ,
1172+ Kind : "ProviderRevision" ,
1173+ Name : "provider-aws-cloudformation-3b2c213545b8" ,
1174+ UID : parentUID ,
1175+ // No namespace = cluster-scoped
1176+ },
1177+ OwnerRefs : []metav1.OwnerReference {},
1178+ }
1179+
1180+ // Create namespaced child with ownerReference to cluster-scoped parent
1181+ namespacedChild := & Resource {
1182+ Ref : corev1.ObjectReference {
1183+ APIVersion : "apps/v1" ,
1184+ Kind : "Deployment" ,
1185+ Name : "provider-aws-cloudformation-3b2c213545b8" ,
1186+ Namespace : "crossplane-system" ,
1187+ UID : types .UID ("child-uid-456" ),
1188+ },
1189+ OwnerRefs : []metav1.OwnerReference {{
1190+ APIVersion : "pkg.crossplane.io/v1" ,
1191+ Kind : "ProviderRevision" ,
1192+ Name : "provider-aws-cloudformation-3b2c213545b8" ,
1193+ // UID: parentUID, // Don't set UID - let it be resolved via cross-namespace lookup
1194+ }},
1195+ }
1196+
1197+ // Create cluster-scoped child with ownerReference to cluster-scoped parent (this should work already)
1198+ clusterScopedChild := & Resource {
1199+ Ref : corev1.ObjectReference {
1200+ APIVersion : "rbac.authorization.k8s.io/v1" ,
1201+ Kind : "ClusterRole" ,
1202+ Name : "crossplane:provider:provider-aws-cloudformation-3b2c213545b8:system" ,
1203+ UID : types .UID ("child-uid-789" ),
1204+ // No namespace = cluster-scoped
1205+ },
1206+ OwnerRefs : []metav1.OwnerReference {{
1207+ APIVersion : "pkg.crossplane.io/v1" ,
1208+ Kind : "ProviderRevision" ,
1209+ Name : "provider-aws-cloudformation-3b2c213545b8" ,
1210+ UID : parentUID ,
1211+ }},
1212+ }
1213+
1214+ // Add all resources to cluster cache
1215+ cluster .setNode (clusterScopedParent )
1216+ cluster .setNode (namespacedChild )
1217+ cluster .setNode (clusterScopedChild )
1218+
1219+ // Test hierarchy traversal starting from cluster-scoped parent
1220+ var visitedResources []* Resource
1221+ cluster .IterateHierarchyV2 (
1222+ []kube.ResourceKey {clusterScopedParent .ResourceKey ()},
1223+ func (resource * Resource , namespaceResources map [kube.ResourceKey ]* Resource ) bool {
1224+ visitedResources = append (visitedResources , resource )
1225+ return true
1226+ },
1227+ )
1228+
1229+ // Should visit parent + both children (3 resources)
1230+ assert .Equal (t , 3 , len (visitedResources ), "Should visit parent and both children" )
1231+
1232+ visitedNames := make ([]string , len (visitedResources ))
1233+ for i , res := range visitedResources {
1234+ visitedNames [i ] = res .Ref .Name
1235+ }
1236+
1237+ // Check we have the expected resources by type and namespace combination
1238+ foundParent := false
1239+ foundClusterChild := false
1240+ foundNamespacedChild := false
1241+
1242+ for _ , res := range visitedResources {
1243+ if res .Ref .Kind == "ProviderRevision" && res .Ref .Namespace == "" {
1244+ foundParent = true
1245+ } else if res .Ref .Kind == "ClusterRole" && res .Ref .Namespace == "" {
1246+ foundClusterChild = true
1247+ } else if res .Ref .Kind == "Deployment" && res .Ref .Namespace == "crossplane-system" {
1248+ foundNamespacedChild = true
1249+ }
1250+ }
1251+
1252+ assert .True (t , foundParent , "Should visit ProviderRevision parent" )
1253+ assert .True (t , foundClusterChild , "Should visit ClusterRole child" )
1254+ assert .True (t , foundNamespacedChild , "Should visit Deployment child (this tests the fix)" )
1255+ }
1256+
11601257// Test_watchEvents_Deadlock validates that starting watches will not create a deadlock
11611258// caused by using improper locking in various callback methods when there is a high load on the
11621259// system.
@@ -1273,7 +1370,7 @@ func BenchmarkBuildGraph(b *testing.B) {
12731370 testResources := buildTestResourceMap ()
12741371 b .ResetTimer ()
12751372 for n := 0 ; n < b .N ; n ++ {
1276- buildGraph (testResources )
1373+ buildGraph (testResources , nil )
12771374 }
12781375}
12791376
0 commit comments