@@ -18,6 +18,7 @@ package http
1818
1919import (
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 {
184190func (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+
331353func (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
394436func (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+ }
0 commit comments