Skip to content

Commit 6910edd

Browse files
authored
feat: virtual workspace which watches ApiBindings and Workspaces (#141)
* feat: virtual workspace which watches ApiBindings and Workspaces On-behalf-of: @SAP [email protected] Signed-off-by: Artem Shcherbatiuk <[email protected]> * chore: errors.Join On-behalf-of: @SAP [email protected] Signed-off-by: Artem Shcherbatiuk <[email protected]> * feat: removed reconcilingFactory due to compexity overhead On-behalf-of: @SAP [email protected] Signed-off-by: Artem Shcherbatiuk <[email protected]> --------- Signed-off-by: Artem Shcherbatiuk <[email protected]>
1 parent b2b1440 commit 6910edd

22 files changed

+978
-281
lines changed

.mockery.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ issue-845-fix: true
22
resolve-type-alias: false
33
with-expecter: true
44
packages:
5+
k8s.io/client-go/discovery:
6+
config:
7+
dir: listener/kcp/mocks
8+
outpkg: mocks
9+
interfaces:
10+
DiscoveryInterface:
511
sigs.k8s.io/controller-runtime/pkg/client:
612
config:
713
dir: gateway/resolver/mocks

cmd/listener.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cmd
22

33
import (
44
"crypto/tls"
5+
"github.com/openmfp/kubernetes-graphql-gateway/listener/discoveryclient"
6+
"k8s.io/client-go/discovery"
57
"os"
68

79
kcpapis "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
@@ -36,7 +38,7 @@ var (
3638
setupLog = ctrl.Log.WithName("setup")
3739
webhookServer webhook.Server
3840
metricsServerOptions metricsserver.Options
39-
appCfg *config.Config
41+
appCfg config.Config
4042
)
4143

4244
var listenCmd = &cobra.Command{
@@ -87,7 +89,8 @@ var listenCmd = &cobra.Command{
8789
}
8890
},
8991
Run: func(cmd *cobra.Command, args []string) {
90-
cfg := ctrl.GetConfigOrDie()
92+
ctx := ctrl.SetupSignalHandler()
93+
restCfg := ctrl.GetConfigOrDie()
9194

9295
mgrOpts := ctrl.Options{
9396
Scheme: scheme,
@@ -98,32 +101,44 @@ var listenCmd = &cobra.Command{
98101
LeaderElectionID: "72231e1f.openmfp.io",
99102
}
100103

101-
clt, err := client.New(cfg, client.Options{
104+
clt, err := client.New(restCfg, client.Options{
102105
Scheme: scheme,
103106
})
104107
if err != nil {
105108
setupLog.Error(err, "failed to create client from config")
106109
os.Exit(1)
107110
}
108111

109-
mf := &kcp.ManagerFactory{
110-
IsKCPEnabled: appCfg.EnableKcp,
111-
}
112+
mf := kcp.NewManagerFactory(appCfg)
112113

113-
mgr, err := mf.NewManager(cfg, mgrOpts, clt)
114+
mgr, err := mf.NewManager(ctx, restCfg, mgrOpts, clt)
114115
if err != nil {
115116
setupLog.Error(err, "unable to start manager")
116117
os.Exit(1)
117118
}
118119

120+
discoveryInterface, err := discovery.NewDiscoveryClientForConfig(restCfg)
121+
if err != nil {
122+
setupLog.Error(err, "failed to create discovery client")
123+
os.Exit(1)
124+
}
125+
119126
reconcilerOpts := kcp.ReconcilerOpts{
120127
Scheme: scheme,
121128
Client: clt,
122-
Config: cfg,
129+
Config: restCfg,
123130
OpenAPIDefinitionsPath: appCfg.OpenApiDefinitionsPath,
124131
}
125132

126-
reconciler, err := kcp.NewReconcilerFactory(appCfg).NewReconciler(reconcilerOpts)
133+
reconciler, err := kcp.NewReconciler(
134+
ctx,
135+
appCfg,
136+
reconcilerOpts,
137+
discoveryInterface,
138+
kcp.PreReconcile,
139+
discoveryclient.NewFactory,
140+
)
141+
127142
if err != nil {
128143
setupLog.Error(err, "unable to instantiate reconciler")
129144
os.Exit(1)
@@ -144,8 +159,7 @@ var listenCmd = &cobra.Command{
144159
}
145160

146161
setupLog.Info("starting manager")
147-
signalHandler := ctrl.SetupSignalHandler()
148-
if err := mgr.Start(signalHandler); err != nil {
162+
if err := mgr.Start(ctx); err != nil {
149163
setupLog.Error(err, "problem running manager")
150164
os.Exit(1)
151165
}

common/config/config.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ type Listener struct {
3838
ProbeAddr string `envconfig:"default=:8081,optional"`
3939
SecureMetrics bool `envconfig:"default=true,optional"`
4040
EnableHTTP2 bool `envconfig:"default=false,optional"`
41+
ApiExportWorkspace string `envconfig:"default=:root,optional"`
42+
ApiExportName string `envconfig:"default=kubernetes.graphql.gateway,optional"`
4143
}
4244

4345
// NewFromEnv creates a Gateway from environment values
44-
func NewFromEnv() (*Config, error) {
45-
cfg := &Config{}
46-
err := envconfig.Init(cfg)
46+
func NewFromEnv() (Config, error) {
47+
cfg := Config{}
48+
err := envconfig.Init(&cfg)
4749
return cfg, err
4850
}

gateway/manager/manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type FileWatcher interface {
3939
}
4040

4141
type Service struct {
42-
appCfg *appConfig.Config
42+
appCfg appConfig.Config
4343
handlers map[string]*graphqlHandler
4444
log *logger.Logger
4545
mu sync.RWMutex
@@ -53,7 +53,7 @@ type graphqlHandler struct {
5353
handler http.Handler
5454
}
5555

56-
func NewManager(log *logger.Logger, cfg *rest.Config, appCfg *appConfig.Config) (*Service, error) {
56+
func NewManager(log *logger.Logger, cfg *rest.Config, appCfg appConfig.Config) (*Service, error) {
5757
watcher, err := fsnotify.NewWatcher()
5858
if err != nil {
5959
return nil, err

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/graphql-go/graphql v0.8.1
1717
github.com/graphql-go/handler v0.2.4
1818
github.com/hashicorp/go-multierror v1.1.1
19+
github.com/kcp-dev/kcp/pkg/apis v0.11.0
1920
github.com/kcp-dev/kcp/sdk v0.27.0
2021
github.com/kcp-dev/logicalcluster/v3 v3.0.5
2122
github.com/mitchellh/mapstructure v1.5.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250223115924-431177b024f3 h1:YwNX7
9494
github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250223115924-431177b024f3/go.mod h1:n0+EV+LGKl1MXXqGbGcn0AaBv7hdKsdazSYuq8nM8Us=
9595
github.com/kcp-dev/controller-runtime v0.19.0-kcp.1.0.20250129100209-5eaf4c7b6056 h1:NaEaA34bHNawPL3npJN8J7jyQhA3eG+UQ0xZvTnOfYo=
9696
github.com/kcp-dev/controller-runtime v0.19.0-kcp.1.0.20250129100209-5eaf4c7b6056/go.mod h1:jwK5sBnpu/xJJ+xdpSzzI0aM52E/EvF0uLF9bR61h/Y=
97+
github.com/kcp-dev/kcp/pkg/apis v0.11.0 h1:K6p+tNHNcvfACCPLcHgY0EMLeaIwR1jS491FyLfXMII=
98+
github.com/kcp-dev/kcp/pkg/apis v0.11.0/go.mod h1:8cUAmfMJcksauz53UtsLYG8Phhx62rvuCnd/5t/Zihk=
9799
github.com/kcp-dev/kcp/sdk v0.27.0 h1:j32awoUYAJJGPKXDEWXZCrYsKWMBuL/7YnDDSLRxJvA=
98100
github.com/kcp-dev/kcp/sdk v0.27.0/go.mod h1:3eRgW42d81Ng60DbG1xbne0FSS2znpcN/GUx4rqJgUo=
99101
github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU=

listener/apischema/builder.go

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

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"k8s.io/apimachinery/pkg/api/meta"
78
"maps"
@@ -19,6 +20,17 @@ import (
1920
"k8s.io/kube-openapi/pkg/validation/spec"
2021
)
2122

23+
var (
24+
ErrGetOpenAPIPaths = errors.New("failed to get OpenAPI paths")
25+
ErrGetCRDGVK = errors.New("failed to get CRD GVK")
26+
ErrParseGroupVersion = errors.New("failed to parse groupVersion")
27+
ErrMarshalOpenAPISchema = errors.New("failed to marshal openAPI v3 runtimeSchema")
28+
ErrConvertOpenAPISchema = errors.New("failed to convert openAPI v3 runtimeSchema to v2")
29+
ErrCRDNoVersions = errors.New("CRD has no versions defined")
30+
ErrMarshalGVK = errors.New("failed to marshal GVK extension")
31+
ErrUnmarshalGVK = errors.New("failed to unmarshal GVK extension")
32+
)
33+
2234
// SchemaBuilder helps construct GraphQL field config arguments
2335
type SchemaBuilder struct {
2436
schemas map[string]*spec.Schema
@@ -32,7 +44,7 @@ func NewSchemaBuilder(oc openapi.Client, preferredApiGroups []string) *SchemaBui
3244

3345
apiv3Paths, err := oc.Paths()
3446
if err != nil {
35-
b.err = multierror.Append(b.err, fmt.Errorf("failed to get OpenAPI paths: %w", err))
47+
b.err = multierror.Append(b.err, errors.Join(ErrGetOpenAPIPaths, err))
3648
return b
3749
}
3850

@@ -66,14 +78,14 @@ func (b *SchemaBuilder) WithScope(rm meta.RESTMapper) *SchemaBuilder {
6678
if gvksVal, ok = schema.VendorExtensible.Extensions[common.GVKExtensionKey]; !ok {
6779
continue
6880
}
69-
b, err := json.Marshal(gvksVal)
81+
jsonBytes, err := json.Marshal(gvksVal)
7082
if err != nil {
71-
//TODO: debug log?
83+
b.err = multierror.Append(b.err, errors.Join(ErrMarshalGVK, err))
7284
continue
7385
}
7486
gvks := make([]*GroupVersionKind, 0, 1)
75-
if err := json.Unmarshal(b, &gvks); err != nil {
76-
//TODO: debug log?
87+
if err := json.Unmarshal(jsonBytes, &gvks); err != nil {
88+
b.err = multierror.Append(b.err, errors.Join(ErrUnmarshalGVK, err))
7789
continue
7890
}
7991

@@ -111,7 +123,7 @@ func (b *SchemaBuilder) WithCRDCategories(crd *apiextensionsv1.CustomResourceDef
111123
}
112124
gvk, err := getCRDGroupVersionKind(crd.Spec)
113125
if err != nil {
114-
b.err = multierror.Append(b.err, fmt.Errorf("failed to get CRD GVK: %w", err))
126+
b.err = multierror.Append(b.err, errors.Join(ErrGetCRDGVK, err))
115127
return b
116128
}
117129

@@ -134,7 +146,7 @@ func (b *SchemaBuilder) WithApiResourceCategories(list []*metav1.APIResourceList
134146

135147
gv, err := runtimeSchema.ParseGroupVersion(apiResourceList.GroupVersion)
136148
if err != nil {
137-
b.err = multierror.Append(b.err, fmt.Errorf("failed to parse groupVersion: %w", err))
149+
b.err = multierror.Append(b.err, errors.Join(ErrParseGroupVersion, err))
138150
continue
139151
}
140152
gvk := metav1.GroupVersionKind{
@@ -162,12 +174,12 @@ func (b *SchemaBuilder) Complete() ([]byte, error) {
162174
},
163175
})
164176
if err != nil {
165-
b.err = multierror.Append(b.err, fmt.Errorf("failed to marshal openAPI v3 runtimeSchema: %w", err))
177+
b.err = multierror.Append(b.err, errors.Join(ErrMarshalOpenAPISchema, err))
166178
return nil, b.err
167179
}
168180
v2JSON, err := ConvertJSON(v3JSON)
169181
if err != nil {
170-
b.err = multierror.Append(b.err, fmt.Errorf("failed to convert openAPI v3 runtimeSchema to v2: %w", err))
182+
b.err = multierror.Append(b.err, errors.Join(ErrConvertOpenAPISchema, err))
171183
return nil, b.err
172184
}
173185

@@ -185,7 +197,7 @@ func getOpenAPISchemaKey(gvk metav1.GroupVersionKind) string {
185197

186198
func getCRDGroupVersionKind(spec apiextensionsv1.CustomResourceDefinitionSpec) (*metav1.GroupVersionKind, error) {
187199
if len(spec.Versions) == 0 {
188-
return nil, fmt.Errorf("CRD has no versions defined")
200+
return nil, ErrCRDNoVersions
189201
}
190202

191203
// Use the first stored version as the preferred one

listener/apischema/crd_resolver.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package apischema
33
import (
44
"encoding/json"
55
"errors"
6-
"fmt"
76
"slices"
87
"strings"
98

@@ -18,9 +17,13 @@ import (
1817
)
1918

2019
var (
21-
ErrInvalidPath = errors.New("path doesn't contain the / separator")
22-
ErrNotPreferred = errors.New("path ApiGroup does not belong to the server preferred APIs")
23-
ErrGVKNotPreferred = errors.New("failed to find CRD GVK in API preferred resources")
20+
ErrInvalidPath = errors.New("path doesn't contain the / separator")
21+
ErrNotPreferred = errors.New("path ApiGroup does not belong to the server preferred APIs")
22+
ErrGVKNotPreferred = errors.New("failed to find CRD GVK in API preferred resources")
23+
ErrGetServerPreferred = errors.New("failed to get server preferred resources")
24+
ErrFilterPreferredResources = errors.New("failed to filter server preferred resources")
25+
ErrGetSchemaForPath = errors.New("failed to get schema for path")
26+
ErrUnmarshalSchemaForPath = errors.New("failed to unmarshal schema for path")
2427
)
2528

2629
type GroupKindVersions struct {
@@ -42,12 +45,12 @@ func (cr *CRDResolver) ResolveApiSchema(crd *apiextensionsv1.CustomResourceDefin
4245

4346
apiResLists, err := cr.ServerPreferredResources()
4447
if err != nil {
45-
return nil, fmt.Errorf("failed to get server preferred resources: %w", err)
48+
return nil, errors.Join(ErrGetServerPreferred, err)
4649
}
4750

4851
preferredApiGroups, err := errorIfCRDNotInPreferredApiGroups(gkv, apiResLists)
4952
if err != nil {
50-
return nil, fmt.Errorf("failed to filter server preferred resources: %w", err)
53+
return nil, errors.Join(ErrFilterPreferredResources, err)
5154
}
5255

5356
return NewSchemaBuilder(cr.OpenAPIV3(), preferredApiGroups).
@@ -114,20 +117,20 @@ func getSchemaForPath(preferredApiGroups []string, path string, gv openapi.Group
114117

115118
b, err := gv.Schema(discovery.AcceptV1)
116119
if err != nil {
117-
return nil, fmt.Errorf("failed to get schema for path %s :%w", path, err)
120+
return nil, errors.Join(ErrGetSchemaForPath, err)
118121
}
119122

120123
resp := &schemaResponse{}
121124
if err := json.Unmarshal(b, resp); err != nil {
122-
return nil, fmt.Errorf("failed to unmarshal schema for path %s :%w", path, err)
125+
return nil, errors.Join(ErrUnmarshalSchemaForPath, err)
123126
}
124127
return resp.Components.Schemas, nil
125128
}
126129

127130
func resolveSchema(dc discovery.DiscoveryInterface, rm meta.RESTMapper) ([]byte, error) {
128131
apiResList, err := dc.ServerPreferredResources()
129132
if err != nil {
130-
return nil, fmt.Errorf("failed to get server preferred resources: %w", err)
133+
return nil, errors.Join(ErrGetServerPreferred, err)
131134
}
132135

133136
var preferredApiGroups []string

listener/apischema/json_converter.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import (
44
"bytes"
55
"encoding/json"
66
"errors"
7-
"fmt"
87
"strings"
98
)
109

10+
var (
11+
ErrFailedToValidateConvertedJson = errors.New("failed to validate converted JSON")
12+
ErrUnmarshalJSON = errors.New("failed to unmarshal JSON")
13+
ErrEncodeJSON = errors.New("failed to encode JSON")
14+
)
15+
1116
type v3Wrapper struct {
1217
Schemas map[string]any `json:"schemas"`
1318
}
@@ -23,25 +28,27 @@ type v2RootWrapper struct {
2328
func ConvertJSON(v3JSON []byte) ([]byte, error) {
2429
data := &v3RootWrapper{}
2530
if err := json.Unmarshal(v3JSON, data); err != nil {
26-
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
31+
return nil, errors.Join(ErrUnmarshalJSON, err)
2732
}
2833

2934
v2JSON := parseJSON(data.Components.Schemas)
3035
v2, ok := v2JSON.(map[string]any)
3136
if !ok {
32-
return nil, errors.New("failed to validate converted JSON")
37+
return nil, ErrFailedToValidateConvertedJson
3338
}
3439
buf := &bytes.Buffer{}
3540
e := json.NewEncoder(buf)
3641
e.SetEscapeHTML(false)
3742
encErr := e.Encode(&v2RootWrapper{
3843
Definitions: v2,
3944
})
40-
return buf.Bytes(), encErr
45+
if encErr != nil {
46+
return nil, errors.Join(ErrEncodeJSON, encErr)
47+
}
48+
return buf.Bytes(), nil
4149
}
4250

4351
func parseJSON(data any) any {
44-
4552
v, ok := data.(map[string]any)
4653
if !ok {
4754
return data

0 commit comments

Comments
 (0)