From da43199970cef549bff2150a9c640d79698b6f53 Mon Sep 17 00:00:00 2001 From: alokkumardalei-wq Date: Thu, 16 Jul 2026 07:39:49 +0530 Subject: [PATCH] feat: implement multi-provider binding support for CRDs (#173) Signed-off-by: alokkumardalei-wq --- .../bindableresourcesrequest_controller.go | 13 ++++--- backend/http/handler.go | 1 + backend/kubernetes/manager.go | 8 ++++ .../kubectl/bind-apiservice/plugin/binder.go | 8 ++-- .../bind-apiservice/plugin/servicebindings.go | 4 +- .../resources/apiexport-kube-bind.io.yaml | 2 +- ...chema-apiservicebindings.kube-bind.io.yaml | 11 +++++- .../crds/kube-bind.io_apiservicebindings.yaml | 9 +++++ .../crd/kube-bind.io_apiservicebindings.yaml | 9 +++++ .../serviceexport/serviceexport_reconcile.go | 8 +++- .../serviceexport/spec/spec_controller.go | 37 ++++++++++++++++++- .../serviceexport/status/status_controller.go | 37 ++++++++++++++++++- .../servicebindingbundle_reconcile.go | 24 ++++++++++++ .../v1alpha2/apiservicebinding_types.go | 13 +++++++ .../v1alpha2/bindingresponse_types.go | 6 +++ 15 files changed, 173 insertions(+), 17 deletions(-) diff --git a/backend/controllers/bindableresourcesrequest/bindableresourcesrequest_controller.go b/backend/controllers/bindableresourcesrequest/bindableresourcesrequest_controller.go index f4f5ba881..52282d843 100644 --- a/backend/controllers/bindableresourcesrequest/bindableresourcesrequest_controller.go +++ b/backend/controllers/bindableresourcesrequest/bindableresourcesrequest_controller.go @@ -273,7 +273,7 @@ func (r *reconciler) reconcile(ctx context.Context, clusterName string, cl clien req.Status.Namespace = result.Namespace // Create or update the BindingResourceResponse secret - if err := r.ensureBindingResponseSecret(ctx, cl, req, result.Kubeconfig, secretName, secretKey); err != nil { + if err := r.ensureBindingResponseSecret(ctx, cl, req, result, secretName, secretKey); err != nil { meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{ Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady), Status: metav1.ConditionFalse, @@ -306,25 +306,26 @@ func (r *reconciler) reconcile(ctx context.Context, clusterName string, cl clien return ctrl.Result{}, nil } -// ensureBindingResponseSecret creates or updates a secret containing the BindingResourceResponse -// with only the kubeconfig set (no authentication or requests). +// with only the kubeconfig, providerID, and providerNamespace set (no authentication or requests). func (r *reconciler) ensureBindingResponseSecret( ctx context.Context, cl client.Client, req *kubebindv1alpha2.BindableResourcesRequest, - kubeconfig []byte, + result *kubernetes.HandleResourcesResult, secretName string, secretKey string, ) error { logger := log.FromContext(ctx) - // Create the BindingResourceResponse with only kubeconfig + // Create the BindingResourceResponse response := kubebindv1alpha2.BindingResourceResponse{ TypeMeta: metav1.TypeMeta{ APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), Kind: "BindingResourceResponse", }, - Kubeconfig: kubeconfig, + Kubeconfig: result.Kubeconfig, + ProviderNamespace: result.Namespace, + ProviderID: result.ProviderID, } responseBytes, err := json.Marshal(&response) diff --git a/backend/http/handler.go b/backend/http/handler.go index 9e73238d7..b89949551 100644 --- a/backend/http/handler.go +++ b/backend/http/handler.go @@ -513,6 +513,7 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { Kubeconfig: handleResult.Kubeconfig, Requests: []runtime.RawExtension{{Raw: requestBytes}}, ProviderNamespace: handleResult.Namespace, + ProviderID: handleResult.ProviderID, BindingName: exportRequestName, } diff --git a/backend/kubernetes/manager.go b/backend/kubernetes/manager.go index 2d30c7be6..849ac6d22 100644 --- a/backend/kubernetes/manager.go +++ b/backend/kubernetes/manager.go @@ -103,6 +103,8 @@ type HandleResourcesResult struct { Kubeconfig []byte // Namespace is the namespace assigned to this binding on the service provider cluster. Namespace string + // ProviderID is the UID of the kube-system namespace of the provider cluster. + ProviderID string } func (m *Manager) HandleResources( @@ -181,9 +183,15 @@ func (m *Manager) HandleResources( return nil, err } + var kubeSystemNS corev1.Namespace + if err := c.Get(ctx, types.NamespacedName{Name: "kube-system"}, &kubeSystemNS); err != nil { + return nil, fmt.Errorf("failed to get kube-system namespace for ProviderID: %w", err) + } + return &HandleResourcesResult{ Kubeconfig: kfgSecret.Data["kubeconfig"], Namespace: ns, + ProviderID: string(kubeSystemNS.UID), }, nil } diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/binder.go b/cli/pkg/kubectl/bind-apiservice/plugin/binder.go index c903b4391..97b95a705 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/binder.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/binder.go @@ -155,7 +155,7 @@ func (b *Binder) BindFromFile(ctx context.Context) ([]*kubebindv1alpha2.APIServi return nil, fmt.Errorf("failed to create kubeconfig secret: %w", err) } - results, err := b.createAPIServiceBindings(ctx, result, secretName) + results, err := b.createAPIServiceBindings(ctx, result, secretName, "", false) if err != nil { return nil, fmt.Errorf("failed to create API service bindings: %w", err) } @@ -247,7 +247,7 @@ func (b *Binder) BindFromResponse(ctx context.Context, response *kubebindv1alpha return nil, fmt.Errorf("failed to create kubeconfig secret: %w", err) } - results, err := b.createAPIServiceBindings(ctx, result, secretName) + results, err := b.createAPIServiceBindings(ctx, result, secretName, response.ProviderID, true) if err != nil { return nil, fmt.Errorf("failed to create API service bindings: %w", err) } @@ -304,11 +304,11 @@ func (b *Binder) createKubeconfigSecret(ctx context.Context, remoteHost, remoteN return tempOpts.createKubeconfigSecret(ctx, b.config, remoteHost, remoteNamespace, remoteKubeconfig) } -func (b *Binder) createAPIServiceBindings(ctx context.Context, request *kubebindv1alpha2.APIServiceExportRequest, secretName string) ([]*kubebindv1alpha2.APIServiceBinding, error) { +func (b *Binder) createAPIServiceBindings(ctx context.Context, request *kubebindv1alpha2.APIServiceExportRequest, secretName string, providerID string, isDefault bool) ([]*kubebindv1alpha2.APIServiceBinding, error) { tempOpts := &BindAPIServiceOptions{ Options: &base.Options{IOStreams: b.opts.IOStreams}, } - return tempOpts.createAPIServiceBindings(ctx, b.config, request, secretName) + return tempOpts.createAPIServiceBindings(ctx, b.config, request, secretName, providerID, isDefault) } func (b *Binder) getRequestManifest() ([]byte, error) { diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go index b539c8b64..9b878e772 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go @@ -34,7 +34,7 @@ import ( bindclient "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned" ) -func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, config *rest.Config, request *kubebindv1alpha2.APIServiceExportRequest, secretName string) ([]*kubebindv1alpha2.APIServiceBinding, error) { +func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, config *rest.Config, request *kubebindv1alpha2.APIServiceExportRequest, secretName string, providerID string, isDefault bool) ([]*kubebindv1alpha2.APIServiceBinding, error) { bindClient, err := bindclient.NewForConfig(config) if err != nil { return nil, err @@ -86,6 +86,8 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co }, Namespace: "kube-bind", }, + ProviderID: providerID, + IsDefault: isDefault, }, }, metav1.CreateOptions{}) if err != nil { diff --git a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml index ab9e3d02b..20617305c 100644 --- a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml @@ -61,7 +61,7 @@ spec: crd: {} - group: kube-bind.io name: apiservicebindings - schema: v260122-c9f0f376.apiservicebindings.kube-bind.io + schema: v260716-5d02230.apiservicebindings.kube-bind.io storage: crd: {} - group: kube-bind.io diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml index 80ff20c6a..a2dcdb925 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiservicebindings.kube-bind.io.yaml @@ -1,7 +1,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: - name: v260122-c9f0f376.apiservicebindings.kube-bind.io + name: v260716-5d02230.apiservicebindings.kube-bind.io spec: conversion: strategy: None @@ -196,6 +196,11 @@ spec: spec specifies how an API service from a service provider should be bound in the local consumer cluster. properties: + isDefault: + description: |- + isDefault indicates whether this binding should be used as the default routing + destination for Custom Resources that do not specify a provider annotation. + type: boolean kubeconfigSecretRef: description: kubeconfigSecretName is the secret ref that contains the kubeconfig of the service cluster. @@ -219,6 +224,10 @@ spec: x-kubernetes-validations: - message: kubeconfigSecretRef is immutable rule: self == oldSelf + providerID: + description: providerID is a stable and unique identifier for the provider + cluster. + type: string required: - kubeconfigSecretRef type: object diff --git a/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml b/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml index c443447f3..455da37f6 100644 --- a/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml +++ b/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml @@ -201,6 +201,11 @@ spec: spec specifies how an API service from a service provider should be bound in the local consumer cluster. properties: + isDefault: + description: |- + isDefault indicates whether this binding should be used as the default routing + destination for Custom Resources that do not specify a provider annotation. + type: boolean kubeconfigSecretRef: description: kubeconfigSecretName is the secret ref that contains the kubeconfig of the service cluster. @@ -224,6 +229,10 @@ spec: x-kubernetes-validations: - message: kubeconfigSecretRef is immutable rule: self == oldSelf + providerID: + description: providerID is a stable and unique identifier for the + provider cluster. + type: string required: - kubeconfigSecretRef type: object diff --git a/deploy/crd/kube-bind.io_apiservicebindings.yaml b/deploy/crd/kube-bind.io_apiservicebindings.yaml index 3a15f95d3..a9f768ea1 100644 --- a/deploy/crd/kube-bind.io_apiservicebindings.yaml +++ b/deploy/crd/kube-bind.io_apiservicebindings.yaml @@ -202,6 +202,11 @@ spec: spec specifies how an API service from a service provider should be bound in the local consumer cluster. properties: + isDefault: + description: |- + isDefault indicates whether this binding should be used as the default routing + destination for Custom Resources that do not specify a provider annotation. + type: boolean kubeconfigSecretRef: description: kubeconfigSecretName is the secret ref that contains the kubeconfig of the service cluster. @@ -225,6 +230,10 @@ spec: x-kubernetes-validations: - message: kubeconfigSecretRef is immutable rule: self == oldSelf + providerID: + description: providerID is a stable and unique identifier for the + provider cluster. + type: string required: - kubeconfigSecretRef type: object diff --git a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go index da788191c..cc75aede1 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go +++ b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go @@ -139,7 +139,7 @@ func (r *reconciler) ensureControllers(ctx context.Context, namespace, name stri } // Start/update controller for this schema - if err := r.ensureControllerForSchema(ctx, export, schema); err != nil { + if err := r.ensureControllerForSchema(ctx, export, binding, schema); err != nil { errs = append(errs, err) } @@ -170,7 +170,7 @@ func (r *reconciler) ensureControllers(ctx context.Context, namespace, name stri return utilerrors.NewAggregate(errs) } -func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kubebindv1alpha2.APIServiceExport, schema *kubebindv1alpha2.BoundSchema) error { +func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kubebindv1alpha2.APIServiceExport, binding *kubebindv1alpha2.APIServiceBinding, schema *kubebindv1alpha2.BoundSchema) error { logger := klog.FromContext(ctx) key := contextstore.NewKey(export.Namespace, export.Name, schema.Name) @@ -296,6 +296,8 @@ func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kube consumerInf.ForResource(gvr), providerInf, r.serviceNamespaceInformer, + binding.Spec.ProviderID, + binding.Spec.IsDefault, ) if err != nil { runtime.HandleError(err) @@ -312,6 +314,8 @@ func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kube consumerInf.ForResource(gvr), providerInf, r.serviceNamespaceInformer, + binding.Spec.ProviderID, + binding.Spec.IsDefault, ) if err != nil { runtime.HandleError(err) diff --git a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go index f3c5ff26b..ddc0ee22d 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go +++ b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_controller.go @@ -63,6 +63,8 @@ func NewController( consumerDynamicInformer informers.GenericInformer, providerDynamicInformer multinsinformer.GetterInformer, serviceNamespaceInformer dynamic.Informer[bindlisters.APIServiceNamespaceLister], + providerID string, + isDefault bool, ) (*controller, error) { queue := workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[string](), workqueue.TypedRateLimitingQueueConfig[string]{Name: controllerName}) @@ -150,12 +152,21 @@ func NewController( if _, err := consumerDynamicInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj any) { + if !shouldProcess(obj, providerID, isDefault) { + return + } c.enqueueConsumer(logger, obj) }, UpdateFunc: func(_, newObj any) { + if !shouldProcess(newObj, providerID, isDefault) { + return + } c.enqueueConsumer(logger, newObj) }, DeleteFunc: func(obj any) { + if !shouldProcess(obj, providerID, isDefault) { + return + } c.enqueueConsumer(logger, obj) }, }); err != nil { @@ -206,7 +217,31 @@ func (c *controller) enqueueConsumer(logger klog.Logger, obj any) { } logger.V(2).Info("queueing Unstructured", "queued", key, "reason", "consumerObject") - c.queue.Add(key) +} + +func shouldProcess(obj any, providerID string, isDefault bool) bool { + u, ok := obj.(*unstructured.Unstructured) + if !ok { + // Try DeletedFinalStateUnknown + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + return false + } + u, ok = tombstone.Obj.(*unstructured.Unstructured) + if !ok { + return false + } + } + + annotations := u.GetAnnotations() + if annotations == nil { + return isDefault + } + val, ok := annotations["provider.kube-bind.io/provider-id"] + if !ok || val == "" { + return isDefault + } + return val == providerID } func (c *controller) enqueueProvider(logger klog.Logger, obj any) { diff --git a/pkg/konnector/controllers/cluster/serviceexport/status/status_controller.go b/pkg/konnector/controllers/cluster/serviceexport/status/status_controller.go index c4b1da98a..9db713a90 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/status/status_controller.go +++ b/pkg/konnector/controllers/cluster/serviceexport/status/status_controller.go @@ -57,6 +57,8 @@ func NewController( consumerDynamicInformer informers.GenericInformer, providerDynamicInformer multinsinformer.GetterInformer, serviceNamespaceInformer dynamic.Informer[bindlisters.APIServiceNamespaceLister], + providerID string, + isDefault bool, ) (*controller, error) { queue := workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[string](), workqueue.TypedRateLimitingQueueConfig[string]{Name: controllerName}) @@ -124,12 +126,21 @@ func NewController( if _, err := consumerDynamicInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj any) { + if !shouldProcess(obj, providerID, isDefault) { + return + } c.enqueueConsumer(logger, obj) }, UpdateFunc: func(_, newObj any) { + if !shouldProcess(newObj, providerID, isDefault) { + return + } c.enqueueConsumer(logger, newObj) }, DeleteFunc: func(obj any) { + if !shouldProcess(obj, providerID, isDefault) { + return + } c.enqueueConsumer(logger, obj) }, }); err != nil { @@ -204,7 +215,31 @@ func (c *controller) enqueueProvider(logger klog.Logger, obj any) { } logger.V(2).Info("queueing Unstructured", "queued", key) - c.queue.Add(key) +} + +func shouldProcess(obj any, providerID string, isDefault bool) bool { + u, ok := obj.(*unstructured.Unstructured) + if !ok { + // Try DeletedFinalStateUnknown + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + return false + } + u, ok = tombstone.Obj.(*unstructured.Unstructured) + if !ok { + return false + } + } + + annotations := u.GetAnnotations() + if annotations == nil { + return isDefault + } + val, ok := annotations["provider.kube-bind.io/provider-id"] + if !ok || val == "" { + return isDefault + } + return val == providerID } func (c *controller) enqueueConsumer(logger klog.Logger, obj any) { diff --git a/pkg/konnector/controllers/servicebindingbundle/servicebindingbundle_reconcile.go b/pkg/konnector/controllers/servicebindingbundle/servicebindingbundle_reconcile.go index 7fec08ae1..538bef6b7 100644 --- a/pkg/konnector/controllers/servicebindingbundle/servicebindingbundle_reconcile.go +++ b/pkg/konnector/controllers/servicebindingbundle/servicebindingbundle_reconcile.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" @@ -183,6 +184,27 @@ func (r *reconciler) syncAPIServiceBindings(ctx context.Context, bundle *kubebin return err } + // Create a standard kubernetes client to get the kube-system namespace UID + kubeClient, err := kubernetes.NewForConfig(providerConfig) + if err != nil { + conditions.MarkFalse( + bundle, + APIServiceBindingBundleConditionSynced, + "ProviderClientError", + conditionsapi.ConditionSeverityError, + "Failed to create provider core client: %v", + err, + ) + return err + } + + var providerID string + if kubeSystemNs, err := kubeClient.CoreV1().Namespaces().Get(ctx, "kube-system", metav1.GetOptions{}); err == nil { + providerID = string(kubeSystemNs.UID) + } else { + logger.Error(err, "Failed to get kube-system namespace from provider, ProviderID will be empty") + } + // List all APIServiceExports from the provider cluster exports, err := providerClient.KubeBindV1alpha2().APIServiceExports(providerNamespace).List(ctx, metav1.ListOptions{}) if err != nil { @@ -261,6 +283,8 @@ func (r *reconciler) syncAPIServiceBindings(ctx context.Context, bundle *kubebin }, Spec: kubebindv1alpha2.APIServiceBindingSpec{ KubeconfigSecretRef: bundle.Spec.KubeconfigSecretRef, + ProviderID: providerID, + IsDefault: true, }, } diff --git a/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go b/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go index 781f4f787..aa8f43a61 100644 --- a/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go @@ -88,6 +88,19 @@ type APIServiceBindingSpec struct { // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="kubeconfigSecretRef is immutable" KubeconfigSecretRef ClusterSecretKeyRef `json:"kubeconfigSecretRef"` + + // providerID is a stable and unique identifier for the provider cluster. + // + // +optional + // +kubebuilder:validation:Optional + ProviderID string `json:"providerID,omitempty"` + + // isDefault indicates whether this binding should be used as the default routing + // destination for Custom Resources that do not specify a provider annotation. + // + // +optional + // +kubebuilder:validation:Optional + IsDefault bool `json:"isDefault,omitempty"` } type APIServiceBindingStatus struct { diff --git a/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go b/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go index 2e470ff39..2d621dade 100644 --- a/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go +++ b/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go @@ -64,6 +64,12 @@ type BindingResourceResponse struct { // +kubebuilder:validation:Optional ProviderNamespace string `json:"providerNamespace,omitempty"` + // providerID is a stable and unique identifier for the provider cluster. + // + // +optional + // +kubebuilder:validation:Optional + ProviderID string `json:"providerID,omitempty"` + // bindingName is the confirmed name for this binding, as created on the service // provider cluster. //