Skip to content

Commit ad375ed

Browse files
committed
feat: relations
On-behalf-of: @SAP [email protected] Signed-off-by: Artem Shcherbatiuk <[email protected]>
1 parent d290261 commit ad375ed

File tree

9 files changed

+707
-59
lines changed

9 files changed

+707
-59
lines changed

gateway/resolver/relations.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package resolver
2+
3+
import (
4+
"context"
5+
6+
"github.com/graphql-go/graphql"
7+
"golang.org/x/text/cases"
8+
"golang.org/x/text/language"
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
)
13+
14+
// RelationResolver handles runtime resolution of relation fields
15+
type RelationResolver struct {
16+
service *Service
17+
}
18+
19+
// NewRelationResolver creates a new relation resolver
20+
func NewRelationResolver(service *Service) *RelationResolver {
21+
return &RelationResolver{
22+
service: service,
23+
}
24+
}
25+
26+
// CreateResolver creates a GraphQL resolver for relation fields
27+
func (rr *RelationResolver) CreateResolver(fieldName string) graphql.FieldResolveFn {
28+
return func(p graphql.ResolveParams) (interface{}, error) {
29+
parentObj, ok := p.Source.(map[string]interface{})
30+
if !ok {
31+
return nil, nil
32+
}
33+
34+
refInfo := rr.extractReferenceInfo(parentObj, fieldName)
35+
if refInfo.name == "" {
36+
return nil, nil
37+
}
38+
39+
return rr.resolveReference(p.Context, refInfo.name, refInfo.namespace, refInfo.kind, refInfo.apiGroup)
40+
}
41+
}
42+
43+
// referenceInfo holds extracted reference details
44+
type referenceInfo struct {
45+
name string
46+
namespace string
47+
kind string
48+
apiGroup string
49+
}
50+
51+
// extractReferenceInfo extracts reference details from a *Ref object
52+
func (rr *RelationResolver) extractReferenceInfo(parentObj map[string]interface{}, fieldName string) referenceInfo {
53+
name, _ := parentObj["name"].(string)
54+
if name == "" {
55+
return referenceInfo{}
56+
}
57+
58+
namespace, _ := parentObj["namespace"].(string)
59+
apiGroup, _ := parentObj["apiGroup"].(string)
60+
61+
kind, _ := parentObj["kind"].(string)
62+
if kind == "" {
63+
// Fallback: infer kind from field name (e.g., "role" -> "Role")
64+
kind = cases.Title(language.English).String(fieldName)
65+
}
66+
67+
return referenceInfo{
68+
name: name,
69+
namespace: namespace,
70+
kind: kind,
71+
apiGroup: apiGroup,
72+
}
73+
}
74+
75+
// resolveReference fetches a referenced Kubernetes resource
76+
func (rr *RelationResolver) resolveReference(ctx context.Context, name, namespace, kind, apiGroup string) (interface{}, error) {
77+
versions := []string{"v1", "v1beta1", "v1alpha1"}
78+
79+
for _, version := range versions {
80+
if obj := rr.tryFetchResource(ctx, name, namespace, kind, apiGroup, version); obj != nil {
81+
return obj, nil
82+
}
83+
}
84+
85+
return nil, nil
86+
}
87+
88+
// tryFetchResource attempts to fetch a Kubernetes resource with the given parameters
89+
func (rr *RelationResolver) tryFetchResource(ctx context.Context, name, namespace, kind, apiGroup, version string) map[string]interface{} {
90+
gvk := schema.GroupVersionKind{
91+
Group: apiGroup,
92+
Version: version,
93+
Kind: kind,
94+
}
95+
96+
obj := &unstructured.Unstructured{}
97+
obj.SetGroupVersionKind(gvk)
98+
99+
key := client.ObjectKey{Name: name}
100+
if namespace != "" {
101+
key.Namespace = namespace
102+
}
103+
104+
if err := rr.service.runtimeClient.Get(ctx, key, obj); err == nil {
105+
return obj.Object
106+
}
107+
108+
return nil
109+
}
110+
111+
// GetSupportedVersions returns the list of API versions to try for resource resolution
112+
func (rr *RelationResolver) GetSupportedVersions() []string {
113+
return []string{"v1", "v1beta1", "v1alpha1"}
114+
}
115+
116+
// SetSupportedVersions allows customizing the API versions to try (for future extensibility)
117+
func (rr *RelationResolver) SetSupportedVersions(versions []string) {
118+
// Future: Store in resolver state for customization
119+
// For now, this is a placeholder for extensibility
120+
}

gateway/resolver/resolver.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Provider interface {
3030
CustomQueriesProvider
3131
CommonResolver() graphql.FieldResolveFn
3232
SanitizeGroupName(string) string
33+
RuntimeClient() client.WithWatch
34+
RelationResolver(fieldName string) graphql.FieldResolveFn
3335
}
3436

