From b3a3553ef2f77b3579351a33ecd458369ea8e276 Mon Sep 17 00:00:00 2001 From: Jacob See <5027680+jacobsee@users.noreply.github.com> Date: Mon, 1 Mar 2021 08:23:49 -0800 Subject: [PATCH] Update DynamicRoles and DynamicClusterRoles when an inherited Role or ClusterRole is modified (#7) --- controllers/clusterrole_controller.go | 61 +++++++++++++++++++++++++++ controllers/crd_controller.go | 30 ++----------- controllers/meta.go | 48 +++++++++++++++++++++ controllers/role_controller.go | 61 +++++++++++++++++++++++++++ helpers/resource_cache.go | 9 +++- helpers/rule_manipulation.go | 4 +- main.go | 18 ++++++++ 7 files changed, 202 insertions(+), 29 deletions(-) create mode 100644 controllers/clusterrole_controller.go create mode 100644 controllers/meta.go create mode 100644 controllers/role_controller.go diff --git a/controllers/clusterrole_controller.go b/controllers/clusterrole_controller.go new file mode 100644 index 0000000..fb3cde0 --- /dev/null +++ b/controllers/clusterrole_controller.go @@ -0,0 +1,61 @@ +/* + + +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 controllers + +import ( + "context" + + "github.com/go-logr/logr" + "github.com/redhat-cop/dynamic-rbac-operator/helpers" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + rbacv1 "k8s.io/api/rbac/v1" +) + +// ClusterRoleReconciler reconciles a ClusterRole object +type ClusterRoleReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cache *helpers.ResourceCache +} + +// +kubebuilder:rbac:groups=rbac.redhatcop.redhat.io,resources=clusterroles,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=rbac.redhatcop.redhat.io,resources=clusterroles/status,verbs=get;update;patch + +func (r *ClusterRoleReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + _ = context.Background() + _ = r.Log.WithValues("clusterrole", req.NamespacedName) + + result := ctrl.Result{} + var err error + + if _, exists := r.Cache.WatchedClusterRoles[req.NamespacedName]; exists { + r.Log.Info("A cluster role referenced by a dynamic resource has been updated - reconciling now") + result, err = UpdateAllDynamicResources(r.Client, r.Log, r.Scheme, r.Cache) + } + + return result, err +} + +func (r *ClusterRoleReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&rbacv1.ClusterRole{}). + Complete(r) +} diff --git a/controllers/crd_controller.go b/controllers/crd_controller.go index e7eab1f..26e5de3 100644 --- a/controllers/crd_controller.go +++ b/controllers/crd_controller.go @@ -28,7 +28,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" - rbacv1alpha1 "github.com/redhat-cop/dynamic-rbac-operator/api/v1alpha1" helpers "github.com/redhat-cop/dynamic-rbac-operator/helpers" ) @@ -87,33 +86,12 @@ func (r *CustomResourceDefinitionReconciler) Reconcile(req ctrl.Request) (ctrl.R r.Cache.AllPolicies = &allPossibleRules r.Log.Info("Rebuilt cluster policy cache") - dynamicRoleList := &rbacv1alpha1.DynamicRoleList{} - err = r.Client.List(context.TODO(), dynamicRoleList) - if err != nil { - r.Log.Error(err, "could not list Dynamic Roles") - return reconcile.Result{}, err - } - for _, dynamicRole := range dynamicRoleList.Items { - _, err := ReconcileDynamicRole(&dynamicRole, r.Client, r.Scheme, r.Log, r.Cache) - if err != nil { - return reconcile.Result{}, err - } - } - dynamicClusterRoleList := &rbacv1alpha1.DynamicClusterRoleList{} - err = r.Client.List(context.TODO(), dynamicClusterRoleList) - if err != nil { - r.Log.Error(err, "could not list Dynamic Cluster Roles") - return reconcile.Result{}, err - } - for _, dynamicClusterRole := range dynamicClusterRoleList.Items { - _, err := ReconcileDynamicClusterRole(&dynamicClusterRole, r.Client, r.Scheme, r.Log, r.Cache) - if err != nil { - return reconcile.Result{}, err - } - } + // Recompute everything using the newly-refreshed cache + result, err := UpdateAllDynamicResources(r.Client, r.Log, r.Scheme, r.Cache) + r.Log.Info("All computed roles have been reconciled") - return ctrl.Result{}, nil + return result, err } func (r *CustomResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error { diff --git a/controllers/meta.go b/controllers/meta.go new file mode 100644 index 0000000..454d10d --- /dev/null +++ b/controllers/meta.go @@ -0,0 +1,48 @@ +package controllers + +import ( + "context" + + "github.com/go-logr/logr" + rbacv1alpha1 "github.com/redhat-cop/dynamic-rbac-operator/api/v1alpha1" + "github.com/redhat-cop/dynamic-rbac-operator/helpers" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// UpdateAllDynamicResources loops through all DynamicRoles and DynamicClusterRoles and updates their rules/specs as required based on current cache info +func UpdateAllDynamicResources(client client.Client, log logr.Logger, scheme *runtime.Scheme, cache *helpers.ResourceCache) (ctrl.Result, error) { + // Clear the watched roles cache maps since we're about to recreate them anyway - gets rid of anything we used to care about but no longer need + cache.WatchedRoles = map[types.NamespacedName]bool{} + cache.WatchedClusterRoles = map[types.NamespacedName]bool{} + + dynamicRoleList := &rbacv1alpha1.DynamicRoleList{} + err := client.List(context.TODO(), dynamicRoleList) + if err != nil { + log.Error(err, "could not list Dynamic Roles") + return reconcile.Result{}, err + } + for _, dynamicRole := range dynamicRoleList.Items { + _, err := ReconcileDynamicRole(&dynamicRole, client, scheme, log, cache) + if err != nil { + return reconcile.Result{}, err + } + } + dynamicClusterRoleList := &rbacv1alpha1.DynamicClusterRoleList{} + err = client.List(context.TODO(), dynamicClusterRoleList) + if err != nil { + log.Error(err, "could not list Dynamic Cluster Roles") + return reconcile.Result{}, err + } + for _, dynamicClusterRole := range dynamicClusterRoleList.Items { + _, err := ReconcileDynamicClusterRole(&dynamicClusterRole, client, scheme, log, cache) + if err != nil { + return reconcile.Result{}, err + } + } + log.Info("All computed roles have been reconciled") + return reconcile.Result{}, nil +} diff --git a/controllers/role_controller.go b/controllers/role_controller.go new file mode 100644 index 0000000..675c47d --- /dev/null +++ b/controllers/role_controller.go @@ -0,0 +1,61 @@ +/* + + +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 controllers + +import ( + "context" + + "github.com/go-logr/logr" + "github.com/redhat-cop/dynamic-rbac-operator/helpers" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + rbacv1 "k8s.io/api/rbac/v1" +) + +// RoleReconciler reconciles a Role object +type RoleReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Cache *helpers.ResourceCache +} + +// +kubebuilder:rbac:groups=rbac.redhatcop.redhat.io,resources=roles,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=rbac.redhatcop.redhat.io,resources=roles/status,verbs=get;update;patch + +func (r *RoleReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + _ = context.Background() + _ = r.Log.WithValues("role", req.NamespacedName) + + result := ctrl.Result{} + var err error + + if _, exists := r.Cache.WatchedRoles[req.NamespacedName]; exists { + r.Log.Info("A role referenced by a dynamic resource has been updated - reconciling now") + result, err = UpdateAllDynamicResources(r.Client, r.Log, r.Scheme, r.Cache) + } + + return result, err +} + +func (r *RoleReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&rbacv1.Role{}). + Complete(r) +} diff --git a/helpers/resource_cache.go b/helpers/resource_cache.go index 0c63f6d..b0c61e2 100755 --- a/helpers/resource_cache.go +++ b/helpers/resource_cache.go @@ -4,6 +4,7 @@ import ( "sync" rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" ) var lock = &sync.Mutex{} @@ -11,8 +12,10 @@ var lock = &sync.Mutex{} // ResourceCache holds information about the kube cluster state and // its policies so that it doesn't need to be queried for every reconciliation. type ResourceCache struct { - CRDs map[string]string - AllPolicies *[]rbacv1.PolicyRule + CRDs map[string]string + AllPolicies *[]rbacv1.PolicyRule + WatchedRoles map[types.NamespacedName]bool + WatchedClusterRoles map[types.NamespacedName]bool } var instance *ResourceCache @@ -25,6 +28,8 @@ func GetCacheInstance() *ResourceCache { if instance == nil { instance = &ResourceCache{} instance.CRDs = map[string]string{} + instance.WatchedRoles = map[types.NamespacedName]bool{} + instance.WatchedClusterRoles = map[types.NamespacedName]bool{} } } return instance diff --git a/helpers/rule_manipulation.go b/helpers/rule_manipulation.go index 622f7e2..5fde974 100755 --- a/helpers/rule_manipulation.go +++ b/helpers/rule_manipulation.go @@ -34,9 +34,10 @@ func BuildPolicyRules(client client.Client, cache ResourceCache, roleType RoleTy if err != nil { return nil, err } - // nonResourceURLs do not make sense to move from a ClusterRole to a Role + cache.WatchedClusterRoles[clusterRoleNamespacedName] = true var enumeratedPolicyRules []v1.PolicyRule if roleType == Role { + // nonResourceURLs do not make sense to move from a ClusterRole to a Role enumeratedPolicyRules, err = EnumeratePolicyRules(StripNonResourceURLs(inheritedClusterRole.Rules), &cache) } else { enumeratedPolicyRules, err = EnumeratePolicyRules(inheritedClusterRole.Rules, &cache) @@ -57,6 +58,7 @@ func BuildPolicyRules(client client.Client, cache ResourceCache, roleType RoleTy if err != nil { return nil, err } + cache.WatchedRoles[roleNamespacedName] = true enumeratedPolicyRules, err := EnumeratePolicyRules(inheritedRole.Rules, &cache) expandedPolicyRules := ExpandPolicyRules(enumeratedPolicyRules) rules = MergeExpandedPolicyRules(rules, expandedPolicyRules) diff --git a/main.go b/main.go index 5f5755c..47093ac 100644 --- a/main.go +++ b/main.go @@ -105,6 +105,24 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DynamicClusterRole") os.Exit(1) } + if err = (&controllers.RoleReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Role"), + Scheme: mgr.GetScheme(), + Cache: cache, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Role") + os.Exit(1) + } + if err = (&controllers.ClusterRoleReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("ClusterRole"), + Scheme: mgr.GetScheme(), + Cache: cache, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterRole") + os.Exit(1) + } // +kubebuilder:scaffold:builder // Begin cache setup