|
| 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 | +} |
0 commit comments