3537
type CrudProvider interface {
@@ -50,16 +52,22 @@ type CustomQueriesProvider interface {
5052
type Service struct {
5153
log *logger.Logger
5254
// 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
55+
groupNames map[string]string // map[sanitizedGroupName]originalGroupName
56+
runtimeClient client.WithWatch
57+
relationResolver *RelationResolver
5558
}
5659

5760
func New(log *logger.Logger, runtimeClient client.WithWatch) *Service {
58-
return &Service{
61+
s := &Service{
5962
log: log,
6063
groupNames: make(map[string]string),
6164
runtimeClient: runtimeClient,
6265
}
66+
67+
// Initialize the relation resolver
68+
s.relationResolver = NewRelationResolver(s)
69+
70+
return s
6371
}
6472

6573
// ListItems returns a GraphQL CommonResolver function that lists Kubernetes resources of the given GroupVersionKind.
@@ -456,3 +464,13 @@ func compareNumbers[T int64 | float64](a, b T) int {
456464
return 0
457465
}
458466
}
467+
468+
// RuntimeClient returns the runtime client for use in relationship resolution
469+
func (r *Service) RuntimeClient() client.WithWatch {
470+
return r.runtimeClient
471+
}
472+
473+
// RelationResolver creates a GraphQL resolver for relation fields
474+
func (r *Service) RelationResolver(fieldName string) graphql.FieldResolveFn {
475+
return r.relationResolver.CreateResolver(fieldName)
476+
}

gateway/schema/relations.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package schema
2+
3+
import (
4+
"strings"
5+
6+
"golang.org/x/text/cases"
7+
"golang.org/x/text/language"
8+
9+
"github.com/go-openapi/spec"
10+
"github.com/graphql-go/graphql"
11+
)
12+
13+
// RelationEnhancer handles schema enhancement for relation fields
14+
type RelationEnhancer struct {
15+
gateway *Gateway
16+
}
17+
18+
// NewRelationEnhancer creates a new relation enhancer
19+
func NewRelationEnhancer(gateway *Gateway) *RelationEnhancer {
20+
return &RelationEnhancer{
21+
gateway: gateway,
22+
}
23+
}
24+
25+
// AddRelationFields adds relation fields to schemas that contain *Ref fields
26+
func (re *RelationEnhancer) AddRelationFields(fields graphql.Fields, properties map[string]spec.Schema) {
27+
for fieldName := range properties {
28+
if !strings.HasSuffix(fieldName, "Ref") {
29+
continue
30+
}
31+
32+
baseName := strings.TrimSuffix(fieldName, "Ref")
33+
sanitizedFieldName := sanitizeFieldName(fieldName)
34+
35+
refField, exists := fields[sanitizedFieldName]
36+
if !exists {
37+
continue
38+
}
39+
40+
enhancedType := re.enhanceRefTypeWithRelation(refField.Type, baseName)
41+
if enhancedType == nil {
42+
continue
43+
}
44+
45+
fields[sanitizedFieldName] = &graphql.Field{
46+
Type: enhancedType,
47+
}
48+
}
49+
}
50+
51+
// enhanceRefTypeWithRelation adds a relation field to a *Ref object type
52+
func (re *RelationEnhancer) enhanceRefTypeWithRelation(originalType graphql.Output, baseName string) graphql.Output {
53+
objType, ok := originalType.(*graphql.Object)
54+
if !ok {
55+
return originalType
56+
}
57+
58+
cacheKey := objType.Name() + "_" + baseName + "_Enhanced"
59+
if enhancedType, exists := re.gateway.enhancedTypesCache[cacheKey]; exists {
60+
return enhancedType
61+
}
62+
63+
enhancedFields := re.copyOriginalFields(objType.Fields())
64+
re.addRelationField(enhancedFields, baseName)
65+
66+
enhancedType := graphql.NewObject(graphql.ObjectConfig{
67+
Name: sanitizeFieldName(cacheKey),
68+
Fields: enhancedFields,
69+
})
70+
71+
re.gateway.enhancedTypesCache[cacheKey] = enhancedType
72+
return enhancedType
73+
}
74+
75+
// copyOriginalFields converts FieldDefinition to Field for reuse
76+
func (re *RelationEnhancer) copyOriginalFields(originalFieldDefs graphql.FieldDefinitionMap) graphql.Fields {
77+
enhancedFields := make(graphql.Fields, len(originalFieldDefs))
78+
for fieldName, fieldDef := range originalFieldDefs {
79+
enhancedFields[fieldName] = &graphql.Field{
80+
Type: fieldDef.Type,
81+
Description: fieldDef.Description,
82+
Resolve: fieldDef.Resolve,
83+
}
84+
}
85+
return enhancedFields
86+
}
87+
88+
// addRelationField adds a single relation field to the enhanced fields
89+
func (re *RelationEnhancer) addRelationField(enhancedFields graphql.Fields, baseName string) {
90+
targetType := re.findRelationTargetType(baseName)
91+
if targetType == nil {
92+
return
93+
}
94+
95+
sanitizedBaseName := sanitizeFieldName(baseName)
96+
enhancedFields[sanitizedBaseName] = &graphql.Field{
97+
Type: targetType,
98+
Resolve: re.gateway.resolver.RelationResolver(baseName),
99+
}
100+
}
101+
102+
// findRelationTargetType finds the GraphQL type for a relation target
103+
func (re *RelationEnhancer) findRelationTargetType(baseName string) graphql.Output {
104+
targetKind := cases.Title(language.English).String(baseName)
105+
106+
for defKey, defSchema := range re.gateway.definitions {
107+
if re.matchesTargetKind(defSchema, targetKind) {
108+
if existingType, exists := re.gateway.typesCache[defKey]; exists {
109+
return existingType
110+
}
111+
112+
if fieldType, _, err := re.gateway.convertSwaggerTypeToGraphQL(defSchema, defKey, []string{}, make(map[string]bool)); err == nil {
113+
return fieldType
114+
}
115+
}
116+
}
117+
118+
return graphql.String
119+
}
120+
121+
// matchesTargetKind checks if a schema definition matches the target kind
122+
func (re *RelationEnhancer) matchesTargetKind(defSchema spec.Schema, targetKind string) bool {
123+
gvkExt, ok := defSchema.Extensions["x-kubernetes-group-version-kind"]
124+
if !ok {
125+
return false
126+
}
127+
128+
gvkSlice, ok := gvkExt.([]any)
129+
if !ok || len(gvkSlice) == 0 {
130+
return false
131+
}
132+
133+
gvkMap, ok := gvkSlice[0].(map[string]any)
134+
if !ok {
135+
return false
136+
}
137+
138+
kind, ok := gvkMap["kind"].(string)
139+
return ok && kind == targetKind
140+
}

gateway/schema/schema.go

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,14 @@ type Provider interface {
2222
}
2323

2424
type Gateway struct {
25-
log *logger.Logger
26-
resolver resolver.Provider
27-
graphqlSchema graphql.Schema
28-
29-
definitions spec.Definitions
30-
31-
// typesCache stores generated GraphQL object types(fields) to prevent redundant repeated generation.
32-
typesCache map[string]*graphql.Object
33-
// inputTypesCache stores generated GraphQL input object types(input fields) to prevent redundant repeated generation.
34-
inputTypesCache map[string]*graphql.InputObject
25+
log *logger.Logger
26+
resolver resolver.Provider
27+
graphqlSchema graphql.Schema
28+
definitions spec.Definitions
29+
typesCache map[string]*graphql.Object
30+
inputTypesCache map[string]*graphql.InputObject
31+
enhancedTypesCache map[string]*graphql.Object // Cache for enhanced *Ref types
32+
relationEnhancer *RelationEnhancer
3533
// Prevents naming conflict in case of the same Kind name in different groups/versions
3634
typeNameRegistry map[string]string // map[Kind]GroupVersion
3735

@@ -41,15 +39,19 @@ type Gateway struct {
4139

4240
func New(log *logger.Logger, definitions spec.Definitions, resolverProvider resolver.Provider) (*Gateway, error) {
4341
g := &Gateway{
44-
log: log,
45-
resolver: resolverProvider,
46-
definitions: definitions,
47-
typesCache: make(map[string]*graphql.Object),
48-
inputTypesCache: make(map[string]*graphql.InputObject),
49-
typeNameRegistry: make(map[string]string),
50-
typeByCategory: make(map[string][]resolver.TypeByCategory),
42+
log: log,
43+
resolver: resolverProvider,
44+
definitions: definitions,
45+
typesCache: make(map[string]*graphql.Object),
46+
inputTypesCache: make(map[string]*graphql.InputObject),
47+
enhancedTypesCache: make(map[string]*graphql.Object),
48+
typeNameRegistry: make(map[string]string),
49+
typeByCategory: make(map[string][]resolver.TypeByCategory),
5150
}
5251

52+
// Initialize the relation enhancer after gateway is created
53+
g.relationEnhancer = NewRelationEnhancer(g)
54+
5355
err := g.generateGraphqlSchema()
5456

5557
return g, err
@@ -336,6 +338,9 @@ func (g *Gateway) generateGraphQLFields(resourceScheme *spec.Schema, typePrefix
336338
}
337339
}
338340

341+
// Add relation fields for any *Ref fields in this schema
342+
g.relationEnhancer.AddRelationFields(fields, resourceScheme.Properties)
343+
339344
return fields, inputFields, nil
340345
}
341346

0 commit comments

Comments
 (0)