Skip to content

Commit 878858c

Browse files
authored
More agnostic backend (#291)
* 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 * backend refactor * remove debug * lint * address reviews --------- Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
1 parent 3baf1a4 commit 878858c

19 files changed

Lines changed: 417 additions & 166 deletions

File tree

backend/http/handler.go

Lines changed: 148 additions & 45 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,
@@ -184,20 +190,24 @@ func generateCookieName(clusterID string) string {
184190
func (h *handler) handleAuthorize(w http.ResponseWriter, r *http.Request) {
185191
logger := getLogger(r)
186192

187-
cluster := mux.Vars(r)["cluster"]
193+
providerCluster := mux.Vars(r)["cluster"]
194+
195+
callbackPort := r.URL.Query().Get("p")
188196

189197
scopes := []string{"openid", "profile", "email", "offline_access"}
190198
code := &AuthCode{
191-
RedirectURL: r.URL.Query().Get("u"),
192-
SessionID: r.URL.Query().Get("s"),
193-
ClusterID: cluster,
199+
RedirectURL: r.URL.Query().Get("u"),
200+
SessionID: r.URL.Query().Get("s"),
201+
ClusterID: r.URL.Query().Get("c"),
202+
ProviderClusterID: providerCluster, // used in multicluster-runtime providers
194203
}
195-
if p := r.URL.Query().Get("p"); p != "" && code.RedirectURL == "" {
196-
code.RedirectURL = fmt.Sprintf("http://localhost:%s/callback", p)
204+
if callbackPort != "" && code.RedirectURL == "" {
205+
code.RedirectURL = fmt.Sprintf("http://localhost:%s/callback", callbackPort)
197206
}
198-
if code.RedirectURL == "" || code.SessionID == "" {
207+
208+
if code.RedirectURL == "" || code.SessionID == "" || code.ClusterID == "" {
199209
logger.Error(errors.New("missing redirect url or session id or cluster id"), "failed to authorize")
200-
http.Error(w, "missing redirect_url or session_id", http.StatusBadRequest)
210+
http.Error(w, "missing redirect_url, session_id or cluster_id", http.StatusBadRequest)
201211
return
202212
}
203213

@@ -279,10 +289,10 @@ func (h *handler) handleCallback(w http.ResponseWriter, r *http.Request) {
279289
secure := false
280290

281291
http.SetCookie(w, session.MakeCookie(r, cookieName, encoded, secure, 1*time.Hour))
282-
if authCode.ClusterID == "" {
283-
http.Redirect(w, r, "/resources", http.StatusFound)
292+
if authCode.ProviderClusterID == "" {
293+
http.Redirect(w, r, "/resources?s="+cookieName, http.StatusFound)
284294
} else {
285-
http.Redirect(w, r, "/clusters/"+authCode.ClusterID+"/resources", http.StatusFound)
295+
http.Redirect(w, r, "/clusters/"+authCode.ProviderClusterID+"/resources?s="+cookieName, http.StatusFound)
286296
}
287297
}
288298

@@ -328,59 +338,91 @@ func createSessionState(authCode *AuthCode, token *oauth2.Token) (*session.State
328338
}, nil
329339
}
330340

341+
type UISchema struct {
342+
Name string
343+
Version string
344+
Group string
345+
Kind string
346+
Scope string // "Namespaced" or "Cluster"
347+
Resource string
348+
349+
// SessionID
350+
SessionID string
351+
}
352+
331353
func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) {
332354
logger := getLogger(r)
333355

334356
prepareNoCache(w)
335357

336-
cluster := mux.Vars(r)["cluster"]
337-
singleClusterScoped := cluster == ""
358+
providerCluster := mux.Vars(r)["cluster"]
359+
sessionID := r.URL.Query().Get("s")
360+
singleClusterScoped := providerCluster == ""
338361

339362
if h.testingAutoSelect != "" {
340363
parts := strings.SplitN(h.testingAutoSelect, ".", 2)
341364
if singleClusterScoped {
342365
http.Redirect(w, r, "/resources/"+parts[0]+"/"+parts[1], http.StatusFound)
343366
} else {
344-
http.Redirect(w, r, "/clusters/"+cluster+"/resources/"+parts[0]+"/"+parts[1], http.StatusFound)
367+
http.Redirect(w, r, "/clusters/"+providerCluster+"/resources/"+parts[0]+"/"+parts[1], http.StatusFound)
345368
}
346369
return
347370
}
348371

349-
labelSelector := labels.Set{
350-
resources.ExportedCRDsLabel: "true",
351-
}
352-
353-
crds, err := h.kubeManager.ListCustomResourceDefinitions(r.Context(), cluster, labelSelector.AsSelector())
372+
apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster)
354373
if err != nil {
355-
logger.Error(err, "failed to list crds")
374+
logger.Error(err, "failed to get dynamic resources")
356375
http.Error(w, "internal error", http.StatusInternalServerError)
357376
return
358377
}
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)
378+
379+
var result []UISchema
380+
for _, item := range apiResourceSchemas.Items {
381+
scope := item.UnstructuredContent()["spec"].(map[string]interface{})["scope"]
382+
if scope == nil {
383+
scope = "-"
366384
}
367-
}
368385

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
386+
group := item.UnstructuredContent()["spec"].(map[string]interface{})["group"]
387+
if group == nil {
388+
group = "-"
389+
}
390+
resource := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["plural"]
391+
if resource == nil {
392+
resource = "-"
393+
}
394+
395+
kind := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["kind"]
396+
if kind == nil {
397+
kind = "-"
398+
}
399+
400+
versions := item.UnstructuredContent()["spec"].(map[string]interface{})["versions"]
401+
if versions == nil {
402+
versions = []interface{}{""}
403+
}
404+
for _, v := range versions.([]interface{}) {
405+
version := v.(map[string]interface{})["name"]
406+
result = append(result, UISchema{
407+
Name: item.GetName(),
408+
Kind: kind.(string),
409+
Scope: scope.(string),
410+
Version: version.(string),
411+
Group: group.(string),
412+
// Important: This MUST be used as UI button class in the url, so tests can 'click it' based on it.
413+
Resource: resource.(string),
414+
SessionID: sessionID,
415+
})
416+
}
374417
}
418+
375419
bs := bytes.Buffer{}
376420
if err := resourcesTemplate.Execute(&bs, struct {
377-
Cluster string
378-
CRDs []*apiextensionsv1.CustomResourceDefinition
379-
APIResourceSchemas []kubebindv1alpha2.APIResourceSchema
421+
Cluster string
422+
Schemas []UISchema
380423
}{
381-
Cluster: cluster,
382-
CRDs: rightScopedCRDs,
383-
APIResourceSchemas: apiResourceSchemas.Items,
424+
Cluster: providerCluster,
425+
Schemas: result,
384426
}); err != nil {
385427
logger.Error(err, "failed to execute template")
386428
http.Error(w, "internal error", http.StatusInternalServerError)
@@ -393,14 +435,16 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) {
393435

394436
func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
395437
logger := getLogger(r)
438+
name := r.URL.Query().Get("name")
396439
group := r.URL.Query().Get("group")
397440
resource := r.URL.Query().Get("resource")
398-
cluster := mux.Vars(r)["cluster"]
441+
version := r.URL.Query().Get("version")
442+
providerCluster := mux.Vars(r)["cluster"]
399443

400444
prepareNoCache(w)
401445

402-
cookieName := generateCookieName(cluster)
403-
ck, err := r.Cookie(cookieName)
446+
cookieName := r.URL.Query().Get("s")
447+
ck, err := r.Cookie(r.URL.Query().Get("s"))
404448
if err != nil {
405449
logger.Error(err, "failed to get session cookie")
406450
http.Error(w, "no session cookie found", http.StatusBadRequest)
@@ -415,7 +459,38 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
415459
return
416460
}
417461

418-
kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, cluster, resource, group)
462+
// There is an intent to bind. We need to create APIResourceSchema if one does not exists.
463+
{
464+
apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster)
465+
if err != nil {
466+
logger.Error(err, "failed to get dynamic resources")
467+
http.Error(w, "internal error", http.StatusInternalServerError)
468+
return
469+
}
470+
471+
schema := &unstructured.Unstructured{}
472+
for _, item := range apiResourceSchemas.Items {
473+
if item.GetName() == name {
474+
schema = &item
475+
break
476+
}
477+
}
478+
if schema == nil || schema.GetName() != name {
479+
logger.Error(nil, "no APIResourceSchema found", "name", name, "group", group, "resource", resource, "version", version)
480+
http.Error(w, fmt.Sprintf("no APIResourceSchema found for %s.%s.%s/%s", group, resource, version, name), http.StatusNotFound)
481+
return
482+
}
483+
484+
// create apiResourceSchema if not exists
485+
err = h.kubeManager.CreateAPIResourceSchema(r.Context(), providerCluster, name, schema)
486+
if err != nil && !apierrors.IsAlreadyExists(err) {
487+
logger.Error(err, "failed to create APIResourceSchema")
488+
http.Error(w, "internal error", http.StatusInternalServerError)
489+
return
490+
}
491+
}
492+
493+
kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, providerCluster)
419494
if err != nil {
420495
logger.Error(err, "failed to handle resources")
421496
http.Error(w, "internal error", http.StatusInternalServerError)
@@ -435,7 +510,10 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
435510
},
436511
Spec: kubebindv1alpha2.APIServiceExportRequestSpec{
437512
Resources: []kubebindv1alpha2.APIServiceExportRequestResource{
438-
{GroupResource: kubebindv1alpha2.GroupResource{Group: group, Resource: resource}},
513+
{
514+
GroupResource: kubebindv1alpha2.GroupResource{Group: group, Resource: resource},
515+
Versions: []string{version},
516+
},
439517
},
440518
},
441519
}
@@ -495,3 +573,28 @@ func mustRead(f func(name string) ([]byte, error), name string) string {
495573
}
496574
return string(bs)
497575
}
576+
577+
func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) (*unstructured.UnstructuredList, error) {
578+
labelSelector := labels.Set{
579+
resources.ExportedCRDsLabel: "true",
580+
}
581+
582+
parts := strings.SplitN(h.schemaSource, ".", 3)
583+
if len(parts) != 3 { // We check this in validation, but just in case.
584+
return nil, fmt.Errorf("invalid schema source: %q", h.schemaSource)
585+
}
586+
587+
gvk := schema.GroupVersionKind{
588+
Kind: parts[0],
589+
Version: parts[1],
590+
Group: parts[2],
591+
}
592+
apiResourceSchemas, err := h.kubeManager.ListDynamicResources(ctx, cluster, gvk, labelSelector.AsSelector())
593+
if err != nil {
594+
return nil, fmt.Errorf("failed to list crds: %w", err)
595+
}
596+
sort.SliceStable(apiResourceSchemas.Items, func(i, j int) bool {
597+
return apiResourceSchemas.Items[i].GetName() < apiResourceSchemas.Items[j].GetName()
598+
})
599+
return apiResourceSchemas, nil
600+
}

backend/http/oidc.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import (
2626
// AuthCode is sent and received by to/from the OIDC provider. It's the state
2727
// we can use to map the OIDC provider's response to the request from the client.
2828
type AuthCode struct {
29-
RedirectURL string `json:"redirectURL"`
30-
SessionID string `json:"sid"`
31-
ClusterID string `json:"cid"`
29+
RedirectURL string `json:"redirectURL"`
30+
SessionID string `json:"sid"`
31+
ClusterID string `json:"cid"`
32+
ProviderClusterID string `json:"pcid"`
3233
}
3334

3435
type OIDCServiceProvider struct {

0 commit comments

Comments
 (0)