Skip to content

Commit 0bf4da3

Browse files
committed
rolebinding -> role
On-behalf-of: @SAP [email protected] Signed-off-by: Artem Shcherbatiuk <[email protected]>
1 parent 1f8a6ca commit 0bf4da3

File tree

4 files changed

+406
-3
lines changed

4 files changed

+406
-3
lines changed

gateway/resolver/relationships.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package resolver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/graphql-go/graphql"
9+
"go.opentelemetry.io/otel"
10+
"go.opentelemetry.io/otel/attribute"
11+
"go.opentelemetry.io/otel/trace"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
"k8s.io/apimachinery/pkg/runtime/schema"
14+
"sigs.k8s.io/controller-runtime/pkg/client"
15+
16+
"github.com/openmfp/golang-commons/logger"
17+
)
18+
19+
// RelationshipResolver handles resolution of relationships between Kubernetes resources
20+
type RelationshipResolver struct {
21+
log *logger.Logger
22+
runtimeClient client.WithWatch
23+
groupNames map[string]string
24+
}
25+
26+
// RelationshipRef represents a reference to another Kubernetes resource
27+
type RelationshipRef struct {
28+
Kind string `json:"kind,omitempty"`
29+
GroupVersion string `json:"groupVersion,omitempty"`
30+
Name string `json:"name"`
31+
Namespace string `json:"namespace,omitempty"`
32+
}
33+
34+
// NewRelationshipResolver creates a new relationship resolver
35+
func NewRelationshipResolver(log *logger.Logger, runtimeClient client.WithWatch, groupNames map[string]string) *RelationshipResolver {
36+
return &RelationshipResolver{
37+
log: log,
38+
runtimeClient: runtimeClient,
39+
groupNames: groupNames,
40+
}
41+
}
42+
43+
// IsRelationshipField checks if a field name follows the relationship convention (<kind>Ref)
44+
func IsRelationshipField(fieldName string) bool {
45+
return strings.HasSuffix(fieldName, "Ref")
46+
}
47+
48+
// ExtractTargetKind extracts the target kind from a relationship field name
49+
// e.g., "roleRef" -> "Role"
50+
func ExtractTargetKind(fieldName string) string {
51+
if !IsRelationshipField(fieldName) {
52+
return ""
53+
}
54+
55+
// Remove "Ref" suffix and capitalize first letter
56+
kindName := strings.TrimSuffix(fieldName, "Ref")
57+
if len(kindName) == 0 {
58+
return ""
59+
}
60+
61+
return strings.ToUpper(kindName[:1]) + kindName[1:]
62+
}
63+
64+
// CreateRelationshipResolver creates a GraphQL field resolver for relationship fields
65+
func (r *RelationshipResolver) CreateRelationshipResolver(sourceGVK schema.GroupVersionKind, fieldName string, targetKind string) graphql.FieldResolveFn {
66+
return func(p graphql.ResolveParams) (interface{}, error) {
67+
ctx, span := otel.Tracer("").Start(p.Context, "ResolveRelationship",
68+
trace.WithAttributes(
69+
attribute.String("sourceKind", sourceGVK.Kind),
70+
attribute.String("fieldName", fieldName),
71+
attribute.String("targetKind", targetKind),
72+
))
73+
defer span.End()
74+
75+
// Get the source object from the parent resolver
76+
sourceObj, ok := p.Source.(map[string]interface{})
77+
if !ok {
78+
return nil, fmt.Errorf("expected source to be map[string]interface{}, got %T", p.Source)
79+
}
80+
81+
// Extract the relationship reference from the source object
82+
refValue, found, err := unstructured.NestedFieldNoCopy(sourceObj, fieldName)
83+
if err != nil {
84+
return nil, fmt.Errorf("error accessing field %s: %v", fieldName, err)
85+
}
86+
if !found {
87+
return nil, nil // Field not present
88+
}
89+
90+
refMap, ok := refValue.(map[string]interface{})
91+
if !ok {
92+
return nil, fmt.Errorf("expected %s to be map[string]interface{}, got %T", fieldName, refValue)
93+
}
94+
95+
// Parse the relationship reference
96+
ref, err := r.parseRelationshipRef(refMap)
97+
if err != nil {
98+
return nil, fmt.Errorf("error parsing relationship ref: %v", err)
99+
}
100+
101+
// If no kind specified in ref, use the target kind from field name
102+
if ref.Kind == "" {
103+
ref.Kind = targetKind
104+
}
105+
106+
// Resolve the target GVK
107+
targetGVK, err := r.resolveTargetGVK(ref, sourceGVK)
108+
if err != nil {
109+
return nil, fmt.Errorf("error resolving target GVK: %v", err)
110+
}
111+
112+
// Fetch the referenced resource
113+
return r.fetchReferencedResource(ctx, ref, targetGVK)
114+
}
115+
}
116+
117+
// parseRelationshipRef parses a relationship reference from a map
118+
func (r *RelationshipResolver) parseRelationshipRef(refMap map[string]interface{}) (*RelationshipRef, error) {
119+
ref := &RelationshipRef{}
120+
121+
if kind, ok := refMap["kind"].(string); ok {
122+
ref.Kind = kind
123+
}
124+
125+
if groupVersion, ok := refMap["groupVersion"].(string); ok {
126+
ref.GroupVersion = groupVersion
127+
}
128+
129+
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 {
138+
// Empty apiGroup means core/v1
139+
ref.GroupVersion = "v1"
140+
}
141+
}
142+
143+
name, ok := refMap["name"].(string)
144+
if !ok {
145+
return nil, fmt.Errorf("name is required in relationship reference")
146+
}
147+
ref.Name = name
148+
149+
if namespace, ok := refMap["namespace"].(string); ok {
150+
ref.Namespace = namespace
151+
}
152+
153+
return ref, nil
154+
}
155+
156+
// resolveTargetGVK resolves the target GroupVersionKind from the relationship reference
157+
func (r *RelationshipResolver) resolveTargetGVK(ref *RelationshipRef, sourceGVK schema.GroupVersionKind) (schema.GroupVersionKind, error) {
158+
targetGVK := schema.GroupVersionKind{
159+
Kind: ref.Kind,
160+
}
161+
162+
if ref.GroupVersion != "" {
163+
// Parse group and version from groupVersion
164+
parts := strings.Split(ref.GroupVersion, "/")
165+
if len(parts) == 2 {
166+
targetGVK.Group = parts[0]
167+
targetGVK.Version = parts[1]
168+
} else if len(parts) == 1 {
169+
// Could be just version (for core resources) or just group
170+
if ref.GroupVersion == "v1" || strings.HasPrefix(ref.GroupVersion, "v") {
171+
targetGVK.Group = ""
172+
targetGVK.Version = ref.GroupVersion
173+
} else {
174+
targetGVK.Group = ref.GroupVersion
175+
targetGVK.Version = "v1" // Default version
176+
}
177+
} else {
178+
return targetGVK, fmt.Errorf("invalid groupVersion format: %s", ref.GroupVersion)
179+
}
180+
} else {
181+
// If no groupVersion specified, inherit from source or use defaults
182+
targetGVK.Group = sourceGVK.Group
183+
targetGVK.Version = sourceGVK.Version
184+
}
185+
186+
return targetGVK, nil
187+
}
188+
189+
// fetchReferencedResource fetches the referenced Kubernetes resource
190+
func (r *RelationshipResolver) fetchReferencedResource(ctx context.Context, ref *RelationshipRef, targetGVK schema.GroupVersionKind) (interface{}, error) {
191+
// Create an unstructured object to hold the result
192+
obj := &unstructured.Unstructured{}
193+
obj.SetGroupVersionKind(targetGVK)
194+
195+
key := client.ObjectKey{
196+
Name: ref.Name,
197+
}
198+
199+
// If namespace is specified in the ref, use it
200+
if ref.Namespace != "" {
201+
key.Namespace = ref.Namespace
202+
}
203+
204+
// Get the object using the runtime client
205+
if err := r.runtimeClient.Get(ctx, key, obj); err != nil {
206+
r.log.Error().Err(err).
207+
Str("name", ref.Name).
208+
Str("namespace", ref.Namespace).
209+
Str("gvk", targetGVK.String()).
210+
Msg("Unable to get referenced object")
211+
return nil, err
212+
}
213+
214+
return obj.Object, nil
215+
}
216+
217+
// getOriginalGroupName converts sanitized group name back to original
218+
func (r *RelationshipResolver) getOriginalGroupName(groupName string) string {
219+
if originalName, ok := r.groupNames[groupName]; ok {
220+
return originalName
221+
}
222+
return groupName
223+
}

