Skip to content

Commit 4767315

Browse files
committed
MVP: More agnostic backend
1 parent 3baf1a4 commit 4767315

5 files changed

Lines changed: 219 additions & 51 deletions

File tree

backend/http/handler.go

Lines changed: 131 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,11 @@ 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"
3636
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3738
"k8s.io/apimachinery/pkg/labels"
3839
"k8s.io/apimachinery/pkg/runtime"
40+
"k8s.io/apimachinery/pkg/runtime/schema"
3941
componentbaseversion "k8s.io/component-base/version"
4042
"k8s.io/klog/v2"
4143

@@ -66,6 +68,7 @@ type handler struct {
6668
backendCallbackURL string
6769
providerPrettyName string
6870
testingAutoSelect string
71+
schemaSource string
6972

7073
cookieEncryptionKey []byte
7174
cookieSigningKey []byte
@@ -78,6 +81,7 @@ func NewHandler(
7881
provider *OIDCServiceProvider,
7982
oidcAuthorizeURL, backendCallbackURL, providerPrettyName, testingAutoSelect string,
8083
cookieSigningKey, cookieEncryptionKey []byte,
84+
schemaSource string,
8185
scope kubebindv1alpha2.InformerScope,
8286
mgr *kubernetes.Manager,
8387
) (*handler, error) {
@@ -87,6 +91,7 @@ func NewHandler(
8791
backendCallbackURL: backendCallbackURL,
8892
providerPrettyName: providerPrettyName,
8993
testingAutoSelect: testingAutoSelect,
94+
schemaSource: schemaSource,
9095
scope: scope,
9196
client: http.DefaultClient,
9297
kubeManager: mgr,
@@ -328,6 +333,15 @@ func createSessionState(authCode *AuthCode, token *oauth2.Token) (*session.State
328333
}, nil
329334
}
330335

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

@@ -346,41 +360,69 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) {
346360
return
347361
}
348362

349-
labelSelector := labels.Set{
350-
resources.ExportedCRDsLabel: "true",
351-
}
352-
353-
crds, err := h.kubeManager.ListCustomResourceDefinitions(r.Context(), cluster, labelSelector.AsSelector())
363+
apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), cluster)
354364
if err != nil {
355-
logger.Error(err, "failed to list crds")
365+
logger.Error(err, "failed to get dynamic resources")
356366
http.Error(w, "internal error", http.StatusInternalServerError)
357367
return
358368
}
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)
369+
370+
var result []UISchema
371+
for _, item := range apiResourceSchemas.Items {
372+
scope := item.UnstructuredContent()["spec"].(map[string]interface{})["scope"]
373+
if scope == nil {
374+
scope = "-"
375+
}
376+
group := item.UnstructuredContent()["spec"].(map[string]interface{})["group"]
377+
if group == nil {
378+
group = "-"
379+
}
380+
resource := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["plural"]
381+
if resource == nil {
382+
resource = "-"
366383
}
367-
}
368384

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
385+
kind := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["kind"]
386+
if kind == nil {
387+
kind = "-"
388+
}
389+
namespace := item.GetNamespace()
390+
if namespace == "" {
391+
namespace = "-"
392+
}
393+
394+
versions := item.UnstructuredContent()["spec"].(map[string]interface{})["versions"]
395+
if versions == nil {
396+
versions = []interface{}{""}
397+
result = append(result, UISchema{
398+
Name: item.GetName(),
399+
Scope: scope.(string),
400+
Version: "-", // we can't parse it.
401+
Group: group.(string),
402+
Resource: resource.(string),
403+
})
404+
continue
405+
}
406+
for _, v := range versions.([]interface{}) {
407+
version := v.(map[string]interface{})["name"]
408+
result = append(result, UISchema{
409+
Name: item.GetName(),
410+
Kind: kind.(string),
411+
Scope: scope.(string),
412+
Version: version.(string),
413+
Group: group.(string),
414+
Resource: resource.(string),
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: cluster,
425+
Schemas: result,
384426
}); err != nil {
385427
logger.Error(err, "failed to execute template")
386428
http.Error(w, "internal error", http.StatusInternalServerError)
@@ -393,8 +435,10 @@ 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")
441+
version := r.URL.Query().Get("version")
398442
cluster := mux.Vars(r)["cluster"]
399443

400444
prepareNoCache(w)
@@ -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(), cluster)
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(), cluster, name, schema)
486+
if err != nil {
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, cluster)
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 {
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/kubernetes/manager.go

Lines changed: 58 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"
@@ -71,8 +75,8 @@ func NewKubernetesManager(
7175
return m, nil
7276
}
7377

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)
78+
func (m *Manager) HandleResources(ctx context.Context, identity, cluster string) ([]byte, error) {
79+
logger := klog.FromContext(ctx).WithValues("identity", identity)
7680
ctx = klog.NewContext(ctx, logger)
7781

7882
cl, err := m.manager.GetCluster(ctx, cluster)
@@ -173,3 +177,55 @@ func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster str
173177

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

backend/options/options.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ type ExtraOptions struct {
5353
ExternalCAFile string
5454
ExternalCA []byte
5555
TLSExternalServerName string
56+
// Defines the source of the schema for the bind screen.
57+
// Options are:
58+
// CustomResourceDefinitions.v1.apiextensions.k8s.io
59+
// APIResourceSchemas.v1alpha2.kube-bind.io
60+
SchemaSource string
5661

5762
TestingAutoSelect string
5863
TestingSkipNameValidation bool
@@ -89,6 +94,7 @@ func NewOptions() *Options {
8994
ConsumerScope: string(kubebindv1alpha2.NamespacedScope),
9095
ClusterScopedIsolation: string(kubebindv1alpha2.IsolationPrefixed),
9196
ServerURL: "",
97+
SchemaSource: "crd",
9298
},
9399
}
94100
}
@@ -99,6 +105,12 @@ var providerAliases = map[string]string{
99105
"": "kubernetes",
100106
}
101107

