diff --git a/backend/auth/middleware.go b/backend/auth/middleware.go index c19c2fe1b..32c9b088f 100644 --- a/backend/auth/middleware.go +++ b/backend/auth/middleware.go @@ -24,6 +24,7 @@ import ( "time" "github.com/gorilla/securecookie" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/klog/v2" "github.com/kube-bind/kube-bind/backend/kubernetes" @@ -88,8 +89,7 @@ func NewAuthMiddleware( func (am *AuthMiddleware) AuthenticateRequest(next http.Handler) http.Handler { return am.authenticate( am.verifyState( - next, - ), + am.authorizeK8S(next)), ) } @@ -116,9 +116,13 @@ func (am *AuthMiddleware) authenticate(next http.Handler) http.Handler { ClusterID: claims.ClusterID, RedirectURL: claims.RedirectURL, } + if claims.ExpiresAt != nil { + authCtx.SessionState.ExpiresAt = claims.ExpiresAt.Time + } + authCtx.ClientType = ClientTypeCLI } else { - logger.V(2).Info("Invalid JWT token", "error", err) + logger.V(2).Error(err, "Invalid JWT token") } } } @@ -140,6 +144,8 @@ func (am *AuthMiddleware) authenticate(next http.Handler) http.Handler { authCtx.SessionState = state authCtx.ClientType = ClientTypeUI } + } else { + logger.V(2).Error(err, "Failed to decode session cookie") } } @@ -168,7 +174,13 @@ func (am *AuthMiddleware) verifyState(next http.Handler) http.Handler { return } - if state.IsExpired() || !am.isValidSession(state.SessionID) { + if state.IsExpired() { + logger.V(2).Info("Session has expired", "sessionID", state.SessionID) + writeErrorResponse(w, http.StatusUnauthorized, kubebindv1alpha2.ErrorCodeAuthenticationFailed, "Authentication required", "Session has expired") + return + } + + if !am.isValidSession(state.SessionID) { logger.V(2).Info("Session expired or invalid", "sessionID", state.SessionID) writeErrorResponse(w, http.StatusUnauthorized, kubebindv1alpha2.ErrorCodeAuthenticationFailed, "Authentication required", "Session has expired or is invalid") return @@ -191,6 +203,30 @@ func (am *AuthMiddleware) isValidSession(sessionID string) bool { return time.Now().Before(sessionInfo.ExpiresAt) } +func (am *AuthMiddleware) authorizeK8S(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger := klog.FromContext(r.Context()) + + authCtx := GetAuthContext(r.Context()) + if !authCtx.IsValid { // should not happen if AuthenticateRequest is used before + logger.V(2).Info("Authentication context is not valid") + writeErrorResponse(w, http.StatusUnauthorized, kubebindv1alpha2.ErrorCodeAuthenticationFailed, "Authentication required", "Authentication context is not valid") + return + } + + // Authorize against Kubernetes RBAC + err := am.kubernetesManager.AuthorizeRequest(r.Context(), authCtx.SessionState.Token.Subject, authCtx.SessionState.Token.Groups, authCtx.SessionState.ClusterID, r.Method, r.URL.Path) + if err != nil { + logger.V(2).Info("Kubernetes RBAC authorization failed", "error", err) + statusCode, code, details := mapErrorToCode(err) + writeErrorResponse(w, statusCode, code, "Cluster authorization failed. Missing required permissions in the cluster to access bindings.", details) + return + } + + next.ServeHTTP(w, r) + }) +} + func GetAuthContext(ctx context.Context) *AuthContext { if authCtx, ok := ctx.Value(AuthContextKey).(*AuthContext); ok { return authCtx @@ -212,3 +248,21 @@ func RequireAuth(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +// mapErrorToCode maps common errors to structured error codes +func mapErrorToCode(err error) (statusCode int, code string, details string) { + if apierrors.IsNotFound(err) { + return http.StatusNotFound, kubebindv1alpha2.ErrorCodeResourceNotFound, err.Error() + } + if apierrors.IsUnauthorized(err) { + return http.StatusUnauthorized, kubebindv1alpha2.ErrorCodeAuthenticationFailed, err.Error() + } + if apierrors.IsForbidden(err) { + return http.StatusForbidden, kubebindv1alpha2.ErrorCodeAuthorizationFailed, err.Error() + } + if apierrors.IsBadRequest(err) { + return http.StatusBadRequest, kubebindv1alpha2.ErrorCodeBadRequest, err.Error() + } + // Default to internal server error + return http.StatusInternalServerError, kubebindv1alpha2.ErrorCodeInternalError, err.Error() +} diff --git a/backend/controllers/cluster/cluster_controller.go b/backend/controllers/cluster/cluster_controller.go new file mode 100644 index 000000000..c5d622c9f --- /dev/null +++ b/backend/controllers/cluster/cluster_controller.go @@ -0,0 +1,122 @@ +/* +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 cluster + +import ( + "context" + "fmt" + "reflect" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "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" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +const ( + controllerName = "kube-bind-backend-cluster" +) + +// ClusterReconciler reconciles a Cluster object. +type ClusterReconciler struct { + manager mcmanager.Manager + opts controller.TypedOptions[mcreconcile.Request] + reconciler reconciler +} + +// NewClusterReconciler returns a new ClusterReconciler to reconcile Clusters +// ands its resources. +func NewClusterReconciler( + _ context.Context, + mgr mcmanager.Manager, + opts controller.TypedOptions[mcreconcile.Request], + allowedGroups []string, + allowedUsers []string, +) (*ClusterReconciler, error) { + r := &ClusterReconciler{ + manager: mgr, + opts: opts, + reconciler: reconciler{ + allowedGroups: allowedGroups, + allowedUsers: allowedUsers, + }, + } + + return r, nil +} + +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;watch;create;update;patch;delete + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *ClusterReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("Reconciling Cluster", "request", req) + + 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() + cache := cl.GetCache() + + cluster := &kubebindv1alpha2.Cluster{} + if err := client.Get(ctx, req.NamespacedName, cluster); err != nil { + if errors.IsNotFound(err) { + logger.Info("Cluster not found, skipping reconciliation") + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to get Cluster: %w", err) + } + + original := cluster.DeepCopy() + if err := r.reconciler.reconcile(ctx, client, cache, cluster); err != nil { + logger.Error(err, "Failed to reconcile Cluster") + return ctrl.Result{}, err + } + + if !reflect.DeepEqual(original, cluster) { + err := client.Update(ctx, cluster) + if err != nil { + logger.Error(err, "Failed to update Cluster status") + return ctrl.Result{}, fmt.Errorf("failed to update Cluster status: %w", err) + } + logger.Info("Cluster status updated") + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ClusterReconciler) SetupWithManager(mgr mcmanager.Manager) error { + return mcbuilder.ControllerManagedBy(mgr). + For(&kubebindv1alpha2.Cluster{}). + Owns(&rbacv1.ClusterRole{}). + Owns(&rbacv1.ClusterRoleBinding{}). + Owns(&rbacv1.RoleBinding{}). + WithOptions(r.opts). + Named(controllerName). + Complete(r) +} diff --git a/backend/controllers/cluster/cluster_reconcile.go b/backend/controllers/cluster/cluster_reconcile.go new file mode 100644 index 000000000..ca946a7fa --- /dev/null +++ b/backend/controllers/cluster/cluster_reconcile.go @@ -0,0 +1,135 @@ +/* +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 cluster + +import ( + "context" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +type reconciler struct { + allowedGroups []string + allowedUsers []string +} + +func (r *reconciler) reconcile(ctx context.Context, client client.Client, _ cache.Cache, cluster *kubebindv1alpha2.Cluster) error { + var errs []error + + if err := r.ensureOIDCRBAC(ctx, client, cluster); err != nil { + errs = append(errs, err) + } + + return utilerrors.NewAggregate(errs) +} + +func (r *reconciler) ensureOIDCRBAC(ctx context.Context, client client.Client, cluster *kubebindv1alpha2.Cluster) error { + if err := r.ensureUserClusterRole(ctx, client, cluster); err != nil { + return err + } + return r.ensureUserClusterRoleBinding(ctx, client, cluster) +} + +func (r *reconciler) ensureUserClusterRole(ctx context.Context, client client.Client, cluster *kubebindv1alpha2.Cluster) error { + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-bind-oidc-user", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"kube-bind.io"}, + Resources: []string{"*"}, + Verbs: []string{"bind"}, + }, + { + NonResourceURLs: []string{"/", "/api", "/api/*", "/apis", "/apis/*"}, + Verbs: []string{"access"}, + }, + }, + } + + if err := controllerutil.SetControllerReference(cluster, clusterRole, client.Scheme()); err != nil { + return err + } + + var existing rbacv1.ClusterRole + err := client.Get(ctx, types.NamespacedName{Name: "kube-bind-oidc-user"}, &existing) + if err != nil { + if errors.IsNotFound(err) { + return client.Create(ctx, clusterRole) + } + return err + } + + existing.Rules = clusterRole.Rules + existing.OwnerReferences = clusterRole.OwnerReferences + return client.Update(ctx, &existing) +} + +func (r *reconciler) ensureUserClusterRoleBinding(ctx context.Context, client client.Client, cluster *kubebindv1alpha2.Cluster) error { + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-bind-oidc-user", + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "kube-bind-oidc-user", + }, + } + + for _, group := range r.allowedGroups { + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ + Kind: "Group", + Name: group, + }) + } + + for _, user := range r.allowedUsers { + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ + Kind: "User", + Name: user, + }) + } + + if err := controllerutil.SetControllerReference(cluster, clusterRoleBinding, client.Scheme()); err != nil { + return err + } + + var existing rbacv1.ClusterRoleBinding + err := client.Get(ctx, types.NamespacedName{Name: "kube-bind-oidc-user"}, &existing) + if err != nil { + if errors.IsNotFound(err) { + return client.Create(ctx, clusterRoleBinding) + } + return err + } + + existing.RoleRef = clusterRoleBinding.RoleRef + existing.Subjects = clusterRoleBinding.Subjects + existing.OwnerReferences = clusterRoleBinding.OwnerReferences + return client.Update(ctx, &existing) +} diff --git a/backend/kubernetes/manager.go b/backend/kubernetes/manager.go index d7846e5fd..35c1d4b5e 100644 --- a/backend/kubernetes/manager.go +++ b/backend/kubernetes/manager.go @@ -21,13 +21,16 @@ import ( "fmt" "strings" + authzv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" @@ -45,7 +48,8 @@ type Manager struct { externalCA []byte externalTLSServerName string - manager mcmanager.Manager + manager mcmanager.Manager + embeddedOIDC bool } func NewKubernetesManager( @@ -56,6 +60,7 @@ func NewKubernetesManager( externalCA []byte, externalTLSServerName string, manager mcmanager.Manager, + embeddedOIDC bool, ) (*Manager, error) { m := &Manager{ namespacePrefix: namespacePrefix, @@ -66,7 +71,8 @@ func NewKubernetesManager( externalCA: externalCA, externalTLSServerName: externalTLSServerName, - manager: manager, + manager: manager, + embeddedOIDC: embeddedOIDC, } if err := m.manager.GetFieldIndexer().IndexField(ctx, &corev1.Namespace{}, NamespacesByIdentity, @@ -240,3 +246,71 @@ func (m *Manager) ListDynamicResources(ctx context.Context, cluster string, gvk return list, nil } + +func (m *Manager) AuthorizeRequest(ctx context.Context, subject string, groups []string, cluster, method, path string) error { + logger := klog.FromContext(ctx).WithValues("subject", subject, "cluster", cluster, "method", method, "path", path, "groups", groups) + + cl, err := m.manager.GetCluster(ctx, cluster) + if err != nil { + return err + } + cfg := cl.GetConfig() + + // Create the authorization clientset + authClient, err := authorizationv1.NewForConfig(cfg) + if err != nil { + return err + } + + if m.embeddedOIDC { + groups = append(groups, "system:authenticated") + } + + // Check if user can bind (basic permission test) + sar := &authzv1.SubjectAccessReview{ + Spec: authzv1.SubjectAccessReviewSpec{ + User: subject, + Groups: groups, + ResourceAttributes: &authzv1.ResourceAttributes{ + Verb: "bind", + Group: "kube-bind.io", + }, + }, + } + // Perform the SubjectAccessReview using the specific clientset + sarResponse, err := authClient.SubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{}) + if err != nil { + logger.Error(err, "Failed to create SubjectAccessReview") + return err + } + + if !sarResponse.Status.Allowed { + logger.Info("User is not allowed to bind", "user", subject, "reason", sarResponse.Status) + // Return a structured authorization error + return errors.NewForbidden( + schema.GroupResource{Group: "kube-bind.io", Resource: "bind"}, + "", + fmt.Errorf("user %q is not authorized to bind resources: %s", subject, sarResponse.Status.Reason), + ) + } + logger.Info("User is allowed to bind", "user", subject) + return nil +} + +func (m *Manager) SeedDefaultCluster(ctx context.Context) error { + logger := klog.FromContext(ctx) + + cl, err := m.manager.GetCluster(ctx, "") + if err != nil { + return err + } + c := cl.GetClient() + + _, err = kuberesources.CreateDefaultCluster(ctx, c) + if err != nil && !errors.IsAlreadyExists(err) { + logger.Error(err, "Failed to create default Cluster resource") + return err + } + logger.Info("Default Cluster resource ensured") + return nil +} diff --git a/backend/kubernetes/resources/cluster.go b/backend/kubernetes/resources/cluster.go new file mode 100644 index 000000000..c449739ec --- /dev/null +++ b/backend/kubernetes/resources/cluster.go @@ -0,0 +1,43 @@ +/* +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 resources + +import ( + "context" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" +) + +func CreateDefaultCluster(ctx context.Context, c client.Client) (*kubebindv1alpha2.Cluster, error) { + cluster := &kubebindv1alpha2.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: kubebindv1alpha2.DefaultClusterName, + }, + Spec: kubebindv1alpha2.ClusterSpec{}, + } + + err := c.Create(ctx, cluster) + if err != nil && !apierrors.IsAlreadyExists(err) { + return nil, err + } + + return cluster, nil +} diff --git a/backend/options/oidc.go b/backend/options/oidc.go index 214360ec3..ab36540fa 100644 --- a/backend/options/oidc.go +++ b/backend/options/oidc.go @@ -39,6 +39,14 @@ type OIDC struct { // TLSConfig is set if an embedded OIDC server is used. TLSConfig *tls.Config OIDCServer *oidc.Server + + // If specified, bindings will be provisioned for these permissions in each cluster kube-bind manages. + // In additon, if type is embedded, embedded groups will added automatically. + + // AllowedGroups is a list of groups allowed to access bindings inside the cluster + AllowedGroups []string + // AllowedUsers is a list of users allowed to access bindings inside the cluster + AllowedUsers []string } func NewOIDC() *OIDC { @@ -55,6 +63,8 @@ func (options *OIDC) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&options.AuthorizeURL, "oidc-authorize-url", options.AuthorizeURL, "OpenID authorize URL") fs.StringVar(&options.CAFile, "oidc-ca-file", options.CAFile, "Path to a CA bundle to use when verifying the OIDC provider's TLS certificate.") fs.StringVar(&options.Type, "oidc-type", options.Type, "Type of OIDC provider (embedded or external)") + fs.StringSliceVar(&options.AllowedGroups, "oidc-allowed-groups", options.AllowedGroups, "List of groups allowed to access bindings inside the cluster. If using embedded OIDC provider, system:authenticated will be added automatically.") + fs.StringSliceVar(&options.AllowedUsers, "oidc-allowed-users", options.AllowedUsers, "List of users allowed to access bindings inside the cluster") } func (options *OIDC) Complete(listener net.Listener) error { @@ -75,6 +85,8 @@ func (options *OIDC) Complete(listener net.Listener) error { options.IssuerClientSecret = cfg.ClientSecret // This should be provided from outside, but in embedded - we detect. options.CallbackURL = cfg.CallbackURL + + options.AllowedGroups = append(options.AllowedGroups, "system:authenticated") } if options.CAFile != "" { @@ -109,5 +121,9 @@ func (options *OIDC) Validate() error { return fmt.Errorf("invalid OIDC provider type: %s", options.Type) } + if options.Type == string(kubebindv1alpha2.OIDCProviderTypeExternal) && len(options.AllowedGroups) == 0 && len(options.AllowedUsers) == 0 { + return fmt.Errorf("when using external OIDC provider, at least one of allowed groups or allowed users must be specified") + } + return nil } diff --git a/backend/provider/kcp/provider.go b/backend/provider/kcp/provider.go index be2c83814..ab58b27fa 100644 --- a/backend/provider/kcp/provider.go +++ b/backend/provider/kcp/provider.go @@ -18,12 +18,18 @@ package kcp import ( "context" + "math" + "time" provider "github.com/kcp-dev/multicluster-provider/apiexport" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/cluster" "sigs.k8s.io/multicluster-runtime/pkg/multicluster" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" ) var _ multicluster.Provider = &Provider{} @@ -73,6 +79,49 @@ type awareWrapper struct { } func (a *awareWrapper) Engage(ctx context.Context, name string, cluster cluster.Cluster) error { - // TODO: Add cluster seeding here in the follow-up PRS. - return a.Aware.Engage(ctx, name, cluster) + ctx, cancel := context.WithCancel(ctx) //nolint:govet // cancel is called in the error case only. + + err := a.Aware.Engage(ctx, name, cluster) + if err != nil { + cancel() + return err + } + + cl := cluster.GetClient() + + obj := &kubebindv1alpha2.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: kubebindv1alpha2.DefaultClusterName, + }, + Spec: kubebindv1alpha2.ClusterSpec{}, + } + + const maxRetries = 3 + for attempt := 0; attempt < maxRetries; attempt++ { + err = cl.Create(ctx, obj) + if err == nil || apierrors.IsAlreadyExists(err) { + break + } + + if !apierrors.IsForbidden(err) { + cancel() + return err + } + + if attempt < maxRetries-1 { + backoff := time.Duration(math.Pow(2, float64(attempt))) * time.Second + select { + case <-time.After(backoff): + case <-ctx.Done(): + cancel() + return ctx.Err() + } + } + } + + if err != nil && !apierrors.IsAlreadyExists(err) { + cancel() + return err + } + return nil //nolint:govet // cancel is called in the error case only. } diff --git a/backend/server.go b/backend/server.go index 10e314ace..e9dcdb874 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/cluster" "github.com/kube-bind/kube-bind/backend/controllers/clusterbinding" "github.com/kube-bind/kube-bind/backend/controllers/serviceexport" "github.com/kube-bind/kube-bind/backend/controllers/serviceexportrequest" @@ -55,6 +56,7 @@ type Controllers struct { ServiceExport *serviceexport.APIServiceExportReconciler ServiceExportRequest *serviceexportrequest.APIServiceExportRequestReconciler ServiceNamespace *servicenamespace.APIServiceNamespaceReconciler + Cluster *cluster.ClusterReconciler } func NewServer(ctx context.Context, c *Config) (*Server, error) { @@ -95,6 +97,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { c.Options.ExternalCA, c.Options.TLSExternalServerName, s.Config.Manager, + c.Options.OIDC.Type == string(kubebindv1alpha2.OIDCProviderTypeEmbedded), ) if err != nil { return nil, fmt.Errorf("error setting up Kubernetes Manager: %w", err) @@ -186,6 +189,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { if err := s.ServiceNamespace.SetupWithManager(s.Config.Manager); err != nil { return nil, fmt.Errorf("error setting up APIServiceNamespace controller with manager: %w", err) } + s.ServiceExportRequest, err = serviceexportrequest.NewAPIServiceExportRequestReconciler( ctx, s.Config.Manager, @@ -197,12 +201,24 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { if err != nil { return nil, fmt.Errorf("error setting up ServiceExportRequest Controller: %w", err) } - - // Register the ServiceExportRequest controller with the manager if err := s.ServiceExportRequest.SetupWithManager(s.Config.Manager); err != nil { return nil, fmt.Errorf("error setting up ServiceExportRequest controller with manager: %w", err) } + s.Cluster, err = cluster.NewClusterReconciler( + ctx, + s.Config.Manager, + opts, + c.Options.OIDC.AllowedGroups, + c.Options.OIDC.AllowedUsers, + ) + if err != nil { + return nil, fmt.Errorf("error setting up LogicalCluster Controller: %w", err) + } + if err := s.Cluster.SetupWithManager(s.Config.Manager); err != nil { + return nil, fmt.Errorf("error setting up LogicalCluster controller with manager: %w", err) + } + return s, nil } @@ -270,9 +286,25 @@ func (s *Server) Run(ctx context.Context) error { } }() + go func() { + // When using native k8s provider/singelton cluster - provider is set to nil + // so we need to seed the default cluster. If provider is set - it is provider responsibility + // to seed the default cluster in each managed cluster either via provider machinery or via provider + // wrapper (see kcp example). + if s.Config.Provider == nil { + if err := s.seedCluster(ctx); err != nil { + logger.Error(err, "Failed to seed default cluster") + } + } + }() + go func() { <-ctx.Done() logger.Info("Context done") }() return s.WebServer.Start(ctx) } + +func (s *Server) seedCluster(ctx context.Context) error { + return s.Kubernetes.SeedDefaultCluster(ctx) +} diff --git a/contrib/kcp/README.md b/contrib/kcp/README.md index 469601ee3..cfbec26fb 100644 --- a/contrib/kcp/README.md +++ b/contrib/kcp/README.md @@ -43,7 +43,7 @@ export KUBECONFIG=.kcp/backend.kubeconfig ``` k ws use :root:kube-bind -./bin/backend \ +go run ./cmd/backend \ --multicluster-runtime-provider kcp \ --apiexport-endpoint-slice-name=kube-bind.io \ --pretty-name="BigCorp.com" \ @@ -78,6 +78,7 @@ kubectl kcp bind apiexport root:kube-bind:kube-bind.io \ --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 ``` diff --git a/contrib/kcp/deploy/examples/rbac-embedded-user.yaml b/contrib/kcp/deploy/examples/rbac-embedded-user.yaml new file mode 100644 index 000000000..1647419dc --- /dev/null +++ b/contrib/kcp/deploy/examples/rbac-embedded-user.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kube-bind-embedded-user +rules: +- apiGroups: ["kube-bind.io"] + resources: ["*"] + verbs: ["bind"] +- nonResourceURLs: ["/", "/api", "/api/*", "/apis", "/apis/*"] + verbs: ["access"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kube-bind-embedded-user +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kube-bind-embedded-user +subjects: +- kind: User + name: kube-bind-embedded-user + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml index 458c5c40e..6985b44e4 100644 --- a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml @@ -29,6 +29,10 @@ spec: - resource: secrets verbs: - '*' + - group: authorization.k8s.io + resource: subjectaccessreviews + verbs: + - '*' - resource: namespaces verbs: - '*' @@ -86,6 +90,11 @@ spec: schema: v250925-56669b8.clusterbindings.kube-bind.io storage: crd: {} + - group: kube-bind.io + name: clusters + schema: v251215-7d342944.clusters.kube-bind.io + storage: + crd: {} - group: kube-bind.io name: collections schema: v251106-640871f.collections.kube-bind.io diff --git a/contrib/kcp/deploy/resources/apiresourceschema-clusters.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-clusters.kube-bind.io.yaml new file mode 100644 index 000000000..a48f14fd8 --- /dev/null +++ b/contrib/kcp/deploy/resources/apiresourceschema-clusters.kube-bind.io.yaml @@ -0,0 +1,102 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + name: v251215-7d342944.clusters.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bind + kind: Cluster + listKind: ClusterList + plural: clusters + singular: cluster + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1alpha2 + schema: + description: Cluster represents a cluster, managed by kube-bind. + 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 represents the data in the newly created ClusterBinding. + type: object + status: + description: status contains reconciliation information for the service + binding. + properties: + conditions: + description: |- + conditions is a list of conditions that apply to the ClusterBinding. It is + updated by the konnector and the service provider. + 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 + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/contrib/kcp/test/e2e/kcp_test.go b/contrib/kcp/test/e2e/kcp_test.go index 1d5d3fe51..5f249e095 100644 --- a/contrib/kcp/test/e2e/kcp_test.go +++ b/contrib/kcp/test/e2e/kcp_test.go @@ -103,6 +103,7 @@ func testKcpIntegration(t *testing.T, name string, scope kubebindv1alpha2.Inform "namespaces.core", "roles.rbac.authorization.k8s.io", "rolebindings.rbac.authorization.k8s.io", + "subjectaccessreviews.authorization.k8s.io", "apiresourceschemas.apis.kcp.io", ), ) diff --git a/deploy/charts/backend/crds/kube-bind.io_clusters.yaml b/deploy/charts/backend/crds/kube-bind.io_clusters.yaml new file mode 100644 index 000000000..7dba0d0e9 --- /dev/null +++ b/deploy/charts/backend/crds/kube-bind.io_clusters.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: clusters.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bind + kind: Cluster + listKind: ClusterList + plural: clusters + singular: cluster + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: Cluster represents a cluster, managed by kube-bind. + 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 represents the data in the newly created ClusterBinding. + type: object + status: + description: status contains reconciliation information for the service + binding. + properties: + conditions: + description: |- + conditions is a list of conditions that apply to the ClusterBinding. It is + updated by the konnector and the service provider. + 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 + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/charts/backend/examples/values-local-development.yaml b/deploy/charts/backend/examples/values-local-development.yaml index be7fde680..e657ad1c0 100644 --- a/deploy/charts/backend/examples/values-local-development.yaml +++ b/deploy/charts/backend/examples/values-local-development.yaml @@ -10,10 +10,17 @@ backend: listenAddress: "0.0.0.0:8443" # OIDC configuration oidc: + type: "external" issuerUrl: "https://auth.genericcontrolplane.io" clientId: "kube-bind" clientSecret: "### REPLACE_ME ###" callbackUrl: "https://kube-bind.genericcontrolplane.io/callback" + # Example allowed groups and users for external OIDC + allowedGroups: + - "kube-bind-users" + - "admins" + allowedUsers: + - "user@example.com" # Cookie encryption keys (generate your own for production!) # example: openssl rand -base64 32 diff --git a/deploy/charts/backend/templates/deployment.yaml b/deploy/charts/backend/templates/deployment.yaml index 7748f160b..767d1ab11 100644 --- a/deploy/charts/backend/templates/deployment.yaml +++ b/deploy/charts/backend/templates/deployment.yaml @@ -94,6 +94,16 @@ spec: {{- else if eq .Values.backend.oidc.type "external" }} - --oidc-type=external {{- end }} + {{- if .Values.backend.oidc.allowedGroups }} + {{- range .Values.backend.oidc.allowedGroups }} + - --oidc-allowed-groups={{ . }} + {{- end }} + {{- end }} + {{- if .Values.backend.oidc.allowedUsers }} + {{- range .Values.backend.oidc.allowedUsers }} + - --oidc-allowed-users={{ . }} + {{- end }} + {{- end }} {{- if .Values.backend.loggingLevel }} - -v={{ .Values.backend.loggingLevel }} {{- end }} diff --git a/deploy/charts/backend/values.yaml b/deploy/charts/backend/values.yaml index 66ffd9997..cf917a2ef 100644 --- a/deploy/charts/backend/values.yaml +++ b/deploy/charts/backend/values.yaml @@ -22,6 +22,12 @@ backend: clientId: "" clientSecret: "" callbackUrl: "" + type: "embedded" # Options: "embedded" or "external" + # List of groups allowed to access bindings inside the cluster + # If using embedded OIDC provider, system:authenticated will be added automatically + allowedGroups: [] + # List of users allowed to access bindings inside the cluster + allowedUsers: [] # General backend configuration prettyName: "" diff --git a/deploy/config/cluster.yaml b/deploy/config/cluster.yaml new file mode 100644 index 000000000..37b20939e --- /dev/null +++ b/deploy/config/cluster.yaml @@ -0,0 +1,5 @@ +apiVersion: kube-bind.io/v1alpha2 +kind: Cluster +metadata: + name: default +spec: {} \ No newline at end of file diff --git a/deploy/crd/kube-bind.io_clusters.yaml b/deploy/crd/kube-bind.io_clusters.yaml new file mode 100644 index 000000000..7dba0d0e9 --- /dev/null +++ b/deploy/crd/kube-bind.io_clusters.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: clusters.kube-bind.io +spec: + group: kube-bind.io + names: + categories: + - kube-bind + kind: Cluster + listKind: ClusterList + plural: clusters + singular: cluster + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: Cluster represents a cluster, managed by kube-bind. + 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 represents the data in the newly created ClusterBinding. + type: object + status: + description: status contains reconciliation information for the service + binding. + properties: + conditions: + description: |- + conditions is a list of conditions that apply to the ClusterBinding. It is + updated by the konnector and the service provider. + 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 + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/docs/content/developers/backend/oidc/index.md b/docs/content/developers/backend/oidc/index.md index 596167a29..c54395ced 100644 --- a/docs/content/developers/backend/oidc/index.md +++ b/docs/content/developers/backend/oidc/index.md @@ -29,8 +29,12 @@ Configure the backend to use an external OIDC provider with these flags: --oidc-callback-url=https://your-backend.com/callback --oidc-authorize-url=https://your-oidc-provider.com/auth --oidc-ca-file=/path/to/ca-bundle.pem # Optional: for custom CA certificates +--oidc-allowed-groups=group1,group2,group3 # Optional: restrict access to specific groups +--oidc-allowed-users=user1,user2,user3 # Optional: restrict access to specific users ``` +**Note**: When using external OIDC providers, at least one of `--oidc-allowed-groups` or `--oidc-allowed-users` must be specified for security reasons. + ### External OIDC Flow 1. User initiates authentication by accessing a protected endpoint @@ -53,6 +57,8 @@ Configure the backend to use the embedded OIDC provider: --oidc-type=embedded --oidc-callback-url=https://your-backend.com/callback --oidc-issuer-url=https://your-backend.com # The backend serves as the OIDC provider +--oidc-allowed-groups=group1,group2,group3 # Optional: restrict access to specific groups +--oidc-allowed-users=user1,user2,user3 # Optional: restrict access to specific users ``` ### Embedded OIDC Flow @@ -88,6 +94,42 @@ When using embedded OIDC, the backend displays a prominent warning message at st - Intended only for local development and testing - Does not persist user data between restarts +## Access Control + +The kube-bind backend provides fine-grained access control through OIDC group and user-based authorization. When a user successfully authenticates via OIDC, the backend automatically creates RBAC resources to control access to cluster binding operations. + +### Access Control Flags + +- `--oidc-allowed-groups`: List of OIDC groups allowed to access bindings inside the cluster. Users must be members of at least one of these groups to gain access. +- `--oidc-allowed-users`: List of specific OIDC users allowed to access bindings inside the cluster. + +**Important**: For external OIDC providers, at least one of these flags must be specified. This validation ensures that access control is properly configured in production environments. + +### How Access Control Works + +1. **RBAC Resource Creation**: For each cluster, the controller automatically creates: + - A `ClusterRole` named `kube-bind-oidc-user` with permissions to perform binding operations + - A `ClusterRoleBinding` that binds the role to the specified groups and users + +2. **Group-based Access**: Users who are members of groups listed in `--oidc-allowed-groups` are granted access via RBAC subject entries of kind `Group`. + +3. **User-based Access**: Individual users listed in `--oidc-allowed-users` are granted access via RBAC subject entries of kind `User`. + +4. **Embedded OIDC Special Behavior**: When using embedded OIDC (`--oidc-type=embedded`), the `system:authenticated` group is automatically added to the allowed groups list, providing access to all authenticated users during development. + +### Example Configuration + +```bash +# Restrict access to specific groups and users +--oidc-allowed-groups=kube-bind-admins,platform-team +--oidc-allowed-users=admin@example.com,operator@example.com +``` + +This configuration would create RBAC bindings allowing: +- All members of the `kube-bind-admins` group +- All members of the `platform-team` group +- The specific users `admin@example.com` and `operator@example.com` + ## Troubleshooting ### Common Issues @@ -96,6 +138,7 @@ When using embedded OIDC, the backend displays a prominent warning message at st 2. **Callback URL mismatches**: Ensure the callback URL matches what's configured in your OIDC provider 3. **Token validation errors**: Check that the issuer URL matches between provider and backend configuration 4. **Network connectivity**: Verify the backend can reach the external OIDC provider +5. **Access control validation errors**: When using external OIDC, ensure at least one of `--oidc-allowed-groups` or `--oidc-allowed-users` is specified ### Debug Logging diff --git a/docs/content/developers/dev-environments.md b/docs/content/developers/dev-environments.md index a533e1022..e6452e0c5 100644 --- a/docs/content/developers/dev-environments.md +++ b/docs/content/developers/dev-environments.md @@ -86,6 +86,7 @@ All the instructions assume you have already cloned the kube-bind repository and --accept-permission-claim serviceaccounts.core \ --accept-permission-claim configmaps.core \ --accept-permission-claim secrets.core \ + --accept-permission-claim subjectaccessreviews.authorization.k8s.io \ --accept-permission-claim namespaces.core \ --accept-permission-claim roles.rbac.authorization.k8s.io \ --accept-permission-claim rolebindings.rbac.authorization.k8s.io \ @@ -125,7 +126,7 @@ All the instructions assume you have already cloned the kube-bind repository and 10. Bind the thing: ```bash - ./bin/kubectl-bind login http://127.0.0.1:8080 --cluster 2myqz7lt9i0u5kzb + ./bin/kubectl-bind login http://127.0.0.1:8080 --cluster 1xiy1uyh4qckje8z ./bin/kubectl-bind --dry-run -o yaml > apiserviceexport.yaml # Extract secret for binding process. Note that secret name is not the same as output from command above. Check secret diff --git a/docs/content/setup/kcp-setup.md b/docs/content/setup/kcp-setup.md index eda3dfad0..610f5a5a7 100644 --- a/docs/content/setup/kcp-setup.md +++ b/docs/content/setup/kcp-setup.md @@ -50,7 +50,7 @@ go run ./cmd/backend \ --oidc-type=embedded \ --pretty-name="BigCorp.com" \ --namespace-prefix="kube-bind-" \ - --consumer-scope=cluster --frontend http://localhost:3000 + --consumer-scope=cluster ``` This process will keep running, so open a new terminal. diff --git a/sdk/apis/kubebind/v1alpha2/cluster_types.go b/sdk/apis/kubebind/v1alpha2/cluster_types.go new file mode 100644 index 000000000..821f49b7a --- /dev/null +++ b/sdk/apis/kubebind/v1alpha2/cluster_types.go @@ -0,0 +1,85 @@ +/* +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" +) + +const ( + // ClusterConditionHealthy is set when the cluster is healthy. + ClusterConditionHealthy = "Healthy" +) + +const ( + // DefaultClusterName is the name of the default cluster. + DefaultClusterName = "default" +) + +// Cluster represents a cluster, managed by kube-bind. +// +// +crd +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,categories=kube-bind +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=`.status.conditions[?(@.type=="Ready")].status`,priority=0 +type Cluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + // spec represents the data in the newly created ClusterBinding. + // +required + // +kubebuilder:validation:Required + Spec ClusterSpec `json:"spec"` + + // status contains reconciliation information for the service binding. + Status ClusterStatus `json:"status"` +} + +func (in *Cluster) GetConditions() conditionsapi.Conditions { + return in.Status.Conditions +} + +func (in *Cluster) SetConditions(conditions conditionsapi.Conditions) { + in.Status.Conditions = conditions +} + +// ClusterSpec represents the data in the newly created Cluster. +type ClusterSpec struct { +} + +// ClusterStatus stores status information about a service binding. It is +// updated by both the konnector and the service provider. +type ClusterStatus struct { + // conditions is a list of conditions that apply to the ClusterBinding. It is + // updated by the konnector and the service provider. + Conditions conditionsapi.Conditions `json:"conditions,omitempty"` +} + +// ClusterList is the objects list that represents the Cluster. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []Cluster `json:"items"` +} diff --git a/sdk/apis/kubebind/v1alpha2/register.go b/sdk/apis/kubebind/v1alpha2/register.go index 8f71322f8..b4a6e683e 100644 --- a/sdk/apis/kubebind/v1alpha2/register.go +++ b/sdk/apis/kubebind/v1alpha2/register.go @@ -64,6 +64,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &APIServiceExportTemplateList{}, &Collection{}, &CollectionList{}, + &Cluster{}, + &ClusterList{}, ) 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 550646334..87fe4ea83 100644 --- a/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go +++ b/sdk/apis/kubebind/v1alpha2/zz_generated.deepcopy.go @@ -1001,6 +1001,34 @@ func (in *BoundSchemaStatus) DeepCopy() *BoundSchemaStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *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 Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Cluster) 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 *ClusterBinding) DeepCopyInto(out *ClusterBinding) { *out = *in @@ -1105,6 +1133,39 @@ func (in *ClusterBindingStatus) DeepCopy() *ClusterBindingStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterList) DeepCopyInto(out *ClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Cluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterList. +func (in *ClusterList) DeepCopy() *ClusterList { + if in == nil { + return nil + } + out := new(ClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterList) 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 *ClusterSecretKeyRef) DeepCopyInto(out *ClusterSecretKeyRef) { *out = *in @@ -1122,6 +1183,45 @@ func (in *ClusterSecretKeyRef) DeepCopy() *ClusterSecretKeyRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. +func (in *ClusterSpec) DeepCopy() *ClusterSpec { + if in == nil { + return nil + } + out := new(ClusterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { + *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 ClusterStatus. +func (in *ClusterStatus) DeepCopy() *ClusterStatus { + if in == nil { + return nil + } + out := new(ClusterStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Collection) DeepCopyInto(out *Collection) { *out = *in diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/cluster.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/cluster.go new file mode 100644 index 000000000..9a61c664b --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/cluster.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 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" +) + +// ClustersGetter has a method to return a ClusterInterface. +// A group's client should implement this interface. +type ClustersGetter interface { + Clusters(namespace string) ClusterInterface +} + +// ClusterInterface has methods to work with Cluster resources. +type ClusterInterface interface { + Create(ctx context.Context, cluster *kubebindv1alpha2.Cluster, opts v1.CreateOptions) (*kubebindv1alpha2.Cluster, error) + Update(ctx context.Context, cluster *kubebindv1alpha2.Cluster, opts v1.UpdateOptions) (*kubebindv1alpha2.Cluster, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, cluster *kubebindv1alpha2.Cluster, opts v1.UpdateOptions) (*kubebindv1alpha2.Cluster, 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.Cluster, error) + List(ctx context.Context, opts v1.ListOptions) (*kubebindv1alpha2.ClusterList, 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.Cluster, err error) + ClusterExpansion +} + +// clusters implements ClusterInterface +type clusters struct { + *gentype.ClientWithList[*kubebindv1alpha2.Cluster, *kubebindv1alpha2.ClusterList] +} + +// newClusters returns a Clusters +func newClusters(c *KubeBindV1alpha2Client, namespace string) *clusters { + return &clusters{ + gentype.NewClientWithList[*kubebindv1alpha2.Cluster, *kubebindv1alpha2.ClusterList]( + "clusters", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *kubebindv1alpha2.Cluster { return &kubebindv1alpha2.Cluster{} }, + func() *kubebindv1alpha2.ClusterList { return &kubebindv1alpha2.ClusterList{} }, + ), + } +} diff --git a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_cluster.go b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_cluster.go new file mode 100644 index 000000000..43e366d24 --- /dev/null +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/fake/fake_cluster.go @@ -0,0 +1,50 @@ +/* +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" +) + +// fakeClusters implements ClusterInterface +type fakeClusters struct { + *gentype.FakeClientWithList[*v1alpha2.Cluster, *v1alpha2.ClusterList] + Fake *FakeKubeBindV1alpha2 +} + +func newFakeClusters(fake *FakeKubeBindV1alpha2, namespace string) kubebindv1alpha2.ClusterInterface { + return &fakeClusters{ + gentype.NewFakeClientWithList[*v1alpha2.Cluster, *v1alpha2.ClusterList]( + fake.Fake, + namespace, + v1alpha2.SchemeGroupVersion.WithResource("clusters"), + v1alpha2.SchemeGroupVersion.WithKind("Cluster"), + func() *v1alpha2.Cluster { return &v1alpha2.Cluster{} }, + func() *v1alpha2.ClusterList { return &v1alpha2.ClusterList{} }, + func(dst, src *v1alpha2.ClusterList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha2.ClusterList) []*v1alpha2.Cluster { return gentype.ToPointerSlice(list.Items) }, + func(list *v1alpha2.ClusterList, items []*v1alpha2.Cluster) { + 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 17d65407f..ac2d7310d 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 @@ -48,6 +48,10 @@ func (c *FakeKubeBindV1alpha2) BoundSchemas(namespace string) v1alpha2.BoundSche return newFakeBoundSchemas(c, namespace) } +func (c *FakeKubeBindV1alpha2) Clusters(namespace string) v1alpha2.ClusterInterface { + return newFakeClusters(c, namespace) +} + func (c *FakeKubeBindV1alpha2) ClusterBindings(namespace string) v1alpha2.ClusterBindingInterface { return newFakeClusterBindings(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 3be2b1782..182625159 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/generated_expansion.go @@ -28,6 +28,8 @@ type APIServiceNamespaceExpansion interface{} type BoundSchemaExpansion interface{} +type ClusterExpansion interface{} + type ClusterBindingExpansion interface{} type CollectionExpansion 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 d31e864bb..92189fb0b 100644 --- a/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go +++ b/sdk/client/clientset/versioned/typed/kubebind/v1alpha2/kubebind_client.go @@ -33,6 +33,7 @@ type KubeBindV1alpha2Interface interface { APIServiceExportRequestsGetter APIServiceNamespacesGetter BoundSchemasGetter + ClustersGetter ClusterBindingsGetter CollectionsGetter } @@ -62,6 +63,10 @@ func (c *KubeBindV1alpha2Client) BoundSchemas(namespace string) BoundSchemaInter return newBoundSchemas(c, namespace) } +func (c *KubeBindV1alpha2Client) Clusters(namespace string) ClusterInterface { + return newClusters(c, namespace) +} + func (c *KubeBindV1alpha2Client) ClusterBindings(namespace string) ClusterBindingInterface { return newClusterBindings(c, namespace) } diff --git a/sdk/client/informers/externalversions/generic.go b/sdk/client/informers/externalversions/generic.go index fa39a9633..9fedd751b 100644 --- a/sdk/client/informers/externalversions/generic.go +++ b/sdk/client/informers/externalversions/generic.go @@ -76,6 +76,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().APIServiceNamespaces().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("boundschemas"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().BoundSchemas().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("clusters"): + return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().Clusters().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("clusterbindings"): return &genericInformer{resource: resource.GroupResource(), informer: f.KubeBind().V1alpha2().ClusterBindings().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("collections"): diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/cluster.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/cluster.go new file mode 100644 index 000000000..a0157b4aa --- /dev/null +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/cluster.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" +) + +// ClusterInformer provides access to a shared informer and lister for +// Clusters. +type ClusterInformer interface { + Informer() cache.SharedIndexInformer + Lister() kubebindv1alpha2.ClusterLister +} + +type clusterInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewClusterInformer constructs a new informer for Cluster 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 NewClusterInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredClusterInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredClusterInformer constructs a new informer for Cluster 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 NewFilteredClusterInformer(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().Clusters(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().Clusters(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().Clusters(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KubeBindV1alpha2().Clusters(namespace).Watch(ctx, options) + }, + }, + &apiskubebindv1alpha2.Cluster{}, + resyncPeriod, + indexers, + ) +} + +func (f *clusterInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredClusterInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *clusterInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apiskubebindv1alpha2.Cluster{}, f.defaultInformer) +} + +func (f *clusterInformer) Lister() kubebindv1alpha2.ClusterLister { + return kubebindv1alpha2.NewClusterLister(f.Informer().GetIndexer()) +} diff --git a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go index 1e7187ba9..9f6cebe08 100644 --- a/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go +++ b/sdk/client/informers/externalversions/kubebind/v1alpha2/interface.go @@ -34,6 +34,8 @@ type Interface interface { APIServiceNamespaces() APIServiceNamespaceInformer // BoundSchemas returns a BoundSchemaInformer. BoundSchemas() BoundSchemaInformer + // Clusters returns a ClusterInformer. + Clusters() ClusterInformer // ClusterBindings returns a ClusterBindingInformer. ClusterBindings() ClusterBindingInformer // Collections returns a CollectionInformer. @@ -76,6 +78,11 @@ func (v *version) BoundSchemas() BoundSchemaInformer { return &boundSchemaInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// Clusters returns a ClusterInformer. +func (v *version) Clusters() ClusterInformer { + return &clusterInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // ClusterBindings returns a ClusterBindingInformer. func (v *version) ClusterBindings() ClusterBindingInformer { return &clusterBindingInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/sdk/client/listers/kubebind/v1alpha2/cluster.go b/sdk/client/listers/kubebind/v1alpha2/cluster.go new file mode 100644 index 000000000..6bba81e5a --- /dev/null +++ b/sdk/client/listers/kubebind/v1alpha2/cluster.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" +) + +// ClusterLister helps list Clusters. +// All objects returned here must be treated as read-only. +type ClusterLister interface { + // List lists all Clusters in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.Cluster, err error) + // Clusters returns an object that can list and get Clusters. + Clusters(namespace string) ClusterNamespaceLister + ClusterListerExpansion +} + +// clusterLister implements the ClusterLister interface. +type clusterLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.Cluster] +} + +// NewClusterLister returns a new ClusterLister. +func NewClusterLister(indexer cache.Indexer) ClusterLister { + return &clusterLister{listers.New[*kubebindv1alpha2.Cluster](indexer, kubebindv1alpha2.Resource("cluster"))} +} + +// Clusters returns an object that can list and get Clusters. +func (s *clusterLister) Clusters(namespace string) ClusterNamespaceLister { + return clusterNamespaceLister{listers.NewNamespaced[*kubebindv1alpha2.Cluster](s.ResourceIndexer, namespace)} +} + +// ClusterNamespaceLister helps list and get Clusters. +// All objects returned here must be treated as read-only. +type ClusterNamespaceLister interface { + // List lists all Clusters in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kubebindv1alpha2.Cluster, err error) + // Get retrieves the Cluster from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*kubebindv1alpha2.Cluster, error) + ClusterNamespaceListerExpansion +} + +// clusterNamespaceLister implements the ClusterNamespaceLister +// interface. +type clusterNamespaceLister struct { + listers.ResourceIndexer[*kubebindv1alpha2.Cluster] +} diff --git a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go index fc8aa4060..08932cb79 100644 --- a/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go +++ b/sdk/client/listers/kubebind/v1alpha2/expansion_generated.go @@ -54,6 +54,14 @@ type BoundSchemaListerExpansion interface{} // BoundSchemaNamespaceLister. type BoundSchemaNamespaceListerExpansion interface{} +// ClusterListerExpansion allows custom methods to be added to +// ClusterLister. +type ClusterListerExpansion interface{} + +// ClusterNamespaceListerExpansion allows custom methods to be added to +// ClusterNamespaceLister. +type ClusterNamespaceListerExpansion interface{} + // ClusterBindingListerExpansion allows custom methods to be added to // ClusterBindingLister. type ClusterBindingListerExpansion interface{} diff --git a/test/e2e/framework/backend.go b/test/e2e/framework/backend.go index 89a40e7b3..fc63c1454 100644 --- a/test/e2e/framework/backend.go +++ b/test/e2e/framework/backend.go @@ -46,6 +46,7 @@ func InstallKubeBindCRDs(t testing.TB, clientConfig *rest.Config) { metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "boundschemas"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "apiserviceexporttemplates"}, metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "collections"}, + metav1.GroupResource{Group: kubebindv1alpha2.GroupName, Resource: "clusters"}, ) require.NoError(t, err) }