gateway/resolver/resolver.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Provider interface {
3030
CustomQueriesProvider
3131
CommonResolver() graphql.FieldResolveFn
3232
SanitizeGroupName(string) string
33+
GetRelationshipResolver() *RelationshipResolver
3334
}
3435

3536
type CrudProvider interface {
@@ -50,16 +51,22 @@ type CustomQueriesProvider interface {
5051
type Service struct {
5152
log *logger.Logger
5253
// groupNames stores relation between sanitized group names and original group names that are used in the Kubernetes API
53-
groupNames map[string]string // map[sanitizedGroupName]originalGroupName
54-
runtimeClient client.WithWatch
54+
groupNames map[string]string // map[sanitizedGroupName]originalGroupName
55+
runtimeClient client.WithWatch
56+
relationshipResolver *RelationshipResolver
5557
}
5658

5759
func New(log *logger.Logger, runtimeClient client.WithWatch) *Service {
58-
return &Service{
60+
service := &Service{
5961
log: log,
6062
groupNames: make(map[string]string),
6163
runtimeClient: runtimeClient,
6264
}
65+
66+
// Initialize relationship resolver
67+
service.relationshipResolver = NewRelationshipResolver(log, runtimeClient, service.groupNames)
68+
69+
return service
6370
}
6471

6572
// ListItems returns a GraphQL CommonResolver function that lists Kubernetes resources of the given GroupVersionKind.
@@ -397,6 +404,10 @@ func (r *Service) getOriginalGroupName(groupName string) string {
397404
return groupName
398405
}
399406

407+
func (r *Service) GetRelationshipResolver() *RelationshipResolver {
408+
return r.relationshipResolver
409+
}
410+
400411
func compareUnstructured(a, b unstructured.Unstructured, fieldPath string) int {
401412
segments := strings.Split(fieldPath, ".")
402413

gateway/resolver/subscription.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ func (r *Service) runWatch(
7979

8080
fieldsToWatch := extractRequestedFields(p.Info)
8181

82+
// Extract relationship watches (for future implementation)
83+
relationshipWatches := r.extractRelationshipWatches(fieldsToWatch, gvk)
84+
if len(relationshipWatches) > 0 {
85+
r.log.Debug().
86+
Int("relationshipCount", len(relationshipWatches)).
87+
Str("sourceGVK", gvk.String()).
88+
Msg("Detected relationships in subscription - multiple watches not yet implemented")
89+
// TODO: Implement multiple watches for relationships
90+
}
91+
8292
list := &unstructured.UnstructuredList{}
8393
list.SetGroupVersionKind(schema.GroupVersionKind{
8494
Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind + "List",
@@ -356,3 +366,75 @@ func CreateSubscriptionResolver(isSingle bool) graphql.FieldResolveFn {
356366
return source, nil
357367
}
358368
}
369+
370+
// RelationshipWatch represents a relationship that needs to be watched
371+
type RelationshipWatch struct {
372+
FieldPath string
373+
TargetGVK schema.GroupVersionKind
374+
TargetScope v1.ResourceScope
375+
}
376+
377+
// extractRelationshipWatches analyzes requested fields to identify relationships that need additional watches
378+
func (r *Service) extractRelationshipWatches(fieldsToWatch []string, sourceGVK schema.GroupVersionKind) []RelationshipWatch {
379+
var relationshipWatches []RelationshipWatch
380+
381+
for _, fieldPath := range fieldsToWatch {
382+
// Check if this field path contains a relationship field
383+
pathParts := strings.Split(fieldPath, ".")
384+
for i, part := range pathParts {
385+
if IsRelationshipField(part) {
386+
targetKind := ExtractTargetKind(part)
387+
if targetKind != "" {
388+
// Try to resolve the target GVK
389+
// For now, we'll make some assumptions about common relationships
390+
targetGVK := r.resolveTargetGVK(targetKind, sourceGVK)
391+
if targetGVK != (schema.GroupVersionKind{}) {
392+
// TODO: Determine target scope - for now assume same as source
393+
relationshipWatches = append(relationshipWatches, RelationshipWatch{
394+
FieldPath: strings.Join(pathParts[:i+1], "."),
395+
TargetGVK: targetGVK,
396+
TargetScope: v1.NamespaceScoped, // Default assumption
397+
})
398+
}
399+
}
400+
}
401+
}
402+
}
403+
404+
return relationshipWatches
405+
}
406+
407+
// resolveTargetGVK attempts to resolve the target GVK for a relationship
408+
func (r *Service) resolveTargetGVK(targetKind string, sourceGVK schema.GroupVersionKind) schema.GroupVersionKind {
409+
// Handle common RBAC relationships
410+
switch targetKind {
411+
case "Role":
412+
return schema.GroupVersionKind{
413+
Group: "rbac.authorization.k8s.io",
414+
Version: "v1",
415+
Kind: "Role",
416+
}
417+
case "ClusterRole":
418+
return schema.GroupVersionKind{
419+
Group: "rbac.authorization.k8s.io",
420+
Version: "v1",
421+
Kind: "ClusterRole",
422+
}
423+
case "ServiceAccount":
424+
return schema.GroupVersionKind{
425+
Group: "",
426+
Version: "v1",
427+
Kind: "ServiceAccount",
428+
}
429+
case "User", "Group":
430+
// These are not Kubernetes resources, so we can't watch them
431+
return schema.GroupVersionKind{}
432+
default:
433+
// For other kinds, assume same group/version as source
434+
return schema.GroupVersionKind{
435+
Group: sourceGVK.Group,
436+
Version: sourceGVK.Version,
437+
Kind: targetKind,
438+
}
439+
}
440+
}

0 commit comments

Comments
 (0)