Skip to content

Commit 92dad14

Browse files
committed
Rafactor backend to be less schema aware (moving towards schemaless)
Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt> On-behalf-of: @SAP mangirdas.judeikis@sap.com
1 parent 3baf1a4 commit 92dad14

11 files changed

Lines changed: 315 additions & 78 deletions

File tree

backend/http/handler.go

Lines changed: 122 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package http
1818

1919
import (
2020
"bytes"
21+
"context"
2122
"encoding/base64"
2223
"encoding/json"
2324
"errors"
@@ -32,10 +33,12 @@ import (
3233
"github.com/gorilla/mux"
3334
"github.com/gorilla/securecookie"
3435
"golang.org/x/oauth2"
35-
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
36+
apierrors "k8s.io/apimachinery/pkg/api/errors"
3637
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3739
"k8s.io/apimachinery/pkg/labels"
3840
"k8s.io/apimachinery/pkg/runtime"
41+
"k8s.io/apimachinery/pkg/runtime/schema"
3942
componentbaseversion "k8s.io/component-base/version"
4043
"k8s.io/klog/v2"
4144

@@ -66,6 +69,7 @@ type handler struct {
6669
backendCallbackURL string
6770
providerPrettyName string
6871
testingAutoSelect string
72+
schemaSource string
6973

7074
cookieEncryptionKey []byte
7175
cookieSigningKey []byte
@@ -78,6 +82,7 @@ func NewHandler(
7882
provider *OIDCServiceProvider,
7983
oidcAuthorizeURL, backendCallbackURL, providerPrettyName, testingAutoSelect string,
8084
cookieSigningKey, cookieEncryptionKey []byte,
85+
schemaSource string,
8186
scope kubebindv1alpha2.InformerScope,
8287
mgr *kubernetes.Manager,
8388
) (*handler, error) {
@@ -87,6 +92,7 @@ func NewHandler(
8792
backendCallbackURL: backendCallbackURL,
8893
providerPrettyName: providerPrettyName,
8994
testingAutoSelect: testingAutoSelect,
95+
schemaSource: schemaSource,
9096
scope: scope,
9197
client: http.DefaultClient,
9298
kubeManager: mgr,
@@ -328,6 +334,15 @@ func createSessionState(authCode *AuthCode, token *oauth2.Token) (*session.State
328334
}, nil
329335
}
330336

337+
type UISchema struct {
338+
Name string
339+
Version string
340+
Group string
341+
Kind string
342+
Scope string // "Namespaced" or "Cluster"
343+
Resource string
344+
}
345+
331346
func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) {
332347
logger := getLogger(r)
333348

@@ -346,41 +361,59 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) {
346361
return
347362
}
348363

349-
labelSelector := labels.Set{
350-
resources.ExportedCRDsLabel: "true",
351-
}
352-
353-
crds, err := h.kubeManager.ListCustomResourceDefinitions(r.Context(), cluster, labelSelector.AsSelector())
364+
apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), cluster)
354365
if err != nil {
355-
logger.Error(err, "failed to list crds")
366+
logger.Error(err, "failed to get dynamic resources")
356367
http.Error(w, "internal error", http.StatusInternalServerError)
357368
return
358369
}
359-
sort.SliceStable(crds.Items, func(i, j int) bool {
360-
return crds.Items[i].Name < crds.Items[j].Name
361-
})
362-
rightScopedCRDs := []*apiextensionsv1.CustomResourceDefinition{}
363-
for _, crd := range crds.Items {
364-
if h.scope == kubebindv1alpha2.ClusterScope || crd.Spec.Scope == apiextensionsv1.NamespaceScoped {
365-
rightScopedCRDs = append(rightScopedCRDs, &crd)
370+
371+
var result []UISchema
372+
for _, item := range apiResourceSchemas.Items {
373+
scope := item.UnstructuredContent()["spec"].(map[string]interface{})["scope"]
374+
if scope == nil {
375+
scope = "-"
366376
}
367-
}
368377

369-
apiResourceSchemas, err := h.kubeManager.ListAPIResourceSchemas(r.Context(), cluster)
370-
if err != nil {
371-
logger.Error(err, "failed to get api resource schemas")
372-
http.Error(w, "internal error", http.StatusInternalServerError)
373-
return
378+
group := item.UnstructuredContent()["spec"].(map[string]interface{})["group"]
379+
if group == nil {
380+
group = "-"
381+
}
382+
resource := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["plural"]
383+
if resource == nil {
384+
resource = "-"
385+
}
386+
387+
kind := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["kind"]
388+
if kind == nil {
389+
kind = "-"
390+
}
391+
392+
versions := item.UnstructuredContent()["spec"].(map[string]interface{})["versions"]
393+
if versions == nil {
394+
versions = []interface{}{""}
395+
}
396+
for _, v := range versions.([]interface{}) {
397+
version := v.(map[string]interface{})["name"]
398+
result = append(result, UISchema{
399+
Name: item.GetName(),
400+
Kind: kind.(string),
401+
Scope: scope.(string),
402+
Version: version.(string),
403+
Group: group.(string),
404+
// Important: This MUST be used as UI button class in the url, so tests can 'click it' based on it.
405+
Resource: resource.(string),
406+
})
407+
}
374408
}
409+
375410
bs := bytes.Buffer{}
376411
if err := resourcesTemplate.Execute(&bs, struct {
377-
Cluster string
378-
CRDs []*apiextensionsv1.CustomResourceDefinition
379-
APIResourceSchemas []kubebindv1alpha2.APIResourceSchema
412+
Cluster string
413+
Schemas []UISchema
380414
}{
381-
Cluster: cluster,
382-
CRDs: rightScopedCRDs,
383-
APIResourceSchemas: apiResourceSchemas.Items,
415+
Cluster: cluster,
416+
Schemas: result,
384417
}); err != nil {
385418
logger.Error(err, "failed to execute template")
386419
http.Error(w, "internal error", http.StatusInternalServerError)
@@ -393,8 +426,10 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) {
393426

394427
func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
395428
logger := getLogger(r)
429+
name := r.URL.Query().Get("name")
396430
group := r.URL.Query().Get("group")
397431
resource := r.URL.Query().Get("resource")
432+
version := r.URL.Query().Get("version")
398433
cluster := mux.Vars(r)["cluster"]
399434

400435
prepareNoCache(w)
@@ -415,7 +450,38 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
415450
return
416451
}
417452

418-
kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, cluster, resource, group)
453+
// There is an intent to bind. We need to create APIResourceSchema if one does not exists.
454+
{
455+
apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), cluster)
456+
if err != nil {
457+
logger.Error(err, "failed to get dynamic resources")
458+
http.Error(w, "internal error", http.StatusInternalServerError)
459+
return
460+
}
461+
462+
schema := &unstructured.Unstructured{}
463+
for _, item := range apiResourceSchemas.Items {
464+
if item.GetName() == name {
465+
schema = &item
466+
break
467+
}
468+
}
469+
if schema == nil || schema.GetName() != name {
470+
logger.Error(nil, "no APIResourceSchema found", "name", name, "group", group, "resource", resource, "version", version)
471+
http.Error(w, fmt.Sprintf("no APIResourceSchema found for %s.%s.%s/%s", group, resource, version, name), http.StatusNotFound)
472+
return
473+
}
474+
475+
// create apiResourceSchema if not exists
476+
err = h.kubeManager.CreateAPIResourceSchema(r.Context(), cluster, name, schema)
477+
if err != nil && !apierrors.IsAlreadyExists(err) {
478+
logger.Error(err, "failed to create APIResourceSchema")
479+
http.Error(w, "internal error", http.StatusInternalServerError)
480+
return
481+
}
482+
}
483+
484+
kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, cluster)
419485
if err != nil {
420486
logger.Error(err, "failed to handle resources")
421487
http.Error(w, "internal error", http.StatusInternalServerError)
@@ -435,7 +501,10 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
435501
},
436502
Spec: kubebindv1alpha2.APIServiceExportRequestSpec{
437503
Resources: []kubebindv1alpha2.APIServiceExportRequestResource{
438-
{GroupResource: kubebindv1alpha2.GroupResource{Group: group, Resource: resource}},
504+
{
505+
GroupResource: kubebindv1alpha2.GroupResource{Group: group, Resource: resource},
506+
Versions: []string{version},
507+
},
439508
},
440509
},
441510
}
@@ -495,3 +564,28 @@ func mustRead(f func(name string) ([]byte, error), name string) string {
495564
}
496565
return string(bs)
497566
}
567+
568+
func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) (*unstructured.UnstructuredList, error) {
569+
labelSelector := labels.Set{
570+
resources.ExportedCRDsLabel: "true",
571+
}
572+
573+
parts := strings.SplitN(h.schemaSource, ".", 3)
574+
if len(parts) != 3 {
575+
return nil, fmt.Errorf("invalid schema source: %q", h.schemaSource)
576+
}
577+
578+
gvk := schema.GroupVersionKind{
579+
Kind: parts[0],
580+
Version: parts[1],
581+
Group: parts[2],
582+
}
583+
apiResourceSchemas, err := h.kubeManager.ListDynamicResources(ctx, cluster, gvk, labelSelector.AsSelector())
584+
if err != nil {
585+
return nil, fmt.Errorf("failed to list crds: %w", err)
586+
}
587+
sort.SliceStable(apiResourceSchemas.Items, func(i, j int) bool {
588+
return apiResourceSchemas.Items[i].GetName() < apiResourceSchemas.Items[j].GetName()
589+
})
590+
return apiResourceSchemas, nil
591+
}

