@@ -31,6 +31,14 @@ type RelationshipRef struct {
31
31
Namespace string `json:"namespace,omitempty"`
32
32
}
33
33
34
+ // EnhancedRef represents our custom relationship reference structure
35
+ type EnhancedRef struct {
36
+ Kind string `json:"kind,omitempty"`
37
+ GroupVersion string `json:"groupVersion,omitempty"`
38
+ Name string `json:"name"`
39
+ Namespace string `json:"namespace,omitempty"`
40
+ }
41
+
34
42
// NewRelationshipResolver creates a new relationship resolver
35
43
func NewRelationshipResolver (log * logger.Logger , runtimeClient client.WithWatch , groupNames map [string ]string ) * RelationshipResolver {
36
44
return & RelationshipResolver {
@@ -93,7 +101,7 @@ func (r *RelationshipResolver) CreateRelationshipResolver(sourceGVK schema.Group
93
101
}
94
102
95
103
// Parse the relationship reference
96
- ref , err := r .parseRelationshipRef (refMap )
104
+ ref , err := r .parseRelationshipRef (refMap , sourceObj , sourceGVK , fieldName )
97
105
if err != nil {
98
106
return nil , fmt .Errorf ("error parsing relationship ref: %v" , err )
99
107
}
@@ -110,12 +118,63 @@ func (r *RelationshipResolver) CreateRelationshipResolver(sourceGVK schema.Group
110
118
}
111
119
112
120
// Fetch the referenced resource
113
- return r .fetchReferencedResource (ctx , ref , targetGVK )
121
+ return r .fetchReferencedResource (ctx , ref , targetGVK , sourceObj , sourceGVK )
122
+ }
123
+ }
124
+
125
+ // CreateRelationsResolver creates a GraphQL field resolver for the relations field
126
+ func (r * RelationshipResolver ) CreateRelationsResolver (sourceGVK schema.GroupVersionKind , relationshipFields []string ) graphql.FieldResolveFn {
127
+ return func (p graphql.ResolveParams ) (interface {}, error ) {
128
+ _ , span := otel .Tracer ("" ).Start (p .Context , "ResolveRelations" ,
129
+ trace .WithAttributes (
130
+ attribute .String ("sourceKind" , sourceGVK .Kind ),
131
+ attribute .Int ("relationshipCount" , len (relationshipFields )),
132
+ ))
133
+ defer span .End ()
134
+
135
+ // Get the source object from the parent resolver
136
+ sourceObj , ok := p .Source .(map [string ]interface {})
137
+ if ! ok {
138
+ return nil , fmt .Errorf ("expected source to be map[string]interface{}, got %T" , p .Source )
139
+ }
140
+
141
+ relations := make (map [string ]interface {})
142
+
143
+ for _ , fieldName := range relationshipFields {
144
+ // Extract the relationship reference from the source object
145
+ refValue , found , err := unstructured .NestedFieldNoCopy (sourceObj , fieldName )
146
+ if err != nil {
147
+ r .log .Debug ().Err (err ).Str ("field" , fieldName ).Msg ("Error accessing relationship field" )
148
+ continue
149
+ }
150
+ if ! found {
151
+ continue // Field not present, skip
152
+ }
153
+
154
+ refMap , ok := refValue .(map [string ]interface {})
155
+ if ! ok {
156
+ r .log .Debug ().Str ("field" , fieldName ).Msg ("Relationship field is not a map" )
157
+ continue
158
+ }
159
+
160
+ // Create enhanced reference
161
+ enhancedRef , err := r .createEnhancedRef (refMap , sourceObj , sourceGVK , fieldName )
162
+ if err != nil {
163
+ r .log .Debug ().Err (err ).Str ("field" , fieldName ).Msg ("Error creating enhanced reference" )
164
+ continue
165
+ }
166
+
167
+ // Add to relations without "Ref" suffix
168
+ relationName := strings .TrimSuffix (fieldName , "Ref" )
169
+ relations [relationName ] = enhancedRef
170
+ }
171
+
172
+ return relations , nil
114
173
}
115
174
}
116
175
117
176
// parseRelationshipRef parses a relationship reference from a map
118
- func (r * RelationshipResolver ) parseRelationshipRef (refMap map [string ]interface {}) (* RelationshipRef , error ) {
177
+ func (r * RelationshipResolver ) parseRelationshipRef (refMap map [string ]interface {}, sourceObj map [ string ] interface {}, sourceGVK schema. GroupVersionKind , fieldName string ) (* RelationshipRef , error ) {
119
178
ref := & RelationshipRef {}
120
179
121
180
if kind , ok := refMap ["kind" ].(string ); ok {
@@ -126,17 +185,17 @@ func (r *RelationshipResolver) parseRelationshipRef(refMap map[string]interface{
126
185
ref .GroupVersion = groupVersion
127
186
}
128
187
188
+ // Handle native Kubernetes apiGroup field (used in roleRef, clusterRoleRef, etc.)
129
189
if apiGroup , ok := refMap ["apiGroup" ].(string ); ok {
130
- // Handle RBAC-style references where apiGroup is specified separately
131
- if apiGroup != "" {
132
- // For RBAC, we need to construct the groupVersion
133
- // Default to v1 if no version specified in the groupVersion field
134
- if ref .GroupVersion == "" {
135
- ref .GroupVersion = apiGroup + "/v1"
136
- }
137
- } else {
190
+ // For RBAC resources, apiGroup + kind determines the groupVersion
191
+ if apiGroup == "rbac.authorization.k8s.io" {
192
+ ref .GroupVersion = "rbac.authorization.k8s.io/v1"
193
+ } else if apiGroup == "" {
138
194
// Empty apiGroup means core/v1
139
195
ref .GroupVersion = "v1"
196
+ } else {
197
+ // Default to v1 for other groups
198
+ ref .GroupVersion = apiGroup + "/v1"
140
199
}
141
200
}
142
201
@@ -148,11 +207,101 @@ func (r *RelationshipResolver) parseRelationshipRef(refMap map[string]interface{
148
207
149
208
if namespace , ok := refMap ["namespace" ].(string ); ok {
150
209
ref .Namespace = namespace
210
+ } else {
211
+ // For native Kubernetes references like roleRef, infer namespace from source
212
+ ref .Namespace = r .inferNamespaceForReference (sourceObj , sourceGVK , fieldName , ref .Kind )
151
213
}
152
214
153
215
return ref , nil
154
216
}
155
217
218
+ // createEnhancedRef transforms a native Kubernetes reference to our enhanced structure
219
+ func (r * RelationshipResolver ) createEnhancedRef (nativeRefMap map [string ]interface {}, sourceObj map [string ]interface {}, sourceGVK schema.GroupVersionKind , fieldName string ) (map [string ]interface {}, error ) {
220
+ enhancedRef := make (map [string ]interface {})
221
+
222
+ // Extract name (required)
223
+ name , ok := nativeRefMap ["name" ].(string )
224
+ if ! ok {
225
+ return nil , fmt .Errorf ("name is required in relationship reference" )
226
+ }
227
+ enhancedRef ["name" ] = name
228
+
229
+ // Extract or infer kind
230
+ if kind , ok := nativeRefMap ["kind" ].(string ); ok {
231
+ enhancedRef ["kind" ] = kind
232
+ } else {
233
+ // Infer kind from field name
234
+ targetKind := ExtractTargetKind (fieldName )
235
+ if targetKind != "" {
236
+ enhancedRef ["kind" ] = targetKind
237
+ }
238
+ }
239
+
240
+ // Extract or construct groupVersion
241
+ if groupVersion , ok := nativeRefMap ["groupVersion" ].(string ); ok {
242
+ enhancedRef ["groupVersion" ] = groupVersion
243
+ } else if apiGroup , ok := nativeRefMap ["apiGroup" ].(string ); ok {
244
+ // Construct groupVersion from apiGroup
245
+ if apiGroup == "rbac.authorization.k8s.io" {
246
+ enhancedRef ["groupVersion" ] = "rbac.authorization.k8s.io/v1"
247
+ } else if apiGroup == "" {
248
+ enhancedRef ["groupVersion" ] = "v1"
249
+ } else {
250
+ enhancedRef ["groupVersion" ] = apiGroup + "/v1"
251
+ }
252
+ }
253
+
254
+ // Extract or infer namespace
255
+ if namespace , ok := nativeRefMap ["namespace" ].(string ); ok {
256
+ enhancedRef ["namespace" ] = namespace
257
+ } else {
258
+ // Infer namespace from source object and relationship context
259
+ targetKind , _ := enhancedRef ["kind" ].(string )
260
+ inferredNamespace := r .inferNamespaceForReference (sourceObj , sourceGVK , fieldName , targetKind )
261
+ if inferredNamespace != "" {
262
+ enhancedRef ["namespace" ] = inferredNamespace
263
+ }
264
+ }
265
+
266
+ r .log .Debug ().
267
+ Str ("fieldName" , fieldName ).
268
+ Str ("name" , name ).
269
+ Interface ("enhancedRef" , enhancedRef ).
270
+ Msg ("Created enhanced reference" )
271
+
272
+ return enhancedRef , nil
273
+ }
274
+
275
+ // inferNamespaceForReference infers the namespace for a reference based on Kubernetes conventions
276
+ func (r * RelationshipResolver ) inferNamespaceForReference (sourceObj map [string ]interface {}, sourceGVK schema.GroupVersionKind , fieldName string , targetKind string ) string {
277
+ // For roleRef in RoleBinding, the referenced Role is in the same namespace as the RoleBinding
278
+ if fieldName == "roleRef" && sourceGVK .Kind == "RoleBinding" && targetKind == "Role" {
279
+ if metadata , found := sourceObj ["metadata" ]; found {
280
+ if metadataMap , ok := metadata .(map [string ]interface {}); ok {
281
+ if namespace , ok := metadataMap ["namespace" ].(string ); ok {
282
+ return namespace
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ // For clusterRoleRef or when targeting ClusterRole, no namespace needed (cluster-scoped)
289
+ if fieldName == "roleRef" && targetKind == "ClusterRole" {
290
+ return ""
291
+ }
292
+
293
+ // Default: try to use source object's namespace for namespaced targets
294
+ if metadata , found := sourceObj ["metadata" ]; found {
295
+ if metadataMap , ok := metadata .(map [string ]interface {}); ok {
296
+ if namespace , ok := metadataMap ["namespace" ].(string ); ok {
297
+ return namespace
298
+ }
299
+ }
300
+ }
301
+
302
+ return ""
303
+ }
304
+
156
305
// resolveTargetGVK resolves the target GroupVersionKind from the relationship reference
157
306
func (r * RelationshipResolver ) resolveTargetGVK (ref * RelationshipRef , sourceGVK schema.GroupVersionKind ) (schema.GroupVersionKind , error ) {
158
307
targetGVK := schema.GroupVersionKind {
@@ -187,7 +336,7 @@ func (r *RelationshipResolver) resolveTargetGVK(ref *RelationshipRef, sourceGVK
187
336
}
188
337
189
338
// fetchReferencedResource fetches the referenced Kubernetes resource
190
- func (r * RelationshipResolver ) fetchReferencedResource (ctx context.Context , ref * RelationshipRef , targetGVK schema.GroupVersionKind ) (interface {}, error ) {
339
+ func (r * RelationshipResolver ) fetchReferencedResource (ctx context.Context , ref * RelationshipRef , targetGVK schema.GroupVersionKind , sourceObj map [ string ] interface {}, sourceGVK schema. GroupVersionKind ) (interface {}, error ) {
191
340
// Create an unstructured object to hold the result
192
341
obj := & unstructured.Unstructured {}
193
342
obj .SetGroupVersionKind (targetGVK )
@@ -196,11 +345,18 @@ func (r *RelationshipResolver) fetchReferencedResource(ctx context.Context, ref
196
345
Name : ref .Name ,
197
346
}
198
347
199
- // If namespace is specified in the ref, use it
348
+ // Set namespace if specified and target is namespaced
200
349
if ref .Namespace != "" {
201
350
key .Namespace = ref .Namespace
202
351
}
203
352
353
+ r .log .Debug ().
354
+ Str ("refName" , ref .Name ).
355
+ Str ("refNamespace" , ref .Namespace ).
356
+ Str ("targetGVK" , targetGVK .String ()).
357
+ Str ("sourceKind" , sourceGVK .Kind ).
358
+ Msg ("Fetching referenced resource" )
359
+
204
360
// Get the object using the runtime client
205
361
if err := r .runtimeClient .Get (ctx , key , obj ); err != nil {
206
362
r .log .Error ().Err (err ).
0 commit comments