diff --git a/backend/controllers/bindableresourcesrequest/bindableresourcesrequest_controller.go b/backend/controllers/bindableresourcesrequest/bindableresourcesrequest_controller.go new file mode 100644 index 000000000..43c9a27f2 --- /dev/null +++ b/backend/controllers/bindableresourcesrequest/bindableresourcesrequest_controller.go @@ -0,0 +1,390 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bindableresourcesrequest + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/log" + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + "github.com/kube-bind/kube-bind/backend/kubernetes" + "github.com/kube-bind/kube-bind/pkg/indexers" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +const ( + controllerName = "kube-bind-backend-bindableresourcesrequest" +) + +// BindableResourcesRequestReconciler reconciles a BindableResourcesRequest object. +// This controller handles direct binding requests created as CRDs when the +// backend is running without the HTTP API/OIDC flow (frontend disabled mode). +// +// The controller: +// 1. Reads the kubeconfig from the referenced secret +// 2. Creates a BindingResourceResponse secret with the kubeconfig +// 3. Creates an APIServiceExportRequest based on the template +type BindableResourcesRequestReconciler struct { + manager mcmanager.Manager + opts controller.TypedOptions[mcreconcile.Request] + + informerScope kubebindv1alpha2.InformerScope + isolation kubebindv1alpha2.Isolation + reconciler reconciler +} + +// NewBindableResourcesRequestReconciler returns a new BindableResourcesRequestReconciler. +func NewBindableResourcesRequestReconciler( + ctx context.Context, + mgr mcmanager.Manager, + opts controller.TypedOptions[mcreconcile.Request], + scope kubebindv1alpha2.InformerScope, + isolation kubebindv1alpha2.Isolation, + kubeManager *kubernetes.Manager, +) (*BindableResourcesRequestReconciler, error) { + // Set up field indexers for BindableResourcesRequests + if err := mgr.GetFieldIndexer().IndexField(ctx, &kubebindv1alpha2.BindableResourcesRequest{}, indexers.BindableResourcesRequestByTemplate, + indexers.IndexBindableResourcesRequestByTemplate); err != nil { + return nil, fmt.Errorf("failed to setup BindableResourcesRequestByTemplate indexer: %w", err) + } + + r := &BindableResourcesRequestReconciler{ + manager: mgr, + opts: opts, + informerScope: scope, + isolation: isolation, + reconciler: reconciler{ + informerScope: scope, + isolation: isolation, + kubeManager: kubeManager, + }, + } + + return r, nil +} + +//+kubebuilder:rbac:groups=kube-bind.io,resources=bindableresourcesrequests,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=kube-bind.io,resources=bindableresourcesrequests/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=kube-bind.io,resources=bindableresourcesrequests/finalizers,verbs=update +//+kubebuilder:rbac:groups=kube-bind.io,resources=apiserviceexportrequests,verbs=get;list;watch;create +//+kubebuilder:rbac:groups=kube-bind.io,resources=apiserviceexporttemplates,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update + +// Reconcile is part of the main kubernetes reconciliation loop. +func (r *BindableResourcesRequestReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("Reconciling BindableResourcesRequest", "cluster", req.ClusterName, "namespace", req.Namespace, "name", req.Name) + + cl, err := r.manager.GetCluster(ctx, req.ClusterName) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get client for cluster %q: %w", req.ClusterName, err) + } + + client := cl.GetClient() + + // Fetch the BindableResourcesRequest instance + bindableRequest := &kubebindv1alpha2.BindableResourcesRequest{} + if err := client.Get(ctx, req.NamespacedName, bindableRequest); err != nil { + if errors.IsNotFound(err) { + logger.Info("BindableResourcesRequest not found, ignoring") + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to get BindableResourcesRequest: %w", err) + } + + // Check TTL-based deletion for completed (ttl) requests + if bindableRequest.Status.Phase == kubebindv1alpha2.BindableResourcesRequestPhaseSucceeded || + bindableRequest.Status.Phase == kubebindv1alpha2.BindableResourcesRequestPhaseFailed { + return r.handleTTL(ctx, client, bindableRequest) + } + + // Create a copy to modify + original := bindableRequest.DeepCopy() + + // Run the reconciliation logic + result, err := r.reconciler.reconcile(ctx, req.ClusterName, client, bindableRequest) + if err != nil { + logger.Error(err, "Failed to reconcile BindableResourcesRequest") + if !reflect.DeepEqual(original.Status, bindableRequest.Status) { + if updateErr := client.Status().Update(ctx, bindableRequest); updateErr != nil { + logger.Error(updateErr, "Failed to update BindableResourcesRequest status") + return ctrl.Result{}, fmt.Errorf("failed to update BindableResourcesRequest status: %w", updateErr) + } + } + return ctrl.Result{}, err + } + + // Update status if it has changed + if !reflect.DeepEqual(original.Status, bindableRequest.Status) { + if err := client.Status().Update(ctx, bindableRequest); err != nil { + logger.Error(err, "Failed to update BindableResourcesRequest status") + return ctrl.Result{}, fmt.Errorf("failed to update BindableResourcesRequest status: %w", err) + } + logger.Info("BindableResourcesRequest status updated", "namespace", bindableRequest.Namespace, "name", bindableRequest.Name) + } + + return result, nil +} + +// handleTTL checks if the request should be deleted based on TTL settings. +func (r *BindableResourcesRequestReconciler) handleTTL(ctx context.Context, cl client.Client, req *kubebindv1alpha2.BindableResourcesRequest) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + // If no TTL is set, don't delete + if req.Spec.TTLAfterFinished == nil { + return ctrl.Result{}, nil + } + + // If no completion time is set, something is wrong - skip + if req.Status.CompletionTime == nil { + return ctrl.Result{}, nil + } + + ttl := req.Spec.TTLAfterFinished.Duration + expireTime := req.Status.CompletionTime.Add(ttl) + now := time.Now() + + if now.After(expireTime) { + logger.Info("TTL expired, deleting BindableResourcesRequest", "name", req.Name, "namespace", req.Namespace) + if err := cl.Delete(ctx, req); err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to delete expired BindableResourcesRequest: %w", err) + } + return ctrl.Result{}, nil + } + + // Requeue to check again when TTL expires + requeueAfter := expireTime.Sub(now) + return ctrl.Result{RequeueAfter: requeueAfter}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BindableResourcesRequestReconciler) SetupWithManager(mgr mcmanager.Manager) error { + return mcbuilder.ControllerManagedBy(mgr). + For(&kubebindv1alpha2.BindableResourcesRequest{}). + Owns(&corev1.Secret{}). + WithOptions(r.opts). + Named(controllerName). + Complete(r) +} + +type reconciler struct { + informerScope kubebindv1alpha2.InformerScope + isolation kubebindv1alpha2.Isolation + kubeManager *kubernetes.Manager +} + +func (r *reconciler) reconcile(ctx context.Context, clusterName string, cl client.Client, req *kubebindv1alpha2.BindableResourcesRequest) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + // Determine the secret name and key to use for the binding response + var secretName, secretKey string + if req.Spec.KubeconfigSecretRef != nil { + // Use the specified secret reference + secretName = req.Spec.KubeconfigSecretRef.Name + secretKey = req.Spec.KubeconfigSecretRef.Key + if secretKey == "" { + secretKey = "kubeconfig" + } + } else { + // Create a new secret with default naming + secretName = req.Name + "-binding-response" + secretKey = "binding-response" + } + + // Update status with the secret reference + req.Status.KubeconfigSecretRef = &kubebindv1alpha2.LocalSecretKeyRef{ + Name: secretName, + Key: secretKey, + } + + // Get the template if specified + var template kubebindv1alpha2.APIServiceExportTemplate + if req.Spec.TemplateRef.Name != "" { + if err := cl.Get(ctx, types.NamespacedName{Name: req.Spec.TemplateRef.Name}, &template); err != nil { + if errors.IsNotFound(err) { + req.Status.Phase = kubebindv1alpha2.BindableResourcesRequestPhaseFailed + now := metav1.Now() + req.Status.CompletionTime = &now + meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{ + Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady), + Status: metav1.ConditionFalse, + Reason: "TemplateNotFound", + Message: fmt.Sprintf("APIServiceExportTemplate %q not found", req.Spec.TemplateRef.Name), + LastTransitionTime: now, + }) + return ctrl.Result{}, nil // Don't retry - template doesn't exist + } + return ctrl.Result{}, fmt.Errorf("failed to get template %q: %w", req.Spec.TemplateRef.Name, err) + } + } + + // Handle resources and get kubeconfig + kfg, err := r.kubeManager.HandleResources(ctx, req.Spec.Author, req.Spec.ClusterIdentity.Identity, clusterName) + if err != nil { + meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{ + Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady), + Status: metav1.ConditionFalse, + Reason: "HandleResourcesFailed", + Message: fmt.Sprintf("Failed to handle resources: %v", err), + LastTransitionTime: metav1.Now(), + }) + return ctrl.Result{}, fmt.Errorf("failed to handle resources for cluster identity %q: %w", req.Spec.ClusterIdentity.Identity, err) + } + + // Create or update the BindingResourceResponse secret + if err := r.ensureBindingResponseSecret(ctx, cl, req, kfg, secretName, secretKey); err != nil { + meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{ + Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady), + Status: metav1.ConditionFalse, + Reason: "SecretCreationFailed", + Message: fmt.Sprintf("Failed to create/update secret: %v", err), + LastTransitionTime: metav1.Now(), + }) + return ctrl.Result{}, fmt.Errorf("failed to ensure binding response secret: %w", err) + } + + // Set success status + now := metav1.Now() + req.Status.Phase = kubebindv1alpha2.BindableResourcesRequestPhaseSucceeded + req.Status.CompletionTime = &now + meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{ + Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady), + Status: metav1.ConditionTrue, + Reason: "SecretReady", + Message: fmt.Sprintf("Binding response secret %q is ready", secretName), + LastTransitionTime: now, + }) + + logger.Info("BindableResourcesRequest succeeded", "name", req.Name, "namespace", req.Namespace, "secret", secretName) + + // If TTL is set, requeue for deletion + if req.Spec.TTLAfterFinished != nil { + return ctrl.Result{RequeueAfter: req.Spec.TTLAfterFinished.Duration}, nil + } + + return ctrl.Result{}, nil +} + +// ensureBindingResponseSecret creates or updates a secret containing the BindingResourceResponse +// with only the kubeconfig set (no authentication or requests). +func (r *reconciler) ensureBindingResponseSecret( + ctx context.Context, + cl client.Client, + req *kubebindv1alpha2.BindableResourcesRequest, + kubeconfig []byte, + secretName string, + secretKey string, +) error { + logger := log.FromContext(ctx) + + // Create the BindingResourceResponse with only kubeconfig + response := kubebindv1alpha2.BindingResourceResponse{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), + Kind: "BindingResourceResponse", + }, + Kubeconfig: kubeconfig, + } + + responseBytes, err := json.Marshal(&response) + if err != nil { + return fmt.Errorf("failed to marshal BindingResourceResponse: %w", err) + } + + // Check if secret already exists + existingSecret := &corev1.Secret{} + err = cl.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: secretName}, existingSecret) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get existing secret: %w", err) + } + + // Build owner reference - the secret is owned by the BindableResourcesRequest + ownerRef := metav1.OwnerReference{ + APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), + Kind: "BindableResourcesRequest", + Name: req.Name, + UID: req.UID, + Controller: func() *bool { b := true; return &b }(), + } + + if errors.IsNotFound(err) { + // Create new secret + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: req.Namespace, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + secretKey: responseBytes, + }, + } + logger.Info("Creating BindingResourceResponse secret", "name", secretName, "namespace", req.Namespace, "key", secretKey) + if err := cl.Create(ctx, secret); err != nil { + if errors.IsAlreadyExists(err) { + return nil // Race condition, secret was created + } + return fmt.Errorf("failed to create secret: %w", err) + } + } else { + // Update existing secret + // Ensure owner reference is set + hasOwnerRef := false + for _, ref := range existingSecret.OwnerReferences { + if ref.UID == req.UID { + hasOwnerRef = true + break + } + } + if !hasOwnerRef { + existingSecret.OwnerReferences = append(existingSecret.OwnerReferences, ownerRef) + } + + // Update data if changed + if existingSecret.Data == nil { + existingSecret.Data = make(map[string][]byte) + } + if string(existingSecret.Data[secretKey]) != string(responseBytes) { + existingSecret.Data[secretKey] = responseBytes + logger.Info("Updating BindingResourceResponse secret", "name", secretName, "namespace", req.Namespace, "key", secretKey) + if err := cl.Update(ctx, existingSecret); err != nil { + return fmt.Errorf("failed to update secret: %w", err) + } + } + } + + return nil +} diff --git a/backend/http/handler.go b/backend/http/handler.go index 596dae561..3a9ea5d37 100644 --- a/backend/http/handler.go +++ b/backend/http/handler.go @@ -361,7 +361,7 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { } // TODO: Move to validating admission. - if bindRequest.ClusterIdentity.Identity == "" { + if bindRequest.Spec.ClusterIdentity.Identity == "" { logger.Error(fmt.Errorf("missing cluster identity"), "invalid bind request") writeErrorResponse(w, http.StatusBadRequest, kubebindv1alpha2.ErrorCodeBadRequest, "Missing cluster identity in bind request", "The cluster identity must be provided in the bind request") return @@ -376,9 +376,9 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { } // Module consist of many resources and permissionClaims. Read it and translate to - template, err := h.kubeManager.GetTemplates(r.Context(), params.ClusterID, bindRequest.TemplateRef.Name) + template, err := h.kubeManager.GetTemplates(r.Context(), params.ClusterID, bindRequest.Spec.TemplateRef.Name) if err != nil { - logger.Error(err, "failed to get template", "template", bindRequest.TemplateRef.Name, "cluster", params.ClusterID) + logger.Error(err, "failed to get template", "template", bindRequest.Spec.TemplateRef.Name, "cluster", params.ClusterID) statusCode, code, details := mapErrorToCode(err) writeErrorResponse(w, statusCode, code, "Failed to retrieve template", details) return diff --git a/backend/options/options.go b/backend/options/options.go index 6a2ab4288..1490515f9 100644 --- a/backend/options/options.go +++ b/backend/options/options.go @@ -68,6 +68,9 @@ type ExtraOptions struct { // If ControllerFrontend starts with http:// it is treated as a URL to a SPA server // Else - it is treated as a path to static files to be served. Frontend string + // FrontendDisabled indicates that no frontend should be served at all, including oidc and api. + // This is useful for backend-only deployments. + FrontendDisabled bool TokenExpiry time.Duration } @@ -101,14 +104,15 @@ func NewOptions() *Options { ProviderKcp: providerkcp.NewOptions(), ExtraOptions: ExtraOptions{ - Provider: "kubernetes", - NamespacePrefix: "cluster-", - PrettyName: "Backend", - ConsumerScope: string(kubebindv1alpha2.NamespacedScope), - Isolation: string(kubebindv1alpha2.IsolationPrefixed), - SchemaSource: CustomResourceDefinitionSource.String(), - Frontend: "embedded", // Not used, but indicates to use embedded frontend using SPA. - TokenExpiry: 1 * time.Hour, + Provider: "kubernetes", + NamespacePrefix: "cluster-", + PrettyName: "Backend", + ConsumerScope: string(kubebindv1alpha2.NamespacedScope), + Isolation: string(kubebindv1alpha2.IsolationPrefixed), + SchemaSource: CustomResourceDefinitionSource.String(), + Frontend: "embedded", // Not used, but indicates to use embedded frontend using SPA. + TokenExpiry: 1 * time.Hour, + FrontendDisabled: false, }, } return opts @@ -159,6 +163,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&options.ExternalCAFile, "external-ca-file", options.ExternalCAFile, "The external CA file for the service provider cluster. If not specified, service account's CA is used.") fs.StringVar(&options.TLSExternalServerName, "external-server-name", options.TLSExternalServerName, "The external (TLS) server name used by consumers to talk to the service provider cluster. This can be useful to select the right certificate via SNI.") fs.StringVar(&options.Frontend, "frontend", options.Frontend, "If starts with http:// it is treated as a URL to a SPA server Else - it is treated as a path to static files to be served.") + fs.BoolVarP(&options.FrontendDisabled, "frontend-disabled", "", false, "If set, will run in backend only mode without API and oidc.") fs.DurationVar(&options.TokenExpiry, "token-expiry", options.TokenExpiry, "The duration for which tokens are valid. Default is 1h.") fs.StringVar(&options.Provider, "multicluster-runtime-provider", options.Provider, @@ -180,20 +185,19 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { } func (options *Options) Complete() (*CompletedOptions, error) { - // Serve must complete first as OIDC may depend on it - // to reuse the listener. - if err := options.Serve.Complete(); err != nil { - return nil, err - } + if !options.FrontendDisabled { + // Serve must complete first as OIDC may depend on it + // to reuse the listener. + if err := options.Serve.Complete(); err != nil { + return nil, err + } - if err := options.OIDC.Complete(options.Serve.Listener); err != nil { - return nil, err - } - if err := options.Cookie.Complete(); err != nil { - return nil, err - } - if err := options.Serve.Complete(); err != nil { - return nil, err + if err := options.OIDC.Complete(options.Serve.Listener); err != nil { + return nil, err + } + if err := options.Cookie.Complete(); err != nil { + return nil, err + } } // normalize the scope and the isolation @@ -252,16 +256,19 @@ func (options *CompletedOptions) Validate() error { return fmt.Errorf("pretty name cannot be empty") } - if err := options.Serve.Validate(); err != nil { - return err - } + if !options.FrontendDisabled { + if err := options.Serve.Validate(); err != nil { + return err + } - if err := options.OIDC.Validate(); err != nil { - return err - } - if err := options.Cookie.Validate(); err != nil { - return err + if err := options.OIDC.Validate(); err != nil { + return err + } + if err := options.Cookie.Validate(); err != nil { + return err + } } + if options.ConsumerScope != string(kubebindv1alpha2.NamespacedScope) && options.ConsumerScope != string(kubebindv1alpha2.ClusterScope) { return fmt.Errorf("consumer scope must be either %q or %q", kubebindv1alpha2.NamespacedScope, kubebindv1alpha2.ClusterScope) } diff --git a/backend/server.go b/backend/server.go index 8960836a8..02f3353e1 100644 --- a/backend/server.go +++ b/backend/server.go @@ -29,6 +29,7 @@ import ( mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" "github.com/kube-bind/kube-bind/backend/auth" + "github.com/kube-bind/kube-bind/backend/controllers/bindableresourcesrequest" "github.com/kube-bind/kube-bind/backend/controllers/cluster" "github.com/kube-bind/kube-bind/backend/controllers/clusterbinding" "github.com/kube-bind/kube-bind/backend/controllers/serviceexport" @@ -52,11 +53,12 @@ type Server struct { } type Controllers struct { - ClusterBinding *clusterbinding.ClusterBindingReconciler - ServiceExport *serviceexport.APIServiceExportReconciler - ServiceExportRequest *serviceexportrequest.APIServiceExportRequestReconciler - ServiceNamespace *servicenamespace.APIServiceNamespaceReconciler - Cluster *cluster.ClusterReconciler + ClusterBinding *clusterbinding.ClusterBindingReconciler + ServiceExport *serviceexport.APIServiceExportReconciler + ServiceExportRequest *serviceexportrequest.APIServiceExportRequestReconciler + ServiceNamespace *servicenamespace.APIServiceNamespaceReconciler + Cluster *cluster.ClusterReconciler + BindableResourcesRequest *bindableresourcesrequest.BindableResourcesRequestReconciler } func NewServer(ctx context.Context, c *Config) (*Server, error) { @@ -67,27 +69,6 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { } var err error - s.WebServer, err = http.NewServer(c.Options.Serve) - if err != nil { - return nil, fmt.Errorf("error setting up HTTP Server: %w", err) - } - - // setup oidc backend - callback := c.Options.OIDC.CallbackURL - if callback == "" { - callback = fmt.Sprintf("http://%s/api/callback", s.WebServer.Addr().String()) - } - - // Use lazy initialization for embedded OIDC to avoid circular dependency - if c.Options.OIDC.OIDCServer != nil { - logger.Info("Using embedded OIDC server; will initialize lazily") - } else { - // External OIDC provider - initialize immediately - s.OIDC, err = s.initializeOIDCProvider(ctx, callback) - if err != nil { - return nil, fmt.Errorf("error setting up OIDC: %w", err) - } - } s.Kubernetes, err = kube.NewKubernetesManager( ctx, c.Options.NamespacePrefix, @@ -103,40 +84,67 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { return nil, fmt.Errorf("error setting up Kubernetes Manager: %w", err) } - signingKey, err := base64.StdEncoding.DecodeString(c.Options.Cookie.SigningKey) - if err != nil { - return nil, fmt.Errorf("error creating signing key: %w", err) - } - - var encryptionKey []byte - if c.Options.Cookie.EncryptionKey != "" { + if c.Options.FrontendDisabled { + logger.Info("Frontend is disabled; running in backend only mode") + } else { var err error - encryptionKey, err = base64.StdEncoding.DecodeString(c.Options.Cookie.EncryptionKey) + s.WebServer, err = http.NewServer(c.Options.Serve) if err != nil { - return nil, fmt.Errorf("error creating encryption key: %w", err) + return nil, fmt.Errorf("error setting up HTTP Server: %w", err) } - } - handler, err := http.NewHandler( - s, - s.Config.Options.OIDC.OIDCServer, - c.Options.OIDC.AuthorizeURL, - callback, - c.Options.PrettyName, - c.Options.TestingAutoSelect, - signingKey, - encryptionKey, - c.Options.SchemaSource, - kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), - s.Kubernetes, - c.Options.Frontend, - c.Options.TokenExpiry, - ) - if err != nil { - return nil, fmt.Errorf("error setting up HTTP Handler: %w", err) - } - if err := handler.AddRoutes(s.WebServer.Router); err != nil { - return nil, fmt.Errorf("error adding routes to HTTP Server: %w", err) + // setup oidc backend + callback := c.Options.OIDC.CallbackURL + if callback == "" { + callback = fmt.Sprintf("http://%s/api/callback", s.WebServer.Addr().String()) + } + + // Use lazy initialization for embedded OIDC to avoid circular dependency + if c.Options.OIDC.OIDCServer != nil { + logger.Info("Using embedded OIDC server; will initialize lazily") + } else { + // External OIDC provider - initialize immediately + s.OIDC, err = s.initializeOIDCProvider(ctx, callback) + if err != nil { + return nil, fmt.Errorf("error setting up OIDC: %w", err) + } + } + + signingKey, err := base64.StdEncoding.DecodeString(c.Options.Cookie.SigningKey) + if err != nil { + return nil, fmt.Errorf("error creating signing key: %w", err) + } + + var encryptionKey []byte + if c.Options.Cookie.EncryptionKey != "" { + var err error + encryptionKey, err = base64.StdEncoding.DecodeString(c.Options.Cookie.EncryptionKey) + if err != nil { + return nil, fmt.Errorf("error creating encryption key: %w", err) + } + } + + handler, err := http.NewHandler( + s, + s.Config.Options.OIDC.OIDCServer, + c.Options.OIDC.AuthorizeURL, + callback, + c.Options.PrettyName, + c.Options.TestingAutoSelect, + signingKey, + encryptionKey, + c.Options.SchemaSource, + kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), + s.Kubernetes, + c.Options.Frontend, + c.Options.TokenExpiry, + ) + if err != nil { + return nil, fmt.Errorf("error setting up HTTP Handler: %w", err) + } + if err := handler.AddRoutes(s.WebServer.Router); err != nil { + return nil, fmt.Errorf("error adding routes to HTTP Server: %w", err) + } } opts := controller.TypedOptions[mcreconcile.Request]{ @@ -219,6 +227,26 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { return nil, fmt.Errorf("error setting up LogicalCluster controller with manager: %w", err) } + // Setup BindableResourcesRequest controller - this handles direct CRD-based binding + // when the frontend is disabled (no HTTP API/OIDC flow) + if c.Options.FrontendDisabled { + s.BindableResourcesRequest, err = bindableresourcesrequest.NewBindableResourcesRequestReconciler( + ctx, + s.Config.Manager, + opts, + kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), + kubebindv1alpha2.Isolation(c.Options.Isolation), + s.Kubernetes, + ) + if err != nil { + return nil, fmt.Errorf("error setting up BindableResourcesRequest Controller: %w", err) + } + if err := s.BindableResourcesRequest.SetupWithManager(s.Config.Manager); err != nil { + return nil, fmt.Errorf("error setting up BindableResourcesRequest controller with manager: %w", err) + } + logger.Info("BindableResourcesRequest controller enabled for CRD-based binding") + } + return s, nil } @@ -298,11 +326,12 @@ func (s *Server) Run(ctx context.Context) error { } }() - go func() { - <-ctx.Done() - logger.Info("Context done") - }() - return s.WebServer.Start(ctx) + if !s.Config.Options.FrontendDisabled { + return s.WebServer.Start(ctx) + } + logger.Info("Frontend is disabled; skipping web server start") + <-ctx.Done() + return nil } func (s *Server) seedCluster(ctx context.Context) error { diff --git a/cli/cmd/kubectl-bind/cmd/kubectlBind.go b/cli/cmd/kubectl-bind/cmd/kubectlBind.go index da8646159..4140a784f 100644 --- a/cli/cmd/kubectl-bind/cmd/kubectlBind.go +++ b/cli/cmd/kubectl-bind/cmd/kubectlBind.go @@ -28,9 +28,11 @@ import ( apiservicecmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-apiservice/cmd" collectionscmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-collections/cmd" + deploycmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-deploy/cmd" logincmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-login/cmd" templatescmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-templates/cmd" bindcmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind/cmd" + clusteridentitycmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/cluster-identity/cmd" devcmd "github.com/kube-bind/kube-bind/cli/pkg/kubectl/dev/cmd" ) @@ -58,6 +60,13 @@ func KubectlBindCommand() *cobra.Command { } rootCmd.AddCommand(apiserviceCmd) + deployCmd, err := deploycmd.New(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v", err) + os.Exit(1) + } + rootCmd.AddCommand(deployCmd) + loginCmd, err := logincmd.New(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) if err != nil { fmt.Fprintf(os.Stderr, "error: %v", err) @@ -86,5 +95,12 @@ func KubectlBindCommand() *cobra.Command { } rootCmd.AddCommand(devCmd) + clusterIdentityCmd, err := clusteridentitycmd.New(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v", err) + os.Exit(1) + } + rootCmd.AddCommand(clusterIdentityCmd) + return rootCmd } diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/bind.go b/cli/pkg/kubectl/bind-apiservice/plugin/bind.go index a2a02550f..9559261d1 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/bind.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/bind.go @@ -351,11 +351,13 @@ func (b *BindAPIServiceOptions) bindTemplate(ctx context.Context) (*bindTemplate ObjectMeta: metav1.ObjectMeta{ Name: b.Name, }, - TemplateRef: kubebindv1alpha2.APIServiceExportTemplateRef{ - Name: b.Template, - }, - ClusterIdentity: kubebindv1alpha2.ClusterIdentity{ - Identity: b.ClusterIdentity, + Spec: kubebindv1alpha2.BindableResourcesRequestSpec{ + TemplateRef: kubebindv1alpha2.APIServiceExportTemplateRef{ + Name: b.Template, + }, + ClusterIdentity: kubebindv1alpha2.ClusterIdentity{ + Identity: b.ClusterIdentity, + }, }, } diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/binder.go b/cli/pkg/kubectl/bind-apiservice/plugin/binder.go index c1c81b2e5..fcfb3d0b9 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/binder.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/binder.go @@ -287,7 +287,7 @@ func (b *Binder) deployKonnector(ctx context.Context) error { DryRun: b.opts.DryRun, KonnectorHostAliasParsed: b.opts.KonnectorHostAliasParsed, } - return tempOpts.deployKonnector(ctx, b.config) + return tempOpts.DeployKonnector(ctx, b.config) } func (b *Binder) createServiceExportRequest(ctx context.Context, remoteConfig *rest.Config, remoteNamespace string, request *kubebindv1alpha2.APIServiceExportRequest) (*kubebindv1alpha2.APIServiceExportRequest, error) { diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/konnector.go b/cli/pkg/kubectl/bind-apiservice/plugin/konnector.go index 5c2c55590..976973d6e 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/konnector.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/konnector.go @@ -41,7 +41,8 @@ const ( konnectorImage = "ghcr.io/kube-bind/konnector" ) -func (b *BindAPIServiceOptions) deployKonnector(ctx context.Context, config *rest.Config) error { +// DeployKonnector deploys the konnector to the cluster. +func (b *BindAPIServiceOptions) DeployKonnector(ctx context.Context, config *rest.Config) error { logger := klog.FromContext(ctx) dynamicClient, err := dynamic.NewForConfig(config) diff --git a/cli/pkg/kubectl/bind-deploy/cmd/cmd.go b/cli/pkg/kubectl/bind-deploy/cmd/cmd.go new file mode 100644 index 000000000..bcacfa9a1 --- /dev/null +++ b/cli/pkg/kubectl/bind-deploy/cmd/cmd.go @@ -0,0 +1,94 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + logsv1 "k8s.io/component-base/logs/api/v1" + + "github.com/kube-bind/kube-bind/cli/pkg/help" + "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-deploy/plugin" + + _ "k8s.io/client-go/plugin/pkg/client/auth/exec" + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" +) + +var ( + bindDeployExampleUses = ` + # Deploy konnector from a BindingResourceResponse stored in a secret + %[1]s deploy --secret kube-bind/my-binding-response + + # Deploy konnector from a BindingResourceResponse file + %[1]s deploy --file binding-response.yaml + + # Deploy konnector reading from stdin + %[1]s deploy --file - + ` +) + +func New(streams genericclioptions.IOStreams) (*cobra.Command, error) { + opts := plugin.NewBindDeployOptions(streams) + cmd := &cobra.Command{ + Use: "deploy", + Short: "Deploy konnector from a BindingResourceResponse", + Long: help.Doc(` + Deploy the konnector and create APIServiceBindings from a BindingResourceResponse. + + This command is useful when you have obtained a BindingResourceResponse through + the CRD-based flow (BindableResourcesRequest) and want to deploy the konnector + to establish the connection. + + The BindingResourceResponse can be provided via: + - A Kubernetes secret (--secret namespace/name) + - A file (--file path/to/file.yaml) + - Standard input (--file -) + `), + Example: fmt.Sprintf(bindDeployExampleUses, "kubectl bind"), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if err := logsv1.ValidateAndApply(opts.Logs, nil); err != nil { + return err + } + + if err := opts.Complete(args); err != nil { + return err + } + + if err := opts.Validate(); err != nil { + return err + } + + if !opts.NoBanner { + yellow := color.New(color.BgRed, color.FgBlack).SprintFunc() + fmt.Fprintf(streams.ErrOut, "%s\n\n", yellow("DISCLAIMER: This is a prototype. It will change in incompatible ways at any time.")) + } + + if err := opts.Run(cmd.Context()); err != nil { + fmt.Fprintf(streams.ErrOut, "Error: %v\n", err) + return nil + } + return nil + }, + } + opts.AddCmdFlags(cmd) + + return cmd, nil +} diff --git a/cli/pkg/kubectl/bind-deploy/plugin/deploy.go b/cli/pkg/kubectl/bind-deploy/plugin/deploy.go new file mode 100644 index 000000000..24ade6803 --- /dev/null +++ b/cli/pkg/kubectl/bind-deploy/plugin/deploy.go @@ -0,0 +1,386 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + kubeclient "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/component-base/logs" + logsv1 "k8s.io/component-base/logs/api/v1" + "sigs.k8s.io/yaml" + + "github.com/kube-bind/kube-bind/cli/pkg/kubectl/base" + bindapiservice "github.com/kube-bind/kube-bind/cli/pkg/kubectl/bind-apiservice/plugin" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +// BindDeployOptions are the options for the kubectl-bind-deploy command. +type BindDeployOptions struct { + *base.Options + Logs *logs.Options + + JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags + OutputFormat string + Print *genericclioptions.PrintFlags + printer printers.ResourcePrinter + + // secretRef is the namespace/name reference to a secret containing the BindingResourceResponse + secretRef string + // secretKey is the key in the secret containing the BindingResourceResponse + secretKey string + // file is the path to a file containing the BindingResourceResponse + file string + + // skipKonnector skips the deployment of the konnector. + SkipKonnector bool + KonnectorImageOverride string + DowngradeKonnector bool + // KonnectorHostAlias is a list of host alias entries to add to the konnector pods. + KonnectorHostAlias []string + // KonnectorHostAliasParsed is the parsed version of KonnectorHostAlias. + KonnectorHostAliasParsed []corev1.HostAlias + + NoBanner bool + DryRun bool +} + +// NewBindDeployOptions returns new BindDeployOptions. +func NewBindDeployOptions(streams genericclioptions.IOStreams) *BindDeployOptions { + return &BindDeployOptions{ + Options: base.NewOptions(streams), + Logs: logs.NewOptions(), + Print: genericclioptions.NewPrintFlags("kubectl-bind-deploy"), + secretKey: "kubeconfig", // default key + } +} + +// AddCmdFlags binds fields to cmd's flagset. +func (b *BindDeployOptions) AddCmdFlags(cmd *cobra.Command) { + b.Options.BindFlags(cmd) + logsv1.AddFlags(b.Logs, cmd.Flags()) + b.Print.AddFlags(cmd) + + // Secret/file source + cmd.Flags().StringVar(&b.secretRef, "secret", b.secretRef, "Reference to a secret containing the BindingResourceResponse (format: namespace/name)") + cmd.Flags().StringVar(&b.secretKey, "secret-key", b.secretKey, "Key in the secret containing the BindingResourceResponse (default: kubeconfig)") + cmd.Flags().StringVarP(&b.file, "file", "f", b.file, "A file with a BindingResourceResponse manifest. Use - to read from stdin") + + // Konnector configuration + cmd.Flags().BoolVar(&b.SkipKonnector, "skip-konnector", b.SkipKonnector, "Skip the deployment of the konnector") + cmd.Flags().BoolVarP(&b.DryRun, "dry-run", "d", b.DryRun, "If true, only print the actions that would be taken without executing them") + cmd.Flags().StringVar(&b.KonnectorImageOverride, "konnector-image", b.KonnectorImageOverride, "The konnector image to use") + cmd.Flags().MarkHidden("konnector-image") //nolint:errcheck + cmd.Flags().StringSliceVarP(&b.KonnectorHostAlias, "konnector-host-alias", "", []string{}, "Add a host alias to the konnector pods in the format IP:hostname1,hostname2") + cmd.Flags().MarkHidden("konnector-host-alias") //nolint:errcheck + cmd.Flags().BoolVar(&b.DowngradeKonnector, "downgrade-konnector", b.DowngradeKonnector, "Downgrade the konnector to the version of the kubectl-bind binary") + cmd.Flags().BoolVar(&b.NoBanner, "no-banner", b.NoBanner, "Do not show the red banner") + cmd.Flags().MarkHidden("no-banner") //nolint:errcheck +} + +// Complete ensures all fields are initialized. +func (b *BindDeployOptions) Complete(args []string) error { + if err := b.Options.Complete(false); err != nil { + return err + } + + printer, err := b.Print.ToPrinter() + if err != nil { + return err + } + b.printer = printer + + // Parse konnector host alias entries + for _, hostAlias := range b.KonnectorHostAlias { + parts := strings.SplitN(hostAlias, ":", 2) + if len(parts) != 2 { + continue + } + hostnames := strings.Split(parts[1], ",") + b.KonnectorHostAliasParsed = append(b.KonnectorHostAliasParsed, corev1.HostAlias{ + IP: parts[0], + Hostnames: hostnames, + }) + } + return nil +} + +// Validate validates the BindDeployOptions are complete and usable. +func (b *BindDeployOptions) Validate() error { + if b.secretRef == "" && b.file == "" { + return errors.New("either --secret or --file is required") + } + + if b.secretRef != "" && b.file != "" { + return errors.New("only one of --secret or --file can be specified") + } + + if b.secretRef != "" { + parts := strings.SplitN(b.secretRef, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return fmt.Errorf("invalid --secret format %q, expected namespace/name", b.secretRef) + } + } + + // Validate konnector host alias entries + for _, hostAlias := range b.KonnectorHostAlias { + parts := strings.SplitN(hostAlias, ":", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid konnector-host-alias entry %q, expected format IP:hostname1,hostname2", hostAlias) + } + if parts[0] == "" { + return fmt.Errorf("invalid konnector-host-alias entry %q, IP address is empty", hostAlias) + } + if parts[1] == "" { + return fmt.Errorf("invalid konnector-host-alias entry %q, hostnames are empty", hostAlias) + } + } + + return b.Options.Validate() +} + +// Run starts the deployment process. +func (b *BindDeployOptions) Run(ctx context.Context) error { + fmt.Fprintf(b.Options.ErrOut, "Starting deployment process...\n") + + config, err := b.Options.ClientConfig.ClientConfig() + if err != nil { + return err + } + + // Get the BindingResourceResponse + response, err := b.getBindingResponse(ctx, config) + if err != nil { + return fmt.Errorf("failed to get BindingResourceResponse: %w", err) + } + + // Use the shared binder to deploy + binderOpts := &bindapiservice.BinderOptions{ + IOStreams: b.Options.IOStreams, + SkipKonnector: b.SkipKonnector, + KonnectorImageOverride: b.KonnectorImageOverride, + KonnectorHostAliasParsed: b.KonnectorHostAliasParsed, + DowngradeKonnector: b.DowngradeKonnector, + DryRun: b.DryRun, + } + binder := bindapiservice.NewBinder(config, binderOpts) + + bindings, err := b.bindFromKubeconfig(ctx, binder, config, response) + if err != nil { + return fmt.Errorf("failed to create bindings: %w", err) + } + + fmt.Fprintln(b.Options.ErrOut) + return b.printTable(ctx, config, bindings) +} + +// getBindingResponse reads the BindingResourceResponse from the specified source. +func (b *BindDeployOptions) getBindingResponse(ctx context.Context, config *rest.Config) (*kubebindv1alpha2.BindingResourceResponse, error) { + var data []byte + var err error + + if b.secretRef != "" { + parts := strings.SplitN(b.secretRef, "/", 2) + namespace, name := parts[0], parts[1] + + kubeClient, err := kubeclient.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create kube client: %w", err) + } + + secret, err := kubeClient.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get secret %s/%s: %w", namespace, name, err) + } + + data, ok := secret.Data[b.secretKey] + if !ok { + return nil, fmt.Errorf("secret %s/%s does not contain key %q", namespace, name, b.secretKey) + } + + var response kubebindv1alpha2.BindingResourceResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to unmarshal BindingResourceResponse from secret: %w", err) + } + + fmt.Fprintf(b.Options.ErrOut, "📦 Read BindingResourceResponse from secret %s/%s\n", namespace, name) + return &response, nil + } + + // Read from file + if b.file == "-" { + data, err = io.ReadAll(b.Options.In) + if err != nil { + return nil, fmt.Errorf("failed to read from stdin: %w", err) + } + } else { + data, err = os.ReadFile(b.file) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", b.file, err) + } + } + + var response kubebindv1alpha2.BindingResourceResponse + if err := yaml.Unmarshal(data, &response); err != nil { + // Try JSON if YAML fails + if err := json.Unmarshal(data, &response); err != nil { + return nil, fmt.Errorf("failed to unmarshal BindingResourceResponse: %w", err) + } + } + + if b.file == "-" { + fmt.Fprintf(b.Options.ErrOut, "📦 Read BindingResourceResponse from stdin\n") + } else { + fmt.Fprintf(b.Options.ErrOut, "📦 Read BindingResourceResponse from file %s\n", b.file) + } + + return &response, nil +} + +// bindFromKubeconfig creates bindings from a BindingResourceResponse that contains only kubeconfig. +func (b *BindDeployOptions) bindFromKubeconfig( + ctx context.Context, + binder *bindapiservice.Binder, + config *rest.Config, + response *kubebindv1alpha2.BindingResourceResponse, +) ([]*kubebindv1alpha2.APIServiceBinding, error) { + // Ensure client side namespace exists + err := b.ensureClientSideNamespaceExists(ctx, config) + if err != nil { + return nil, fmt.Errorf("failed to ensure kube-bind namespace exists: %w", err) + } + + // Parse remote kubeconfig to get host and namespace + remoteHost, remoteNamespace, err := base.ParseRemoteKubeconfig(response.Kubeconfig) + if err != nil { + return nil, fmt.Errorf("failed to parse kubeconfig: %w", err) + } + + kubeClient, err := kubeclient.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create kube client: %w", err) + } + + // Find or create the kubeconfig secret + secretName, err := base.FindRemoteKubeconfig(ctx, kubeClient, remoteNamespace, remoteHost) + if err != nil { + return nil, err + } + + secret, created, err := base.EnsureKubeconfigSecret(ctx, string(response.Kubeconfig), secretName, kubeClient) + if err != nil { + return nil, err + } + + if created { + fmt.Fprintf(b.Options.IOStreams.ErrOut, "🔒 Created secret %s/%s for host %s, namespace %s\n", "kube-bind", secret.Name, remoteHost, remoteNamespace) + } else { + fmt.Fprintf(b.Options.IOStreams.ErrOut, "🔒 Updated secret %s/%s for host %s, namespace %s\n", "kube-bind", secret.Name, remoteHost, remoteNamespace) + } + + if b.DryRun { + return nil, nil + } + + // Load remote config + remoteKubeConfig, err := clientcmd.Load(response.Kubeconfig) + if err != nil { + return nil, fmt.Errorf("failed to load remote kubeconfig: %w", err) + } + + _, err = clientcmd.NewDefaultClientConfig(*remoteKubeConfig, &clientcmd.ConfigOverrides{}).ClientConfig() + if err != nil { + return nil, fmt.Errorf("failed to create remote config: %w", err) + } + + // Deploy konnector + if !b.SkipKonnector { + if err := b.deployKonnector(ctx, config); err != nil { + return nil, fmt.Errorf("failed to deploy konnector: %w", err) + } + } + + // If response has requests, process them (for full BindingResourceResponse flow) + if len(response.Requests) > 0 { + return binder.BindFromResponse(ctx, response) + } + + return []*kubebindv1alpha2.APIServiceBinding{}, nil +} + +func (b *BindDeployOptions) ensureClientSideNamespaceExists(ctx context.Context, config *rest.Config) error { + kubeClient, err := kubeclient.NewForConfig(config) + if err != nil { + return err + } + + _, err = kubeClient.CoreV1().Namespaces().Get(ctx, "kube-bind", metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return err + } else if apierrors.IsNotFound(err) { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-bind", + }, + } + if _, err = kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); err != nil { + return err + } + fmt.Fprintf(b.Options.IOStreams.ErrOut, "📦 Created kube-bind namespace.\n") + } + return nil +} + +func (b *BindDeployOptions) deployKonnector(ctx context.Context, config *rest.Config) error { + tempOpts := &bindapiservice.BindAPIServiceOptions{ + Options: b.Options, + SkipKonnector: b.SkipKonnector, + KonnectorImageOverride: b.KonnectorImageOverride, + DowngradeKonnector: b.DowngradeKonnector, + DryRun: b.DryRun, + KonnectorHostAliasParsed: b.KonnectorHostAliasParsed, + } + return tempOpts.DeployKonnector(ctx, config) +} + +func (b *BindDeployOptions) printTable(ctx context.Context, config *rest.Config, bindings []*kubebindv1alpha2.APIServiceBinding) error { + if len(bindings) == 0 { + fmt.Fprintf(b.Options.IOStreams.ErrOut, "No bindings created.\n") + return nil + } + + fmt.Fprintf(b.Options.IOStreams.ErrOut, "\nCreated bindings:\n") + for _, binding := range bindings { + fmt.Fprintf(b.Options.IOStreams.Out, " - %s\n", binding.Name) + } + + return nil +} diff --git a/cli/pkg/kubectl/cluster-identity/cmd/cmd.go b/cli/pkg/kubectl/cluster-identity/cmd/cmd.go new file mode 100644 index 000000000..0e55862f9 --- /dev/null +++ b/cli/pkg/kubectl/cluster-identity/cmd/cmd.go @@ -0,0 +1,79 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + logsv1 "k8s.io/component-base/logs/api/v1" + + "github.com/kube-bind/kube-bind/cli/pkg/kubectl/cluster-identity/plugin" +) + +var ( + ClusterIdentityExampleUses = ` + # Get the cluster identity for the current kubeconfig context + %[1]s cluster-identity + + # Get the cluster identity using a specific kubeconfig + %[1]s cluster-identity --kubeconfig /path/to/kubeconfig + + # Get the cluster identity using a specific context + %[1]s cluster-identity --context my-context + + # Get the cluster identity from a specific namespace (default: kube-system) + %[1]s cluster-identity --namespace my-namespace + ` +) + +func New(streams genericclioptions.IOStreams) (*cobra.Command, error) { + opts := plugin.NewClusterIdentityOptions(streams) + cmd := &cobra.Command{ + Use: "cluster-identity", + Short: "Get the cluster identity for the current Kubernetes cluster", + Long: `Get the cluster identity for the current Kubernetes cluster. + +The cluster identity is derived from the UID of a namespace (by default kube-system). +This identity is used by kube-bind to uniquely identify consumer clusters when +binding to service providers. + +This is a helper command for users who are not using the kube-bind web server +and need to manually provide the cluster identity.`, + Example: fmt.Sprintf(ClusterIdentityExampleUses, "kubectl bind"), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if err := logsv1.ValidateAndApply(opts.Logs, nil); err != nil { + return err + } + + if err := opts.Complete(args); err != nil { + return err + } + + if err := opts.Validate(); err != nil { + return err + } + + return opts.Run(cmd.Context()) + }, + } + opts.AddCmdFlags(cmd) + + return cmd, nil +} diff --git a/cli/pkg/kubectl/cluster-identity/plugin/cluster_identity.go b/cli/pkg/kubectl/cluster-identity/plugin/cluster_identity.go new file mode 100644 index 000000000..18e449ac2 --- /dev/null +++ b/cli/pkg/kubectl/cluster-identity/plugin/cluster_identity.go @@ -0,0 +1,118 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/component-base/logs" + logsv1 "k8s.io/component-base/logs/api/v1" + + "github.com/kube-bind/kube-bind/cli/pkg/kubectl/base" +) + +const ( + defaultIdentityNamespace = "kube-system" +) + +// ClusterIdentityOptions contains the options for getting the cluster identity +type ClusterIdentityOptions struct { + genericclioptions.IOStreams + Logs *logs.Options + + // Kubeconfig specifies kubeconfig file(s). + Kubeconfig string + // KubectlOverrides stores the extra client connection fields, such as context, user, etc. + KubectlOverrides *clientcmd.ConfigOverrides + // Namespace is the namespace to use for deriving the cluster identity + Namespace string + // ClientConfig is the resolved clientcmd.ClientConfig based on the client connection flags. + ClientConfig clientcmd.ClientConfig +} + +// NewClusterIdentityOptions creates a new ClusterIdentityOptions +func NewClusterIdentityOptions(streams genericclioptions.IOStreams) *ClusterIdentityOptions { + return &ClusterIdentityOptions{ + IOStreams: streams, + Logs: logs.NewOptions(), + KubectlOverrides: &clientcmd.ConfigOverrides{}, + Namespace: defaultIdentityNamespace, + } +} + +// AddCmdFlags adds command line flags +func (o *ClusterIdentityOptions) AddCmdFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "path to the kubeconfig file") + cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", o.Namespace, "namespace to use for deriving the cluster identity (default: kube-system)") + + // Add context flag from kubectl overrides + kubectlConfigOverrideFlags := clientcmd.RecommendedConfigOverrideFlags("") + kubectlConfigOverrideFlags.AuthOverrideFlags.ClientCertificate.LongName = "" + kubectlConfigOverrideFlags.AuthOverrideFlags.ClientKey.LongName = "" + kubectlConfigOverrideFlags.AuthOverrideFlags.Impersonate.LongName = "" + kubectlConfigOverrideFlags.AuthOverrideFlags.ImpersonateGroups.LongName = "" + kubectlConfigOverrideFlags.ContextOverrideFlags.ClusterName.LongName = "" + kubectlConfigOverrideFlags.Timeout.LongName = "" + + clientcmd.BindOverrideFlags(o.KubectlOverrides, cmd.PersistentFlags(), kubectlConfigOverrideFlags) + + logsv1.AddFlags(o.Logs, cmd.Flags()) +} + +// Complete completes the options +func (o *ClusterIdentityOptions) Complete(args []string) error { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + loadingRules.ExplicitPath = o.Kubeconfig + + startingConfig, err := loadingRules.GetStartingConfig() + if err != nil { + return err + } + + o.ClientConfig = clientcmd.NewDefaultClientConfig(*startingConfig, o.KubectlOverrides) + + return nil +} + +// Validate validates the options +func (o *ClusterIdentityOptions) Validate() error { + if o.Namespace == "" { + return fmt.Errorf("namespace cannot be empty") + } + return nil +} + +// Run executes the cluster-identity command +func (o *ClusterIdentityOptions) Run(ctx context.Context) error { + restConfig, err := o.ClientConfig.ClientConfig() + if err != nil { + return fmt.Errorf("failed to get REST config: %w", err) + } + + identity, err := base.GetClusterIdentityFromNamespace(ctx, restConfig, o.Namespace) + if err != nil { + return fmt.Errorf("failed to get cluster identity: %w", err) + } + + fmt.Fprintln(o.Out, identity) + + return nil +} diff --git a/contrib/kcp/README.md b/contrib/kcp/README.md index cfbec26fb..ef9c2c282 100644 --- a/contrib/kcp/README.md +++ b/contrib/kcp/README.md @@ -143,4 +143,230 @@ Start konnector: ```bash kubectl apply -f deploy/examples/cr-cowboy.yaml kubectl apply -f deploy/examples/cr-sheriff.yaml -``` \ No newline at end of file +``` + + +# Backend only mode + +Sometimes when integrating with existing systems, you might want to run kube-bind backend without any frontend, API and OIDC. +This is useful when running in multi-tenant environments where each tenant has its own identity provider and frontend (or no frontend at all). + + +1. Start kcp + +```bash +make run-kcp +``` + +## Backend + +2. Bootstrap kcp: +```bash +cp .kcp/admin.kubeconfig .kcp/backend.kubeconfig +export KUBECONFIG=.kcp/backend.kubeconfig +./bin/kcp-init --kcp-kubeconfig $KUBECONFIG +``` +3. Run the backend with `--isolation-mode=None` for no isolation mode. +``` +k ws use :root:kube-bind + +go run ./cmd/backend \ + --multicluster-runtime-provider kcp \ + --apiexport-endpoint-slice-name=kube-bind.io \ + --pretty-name="BigCorp.com" \ + --frontend-disabled=true \ + --namespace-prefix="kube-bind-" \ + --schema-source apiresourceschemas \ + --consumer-scope=cluster \ + --isolation=None +``` + +This process will keep running, so open a new terminal. + +## Provider + +4. Copy the kubeconfig to the provider and create provider workspace: +```bash +cp .kcp/admin.kubeconfig .kcp/provider.kubeconfig +export KUBECONFIG=.kcp/provider.kubeconfig +k ws use :root +kubectl create-workspace provider --enter +``` + +5. Bind the APIExport to the provider workspace +```bash +kubectl kcp bind apiexport root:kube-bind:kube-bind.io \ + --accept-permission-claim clusterrolebindings.rbac.authorization.k8s.io \ + --accept-permission-claim clusterroles.rbac.authorization.k8s.io \ + --accept-permission-claim customresourcedefinitions.apiextensions.k8s.io \ + --accept-permission-claim serviceaccounts.core \ + --accept-permission-claim configmaps.core \ + --accept-permission-claim secrets.core \ + --accept-permission-claim namespaces.core \ + --accept-permission-claim roles.rbac.authorization.k8s.io \ + --accept-permission-claim subjectaccessreviews.authorization.k8s.io \ + --accept-permission-claim rolebindings.rbac.authorization.k8s.io \ + --accept-permission-claim apiresourceschemas.apis.kcp.io +``` + +6. Create CRD in provider: +```bash +kubectl apply -f contrib/kcp/deploy/examples/apiexport.yaml +kubectl apply -f contrib/kcp/deploy/examples/apiresourceschema-cowboys.yaml +kubectl apply -f contrib/kcp/deploy/examples/apiresourceschema-sheriffs.yaml +kubectl kcp bind apiexport root:provider:cowboys-stable + +kubectl apply -f deploy/examples/template-cowboys.yaml +kubectl apply -f deploy/examples/template-sheriffs.yaml +kubectl apply -f deploy/examples/collection.yaml +``` + +7. Get LogicalCluster: + +```bash +kubectl get logicalcluster +# NAME PHASE URL AGE +# cluster Ready https://192.168.2.166:6443/clusters/20nuv280snhqd5j4 +``` + +## Consumer + +8. Now we gonna initiate consumer: +```bash +cp .kcp/admin.kubeconfig .kcp/consumer.kubeconfig +export KUBECONFIG=.kcp/consumer.kubeconfig +kubectl ws use :root +kubectl ws create consumer --enter +``` + +This is where it starts to differ from normal setup. CLI login and binding will not work without OIDC and API. +So we need manually create binding request. + +**Backend-Only Binding Process:** + +In traditional mode, `BindableResourcesRequest` is sent as a REST API payload to the backend HTTP server, which then: +1. Creates a dedicated namespace for the consumer +2. Generates a kubeconfig for that namespace +3. Returns it as an HTTP response + +In backend-only mode, `BindableResourcesRequest` is a **CRD** that triggers the same binding process: +1. A controller watches for `BindableResourcesRequest` resources +2. Creates the namespace and kubeconfig automatically +3. Stores the response in a Secret (specified by `kubeconfigSecretRef`) + +This enables GitOps and automation without requiring HTTP API access. + +We need first to get identity for the cluster we gonna use. +``` +kubectl bind cluster-identity +```bash +kubectl apply -f - < remote.data +``` + +9. Bind the thing on the consumer cluster: + +```bash +./bin/kubectl-bind deploy --file remote.data --skip-konnector +``` + +### Consumer Konnector + +Start konnector: + +```bash +./bin/konnector --lease-namespace default --kubeconfig .kcp/consumer.kubeconfig +``` + +We will be doing wild card pull, meaning pull every contract we have access to. + +```bash +kubectl apply -f - <-binding-response". + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + templateRef: + description: TemplateRef specifies the APIServiceExportTemplate to bind to. + properties: + name: + description: name is the name of the APIServiceExportTemplate to + bind to. + type: string + required: + - name + type: object + ttlAfterFinished: + description: |- + ttlAfterFinished is the TTL after the request has succeeded or failed + before it is automatically deleted. If not set, the request will not be + automatically deleted. Example values: "1h", "30m", "300s". type: string required: - - name + - author + - clusterIdentity + type: object + status: + default: {} + description: status contains reconciliation information for the binding + request. + properties: + completionTime: + description: |- + completionTime is the time when the request finished processing (succeeded or failed). + Used for TTL-based cleanup. + format: date-time + type: string + conditions: + description: conditions contains the current conditions of the binding + request. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + kubeconfigSecretRef: + description: |- + kubeconfigSecretRef is a reference to a secret containing the kubeconfig, used + to be used by the konnector agent. + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + phase: + default: Pending + description: phase is the current phase of the binding request. + enum: + - Pending + - Failed + - Succeeded + type: string type: object required: - - clusterIdentity - - templateRef + - spec type: object served: true storage: true - subresources: {} + subresources: + status: {} diff --git a/contrib/kcp/deploy/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml index d9ac8b6b2..dc44eb2b1 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-clusterbindings.kube-bind.io.yaml @@ -1,7 +1,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: - name: v250925-56669b8.clusterbindings.kube-bind.io + name: v260122-c9f0f376.clusterbindings.kube-bind.io spec: conversion: strategy: None @@ -214,9 +214,7 @@ spec: kubeconfig of the service cluster. properties: key: - description: The key of the secret to select from. Must be "kubeconfig". - enum: - - kubeconfig + description: The key of the secret to select from. type: string name: description: Name of the referent. diff --git a/contrib/kcp/test/e2e/binding.go b/contrib/kcp/test/e2e/binding.go index c06ab27aa..8034081c8 100644 --- a/contrib/kcp/test/e2e/binding.go +++ b/contrib/kcp/test/e2e/binding.go @@ -54,11 +54,13 @@ func performBinding( ObjectMeta: metav1.ObjectMeta{ Name: "test-binding", }, - TemplateRef: kubebindv1alpha2.APIServiceExportTemplateRef{ - Name: templateRef, - }, - ClusterIdentity: kubebindv1alpha2.ClusterIdentity{ - Identity: identity.String(), + Spec: kubebindv1alpha2.BindableResourcesRequestSpec{ + TemplateRef: kubebindv1alpha2.APIServiceExportTemplateRef{ + Name: templateRef, + }, + ClusterIdentity: kubebindv1alpha2.ClusterIdentity{ + Identity: identity.String(), + }, }, }) require.NoError(t, err) diff --git a/deploy/charts/backend/crds/kube-bind.io_apiservicebindingbundles.yaml b/deploy/charts/backend/crds/kube-bind.io_apiservicebindingbundles.yaml new file mode 100644 index 000000000..849902618 --- /dev/null +++ b/deploy/charts/backend/crds/kube-bind.io_apiservicebindingbundles.yaml @@ -0,0 +1,149 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: apiservicebindingbundles.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceBindingBundle + listKind: APIServiceBindingBundleList + plural: apiservicebindingbundles + shortNames: + - sbb + singular: apiservicebindingbundle + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.providerPrettyName + name: Provider + type: string + - jsonPath: .metadata.annotations.kube-bind\.io/resources + name: Resources + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Message + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + APIServiceBindingBundle automatically binds multiple API services represented by + APIServiceExports in a service provider cluster into a consumer cluster. This object lives in consumer clusters, + and pulls in all API services defined in the provider clusters, based on the kubeconfig secret provided. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec specifies how an API service from a service provider should be bound in the + local consumer cluster. + properties: + kubeconfigSecretRef: + description: kubeconfigSecretName is the secret ref that contains + the kubeconfig of the service cluster. + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + namespace: + description: Namespace of the referent. + minLength: 1 + type: string + required: + - key + - name + - namespace + type: object + x-kubernetes-validations: + - message: kubeconfigSecretRef is immutable + rule: self == oldSelf + required: + - kubeconfigSecretRef + type: object + status: + description: status contains reconciliation information for a service + binding. + properties: + conditions: + description: conditions is a list of conditions that apply to the + APIServiceBindingBundle. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml b/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml index 95304a382..c443447f3 100644 --- a/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml +++ b/deploy/charts/backend/crds/kube-bind.io_apiservicebindings.yaml @@ -206,9 +206,7 @@ spec: the kubeconfig of the service cluster. properties: key: - description: The key of the secret to select from. Must be "kubeconfig". - enum: - - kubeconfig + description: The key of the secret to select from. type: string name: description: Name of the referent. diff --git a/deploy/charts/backend/crds/kube-bind.io_bindableresourcesrequests.yaml b/deploy/charts/backend/crds/kube-bind.io_bindableresourcesrequests.yaml index 5c1210e7e..411178bbd 100644 --- a/deploy/charts/backend/crds/kube-bind.io_bindableresourcesrequests.yaml +++ b/deploy/charts/backend/crds/kube-bind.io_bindableresourcesrequests.yaml @@ -8,19 +8,35 @@ metadata: spec: group: kube-bind.io names: + categories: + - kube-bindings kind: BindableResourcesRequest listKind: BindableResourcesRequestList plural: bindableresourcesrequests singular: bindableresourcesrequest scope: Namespaced versions: - - name: v1alpha2 + - additionalPrinterColumns: + - jsonPath: .spec.templateRef.name + name: Template + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 schema: openAPIV3Schema: description: |- BindableResourcesRequest is sent by the consumer to the service provider - to indicate which resources the user wants to bind to. It is sent after - authentication and resource selection on the service provider website. + to indicate which resources the user wants to bind to. It can be sent via + the HTTP API after authentication, or created directly as a CRD when the + backend is running without the HTTP API/OIDC flow (frontend disabled mode). + + When created as a CRD, a controller will process the request and create the + necessary APIServiceExport and related resources. properties: apiVersion: description: |- @@ -29,15 +45,6 @@ spec: may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - clusterIdentity: - description: |- - ClusterIdentity contains information that uniquely identifies the cluster. - When doing dry run, we expect the client to fill this field in (or it will be taken from local cluster where context is available). - properties: - identity: - description: Identity is the unique identifier of the cluster. - type: string - type: object kind: description: |- Kind is a string value representing the REST resource this object represents. @@ -48,20 +55,160 @@ spec: type: string metadata: type: object - templateRef: - description: TemplateRef specifies the APIServiceExportTemplate to bind - to. + spec: + description: spec specifies the binding request details. properties: - name: - description: name is the name of the APIServiceExportTemplate to bind - to. + author: + description: |- + Author is the identifier of the entity that created this binding request. + This is used for audit purposes and to track who initiated the binding. + type: string + clusterIdentity: + description: |- + ClusterIdentity contains information that uniquely identifies the cluster. + This is used when the request is made via the HTTP API after authentication. + properties: + identity: + description: Identity is the unique identifier of the cluster. + type: string + type: object + kubeconfigSecretRef: + description: |- + kubeconfigSecretRef is a reference to an existing secret where the binding response + will be stored. If specified, the controller will update this secret with the + binding response data. If not specified, a new secret will be created with + the name "-binding-response". + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + templateRef: + description: TemplateRef specifies the APIServiceExportTemplate to + bind to. + properties: + name: + description: name is the name of the APIServiceExportTemplate + to bind to. + type: string + required: + - name + type: object + ttlAfterFinished: + description: |- + ttlAfterFinished is the TTL after the request has succeeded or failed + before it is automatically deleted. If not set, the request will not be + automatically deleted. Example values: "1h", "30m", "300s". type: string required: - - name + - author + - clusterIdentity + type: object + status: + default: {} + description: status contains reconciliation information for the binding + request. + properties: + completionTime: + description: |- + completionTime is the time when the request finished processing (succeeded or failed). + Used for TTL-based cleanup. + format: date-time + type: string + conditions: + description: conditions contains the current conditions of the binding + request. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + kubeconfigSecretRef: + description: |- + kubeconfigSecretRef is a reference to a secret containing the kubeconfig, used + to be used by the konnector agent. + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + phase: + default: Pending + description: phase is the current phase of the binding request. + enum: + - Pending + - Failed + - Succeeded + type: string type: object required: - - clusterIdentity - - templateRef + - spec type: object served: true storage: true + subresources: + status: {} diff --git a/deploy/charts/backend/crds/kube-bind.io_clusterbindings.yaml b/deploy/charts/backend/crds/kube-bind.io_clusterbindings.yaml index 366a382e6..d798ab5f2 100644 --- a/deploy/charts/backend/crds/kube-bind.io_clusterbindings.yaml +++ b/deploy/charts/backend/crds/kube-bind.io_clusterbindings.yaml @@ -217,9 +217,7 @@ spec: the kubeconfig of the service cluster. properties: key: - description: The key of the secret to select from. Must be "kubeconfig". - enum: - - kubeconfig + description: The key of the secret to select from. type: string name: description: Name of the referent. diff --git a/deploy/charts/backend/templates/role.yaml b/deploy/charts/backend/templates/role.yaml index 1a83d9a3d..635ebfcec 100644 --- a/deploy/charts/backend/templates/role.yaml +++ b/deploy/charts/backend/templates/role.yaml @@ -49,6 +49,7 @@ rules: - apiserviceexportrequests - apiserviceexports - apiservicenamespaces + - bindableresourcesrequests - boundschemas - clusterbindings verbs: @@ -65,6 +66,7 @@ rules: - apiserviceexportrequests/finalizers - apiserviceexports/finalizers - apiservicenamespaces/finalizers + - bindableresourcesrequests/finalizers - clusterbindings/finalizers verbs: - update @@ -74,6 +76,7 @@ rules: - apiserviceexportrequests/status - apiserviceexports/status - apiservicenamespaces/status + - bindableresourcesrequests/status - clusterbindings/status verbs: - get diff --git a/deploy/crd/kube-bind.io_apiservicebindingbundles.yaml b/deploy/crd/kube-bind.io_apiservicebindingbundles.yaml new file mode 100644 index 000000000..849902618 --- /dev/null +++ b/deploy/crd/kube-bind.io_apiservicebindingbundles.yaml @@ -0,0 +1,149 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: apiservicebindingbundles.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bindings + kind: APIServiceBindingBundle + listKind: APIServiceBindingBundleList + plural: apiservicebindingbundles + shortNames: + - sbb + singular: apiservicebindingbundle + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.providerPrettyName + name: Provider + type: string + - jsonPath: .metadata.annotations.kube-bind\.io/resources + name: Resources + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Message + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + APIServiceBindingBundle automatically binds multiple API services represented by + APIServiceExports in a service provider cluster into a consumer cluster. This object lives in consumer clusters, + and pulls in all API services defined in the provider clusters, based on the kubeconfig secret provided. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec specifies how an API service from a service provider should be bound in the + local consumer cluster. + properties: + kubeconfigSecretRef: + description: kubeconfigSecretName is the secret ref that contains + the kubeconfig of the service cluster. + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + namespace: + description: Namespace of the referent. + minLength: 1 + type: string + required: + - key + - name + - namespace + type: object + x-kubernetes-validations: + - message: kubeconfigSecretRef is immutable + rule: self == oldSelf + required: + - kubeconfigSecretRef + type: object + status: + description: status contains reconciliation information for a service + binding. + properties: + conditions: + description: conditions is a list of conditions that apply to the + APIServiceBindingBundle. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/crd/kube-bind.io_apiservicebindings.yaml b/deploy/crd/kube-bind.io_apiservicebindings.yaml index ef211fe6e..3a15f95d3 100644 --- a/deploy/crd/kube-bind.io_apiservicebindings.yaml +++ b/deploy/crd/kube-bind.io_apiservicebindings.yaml @@ -207,9 +207,7 @@ spec: the kubeconfig of the service cluster. properties: key: - description: The key of the secret to select from. Must be "kubeconfig". - enum: - - kubeconfig + description: The key of the secret to select from. type: string name: description: Name of the referent. diff --git a/deploy/crd/kube-bind.io_bindableresourcesrequests.yaml b/deploy/crd/kube-bind.io_bindableresourcesrequests.yaml index 5c1210e7e..411178bbd 100644 --- a/deploy/crd/kube-bind.io_bindableresourcesrequests.yaml +++ b/deploy/crd/kube-bind.io_bindableresourcesrequests.yaml @@ -8,19 +8,35 @@ metadata: spec: group: kube-bind.io names: + categories: + - kube-bindings kind: BindableResourcesRequest listKind: BindableResourcesRequestList plural: bindableresourcesrequests singular: bindableresourcesrequest scope: Namespaced versions: - - name: v1alpha2 + - additionalPrinterColumns: + - jsonPath: .spec.templateRef.name + name: Template + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 schema: openAPIV3Schema: description: |- BindableResourcesRequest is sent by the consumer to the service provider - to indicate which resources the user wants to bind to. It is sent after - authentication and resource selection on the service provider website. + to indicate which resources the user wants to bind to. It can be sent via + the HTTP API after authentication, or created directly as a CRD when the + backend is running without the HTTP API/OIDC flow (frontend disabled mode). + + When created as a CRD, a controller will process the request and create the + necessary APIServiceExport and related resources. properties: apiVersion: description: |- @@ -29,15 +45,6 @@ spec: may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - clusterIdentity: - description: |- - ClusterIdentity contains information that uniquely identifies the cluster. - When doing dry run, we expect the client to fill this field in (or it will be taken from local cluster where context is available). - properties: - identity: - description: Identity is the unique identifier of the cluster. - type: string - type: object kind: description: |- Kind is a string value representing the REST resource this object represents. @@ -48,20 +55,160 @@ spec: type: string metadata: type: object - templateRef: - description: TemplateRef specifies the APIServiceExportTemplate to bind - to. + spec: + description: spec specifies the binding request details. properties: - name: - description: name is the name of the APIServiceExportTemplate to bind - to. + author: + description: |- + Author is the identifier of the entity that created this binding request. + This is used for audit purposes and to track who initiated the binding. + type: string + clusterIdentity: + description: |- + ClusterIdentity contains information that uniquely identifies the cluster. + This is used when the request is made via the HTTP API after authentication. + properties: + identity: + description: Identity is the unique identifier of the cluster. + type: string + type: object + kubeconfigSecretRef: + description: |- + kubeconfigSecretRef is a reference to an existing secret where the binding response + will be stored. If specified, the controller will update this secret with the + binding response data. If not specified, a new secret will be created with + the name "-binding-response". + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + templateRef: + description: TemplateRef specifies the APIServiceExportTemplate to + bind to. + properties: + name: + description: name is the name of the APIServiceExportTemplate + to bind to. + type: string + required: + - name + type: object + ttlAfterFinished: + description: |- + ttlAfterFinished is the TTL after the request has succeeded or failed + before it is automatically deleted. If not set, the request will not be + automatically deleted. Example values: "1h", "30m", "300s". type: string required: - - name + - author + - clusterIdentity + type: object + status: + default: {} + description: status contains reconciliation information for the binding + request. + properties: + completionTime: + description: |- + completionTime is the time when the request finished processing (succeeded or failed). + Used for TTL-based cleanup. + format: date-time + type: string + conditions: + description: conditions contains the current conditions of the binding + request. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + kubeconfigSecretRef: + description: |- + kubeconfigSecretRef is a reference to a secret containing the kubeconfig, used + to be used by the konnector agent. + properties: + key: + description: The key of the secret to select from. + type: string + name: + description: Name of the referent. + minLength: 1 + type: string + required: + - key + - name + type: object + phase: + default: Pending + description: phase is the current phase of the binding request. + enum: + - Pending + - Failed + - Succeeded + type: string type: object required: - - clusterIdentity - - templateRef + - spec type: object served: true storage: true + subresources: + status: {} diff --git a/deploy/crd/kube-bind.io_clusterbindings.yaml b/deploy/crd/kube-bind.io_clusterbindings.yaml index b9c3b5028..0731aedc4 100644 --- a/deploy/crd/kube-bind.io_clusterbindings.yaml +++ b/deploy/crd/kube-bind.io_clusterbindings.yaml @@ -218,9 +218,7 @@ spec: the kubeconfig of the service cluster. properties: key: - description: The key of the secret to select from. Must be "kubeconfig". - enum: - - kubeconfig + description: The key of the secret to select from. type: string name: description: Name of the referent. diff --git a/docs/proposals/backend-only-bindings.md b/docs/proposals/backend-only-bindings.md new file mode 100644 index 000000000..2a5fee69c --- /dev/null +++ b/docs/proposals/backend-only-bindings.md @@ -0,0 +1,114 @@ +# Backend-Only Bindings + +**Status**: Implemented +**Date**: January 2025 + +## Problem + +Traditional kube-bind requires OIDC authentication and manual web UI interaction to bind services. This doesn't work for: +- CI/CD pipelines and automation +- Headless servers +- GitOps workflows + +## Solution + +**APIServiceBindingBundle**: A single resource that automatically discovers and binds ALL services from a provider cluster using only a kubeconfig. + +**One command instead of a multi-step handshake:** + +```bash +kubectl create secret generic provider-kubeconfig \ + --from-file=kubeconfig=provider.kubeconfig + +kubectl apply -f - < 0 { + conditions.MarkFalse( + bundle, + APIServiceBindingBundleConditionSynced, + "SyncErrors", + conditionsapi.ConditionSeverityWarning, + "Failed to sync some APIServiceBindings: %d error(s)", + len(errs), + ) + return utilerrors.NewAggregate(errs) + } + + conditions.MarkTrue( + bundle, + APIServiceBindingBundleConditionSynced, + ) + + return nil +} diff --git a/pkg/konnector/konnector_controller.go b/pkg/konnector/konnector_controller.go index edd01b95a..2405f27dd 100644 --- a/pkg/konnector/konnector_controller.go +++ b/pkg/konnector/konnector_controller.go @@ -39,6 +39,7 @@ import ( "github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/dynamic" "github.com/kube-bind/kube-bind/pkg/konnector/controllers/servicebinding" + "github.com/kube-bind/kube-bind/pkg/konnector/controllers/servicebindingbundle" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" bindclient "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned" bindinformers "github.com/kube-bind/kube-bind/sdk/client/informers/externalversions/kubebind/v1alpha2" @@ -53,9 +54,11 @@ const ( func New( consumerConfig *rest.Config, serviceBindingInformer bindinformers.APIServiceBindingInformer, + serviceBindingBundleInformer bindinformers.APIServiceBindingBundleInformer, secretInformer coreinformers.SecretInformer, namespaceInformer coreinformers.NamespaceInformer, crdInformer crdinformers.CustomResourceDefinitionInformer, + bundlePollingInterval time.Duration, ) (*Controller, error) { queue := workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[string](), workqueue.TypedRateLimitingQueueConfig[string]{Name: controllerName}) @@ -74,6 +77,11 @@ func New( return nil, err } + servicebindingbundleCtrl, err := servicebindingbundle.NewController(consumerConfig, serviceBindingBundleInformer, serviceBindingInformer, secretInformer, bundlePollingInterval) + if err != nil { + return nil, err + } + namespaceDynamicInformer, err := dynamic.NewDynamicInformer(namespaceInformer) if err != nil { return nil, err @@ -99,7 +107,8 @@ func New( secretLister: secretInformer.Lister(), secretIndexer: secretInformer.Informer().GetIndexer(), - ServiceBindingCtrl: servicebindingCtrl, + ServiceBindingCtrl: servicebindingCtrl, + ServiceBindingBundleCtrl: servicebindingbundleCtrl, reconciler: reconciler{ controllers: map[string]*controllerContext{}, @@ -191,7 +200,8 @@ type Controller struct { secretLister corelisters.SecretLister secretIndexer cache.Indexer - ServiceBindingCtrl GenericController + ServiceBindingCtrl GenericController + ServiceBindingBundleCtrl GenericController reconciler @@ -256,6 +266,7 @@ func (c *Controller) Start(ctx context.Context, numThreads int) { } go c.ServiceBindingCtrl.Start(ctx, numThreads) + go c.ServiceBindingBundleCtrl.Start(ctx, numThreads) <-ctx.Done() } diff --git a/pkg/konnector/options/options.go b/pkg/konnector/options/options.go index 825f12ce9..50d305ff3 100644 --- a/pkg/konnector/options/options.go +++ b/pkg/konnector/options/options.go @@ -20,6 +20,7 @@ import ( "fmt" "math/rand" "os" + "time" "github.com/spf13/pflag" "k8s.io/component-base/logs" @@ -39,7 +40,8 @@ type ExtraOptions struct { LeaseLockNamespace string LeaseLockIdentity string - ServerAddr string + ServerAddr string + ProviderPollingInterval time.Duration } type completedOptions struct { @@ -81,6 +83,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&options.LeaseLockName, "lease-name", options.LeaseLockName, "Name of lease lock") fs.StringVar(&options.LeaseLockNamespace, "lease-namespace", options.LeaseLockNamespace, "Name of lease lock namespace") fs.StringVar(&options.ServerAddr, "server-address", options.ServerAddr, "Address for server") + fs.DurationVar(&options.ProviderPollingInterval, "provider-polling-interval", 15*time.Second, "Interval for polling provider for updates") } func (options *Options) Complete() (*CompletedOptions, error) { diff --git a/pkg/konnector/server.go b/pkg/konnector/server.go index aa55e9b78..1da470c16 100644 --- a/pkg/konnector/server.go +++ b/pkg/konnector/server.go @@ -40,9 +40,11 @@ func NewServer(config *Config) (*Server, error) { k, err := New( config.ClientConfig, config.BindInformers.KubeBind().V1alpha2().APIServiceBindings(), + config.BindInformers.KubeBind().V1alpha2().APIServiceBindingBundles(), config.KubeInformers.Core().V1().Secrets(), // TODO(sttts): watch individual secrets for security and memory consumption config.KubeInformers.Core().V1().Namespaces(), config.ApiextensionsInformers.Apiextensions().V1().CustomResourceDefinitions(), + config.ProviderPollingInterval, ) if err != nil { return nil, err @@ -84,6 +86,12 @@ func (s *Server) PrepareRun(ctx context.Context) (Prepared, error) { ); err != nil { return Prepared{}, err } + if err := crd.Create(ctx, + s.Config.ApiextensionsClient.ApiextensionsV1().CustomResourceDefinitions(), + metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiservicebindingbundles"}, + ); err != nil { + return Prepared{}, err + } return Prepared{ prepared: &prepared{ Server: *s, diff --git a/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go b/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go index a2d05eaba..781f4f787 100644 --- a/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiservicebinding_types.go @@ -132,11 +132,10 @@ type LocalSecretKeyRef struct { // +kubebuilder:validation:MinLength=1 Name string `json:"name"` - // The key of the secret to select from. Must be "kubeconfig". + // The key of the secret to select from. // // +required // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=kubeconfig Key string `json:"key"` } diff --git a/sdk/apis/kubebind/v1alpha2/apiservicebindingbundle_types.go b/sdk/apis/kubebind/v1alpha2/apiservicebindingbundle_types.go new file mode 100644 index 000000000..c2408003d --- /dev/null +++ b/sdk/apis/kubebind/v1alpha2/apiservicebindingbundle_types.go @@ -0,0 +1,83 @@ +/* +Copyright 2025 The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" +) + +// APIServiceBindingBundle automatically binds multiple API services represented by +// APIServiceExports in a service provider cluster into a consumer cluster. This object lives in consumer clusters, +// and pulls in all API services defined in the provider clusters, based on the kubeconfig secret provided. +// +// +crd +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,categories=kube-bindings,shortName=sbb +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="Provider",type="string",JSONPath=`.status.providerPrettyName`,priority=0 +// +kubebuilder:printcolumn:name="Resources",type="string",JSONPath=`.metadata.annotations.kube-bind\.io/resources`,priority=1 +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=`.status.conditions[?(@.type=="Ready")].status`,priority=0 +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=0 +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp`,priority=0 +type APIServiceBindingBundle struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + // spec specifies how an API service from a service provider should be bound in the + // local consumer cluster. + Spec APIServiceBindingBundleSpec `json:"spec"` + + // status contains reconciliation information for a service binding. + Status APIServiceBindingBundleStatus `json:"status"` +} + +func (in *APIServiceBindingBundle) GetConditions() conditionsapi.Conditions { + return in.Status.Conditions +} + +func (in *APIServiceBindingBundle) SetConditions(conditions conditionsapi.Conditions) { + in.Status.Conditions = conditions +} + +type APIServiceBindingBundleSpec struct { + // kubeconfigSecretName is the secret ref that contains the kubeconfig of the service cluster. + // + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="kubeconfigSecretRef is immutable" + KubeconfigSecretRef ClusterSecretKeyRef `json:"kubeconfigSecretRef"` +} + +type APIServiceBindingBundleStatus struct { + // conditions is a list of conditions that apply to the APIServiceBindingBundle. + Conditions conditionsapi.Conditions `json:"conditions,omitempty"` +} + +// APIServiceBindingBundleList is a list of APIServiceBindingBundles. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type APIServiceBindingBundleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []APIServiceBindingBundle `json:"items"` +} diff --git a/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go b/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go index 75a788888..b3d1afe8e 100644 --- a/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go +++ b/sdk/apis/kubebind/v1alpha2/bindingresponse_types.go @@ -80,21 +80,72 @@ type BindingResponseAuthenticationOAuth2CodeGrant struct { } // BindableResourcesRequest is sent by the consumer to the service provider -// to indicate which resources the user wants to bind to. It is sent after -// authentication and resource selection on the service provider website. +// to indicate which resources the user wants to bind to. It can be sent via +// the HTTP API after authentication, or created directly as a CRD when the +// backend is running without the HTTP API/OIDC flow (frontend disabled mode). +// +// When created as a CRD, a controller will process the request and create the +// necessary APIServiceExport and related resources. +// +// +crd +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,categories=kube-bindings +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="Template",type="string",JSONPath=`.spec.templateRef.name`,priority=0 +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=`.status.phase`,priority=0 +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=`.metadata.creationTimestamp`,priority=0 type BindableResourcesRequest struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata"` - // TemplateRef specifies the APIServiceExportTemplate to bind to. + // spec specifies the binding request details. + // // +required // +kubebuilder:validation:Required + Spec BindableResourcesRequestSpec `json:"spec"` + + // status contains reconciliation information for the binding request. + // +kubebuilder:default={} + Status BindableResourcesRequestStatus `json:"status,omitempty"` +} + +// BindableResourcesRequestSpec defines the desired state of BindableResourcesRequest. +type BindableResourcesRequestSpec struct { + // TemplateRef specifies the APIServiceExportTemplate to bind to. + // +optional + // +kubebuilder:validation:Optional TemplateRef APIServiceExportTemplateRef `json:"templateRef"` + // ClusterIdentity contains information that uniquely identifies the cluster. - // When doing dry run, we expect the client to fill this field in (or it will be taken from local cluster where context is available). + // This is used when the request is made via the HTTP API after authentication. + // + // +required + // +kubebuilder:validation:Required + ClusterIdentity ClusterIdentity `json:"clusterIdentity,omitempty"` + + // Author is the identifier of the entity that created this binding request. + // This is used for audit purposes and to track who initiated the binding. + // // +required // +kubebuilder:validation:Required - ClusterIdentity ClusterIdentity `json:"clusterIdentity"` + Author string `json:"author"` + + // kubeconfigSecretRef is a reference to an existing secret where the binding response + // will be stored. If specified, the controller will update this secret with the + // binding response data. If not specified, a new secret will be created with + // the name "-binding-response". + // +optional + // +kubebuilder:validation:Optional + KubeconfigSecretRef *LocalSecretKeyRef `json:"kubeconfigSecretRef,omitempty"` + + // ttlAfterFinished is the TTL after the request has succeeded or failed + // before it is automatically deleted. If not set, the request will not be + // automatically deleted. Example values: "1h", "30m", "300s". + // +optional + // +kubebuilder:validation:Optional + TTLAfterFinished *metav1.Duration `json:"ttlAfterFinished,omitempty"` } type APIServiceExportTemplateRef struct { @@ -105,9 +156,66 @@ type APIServiceExportTemplateRef struct { Name string `json:"name"` } +// BindableResourcesRequestPhase describes the phase of a binding request. +type BindableResourcesRequestPhase string + +const ( + // BindableResourcesRequestPhasePending indicates that the binding request is being processed. + BindableResourcesRequestPhasePending BindableResourcesRequestPhase = "Pending" + // BindableResourcesRequestPhaseFailed indicates that the binding request has failed. + BindableResourcesRequestPhaseFailed BindableResourcesRequestPhase = "Failed" + // BindableResourcesRequestPhaseSucceeded indicates that the binding request has succeeded. + BindableResourcesRequestPhaseSucceeded BindableResourcesRequestPhase = "Succeeded" +) + +// BindableResourcesRequestStatus defines the observed state of BindableResourcesRequest. +type BindableResourcesRequestStatus struct { + // phase is the current phase of the binding request. + // + // +optional + // +kubebuilder:validation:Optional + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Failed;Succeeded + Phase BindableResourcesRequestPhase `json:"phase,omitempty"` + + // kubeconfigSecretRef is a reference to a secret containing the kubeconfig, used + // to be used by the konnector agent. + KubeconfigSecretRef *LocalSecretKeyRef `json:"kubeconfigSecretRef,omitempty"` + + // completionTime is the time when the request finished processing (succeeded or failed). + // Used for TTL-based cleanup. + // +optional + // +kubebuilder:validation:Optional + CompletionTime *metav1.Time `json:"completionTime,omitempty"` + + // conditions contains the current conditions of the binding request. + // + // +optional + // +kubebuilder:validation:Optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// BindableResourcesRequestConditionType defines the condition types for BindableResourcesRequest. +type BindableResourcesRequestConditionType string + +const ( + // BindableResourcesRequestConditionReady indicates that the binding response secret is ready. + BindableResourcesRequestConditionReady BindableResourcesRequestConditionType = "Ready" +) + +// BindableResourcesRequestList is the list of BindableResourcesRequest. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type BindableResourcesRequestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []BindableResourcesRequest `json:"items"` +} + func (r *BindableResourcesRequest) Validate() error { - if r.TemplateRef.Name == "" { - return errors.New("templateRef.name is required") + if r.Spec.TemplateRef.Name == "" { + return errors.New("spec.templateRef.name is required") } if r.Name == "" { diff --git a/sdk/apis/kubebind/v1alpha2/register.go b/sdk/apis/kubebind/v1alpha2/register.go index b4a6e683e..d54c4377a 100644 --- a/sdk/apis/kubebind/v1alpha2/register.go +++ b/sdk/apis/kubebind/v1alpha2/register.go @@ -50,6 +50,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &BoundSchemaList{}, &APIServiceBinding{}, &APIServiceBindingList{}, + &APIServiceBindingBundle{}, + &APIServiceBindingBundleList{}, &APIServiceExport{}, &APIServiceExportList{}, &APIServiceExportRequest{}, @@ -66,6 +68,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &CollectionList{}, &Cluster{}, &ClusterList{}, + &BindableResourcesRequest{}, + &BindableResourcesRequestList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) diff --git a/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go b/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go index 0c40cc6ba..739f66fd1 100644 --- a/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go +++ b/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go @@ -113,6 +113,107 @@ func (in *APIServiceBinding) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceBindingBundle) DeepCopyInto(out *APIServiceBindingBundle) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceBindingBundle. +func (in *APIServiceBindingBundle) DeepCopy() *APIServiceBindingBundle { + if in == nil { + return nil + } + out := new(APIServiceBindingBundle) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIServiceBindingBundle) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceBindingBundleList) DeepCopyInto(out *APIServiceBindingBundleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIServiceBindingBundle, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceBindingBundleList. +func (in *APIServiceBindingBundleList) DeepCopy() *APIServiceBindingBundleList { + if in == nil { + return nil + } + out := new(APIServiceBindingBundleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIServiceBindingBundleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceBindingBundleSpec) DeepCopyInto(out *APIServiceBindingBundleSpec) { + *out = *in + out.KubeconfigSecretRef = in.KubeconfigSecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceBindingBundleSpec. +func (in *APIServiceBindingBundleSpec) DeepCopy() *APIServiceBindingBundleSpec { + if in == nil { + return nil + } + out := new(APIServiceBindingBundleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServiceBindingBundleStatus) DeepCopyInto(out *APIServiceBindingBundleStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1alpha1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServiceBindingBundleStatus. +func (in *APIServiceBindingBundleStatus) DeepCopy() *APIServiceBindingBundleStatus { + if in == nil { + return nil + } + out := new(APIServiceBindingBundleStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *APIServiceBindingList) DeepCopyInto(out *APIServiceBindingList) { *out = *in @@ -756,8 +857,8 @@ func (in *BindableResourcesRequest) DeepCopyInto(out *BindableResourcesRequest) *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.TemplateRef = in.TemplateRef - out.ClusterIdentity = in.ClusterIdentity + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } @@ -771,6 +872,107 @@ func (in *BindableResourcesRequest) DeepCopy() *BindableResourcesRequest { return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BindableResourcesRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BindableResourcesRequestList) DeepCopyInto(out *BindableResourcesRequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BindableResourcesRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindableResourcesRequestList. +func (in *BindableResourcesRequestList) DeepCopy() *BindableResourcesRequestList { + if in == nil { + return nil + } + out := new(BindableResourcesRequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BindableResourcesRequestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BindableResourcesRequestSpec) DeepCopyInto(out *BindableResourcesRequestSpec) { + *out = *in + out.TemplateRef = in.TemplateRef + out.ClusterIdentity = in.ClusterIdentity + if in.KubeconfigSecretRef != nil { + in, out := &in.KubeconfigSecretRef, &out.KubeconfigSecretRef + *out = new(LocalSecretKeyRef) + **out = **in + } + if in.TTLAfterFinished != nil { + in, out := &in.TTLAfterFinished, &out.TTLAfterFinished + *out = new(metav1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindableResourcesRequestSpec. +func (in *BindableResourcesRequestSpec) DeepCopy() *BindableResourcesRequestSpec { + if in == nil { + return nil + } + out := new(BindableResourcesRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BindableResourcesRequestStatus) DeepCopyInto(out *BindableResourcesRequestStatus) { + *out = *in + if in.KubeconfigSecretRef != nil { + in, out := &in.KubeconfigSecretRef, &out.KubeconfigSecretRef + *out = new(LocalSecretKeyRef) + **out = **in + } + if in.CompletionTime != nil { + in, out := &in.CompletionTime, &out.CompletionTime + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindableResourcesRequestStatus. +func (in *BindableResourcesRequestStatus) DeepCopy() *BindableResourcesRequestStatus { + if in == nil { + return nil + } + out := new(BindableResourcesRequestStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BindingProvider) DeepCopyInto(out *BindingProvider) { *out = *in diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/apiservicebindingbundle.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/apiservicebindingbundle.go new file mode 100644 index 000000000..032a552f2 --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/apiservicebindingbundle.go @@ -0,0 +1,72 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + context "context" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + scheme "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// APIServiceBindingBundlesGetter has a method to return a APIServiceBindingBundleInterface. +// A group's client should implement this interface. +type APIServiceBindingBundlesGetter interface { + APIServiceBindingBundles() APIServiceBindingBundleInterface +} + +// APIServiceBindingBundleInterface has methods to work with APIServiceBindingBundle resources. +type APIServiceBindingBundleInterface interface { + Create(ctx context.Context, aPIServiceBindingBundle *kubebindv1alpha2.APIServiceBindingBundle, opts v1.CreateOptions) (*kubebindv1alpha2.APIServiceBindingBundle, error) + Update(ctx context.Context, aPIServiceBindingBundle *kubebindv1alpha2.APIServiceBindingBundle, opts v1.UpdateOptions) (*kubebindv1alpha2.APIServiceBindingBundle, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, aPIServiceBindingBundle *kubebindv1alpha2.APIServiceBindingBundle, opts v1.UpdateOptions) (*kubebindv1alpha2.APIServiceBindingBundle, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*kubebindv1alpha2.APIServiceBindingBundle, error) + List(ctx context.Context, opts v1.ListOptions) (*kubebindv1alpha2.APIServiceBindingBundleList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *kubebindv1alpha2.APIServiceBindingBundle, err error) + APIServiceBindingBundleExpansion +} + +// aPIServiceBindingBundles implements APIServiceBindingBundleInterface +type aPIServiceBindingBundles struct { + *gentype.ClientWithList[*kubebindv1alpha2.APIServiceBindingBundle, *kubebindv1alpha2.APIServiceBindingBundleList] +} + +// newAPIServiceBindingBundles returns a APIServiceBindingBundles +func newAPIServiceBindingBundles(c *KubeBindV1alpha2Client) *aPIServiceBindingBundles { + return &aPIServiceBindingBundles{ + gentype.NewClientWithList[*kubebindv1alpha2.APIServiceBindingBundle, *kubebindv1alpha2.APIServiceBindingBundleList]( + "apiservicebindingbundles", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *kubebindv1alpha2.APIServiceBindingBundle { return &kubebindv1alpha2.APIServiceBindingBundle{} }, + func() *kubebindv1alpha2.APIServiceBindingBundleList { + return &kubebindv1alpha2.APIServiceBindingBundleList{} + }, + ), + } +} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/bindableresourcesrequest.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/bindableresourcesrequest.go new file mode 100644 index 000000000..d9415018f --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/bindableresourcesrequest.go @@ -0,0 +1,72 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + context "context" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + scheme "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// BindableResourcesRequestsGetter has a method to return a BindableResourcesRequestInterface. +// A group's client should implement this interface. +type BindableResourcesRequestsGetter interface { + BindableResourcesRequests(namespace string) BindableResourcesRequestInterface +} + +// BindableResourcesRequestInterface has methods to work with BindableResourcesRequest resources. +type BindableResourcesRequestInterface interface { + Create(ctx context.Context, bindableResourcesRequest *kubebindv1alpha2.BindableResourcesRequest, opts v1.CreateOptions) (*kubebindv1alpha2.BindableResourcesRequest, error) + Update(ctx context.Context, bindableResourcesRequest *kubebindv1alpha2.BindableResourcesRequest, opts v1.UpdateOptions) (*kubebindv1alpha2.BindableResourcesRequest, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, bindableResourcesRequest *kubebindv1alpha2.BindableResourcesRequest, opts v1.UpdateOptions) (*kubebindv1alpha2.BindableResourcesRequest, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*kubebindv1alpha2.BindableResourcesRequest, error) + List(ctx context.Context, opts v1.ListOptions) (*kubebindv1alpha2.BindableResourcesRequestList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *kubebindv1alpha2.BindableResourcesRequest, err error) + BindableResourcesRequestExpansion +} + +// bindableResourcesRequests implements BindableResourcesRequestInterface +type bindableResourcesRequests struct { + *gentype.ClientWithList[*kubebindv1alpha2.BindableResourcesRequest, *kubebindv1alpha2.BindableResourcesRequestList] +} + +// newBindableResourcesRequests returns a BindableResourcesRequests +func newBindableResourcesRequests(c *KubeBindV1alpha2Client, namespace string) *bindableResourcesRequests { + return &bindableResourcesRequests{ + gentype.NewClientWithList[*kubebindv1alpha2.BindableResourcesRequest, *kubebindv1alpha2.BindableResourcesRequestList]( + "bindableresourcesrequests", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *kubebindv1alpha2.BindableResourcesRequest { return &kubebindv1alpha2.BindableResourcesRequest{} }, + func() *kubebindv1alpha2.BindableResourcesRequestList { + return &kubebindv1alpha2.BindableResourcesRequestList{} + }, + ), + } +} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_apiservicebindingbundle.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_apiservicebindingbundle.go new file mode 100644 index 000000000..94fc38350 --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_apiservicebindingbundle.go @@ -0,0 +1,52 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/typed/kubebind/v1alpha2" + gentype "k8s.io/client-go/gentype" +) + +// fakeAPIServiceBindingBundles implements APIServiceBindingBundleInterface +type fakeAPIServiceBindingBundles struct { + *gentype.FakeClientWithList[*v1alpha2.APIServiceBindingBundle, *v1alpha2.APIServiceBindingBundleList] + Fake *FakeKubeBindV1alpha2 +} + +func newFakeAPIServiceBindingBundles(fake *FakeKubeBindV1alpha2) kubebindv1alpha2.APIServiceBindingBundleInterface { + return &fakeAPIServiceBindingBundles{ + gentype.NewFakeClientWithList[*v1alpha2.APIServiceBindingBundle, *v1alpha2.APIServiceBindingBundleList]( + fake.Fake, + "", + v1alpha2.SchemeGroupVersion.WithResource("apiservicebindingbundles"), + v1alpha2.SchemeGroupVersion.WithKind("APIServiceBindingBundle"), + func() *v1alpha2.APIServiceBindingBundle { return &v1alpha2.APIServiceBindingBundle{} }, + func() *v1alpha2.APIServiceBindingBundleList { return &v1alpha2.APIServiceBindingBundleList{} }, + func(dst, src *v1alpha2.APIServiceBindingBundleList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha2.APIServiceBindingBundleList) []*v1alpha2.APIServiceBindingBundle { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha2.APIServiceBindingBundleList, items []*v1alpha2.APIServiceBindingBundle) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_bindableresourcesrequest.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_bindableresourcesrequest.go new file mode 100644 index 000000000..7e8e304e0 --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_bindableresourcesrequest.go @@ -0,0 +1,52 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned/typed/kubebind/v1alpha2" + gentype "k8s.io/client-go/gentype" +) + +// fakeBindableResourcesRequests implements BindableResourcesRequestInterface +type fakeBindableResourcesRequests struct { + *gentype.FakeClientWithList[*v1alpha2.BindableResourcesRequest, *v1alpha2.BindableResourcesRequestList] + Fake *FakeKubeBindV1alpha2 +} + +func newFakeBindableResourcesRequests(fake *FakeKubeBindV1alpha2, namespace string) kubebindv1alpha2.BindableResourcesRequestInterface { + return &fakeBindableResourcesRequests{ + gentype.NewFakeClientWithList[*v1alpha2.BindableResourcesRequest, *v1alpha2.BindableResourcesRequestList]( + fake.Fake, + namespace, + v1alpha2.SchemeGroupVersion.WithResource("bindableresourcesrequests"), + v1alpha2.SchemeGroupVersion.WithKind("BindableResourcesRequest"), + func() *v1alpha2.BindableResourcesRequest { return &v1alpha2.BindableResourcesRequest{} }, + func() *v1alpha2.BindableResourcesRequestList { return &v1alpha2.BindableResourcesRequestList{} }, + func(dst, src *v1alpha2.BindableResourcesRequestList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha2.BindableResourcesRequestList) []*v1alpha2.BindableResourcesRequest { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha2.BindableResourcesRequestList, items []*v1alpha2.BindableResourcesRequest) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go index ac2d7310d..8c773e01b 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_kubebind_client.go @@ -32,6 +32,10 @@ func (c *FakeKubeBindV1alpha2) APIServiceBindings() v1alpha2.APIServiceBindingIn return newFakeAPIServiceBindings(c) } +func (c *FakeKubeBindV1alpha2) APIServiceBindingBundles() v1alpha2.APIServiceBindingBundleInterface { + return newFakeAPIServiceBindingBundles(c) +} + func (c *FakeKubeBindV1alpha2) APIServiceExports(namespace string) v1alpha2.APIServiceExportInterface { return newFakeAPIServiceExports(c, namespace) } @@ -44,6 +48,10 @@ func (c *FakeKubeBindV1alpha2) APIServiceNamespaces(namespace string) v1alpha2.A return newFakeAPIServiceNamespaces(c, namespace) } +func (c *FakeKubeBindV1alpha2) BindableResourcesRequests(namespace string) v1alpha2.BindableResourcesRequestInterface { + return newFakeBindableResourcesRequests(c, namespace) +} + func (c *FakeKubeBindV1alpha2) BoundSchemas(namespace string) v1alpha2.BoundSchemaInterface { return newFakeBoundSchemas(c, namespace) } diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go index 182625159..3c86f5c52 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go @@ -20,12 +20,16 @@ package v1alpha2 type APIServiceBindingExpansion interface{} +type APIServiceBindingBundleExpansion interface{} + type APIServiceExportExpansion interface{} type APIServiceExportRequestExpansion interface{} type APIServiceNamespaceExpansion interface{} +type BindableResourcesRequestExpansion interface{} + type BoundSchemaExpansion interface{} type ClusterExpansion interface{} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go index 92189fb0b..e59c85f49 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go @@ -29,9 +29,11 @@ import ( type KubeBindV1alpha2Interface interface { RESTClient() rest.Interface APIServiceBindingsGetter + APIServiceBindingBundlesGetter APIServiceExportsGetter APIServiceExportRequestsGetter APIServiceNamespacesGetter + BindableResourcesRequestsGetter BoundSchemasGetter ClustersGetter ClusterBindingsGetter @@ -47,6 +49,10 @@ func (c *KubeBindV1alpha2Client) APIServiceBindings() APIServiceBindingInterface return newAPIServiceBindings(c) } +func (c *KubeBindV1alpha2Client) APIServiceBindingBundles() APIServiceBindingBundleInterface { + return newAPIServiceBindingBundles(c) +} + func (c *KubeBindV1alpha2Client) APIServiceExports(namespace string) APIServiceExportInterface { return newAPIServiceExports(c, namespace) } @@ -59,6 +65,10 @@ func (c *KubeBindV1alpha2Client) APIServiceNamespaces(namespace string) APIServi return newAPIServiceNamespaces(c, namespace) } +func (c *KubeBindV1alpha2Client) BindableResourcesRequests(namespace string) BindableResourcesRequestInterface { + return newBindableResourcesRequests(c, namespace) +} + func (c *KubeBindV1alpha2Client) BoundSchemas(namespace string) BoundSchemaInterface { return newBoundSchemas(c, namespace) } diff --git a/sdk/client/informers/externalversions/generic.go b/sdk/client/informers/externalversions/generic.go index 9fedd751b..a5ec5b5c6 100644 --- a/sdk/client/informers/externalversions/generic.go +++ b/sdk/client/informers/externalversions/generic.go @@ -68,12 +68,16 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=kube-bind.io, Version=v1alpha2 case v1alpha2.SchemeGroupVersion.WithResource("apiservicebindings"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceBindings().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("apiservicebindingbundles"): + return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceBindingBundles().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("apiserviceexports"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceExports().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("apiserviceexportrequests"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceExportRequests().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("apiservicenamespaces"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceNamespaces().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("bindableresourcesrequests"): + return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().BindableResourcesRequests().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("boundschemas"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().BoundSchemas().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("clusters"): diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/apiservicebindingbundle.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/apiservicebindingbundle.go new file mode 100644 index 000000000..0cfd68cf3 --- /dev/null +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/apiservicebindingbundle.go @@ -0,0 +1,101 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + context "context" + time "time" + + apiskubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + versioned "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned" + internalinterfaces "github.com/kube-bind/kube-bind/sdk/client/informers/externalversions/internalinterfaces" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// APIServiceBindingBundleInformer provides access to a shared informer and lister for +// APIServiceBindingBundles. +type APIServiceBindingBundleInformer interface { + Informer() cache.SharedIndexInformer + Lister() kubebindv1alpha2.APIServiceBindingBundleLister +} + +type aPIServiceBindingBundleInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewAPIServiceBindingBundleInformer constructs a new informer for APIServiceBindingBundle type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewAPIServiceBindingBundleInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredAPIServiceBindingBundleInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredAPIServiceBindingBundleInformer constructs a new informer for APIServiceBindingBundle type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredAPIServiceBindingBundleInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().APIServiceBindingBundles().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().APIServiceBindingBundles().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().APIServiceBindingBundles().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().APIServiceBindingBundles().Watch(ctx, options) + }, + }, + &apiskubebindv1alpha2.APIServiceBindingBundle{}, + resyncPeriod, + indexers, + ) +} + +func (f *aPIServiceBindingBundleInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredAPIServiceBindingBundleInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *aPIServiceBindingBundleInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apiskubebindv1alpha2.APIServiceBindingBundle{}, f.defaultInformer) +} + +func (f *aPIServiceBindingBundleInformer) Lister() kubebindv1alpha2.APIServiceBindingBundleLister { + return kubebindv1alpha2.NewAPIServiceBindingBundleLister(f.Informer().GetIndexer()) +} diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/bindableresourcesrequest.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/bindableresourcesrequest.go new file mode 100644 index 000000000..98a4a88c4 --- /dev/null +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/bindableresourcesrequest.go @@ -0,0 +1,102 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + context "context" + time "time" + + apiskubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + versioned "github.com/kube-bind/kube-bind/sdk/client/clientset/versioned" + internalinterfaces "github.com/kube-bind/kube-bind/sdk/client/informers/externalversions/internalinterfaces" + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/client/listers/kubebind/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// BindableResourcesRequestInformer provides access to a shared informer and lister for +// BindableResourcesRequests. +type BindableResourcesRequestInformer interface { + Informer() cache.SharedIndexInformer + Lister() kubebindv1alpha2.BindableResourcesRequestLister +} + +type bindableResourcesRequestInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewBindableResourcesRequestInformer constructs a new informer for BindableResourcesRequest type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewBindableResourcesRequestInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredBindableResourcesRequestInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredBindableResourcesRequestInformer constructs a new informer for BindableResourcesRequest type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredBindableResourcesRequestInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().BindableResourcesRequests(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().BindableResourcesRequests(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().BindableResourcesRequests(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().BindableResourcesRequests(namespace).Watch(ctx, options) + }, + }, + &apiskubebindv1alpha2.BindableResourcesRequest{}, + resyncPeriod, + indexers, + ) +} + +func (f *bindableResourcesRequestInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredBindableResourcesRequestInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *bindableResourcesRequestInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apiskubebindv1alpha2.BindableResourcesRequest{}, f.defaultInformer) +} + +func (f *bindableResourcesRequestInformer) Lister() kubebindv1alpha2.BindableResourcesRequestLister { + return kubebindv1alpha2.NewBindableResourcesRequestLister(f.Informer().GetIndexer()) +} diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go index 9f6cebe08..337fcf5fb 100644 --- a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go @@ -26,12 +26,16 @@ import ( type Interface interface { // APIServiceBindings returns a APIServiceBindingInformer. APIServiceBindings() APIServiceBindingInformer + // APIServiceBindingBundles returns a APIServiceBindingBundleInformer. + APIServiceBindingBundles() APIServiceBindingBundleInformer // APIServiceExports returns a APIServiceExportInformer. APIServiceExports() APIServiceExportInformer // APIServiceExportRequests returns a APIServiceExportRequestInformer. APIServiceExportRequests() APIServiceExportRequestInformer // APIServiceNamespaces returns a APIServiceNamespaceInformer. APIServiceNamespaces() APIServiceNamespaceInformer + // BindableResourcesRequests returns a BindableResourcesRequestInformer. + BindableResourcesRequests() BindableResourcesRequestInformer // BoundSchemas returns a BoundSchemaInformer. BoundSchemas() BoundSchemaInformer // Clusters returns a ClusterInformer. @@ -58,6 +62,11 @@ func (v *version) APIServiceBindings() APIServiceBindingInformer { return &aPIServiceBindingInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } +// APIServiceBindingBundles returns a APIServiceBindingBundleInformer. +func (v *version) APIServiceBindingBundles() APIServiceBindingBundleInformer { + return &aPIServiceBindingBundleInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + // APIServiceExports returns a APIServiceExportInformer. func (v *version) APIServiceExports() APIServiceExportInformer { return &aPIServiceExportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} @@ -73,6 +82,11 @@ func (v *version) APIServiceNamespaces() APIServiceNamespaceInformer { return &aPIServiceNamespaceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// BindableResourcesRequests returns a BindableResourcesRequestInformer. +func (v *version) BindableResourcesRequests() BindableResourcesRequestInformer { + return &bindableResourcesRequestInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // BoundSchemas returns a BoundSchemaInformer. func (v *version) BoundSchemas() BoundSchemaInformer { return &boundSchemaInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/sdk/client/listers/kubebind/v1alpha2/apiservicebindingbundle.go b/sdk/client/listers/kubebind/v1alpha2/apiservicebindingbundle.go new file mode 100644 index 000000000..cffa0c89c --- /dev/null +++ b/sdk/client/listers/kubebind/v1alpha2/apiservicebindingbundle.go @@ -0,0 +1,48 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// APIServiceBindingBundleLister helps list APIServiceBindingBundles. +// All objects returned here must be treated as read-only. +type APIServiceBindingBundleLister interface { + // List lists all APIServiceBindingBundles in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.APIServiceBindingBundle, err error) + // Get retrieves the APIServiceBindingBundle from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*kubebindv1alpha2.APIServiceBindingBundle, error) + APIServiceBindingBundleListerExpansion +} + +// aPIServiceBindingBundleLister implements the APIServiceBindingBundleLister interface. +type aPIServiceBindingBundleLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.APIServiceBindingBundle] +} + +// NewAPIServiceBindingBundleLister returns a new APIServiceBindingBundleLister. +func NewAPIServiceBindingBundleLister(indexer cache.Indexer) APIServiceBindingBundleLister { + return &aPIServiceBindingBundleLister{listers.New[*kubebindv1alpha2.APIServiceBindingBundle](indexer, kubebindv1alpha2.Resource("apiservicebindingbundle"))} +} diff --git a/sdk/client/listers/kubebind/v1alpha2/bindableresourcesrequest.go b/sdk/client/listers/kubebind/v1alpha2/bindableresourcesrequest.go new file mode 100644 index 000000000..d7432ca33 --- /dev/null +++ b/sdk/client/listers/kubebind/v1alpha2/bindableresourcesrequest.go @@ -0,0 +1,70 @@ +/* +Copyright The Kube Bind Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// BindableResourcesRequestLister helps list BindableResourcesRequests. +// All objects returned here must be treated as read-only. +type BindableResourcesRequestLister interface { + // List lists all BindableResourcesRequests in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.BindableResourcesRequest, err error) + // BindableResourcesRequests returns an object that can list and get BindableResourcesRequests. + BindableResourcesRequests(namespace string) BindableResourcesRequestNamespaceLister + BindableResourcesRequestListerExpansion +} + +// bindableResourcesRequestLister implements the BindableResourcesRequestLister interface. +type bindableResourcesRequestLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.BindableResourcesRequest] +} + +// NewBindableResourcesRequestLister returns a new BindableResourcesRequestLister. +func NewBindableResourcesRequestLister(indexer cache.Indexer) BindableResourcesRequestLister { + return &bindableResourcesRequestLister{listers.New[*kubebindv1alpha2.BindableResourcesRequest](indexer, kubebindv1alpha2.Resource("bindableresourcesrequest"))} +} + +// BindableResourcesRequests returns an object that can list and get BindableResourcesRequests. +func (s *bindableResourcesRequestLister) BindableResourcesRequests(namespace string) BindableResourcesRequestNamespaceLister { + return bindableResourcesRequestNamespaceLister{listers.NewNamespaced[*kubebindv1alpha2.BindableResourcesRequest](s.ResourceIndexer, namespace)} +} + +// BindableResourcesRequestNamespaceLister helps list and get BindableResourcesRequests. +// All objects returned here must be treated as read-only. +type BindableResourcesRequestNamespaceLister interface { + // List lists all BindableResourcesRequests in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.BindableResourcesRequest, err error) + // Get retrieves the BindableResourcesRequest from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*kubebindv1alpha2.BindableResourcesRequest, error) + BindableResourcesRequestNamespaceListerExpansion +} + +// bindableResourcesRequestNamespaceLister implements the BindableResourcesRequestNamespaceLister +// interface. +type bindableResourcesRequestNamespaceLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.BindableResourcesRequest] +} diff --git a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go index 08932cb79..dee68fbb5 100644 --- a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go +++ b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go @@ -22,6 +22,10 @@ package v1alpha2 // APIServiceBindingLister. type APIServiceBindingListerExpansion interface{} +// APIServiceBindingBundleListerExpansion allows custom methods to be added to +// APIServiceBindingBundleLister. +type APIServiceBindingBundleListerExpansion interface{} + // APIServiceExportListerExpansion allows custom methods to be added to // APIServiceExportLister. type APIServiceExportListerExpansion interface{} @@ -46,6 +50,14 @@ type APIServiceNamespaceListerExpansion interface{} // APIServiceNamespaceNamespaceLister. type APIServiceNamespaceNamespaceListerExpansion interface{} +// BindableResourcesRequestListerExpansion allows custom methods to be added to +// BindableResourcesRequestLister. +type BindableResourcesRequestListerExpansion interface{} + +// BindableResourcesRequestNamespaceListerExpansion allows custom methods to be added to +// BindableResourcesRequestNamespaceLister. +type BindableResourcesRequestNamespaceListerExpansion interface{} + // BoundSchemaListerExpansion allows custom methods to be added to // BoundSchemaLister. type BoundSchemaListerExpansion interface{} diff --git a/test/e2e/bind/happy-case_test.go b/test/e2e/bind/happy-case_test.go index d9b1f8eb0..5be8dcc03 100644 --- a/test/e2e/bind/happy-case_test.go +++ b/test/e2e/bind/happy-case_test.go @@ -371,11 +371,13 @@ func testHappyCase( ObjectMeta: metav1.ObjectMeta{ Name: "test-binding", }, - TemplateRef: kubebindv1alpha2.APIServiceExportTemplateRef{ - Name: templateRef, - }, - ClusterIdentity: kubebindv1alpha2.ClusterIdentity{ - Identity: consumer.clusterIdentity.String(), + Spec: kubebindv1alpha2.BindableResourcesRequestSpec{ + TemplateRef: kubebindv1alpha2.APIServiceExportTemplateRef{ + Name: templateRef, + }, + ClusterIdentity: kubebindv1alpha2.ClusterIdentity{ + Identity: consumer.clusterIdentity.String(), + }, }, }) require.NoError(t, err)