// Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 package rbac import ( "context" "fmt" "github.com/go-logr/logr" "github.com/hashicorp/go-multierror" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" capsulev1beta2 "github.com/clastix/capsule/api/v1beta2" "github.com/clastix/capsule/controllers/utils" "github.com/clastix/capsule/pkg/configuration" ) type Manager struct { Log logr.Logger Client client.Client Configuration configuration.Configuration } // InjectClient injects the Client interface, required by the Runnable interface. func (r *Manager) InjectClient(c client.Client) error { r.Client = c return nil } func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, configurationName string) (err error) { namesPredicate := utils.NamesMatchingPredicate(ProvisionerRoleName, DeleterRoleName) crErr := ctrl.NewControllerManagedBy(mgr). For(&rbacv1.ClusterRole{}, namesPredicate). Complete(r) if crErr != nil { err = multierror.Append(err, crErr) } crbErr := ctrl.NewControllerManagedBy(mgr). For(&rbacv1.ClusterRoleBinding{}, namesPredicate). Watches(source.NewKindWithCache(&capsulev1beta2.CapsuleConfiguration{}, mgr.GetCache()), handler.Funcs{ UpdateFunc: func(updateEvent event.UpdateEvent, limitingInterface workqueue.RateLimitingInterface) { if updateEvent.ObjectNew.GetName() == configurationName { if crbErr := r.EnsureClusterRoleBindings(ctx); crbErr != nil { r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update") } } }, }). Complete(r) if crbErr != nil { err = multierror.Append(err, crbErr) } return } // Reconcile serves both required ClusterRole and ClusterRoleBinding resources: that's ok, we're watching for multiple // Resource kinds and we're just interested to the ones with the said name since they're bounded together. func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) { switch request.Name { case ProvisionerRoleName: if err = r.EnsureClusterRole(ctx, ProvisionerRoleName); err != nil { r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", ProvisionerRoleName) break } if err = r.EnsureClusterRoleBindings(ctx); err != nil { r.Log.Error(err, "Reconciliation for ClusterRoleBindings failed") break } case DeleterRoleName: if err = r.EnsureClusterRole(ctx, DeleterRoleName); err != nil { r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", DeleterRoleName) } } return } func (r *Manager) EnsureClusterRoleBindings(ctx context.Context) (err error) { crb := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: ProvisionerRoleName, }, } _, err = controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() (err error) { crb.RoleRef = provisionerClusterRoleBinding.RoleRef crb.Subjects = []rbacv1.Subject{} for _, group := range r.Configuration.UserGroups() { crb.Subjects = append(crb.Subjects, rbacv1.Subject{ Kind: "Group", Name: group, }) } return }) return } func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err error) { role, ok := clusterRoles[roleName] if !ok { return fmt.Errorf("clusterRole %s is not mapped", roleName) } clusterRole := &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: role.GetName(), }, } _, err = controllerutil.CreateOrUpdate(ctx, r.Client, clusterRole, func() error { clusterRole.Rules = role.Rules return nil }) return } // Start is the Runnable function triggered upon Manager start-up to perform the first RBAC reconciliation // since we're not creating empty CR and CRB upon Capsule installation: it's a run-once task, since the reconciliation // is handled by the Reconciler implemented interface. func (r *Manager) Start(ctx context.Context) error { for roleName := range clusterRoles { r.Log.Info("setting up ClusterRoles", "ClusterRole", roleName) if err := r.EnsureClusterRole(ctx, roleName); err != nil { if apierrors.IsAlreadyExists(err) { continue } return err } } r.Log.Info("setting up ClusterRoleBindings") if err := r.EnsureClusterRoleBindings(ctx); err != nil { if apierrors.IsAlreadyExists(err) { return nil } return err } return nil }