Skip to content

Commit 29575be

Browse files
authored
refactor(gateway, manager, resolver): clean up code and improve error handling (#42)
1 parent fe6b9d1 commit 29575be

File tree

3 files changed

+53
-106
lines changed

3 files changed

+53
-106
lines changed

internal/gateway/gateway.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package gateway
33
import (
44
"errors"
55
"fmt"
6+
"regexp"
7+
"strings"
8+
69
"github.com/go-openapi/spec"
710
"github.com/graphql-go/graphql"
811
"github.com/openmfp/crd-gql-gateway/internal/resolver"
912
"github.com/openmfp/golang-commons/logger"
1013
"k8s.io/apimachinery/pkg/runtime/schema"
11-
"regexp"
12-
"strings"
1314
)
1415

1516
type Provider interface {
@@ -21,8 +22,7 @@ type Gateway struct {
2122
resolver resolver.Provider
2223
graphqlSchema graphql.Schema
2324

24-
definitions spec.Definitions
25-
subscriptions graphql.Fields
25+
definitions spec.Definitions
2626

2727
// typesCache stores generated GraphQL object types(fields) to prevent redundant repeated generation.
2828
typesCache map[string]*graphql.Object
@@ -37,7 +37,6 @@ func New(log *logger.Logger, definitions spec.Definitions, resolver resolver.Pro
3737
log: log,
3838
resolver: resolver,
3939
definitions: definitions,
40-
subscriptions: graphql.Fields{},
4140
typesCache: make(map[string]*graphql.Object),
4241
inputTypesCache: make(map[string]*graphql.InputObject),
4342
typeNameRegistry: make(map[string]string),
@@ -248,8 +247,7 @@ func (g *Gateway) generateGraphQLFields(resourceScheme *spec.Schema, typePrefix
248247
}
249248

250249
fields[sanitizedFieldName] = &graphql.Field{
251-
Type: fieldType,
252-
Resolve: g.resolver.UnstructuredFieldResolver(fieldName),
250+
Type: fieldType,
253251
}
254252

255253
inputFields[sanitizedFieldName] = &graphql.InputObjectFieldConfig{

internal/manager/manager.go

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ func (s *Service) OnFileDeleted(filename string) {
143143
defer s.mu.Unlock()
144144

145145
delete(s.handlers, filename)
146-
147146
}
148147

149148
func (s *Service) loadSchemaFromFile(filename string) (*graphql.Schema, error) {
@@ -174,24 +173,35 @@ func (s *Service) createHandler(schema *graphql.Schema) *graphqlHandler {
174173
}
175174

176175
func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
177-
workspace, endpoint, err := s.parsePath(r.URL.Path)
176+
workspace, err := s.parsePath(r.URL.Path)
178177
if err != nil {
178+
s.log.Error().Err(err).Str("path", r.URL.Path).Msg("Error parsing path")
179179
http.NotFound(w, r)
180180
return
181181
}
182182

183-
h, ok := s.getHandler(workspace)
183+
s.mu.RLock()
184+
h, ok := s.handlers[workspace]
185+
s.mu.RUnlock()
186+
184187
if !ok {
188+
s.log.Info().Str("workspace", workspace).Msg("no handler found for workspace")
185189
http.NotFound(w, r)
186190
return
187191
}
188192

189-
if endpoint == "graphql" && r.Method == http.MethodGet {
193+
if r.Method == http.MethodGet {
190194
h.handler.ServeHTTP(w, r)
191195
return
192196
}
193197

194-
cfg, err := s.getConfigForRuntimeClient(workspace, r.Header.Get("Authorization"))
198+
token := r.Header.Get("Authorization")
199+
if token == "" {
200+
http.Error(w, "Authorization header is required", http.StatusUnauthorized)
201+
return
202+
}
203+
204+
cfg, err := s.getConfigForRuntimeClient(workspace, token)
195205
if err != nil {
196206
writeJSONError(w, http.StatusInternalServerError, "Error getting a runtime client's config")
197207
return
@@ -228,28 +238,17 @@ func writeJSONError(w http.ResponseWriter, status int, message string) {
228238
}
229239

230240
// parsePath extracts filename and endpoint from the requested URL path.
231-
func (s *Service) parsePath(path string) (workspace, endpoint string, err error) {
241+
func (s *Service) parsePath(path string) (workspace string, err error) {
232242
parts := strings.Split(strings.Trim(path, "/"), "/")
233243
if len(parts) != 2 {
234-
return "", "", fmt.Errorf("invalid path")
244+
return "", fmt.Errorf("invalid path")
235245
}
236-
return parts[0], parts[1], nil
237-
}
238246

239-
// getHandler retrieves the graphqlHandler associated with the given filename.
240-
func (s *Service) getHandler(filename string) (*graphqlHandler, bool) {
241-
s.mu.RLock()
242-
defer s.mu.RUnlock()
243-
h, ok := s.handlers[filename]
244-
return h, ok
247+
return parts[0], nil
245248
}
246249

247250
// getConfigForRuntimeClient initializes a runtime client for the given server address.
248251
func (s *Service) getConfigForRuntimeClient(workspace, token string) (*rest.Config, error) {
249-
if token == "" { // if no token, use current-context
250-
return s.restCfg, nil
251-
}
252-
253252
requestConfig := rest.CopyConfig(s.restCfg)
254253
requestConfig.BearerToken = token
255254
u, err := url.Parse(s.restCfg.Host)
@@ -270,8 +269,6 @@ func (s *Service) handleSubscription(w http.ResponseWriter, r *http.Request, sch
270269
w.Header().Set("Cache-Control", "no-cache")
271270
w.Header().Set("Connection", "keep-alive")
272271

273-
flusher := http.NewResponseController(w)
274-
275272
var params struct {
276273
Query string `json:"query"`
277274
OperationName string `json:"operationName"`
@@ -283,6 +280,10 @@ func (s *Service) handleSubscription(w http.ResponseWriter, r *http.Request, sch
283280
return
284281
}
285282

283+
flusher := http.NewResponseController(w)
284+
285+
r.Body.Close()
286+
286287
subscriptionParams := graphql.Params{
287288
Schema: *schema,
288289
RequestString: params.Query,
@@ -299,8 +300,10 @@ func (s *Service) handleSubscription(w http.ResponseWriter, r *http.Request, sch
299300

300301
data, err := json.Marshal(res)
301302
if err != nil {
303+
s.log.Error().Err(err).Msg("Error marshalling subscription response")
302304
continue
303305
}
306+
304307
fmt.Fprintf(w, "event: next\ndata: %s\n\n", data)
305308
flusher.Flush()
306309
}
@@ -309,13 +312,14 @@ func (s *Service) handleSubscription(w http.ResponseWriter, r *http.Request, sch
309312
}
310313

311314
func readDefinitionFromFile(filePath string) (spec.Definitions, error) {
312-
data, err := os.ReadFile(filePath)
315+
f, err := os.Open(filePath)
313316
if err != nil {
314317
return nil, err
315318
}
319+
defer f.Close()
316320

317321
var swagger spec.Swagger
318-
err = json.Unmarshal(data, &swagger)
322+
err = json.NewDecoder(f).Decode(&swagger)
319323
if err != nil {
320324
return nil, err
321325
}

internal/resolver/resolver.go

Lines changed: 21 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"regexp"
8-
"sort"
98

109
"github.com/graphql-go/graphql"
1110
"go.opentelemetry.io/otel"
@@ -47,7 +46,6 @@ type CrudProvider interface {
4746

4847
type FieldResolverProvider interface {
4948
CommonResolver() graphql.FieldResolveFn
50-
UnstructuredFieldResolver(fieldName string) graphql.FieldResolveFn
5149
SanitizeGroupName(string) string
5250
GetOriginalGroupName(string) string
5351
}
@@ -71,38 +69,6 @@ func New(log *logger.Logger) *Service {
7169
}
7270
}
7371

74-
// UnstructuredFieldResolver returns a GraphQL FieldResolveFn to resolve a field from an unstructured object.
75-
func (r *Service) UnstructuredFieldResolver(fieldName string) graphql.FieldResolveFn {
76-
return func(p graphql.ResolveParams) (interface{}, error) {
77-
var objMap map[string]interface{}
78-
79-
switch source := p.Source.(type) {
80-
case *unstructured.Unstructured:
81-
objMap = source.Object
82-
case unstructured.Unstructured:
83-
objMap = source.Object
84-
case map[string]interface{}:
85-
objMap = source
86-
default:
87-
r.log.Error().
88-
Str("type", fmt.Sprintf("%T", p.Source)).
89-
Msg("Source is of unexpected type")
90-
return nil, errors.New("source is of unexpected type")
91-
}
92-
93-
value, found, err := unstructured.NestedFieldNoCopy(objMap, fieldName)
94-
if err != nil {
95-
r.log.Error().Err(err).Str("field", fieldName).Msg("Error retrieving field")
96-
return nil, err
97-
}
98-
if !found {
99-
return nil, nil
100-
}
101-
102-
return value, nil
103-
}
104-
}
105-
10672
// ListItems returns a GraphQL CommonResolver function that lists Kubernetes resources of the given GroupVersionKind.
10773
func (r *Service) ListItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
10874
return func(p graphql.ResolveParams) (interface{}, error) {
@@ -130,20 +96,14 @@ func (r *Service) ListItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn
13096

13197
// Create an unstructured list to hold the results
13298
list := &unstructured.UnstructuredList{}
133-
list.SetGroupVersionKind(schema.GroupVersionKind{
134-
Group: gvk.Group,
135-
Version: gvk.Version,
136-
Kind: gvk.Kind + "List",
137-
})
99+
list.SetGroupVersionKind(gvk)
138100

139101
var opts []client.ListOption
140102
// Handle label selector argument
141103
if labelSelector, ok := p.Args[labelSelectorArg].(string); ok && labelSelector != "" {
142104
selector, err := labels.Parse(labelSelector)
143105
if err != nil {
144-
log.Error().Err(err).
145-
Str(labelSelectorArg, labelSelector).
146-
Msg("Unable to parse given label selector")
106+
log.Error().Err(err).Str(labelSelectorArg, labelSelector).Msg("Unable to parse given label selector")
147107
return nil, err
148108
}
149109
opts = append(opts, client.MatchingLabelsSelector{Selector: selector})
@@ -155,18 +115,14 @@ func (r *Service) ListItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn
155115
}
156116

157117
if err = runtimeClient.List(ctx, list, opts...); err != nil {
158-
log.Error().
159-
Err(err).
160-
Msg("Unable to list objects")
118+
log.Error().Err(err).Msg("Unable to list objects")
161119
return nil, err
162120
}
163121

164-
items := list.Items
165-
166-
// Sort the items by name for consistent ordering
167-
sort.Slice(items, func(i, j int) bool {
168-
return items[i].GetName() < items[j].GetName()
169-
})
122+
items := make([]map[string]any, len(list.Items))
123+
for i, item := range list.Items {
124+
items[i] = item.Object
125+
}
170126

171127
return items, nil
172128
}
@@ -198,39 +154,32 @@ func (r *Service) GetItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
198154
}
199155

200156
// Retrieve required arguments
201-
name, nameOK := p.Args["name"].(string)
202-
namespace, nsOK := p.Args["namespace"].(string)
203-
204-
if !nameOK || name == "" {
205-
err := errors.New("missing required argument: name")
206-
log.Error().Err(err).Msg("Name argument is required")
157+
name, ok := p.Args["name"].(string)
158+
if !ok || name == "" {
159+
log.Error().Err(errors.New("missing required argument: name")).Msg("Name argument is required")
207160
return nil, err
208161
}
209-
if !nsOK || namespace == "" {
210-
err := errors.New("missing required argument: namespace")
211-
log.Error().Err(err).Msg("Namespace argument is required")
162+
163+
namespace, ok := p.Args["namespace"].(string)
164+
if !ok || namespace == "" {
165+
log.Error().Err(errors.New("missing required argument: namespace")).Msg("Namespace argument is required")
212166
return nil, err
213167
}
214168

215169
// Create an unstructured object to hold the result
216170
obj := &unstructured.Unstructured{}
217171
obj.SetGroupVersionKind(gvk)
218172

219-
key := client.ObjectKey{
173+
// Get the object using the runtime client
174+
if err = runtimeClient.Get(ctx, client.ObjectKey{
220175
Namespace: namespace,
221176
Name: name,
222-
}
223-
224-
// Get the object using the runtime client
225-
if err = runtimeClient.Get(ctx, key, obj); err != nil {
226-
log.Error().Err(err).
227-
Str("name", name).
228-
Str("namespace", namespace).
229-
Msg("Unable to get object")
177+
}, obj); err != nil {
178+
log.Error().Err(err).Str("name", name).Str("namespace", namespace).Msg("Unable to get object")
230179
return nil, err
231180
}
232181

233-
return obj, nil
182+
return obj.Object, nil
234183
}
235184
}
236185

@@ -266,7 +215,7 @@ func (r *Service) CreateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
266215
return nil, err
267216
}
268217

269-
return obj, nil
218+
return obj.Object, nil
270219
}
271220
}
272221

@@ -317,7 +266,7 @@ func (r *Service) UpdateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
317266
return nil, err
318267
}
319268

320-
return existingObj, nil
269+
return existingObj.Object, nil
321270
}
322271
}
323272

@@ -355,10 +304,6 @@ func (r *Service) DeleteItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
355304

356305
func (r *Service) CommonResolver() graphql.FieldResolveFn {
357306
return func(p graphql.ResolveParams) (interface{}, error) {
358-
if p.Source == nil {
359-
// At the example level, return a non-nil value (e.g., an empty map)
360-
return map[string]interface{}{}, nil
361-
}
362307
return p.Source, nil
363308
}
364309
}

0 commit comments

Comments
 (0)