backend/kubernetes/manager.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ package kubernetes
1919
import (
2020
"context"
2121
"fmt"
22+
"strings"
2223

2324
corev1 "k8s.io/api/core/v1"
2425
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2526
"k8s.io/apimachinery/pkg/api/errors"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2628
"k8s.io/apimachinery/pkg/labels"
29+
"k8s.io/apimachinery/pkg/runtime"
30+
"k8s.io/apimachinery/pkg/runtime/schema"
2731
"k8s.io/apimachinery/pkg/types"
2832
"k8s.io/klog/v2"
2933
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -36,6 +40,7 @@ import (
3640
type Manager struct {
3741
namespacePrefix string
3842
providerPrettyName string
43+
scope kubebindv1alpha2.InformerScope
3944

4045
externalAddressGenerator kuberesources.ExternalAddreesGeneratorFunc
4146
externalCA []byte
@@ -48,13 +53,15 @@ func NewKubernetesManager(
4853
ctx context.Context,
4954
namespacePrefix, providerPrettyName string,
5055
externalAddressGenerator kuberesources.ExternalAddreesGeneratorFunc,
56+
scope kubebindv1alpha2.InformerScope,
5157
externalCA []byte,
5258
externalTLSServerName string,
5359
manager mcmanager.Manager,
5460
) (*Manager, error) {
5561
m := &Manager{
5662
namespacePrefix: namespacePrefix,
5763
providerPrettyName: providerPrettyName,
64+
scope: scope,
5865

5966
externalAddressGenerator: externalAddressGenerator,
6067
externalCA: externalCA,
@@ -71,8 +78,8 @@ func NewKubernetesManager(
7178
return m, nil
7279
}
7380

74-
func (m *Manager) HandleResources(ctx context.Context, identity, cluster, resource, group string) ([]byte, error) {
75-
logger := klog.FromContext(ctx).WithValues("identity", identity, "resource", resource, "group", group)
81+
func (m *Manager) HandleResources(ctx context.Context, identity, cluster string) ([]byte, error) {
82+
logger := klog.FromContext(ctx).WithValues("identity", identity)
7683
ctx = klog.NewContext(ctx, logger)
7784

7885
cl, err := m.manager.GetCluster(ctx, cluster)
@@ -173,3 +180,51 @@ func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster str
173180

174181
return &crds, nil
175182
}
183+
184+
func (m *Manager) ListDynamicResources(ctx context.Context, cluster string, gvk schema.GroupVersionKind, selector labels.Selector) (*unstructured.UnstructuredList, error) {
185+
cl, err := m.manager.GetCluster(ctx, cluster)
186+
if err != nil {
187+
return nil, err
188+
}
189+
c := cl.GetClient()
190+
191+
// Ensure we have the List kind
192+
listGVK := gvk
193+
if !strings.HasSuffix(listGVK.Kind, "List") {
194+
listGVK.Kind += "List"
195+
}
196+
197+
list := &unstructured.UnstructuredList{}
198+
list.SetGroupVersionKind(listGVK)
199+
200+
listOpts := []client.ListOption{}
201+
if selector != nil {
202+
listOpts = append(listOpts, client.MatchingLabelsSelector{Selector: selector})
203+
}
204+
205+
if err := c.List(ctx, list, listOpts...); err != nil {
206+
return nil, err
207+
}
208+
209+
return list, nil
210+
}
211+
212+
func (m *Manager) CreateAPIResourceSchema(ctx context.Context, cluster string, name string, u *unstructured.Unstructured) error {
213+
cl, err := m.manager.GetCluster(ctx, cluster)
214+
if err != nil {
215+
return err
216+
}
217+
c := cl.GetClient()
218+
219+
apiResourceSchema := &kubebindv1alpha2.APIResourceSchema{}
220+
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), apiResourceSchema)
221+
if err != nil {
222+
return err
223+
}
224+
225+
apiResourceSchema.ResourceVersion = ""
226+
apiResourceSchema.Name = name
227+
apiResourceSchema.Spec.InformerScope = m.scope
228+
229+
return c.Create(ctx, apiResourceSchema)
230+
}

0 commit comments

Comments
 (0)