Skip to content

Commit ba7977b

Browse files
authored
Feat: Listener: Expose scope information in x-openmfp-scope schema extension (#92)
* expose scope information in x-openmfp-namespaced extension * expose scope information in x-openmfp-namespaced extension * review suggestion: switch to k8s convention: use x-openmfp-scope with an enum value
1 parent 52b3349 commit ba7977b

File tree

5 files changed

+132
-12
lines changed

5 files changed

+132
-12
lines changed

listener/apischema/crd_resolver.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
12+
"k8s.io/apimachinery/pkg/api/meta"
1213
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1314
"k8s.io/client-go/discovery"
1415
"k8s.io/client-go/openapi"
@@ -23,10 +24,11 @@ var (
2324

2425
type CRDResolver struct {
2526
*discovery.DiscoveryClient
27+
meta.RESTMapper
2628
}
2729

2830
func (cr *CRDResolver) Resolve() ([]byte, error) {
29-
return resolveSchema(cr.DiscoveryClient)
31+
return resolveSchema(cr.DiscoveryClient, cr.RESTMapper)
3032
}
3133

3234
func (cr *CRDResolver) ResolveApiSchema(crd *apiextensionsv1.CustomResourceDefinition) ([]byte, error) {
@@ -45,7 +47,7 @@ func (cr *CRDResolver) ResolveApiSchema(crd *apiextensionsv1.CustomResourceDefin
4547
return nil, fmt.Errorf("failed to filter server preferred resources: %w", err)
4648
}
4749

48-
return resolveForPaths(cr.OpenAPIV3(), preferredApiGroups)
50+
return resolveForPaths(cr.OpenAPIV3(), preferredApiGroups, cr.RESTMapper)
4951
}
5052

5153
func errorIfCRDNotInPreferredApiGroups(gvk *metav1.GroupVersionKind, apiResLists []*metav1.APIResourceList) ([]string, error) {
@@ -112,7 +114,7 @@ func getSchemaForPath(preferredApiGroups []string, path string, gv openapi.Group
112114
return resp.Components.Schemas, nil
113115
}
114116

115-
func resolveForPaths(oc openapi.Client, preferredApiGroups []string) ([]byte, error) {
117+
func resolveForPaths(oc openapi.Client, preferredApiGroups []string, rm meta.RESTMapper) ([]byte, error) {
116118
apiv3Paths, err := oc.Paths()
117119
if err != nil {
118120
return nil, fmt.Errorf("failed to get OpenAPI paths: %w", err)
@@ -127,23 +129,29 @@ func resolveForPaths(oc openapi.Client, preferredApiGroups []string) ([]byte, er
127129
}
128130
maps.Copy(schemas, schema)
129131
}
132+
133+
scopedSchemas, err := addScopeInfo(schemas, rm)
134+
if err != nil {
135+
return nil, fmt.Errorf("failed to add scope info to v3 schema: %w", err)
136+
}
137+
130138
v3JSON, err := json.Marshal(&schemaResponse{
131139
Components: schemasComponentsWrapper{
132-
Schemas: schemas,
140+
Schemas: scopedSchemas,
133141
},
134142
})
135143
if err != nil {
136144
return nil, fmt.Errorf("failed to marshal openAPI v3 schema: %w", err)
137145
}
138-
v2JSON, err := ConvertJSON(v3JSON)
146+
v2JSON, err := convertJSON(v3JSON)
139147
if err != nil {
140148
return nil, fmt.Errorf("failed to convert openAPI v3 schema to v2: %w", err)
141149
}
142150

143151
return v2JSON, nil
144152
}
145153

146-
func resolveSchema(dc discovery.DiscoveryInterface) ([]byte, error) {
154+
func resolveSchema(dc discovery.DiscoveryInterface, rm meta.RESTMapper) ([]byte, error) {
147155
preferredApiGroups := []string{}
148156
apiResList, err := dc.ServerPreferredResources()
149157
if err != nil {
@@ -153,5 +161,5 @@ func resolveSchema(dc discovery.DiscoveryInterface) ([]byte, error) {
153161
preferredApiGroups = append(preferredApiGroups, apiRes.GroupVersion)
154162
}
155163

156-
return resolveForPaths(dc.OpenAPIV3(), preferredApiGroups)
164+
return resolveForPaths(dc.OpenAPIV3(), preferredApiGroups, rm)
157165
}

listener/apischema/json_converter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type v2RootWrapper struct {
2020
Definitions map[string]any `json:"definitions"`
2121
}
2222

23-
func ConvertJSON(v3JSON []byte) ([]byte, error) {
23+
func convertJSON(v3JSON []byte) ([]byte, error) {
2424
data := &v3RootWrapper{}
2525
if err := json.Unmarshal(v3JSON, data); err != nil {
2626
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)

listener/apischema/resolver.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package apischema
22

33
import (
4+
"errors"
5+
6+
"k8s.io/apimachinery/pkg/api/meta"
47
"k8s.io/client-go/discovery"
58
"k8s.io/kube-openapi/pkg/validation/spec"
69
)
@@ -21,13 +24,17 @@ type Resolver interface {
2124
Resolve(dc discovery.DiscoveryInterface) ([]byte, error)
2225
}
2326

24-
func NewResolver() *ResolverImpl {
25-
return &ResolverImpl{}
27+
func NewResolver(rm meta.RESTMapper) (*ResolverImpl, error) {
28+
if rm == nil {
29+
return nil, errors.New("rest mapper might not be nil")
30+
}
31+
return &ResolverImpl{RESTMapper: rm}, nil
2632
}
2733

2834
type ResolverImpl struct {
35+
meta.RESTMapper
2936
}
3037

3138
func (r *ResolverImpl) Resolve(dc discovery.DiscoveryInterface) ([]byte, error) {
32-
return resolveSchema(dc)
39+
return resolveSchema(dc, r.RESTMapper)
3340
}

listener/apischema/scope_resolver.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package apischema
2+
3+
import (
4+
"encoding/json"
5+
6+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
7+
"k8s.io/apimachinery/pkg/api/meta"
8+
k8sschema "k8s.io/apimachinery/pkg/runtime/schema"
9+
"k8s.io/kube-openapi/pkg/validation/spec"
10+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
11+
)
12+
13+
const (
14+
gvkExtensionKey = "x-kubernetes-group-version-kind"
15+
scopeExtensionKey = "x-openmfp-scope"
16+
)
17+
18+
type GroupVersionKind struct {
19+
Group string `json:"group"`
20+
Version string `json:"version"`
21+
Kind string `json:"kind"`
22+
}
23+
24+
func addScopeInfo(schemas map[string]*spec.Schema, rm meta.RESTMapper) (map[string]*spec.Schema, error) {
25+
scopedSchemas := make(map[string]*spec.Schema)
26+
for name, schema := range schemas {
27+
//skip resources that do not have the GVK extension:
28+
//assumption: sub-resources do not have GVKs
29+
if schema.VendorExtensible.Extensions == nil {
30+
continue
31+
}
32+
var gvksVal any
33+
var ok bool
34+
if gvksVal, ok = schema.VendorExtensible.Extensions[gvkExtensionKey]; !ok {
35+
continue
36+
}
37+
b, err := json.Marshal(gvksVal)
38+
if err != nil {
39+
//TODO: debug log?
40+
continue
41+
}
42+
gvks := make([]*GroupVersionKind, 0, 1)
43+
if err := json.Unmarshal(b, &gvks); err != nil {
44+
//TODO: debug log?
45+
continue
46+
}
47+
48+
if len(gvks) != 1 {
49+
//TODO: debug log?
50+
continue
51+
}
52+
53+
gvk := gvks[0]
54+
55+
namespaced, err := apiutil.IsGVKNamespaced(k8sschema.GroupVersionKind{
56+
Group: gvk.Group,
57+
Version: gvk.Version,
58+
Kind: gvk.Kind,
59+
}, rm)
60+
61+
if err != nil {
62+
//TODO: debug log?
63+
continue
64+
}
65+
66+
if namespaced {
67+
schema.VendorExtensible.AddExtension(scopeExtensionKey, apiextensionsv1.NamespaceScoped)
68+
} else {
69+
schema.VendorExtensible.AddExtension(scopeExtensionKey, apiextensionsv1.ClusterScoped)
70+
}
71+
72+
scopedSchemas[name] = schema
73+
}
74+
return scopedSchemas, nil
75+
}

listener/kcp/reconciler_factory.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import (
1010
"github.com/openmfp/crd-gql-gateway/listener/discoveryclient"
1111
"github.com/openmfp/crd-gql-gateway/listener/flags"
1212
"github.com/openmfp/crd-gql-gateway/listener/workspacefile"
13+
"k8s.io/apimachinery/pkg/api/meta"
1314
"k8s.io/apimachinery/pkg/runtime"
1415
"k8s.io/client-go/discovery"
1516
"k8s.io/client-go/rest"
1617
ctrl "sigs.k8s.io/controller-runtime"
1718
"sigs.k8s.io/controller-runtime/pkg/client"
19+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
1820
)
1921

2022
const kubernetesClusterName = "kubernetes"
@@ -56,8 +58,14 @@ func NewReconciler(opts ReconcilerOpts) (CustomReconciler, error) {
5658
return nil, fmt.Errorf("failed to create IO Handler: %w", err)
5759
}
5860

61+
rm, err := restMapperFromConfig(opts.Config)
62+
if err != nil {
63+
return nil, fmt.Errorf("failed to create rest mapper from config: %w", err)
64+
}
65+
5966
schemaResolver := &apischema.CRDResolver{
6067
DiscoveryClient: dc,
68+
RESTMapper: rm,
6169
}
6270

6371
if err := preReconcile(schemaResolver, ioHandler); err != nil {
@@ -96,11 +104,33 @@ func NewKcpReconciler(opts ReconcilerOpts) (CustomReconciler, error) {
96104
return nil, fmt.Errorf("failed to create Discovery client factory: %w", err)
97105
}
98106

107+
rm, err := restMapperFromConfig(opts.Config)
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to create rest mapper from config: %w", err)
110+
}
111+
112+
sc, err := apischema.NewResolver(rm)
113+
if err != nil {
114+
return nil, fmt.Errorf("failed to create schema resolver: %w", err)
115+
}
116+
99117
return controller.NewAPIBindingReconciler(
100-
ioHandler, df, apischema.NewResolver(), &clusterpath.Resolver{
118+
ioHandler, df, sc, &clusterpath.Resolver{
101119
Scheme: opts.Scheme,
102120
Config: opts.Config,
103121
ResolverFunc: clusterpath.Resolve,
104122
},
105123
), nil
106124
}
125+
126+
func restMapperFromConfig(cfg *rest.Config) (meta.RESTMapper, error) {
127+
httpClt, err := rest.HTTPClientFor(cfg)
128+
if err != nil {
129+
return nil, fmt.Errorf("failed to create http client: %w", err)
130+
}
131+
rm, err := apiutil.NewDynamicRESTMapper(cfg, httpClt)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to create rest mapper: %w", err)
134+
}
135+
return rm, nil
136+
}

0 commit comments

Comments
 (0)