108+
var schemaSourceAliases = map[string]string{
109+
"crd": "CustomResourceDefinition.v1.apiextensions.k8s.io",
110+
"apiresourceschemas": "APIResourceSchema.v1alpha2.kube-bind.io",
111+
"customresourcedefinitions": "CustomResourceDefinition.v1.apiextensions.k8s.io",
112+
}
113+
102114
func (options *Options) AddFlags(fs *pflag.FlagSet) {
103115
logsv1.AddFlags(options.Logs, fs)
104116
options.OIDC.AddFlags(fs)
@@ -118,6 +130,11 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) {
118130
fmt.Sprintf("The multicluster runtime provider. Possible values are: %v", sets.List(sets.Set[string](sets.StringKeySet(providerAliases)))),
119131
)
120132

133+
fs.StringVar(&options.SchemaSource, "schema-source", options.SchemaSource,
134+
fmt.Sprintf("Defines the source of the schema in KindVersionGroup format for the bind screen. Defaults to CustomResourceDefinition.v1.apiextensions.k8s.io. Possible values are: %v",
135+
sets.List(sets.Set[string](sets.StringKeySet(schemaSourceAliases)))),
136+
)
137+
121138
fs.StringVar(&options.ServerURL, "server-url", options.ServerURL, "The URL of the backend server. If not specified, it will be derived from the kubeconfig or service account's hosts.")
122139

123140
fs.StringVar(&options.TestingAutoSelect, "testing-auto-select", options.TestingAutoSelect, "<resource>.<group> that is automatically selected on th bind screen for testing")
@@ -206,5 +223,11 @@ func (options *CompletedOptions) Validate() error {
206223
}
207224
options.Provider = provider
208225

226+
schemaSource := schemaSourceAliases[options.SchemaSource]
227+
if schemaSource == "" {
228+
return fmt.Errorf("unknown schema source %q, must be one of %v", options.SchemaSource, sets.List(sets.Set[string](sets.StringKeySet(schemaSourceAliases))))
229+
}
230+
options.SchemaSource = schemaSource
231+
209232
return nil
210233
}

0 commit comments

Comments
 (0)