Skip to content

Commit 391a176

Browse files
vertex451Artem Shcherbatiukaaronschweig
authored
Feat: type by category (#100)
* added typeByCategory query * Fix: queries and mutations for cluster scoped resources (#105) * refactor: simplify namespace argument handling in GraphQL resolvers (#109) --------- Co-authored-by: Artem Shcherbatiuk <[email protected]> Co-authored-by: Aaron Schweig <[email protected]>
1 parent 15d216f commit 391a176

File tree

19 files changed

+652
-337
lines changed

19 files changed

+652
-337
lines changed

.github/workflows/pipeline.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ jobs:
2121
imageTagName: ghcr.io/openmfp/crd-gql-gateway
2222
coverageThreasholdFile: 0
2323
coverageThresholdPackage: 0
24-
coverageThreasholdTotal: 0
24+
coverageThreasholdTotal: 49
2525

.mockery.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
issue-845-fix: True
2-
resolve-type-alias: False
1+
issue-845-fix: true
2+
resolve-type-alias: false
33
with-expecter: true
44
packages:
55
sigs.k8s.io/controller-runtime/pkg/client:

.testcoverage.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ exclude:
22
paths:
33
- main.go
44
- cmd
5-
- gateway
65
- tests
7-
- internal/config/config.go
6+
- gateway/config/config.go
87
- listener/*
8+
- deprecated/*

common/const.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package common
2+
3+
const (
4+
CategoriesExtensionKey = "x-kubernetes-categories"
5+
GVKExtensionKey = "x-kubernetes-group-version-kind"
6+
ScopeExtensionKey = "x-kubernetes-scope"
7+
)

gateway/resolver/arguments.go

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package resolver
22

33
import (
44
"errors"
5+
"maps"
6+
57
"github.com/graphql-go/graphql"
68
"github.com/rs/zerolog/log"
9+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
710
)
811

912
const (
@@ -39,6 +42,7 @@ func (b *FieldConfigArgumentsBuilder) WithNamespaceArg() *FieldConfigArgumentsBu
3942
Type: graphql.String,
4043
Description: "The namespace in which to search for the objects",
4144
}
45+
4246
return b
4347
}
4448

@@ -67,23 +71,9 @@ func (b *FieldConfigArgumentsBuilder) WithSubscribeToAllArg() *FieldConfigArgume
6771
return b
6872
}
6973

70-
// Complete returns the constructed arguments
74+
// Complete returns the constructed arguments and dereferences the builder
7175
func (b *FieldConfigArgumentsBuilder) Complete() graphql.FieldConfigArgument {
72-
return b.arguments
73-
}
74-
75-
func getRequiredNameAndNamespaceArgs(args map[string]interface{}) (string, string, error) {
76-
name, err := getStringArg(args, NameArg, true)
77-
if err != nil {
78-
return "", "", err
79-
}
80-
81-
namespace, err := getStringArg(args, NamespaceArg, true)
82-
if err != nil {
83-
return "", "", err
84-
}
85-
86-
return name, namespace, nil
76+
return maps.Clone(b.arguments)
8777
}
8878

8979
func getStringArg(args map[string]interface{}, key string, required bool) (string, error) {
@@ -135,3 +125,7 @@ func getBoolArg(args map[string]interface{}, key string, required bool) (bool, e
135125

136126
return res, nil
137127
}
128+
129+
func isResourceNamespaceScoped(resourceScope apiextensionsv1.ResourceScope) bool {
130+
return resourceScope == apiextensionsv1.NamespaceScoped
131+
}

gateway/resolver/custom_queries.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package resolver
2+
3+
import (
4+
"github.com/graphql-go/graphql"
5+
)
6+
7+
type TypeByCategory struct {
8+
Group string
9+
Version string
10+
Kind string
11+
Scope string
12+
}
13+
14+
func (r *Service) TypeByCategory(m map[string][]TypeByCategory) graphql.FieldResolveFn {
15+
return func(p graphql.ResolveParams) (interface{}, error) {
16+
name, err := getStringArg(p.Args, NameArg, true)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
return m[name], nil
22+
}
23+
}

gateway/resolver/resolver.go

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"gopkg.in/yaml.v3"
9+
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
910
"regexp"
1011

1112
"github.com/graphql-go/graphql"
@@ -23,19 +24,24 @@ import (
2324

2425
type Provider interface {
2526
CrudProvider
27+
CustomQueriesProvider
2628
CommonResolver() graphql.FieldResolveFn
2729
SanitizeGroupName(string) string
2830
}
2931

3032
type CrudProvider interface {
31-
ListItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn
32-
GetItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
33-
GetItemAsYAML(gvk schema.GroupVersionKind) graphql.FieldResolveFn
34-
CreateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
35-
UpdateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
36-
DeleteItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
37-
SubscribeItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
38-
SubscribeItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn
33+
ListItems(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
34+
GetItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
35+
GetItemAsYAML(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
36+
CreateItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
37+
UpdateItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
38+
DeleteItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
39+
SubscribeItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
40+
SubscribeItems(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn
41+
}
42+
43+
type CustomQueriesProvider interface {
44+
TypeByCategory(m map[string][]TypeByCategory) graphql.FieldResolveFn
3945
}
4046

4147
type Service struct {
@@ -54,7 +60,7 @@ func New(log *logger.Logger, runtimeClient client.WithWatch) *Service {
5460
}
5561

5662
// ListItems returns a GraphQL CommonResolver function that lists Kubernetes resources of the given GroupVersionKind.
57-
func (r *Service) ListItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
63+
func (r *Service) ListItems(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn {
5864
return func(p graphql.ResolveParams) (interface{}, error) {
5965
ctx, span := otel.Tracer("").Start(p.Context, "ListItems", trace.WithAttributes(attribute.String("kind", gvk.Kind)))
6066
defer span.End()
@@ -88,9 +94,14 @@ func (r *Service) ListItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn
8894
opts = append(opts, client.MatchingLabelsSelector{Selector: selector})
8995
}
9096

91-
// Handle namespace argument
92-
if namespace, ok := p.Args[NamespaceArg].(string); ok && namespace != "" {
93-
opts = append(opts, client.InNamespace(namespace))
97+
if isResourceNamespaceScoped(scope) {
98+
namespace, err := getStringArg(p.Args, NamespaceArg, false)
99+
if err != nil {
100+
return nil, err
101+
}
102+
if namespace != "" {
103+
opts = append(opts, client.InNamespace(namespace))
104+
}
94105
}
95106

96107
if err = r.runtimeClient.List(ctx, list, opts...); err != nil {
@@ -108,7 +119,7 @@ func (r *Service) ListItems(gvk schema.GroupVersionKind) graphql.FieldResolveFn
108119
}
109120

110121
// GetItem returns a GraphQL CommonResolver function that retrieves a single Kubernetes resource of the given GroupVersionKind.
111-
func (r *Service) GetItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
122+
func (r *Service) GetItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn {
112123
return func(p graphql.ResolveParams) (interface{}, error) {
113124
ctx, span := otel.Tracer("").Start(p.Context, "GetItem", trace.WithAttributes(attribute.String("kind", gvk.Kind)))
114125
defer span.End()
@@ -128,7 +139,7 @@ func (r *Service) GetItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
128139
}
129140

130141
// Retrieve required arguments
131-
name, namespace, err := getRequiredNameAndNamespaceArgs(p.Args)
142+
name, err := getStringArg(p.Args, NameArg, true)
132143
if err != nil {
133144
return nil, err
134145
}
@@ -137,26 +148,36 @@ func (r *Service) GetItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
137148
obj := &unstructured.Unstructured{}
138149
obj.SetGroupVersionKind(gvk)
139150

151+
key := client.ObjectKey{
152+
Name: name,
153+
}
154+
155+
if isResourceNamespaceScoped(scope) {
156+
namespace, err := getStringArg(p.Args, NamespaceArg, true)
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
key.Namespace = namespace
162+
}
163+
140164
// Get the object using the runtime client
141-
if err = r.runtimeClient.Get(ctx, client.ObjectKey{
142-
Namespace: namespace,
143-
Name: name,
144-
}, obj); err != nil {
145-
log.Error().Err(err).Str("name", name).Str("namespace", namespace).Msg("Unable to get object")
165+
if err = r.runtimeClient.Get(ctx, key, obj); err != nil {
166+
log.Error().Err(err).Str("name", name).Str("scope", string(scope)).Msg("Unable to get object")
146167
return nil, err
147168
}
148169

149170
return obj.Object, nil
150171
}
151172
}
152173

153-
func (r *Service) GetItemAsYAML(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
174+
func (r *Service) GetItemAsYAML(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn {
154175
return func(p graphql.ResolveParams) (interface{}, error) {
155176
var span trace.Span
156177
p.Context, span = otel.Tracer("").Start(p.Context, "GetItemAsYAML", trace.WithAttributes(attribute.String("kind", gvk.Kind)))
157178
defer span.End()
158179

159-
out, err := r.GetItem(gvk)(p)
180+
out, err := r.GetItem(gvk, scope)(p)
160181
if err != nil {
161182
return "", err
162183
}
@@ -170,7 +191,7 @@ func (r *Service) GetItemAsYAML(gvk schema.GroupVersionKind) graphql.FieldResolv
170191
}
171192
}
172193

173-
func (r *Service) CreateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
194+
func (r *Service) CreateItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn {
174195
return func(p graphql.ResolveParams) (interface{}, error) {
175196
ctx, span := otel.Tracer("").Start(p.Context, "CreateItem", trace.WithAttributes(attribute.String("kind", gvk.Kind)))
176197
defer span.End()
@@ -179,14 +200,20 @@ func (r *Service) CreateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
179200

180201
log := r.log.With().Str("operation", "create").Str("kind", gvk.Kind).Logger()
181202

182-
namespace := p.Args[NamespaceArg].(string)
183203
objectInput := p.Args["object"].(map[string]interface{})
184204

185205
obj := &unstructured.Unstructured{
186206
Object: objectInput,
187207
}
188208
obj.SetGroupVersionKind(gvk)
189-
obj.SetNamespace(namespace)
209+
210+
if isResourceNamespaceScoped(scope) {
211+
namespace, err := getStringArg(p.Args, NamespaceArg, true)
212+
if err != nil {
213+
return nil, err
214+
}
215+
obj.SetNamespace(namespace)
216+
}
190217

191218
if obj.GetName() == "" {
192219
return nil, errors.New("object metadata.name is required")
@@ -201,7 +228,7 @@ func (r *Service) CreateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
201228
}
202229
}
203230

204-
func (r *Service) UpdateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
231+
func (r *Service) UpdateItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn {
205232
return func(p graphql.ResolveParams) (interface{}, error) {
206233
ctx, span := otel.Tracer("").Start(p.Context, "UpdateItem", trace.WithAttributes(attribute.String("kind", gvk.Kind)))
207234
defer span.End()
@@ -210,7 +237,7 @@ func (r *Service) UpdateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
210237

211238
log := r.log.With().Str("operation", "update").Str("kind", gvk.Kind).Logger()
212239

213-
name, namespace, err := getRequiredNameAndNamespaceArgs(p.Args)
240+
name, err := getStringArg(p.Args, NameArg, true)
214241
if err != nil {
215242
return nil, err
216243
}
@@ -226,8 +253,17 @@ func (r *Service) UpdateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
226253
existingObj := &unstructured.Unstructured{}
227254
existingObj.SetGroupVersionKind(gvk)
228255

256+
key := client.ObjectKey{Name: name}
257+
if isResourceNamespaceScoped(scope) {
258+
namespace, err := getStringArg(p.Args, NamespaceArg, true)
259+
if err != nil {
260+
return nil, err
261+
}
262+
key.Namespace = namespace
263+
}
264+
229265
// Fetch the existing object from the cluster
230-
err = r.runtimeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, existingObj)
266+
err = r.runtimeClient.Get(ctx, key, existingObj)
231267
if err != nil {
232268
log.Error().Err(err).Msg("Failed to get existing object")
233269
return nil, err
@@ -245,7 +281,7 @@ func (r *Service) UpdateItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
245281
}
246282

247283
// DeleteItem returns a CommonResolver function for deleting a resource.
248-
func (r *Service) DeleteItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn {
284+
func (r *Service) DeleteItem(gvk schema.GroupVersionKind, scope v1.ResourceScope) graphql.FieldResolveFn {
249285
return func(p graphql.ResolveParams) (interface{}, error) {
250286
ctx, span := otel.Tracer("").Start(p.Context, "DeleteItem", trace.WithAttributes(attribute.String("kind", gvk.Kind)))
251287
defer span.End()
@@ -254,16 +290,23 @@ func (r *Service) DeleteItem(gvk schema.GroupVersionKind) graphql.FieldResolveFn
254290

255291
log := r.log.With().Str("operation", "delete").Str("kind", gvk.Kind).Logger()
256292

257-
name, namespace, err := getRequiredNameAndNamespaceArgs(p.Args)
293+
name, err := getStringArg(p.Args, NameArg, true)
258294
if err != nil {
259295
return nil, err
260296
}
261297

262298
obj := &unstructured.Unstructured{}
263299
obj.SetGroupVersionKind(gvk)
264-
obj.SetNamespace(namespace)
265300
obj.SetName(name)
266301

302+
if isResourceNamespaceScoped(scope) {
303+
namespace, err := getStringArg(p.Args, NamespaceArg, true)
304+
if err != nil {
305+
return nil, err
306+
}
307+
obj.SetNamespace(namespace)
308+
}
309+
267310
if err := r.runtimeClient.Delete(ctx, obj); err != nil {
268311
log.Error().Err(err).Msg("Failed to delete object")
269312
return nil, err

gateway/resolver/resolver_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package resolver
22

33
import (
44
"context"
5+
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
56
"testing"
67

78
"github.com/graphql-go/graphql"
@@ -88,7 +89,7 @@ func TestListItems(t *testing.T) {
8889
Group: "group",
8990
Version: "version",
9091
Kind: "kind",
91-
})(graphql.ResolveParams{
92+
}, v1.NamespaceScoped)(graphql.ResolveParams{
9293
Context: context.Background(),
9394
Args: tt.args,
9495
})
@@ -179,7 +180,7 @@ func TestGetItem(t *testing.T) {
179180
Group: "group",
180181
Version: "version",
181182
Kind: "kind",
182-
})(graphql.ResolveParams{
183+
}, v1.NamespaceScoped)(graphql.ResolveParams{
183184
Context: context.Background(),
184185
Args: tt.args,
185186
})
@@ -269,7 +270,7 @@ func TestCreateItem(t *testing.T) {
269270
Group: "group",
270271
Version: "version",
271272
Kind: "kind",
272-
})(graphql.ResolveParams{
273+
}, v1.NamespaceScoped)(graphql.ResolveParams{
273274
Context: context.Background(),
274275
Args: tt.args,
275276
})
@@ -390,7 +391,7 @@ func TestUpdateItem(t *testing.T) {
390391
Group: "group",
391392
Version: "version",
392393
Kind: "kind",
393-
})(graphql.ResolveParams{
394+
}, v1.NamespaceScoped)(graphql.ResolveParams{
394395
Context: context.Background(),
395396
Args: tt.args,
396397
})
@@ -467,7 +468,7 @@ func TestDeleteItem(t *testing.T) {
467468
Group: "group",
468469
Version: "version",
469470
Kind: "kind",
470-
})(graphql.ResolveParams{
471+
}, v1.NamespaceScoped)(graphql.ResolveParams{
471472
Context: context.Background(),
472473
Args: tt.args,
473474
})

0 commit comments

Comments
 (0)