diff --git a/charts/fluent-operator/templates/fluent-operator-deployment.yaml b/charts/fluent-operator/templates/fluent-operator-deployment.yaml index 6089a2fd1..1c67cde8d 100644 --- a/charts/fluent-operator/templates/fluent-operator-deployment.yaml +++ b/charts/fluent-operator/templates/fluent-operator-deployment.yaml @@ -55,6 +55,9 @@ spec: {{- with .Values.operator.disableComponentControllers }} - --disable-component-controllers={{ . }} {{- end }} + {{- with .Values.operator.watchNamespaces }} + - {{ printf "--watch-namespaces=%s" . | quote }} + {{- end }} {{- with .Values.operator.livenessProbe }} livenessProbe: {{- toYaml . | nindent 10 }} diff --git a/charts/fluent-operator/values.yaml b/charts/fluent-operator/values.yaml index 7dc5d56b0..15a524d54 100644 --- a/charts/fluent-operator/values.yaml +++ b/charts/fluent-operator/values.yaml @@ -103,6 +103,10 @@ operator: # myExampleLabel: someValue # -- Disable specific component controllers. Value can be "fluent-bit" or "fluentd" to disable that controller disableComponentControllers: "" + # -- Comma separated list of namespaces the operator should watch and manage resources in. When set, the + # operator scopes its cache and creates namespaced Roles/RoleBindings (instead of cluster-wide) for the + # agents it manages, allowing the operator's own RBAC to be reduced. Defaults to cluster scope when empty. + watchNamespaces: "" # -- Extra arguments for the Fluent Operator controller extraArgs: [] diff --git a/cmd/fluent-manager/main.go b/cmd/fluent-manager/main.go index 407022742..b7b994f76 100644 --- a/cmd/fluent-manager/main.go +++ b/cmd/fluent-manager/main.go @@ -232,9 +232,10 @@ func main() { } if err = (&controllers.CollectorReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Collector"), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Collector"), + Scheme: mgr.GetScheme(), + Namespaced: namespacedController, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Collector") os.Exit(1) @@ -266,9 +267,10 @@ func main() { } if err = (&controllers.FluentdReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Fluentd"), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Fluentd"), + Scheme: mgr.GetScheme(), + Namespaced: namespacedController, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Fluentd") os.Exit(1) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index abcbd7d48..77a5102e3 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -123,6 +123,7 @@ rules: resources: - clusterrolebindings - clusterroles + - roles verbs: - create - get @@ -133,7 +134,6 @@ rules: - rbac.authorization.k8s.io resources: - rolebindings - - roles verbs: - create - delete diff --git a/controllers/collector_controller.go b/controllers/collector_controller.go index cdb5a0bcf..e41fd314a 100644 --- a/controllers/collector_controller.go +++ b/controllers/collector_controller.go @@ -38,8 +38,9 @@ import ( // CollectorReconciler reconciles a FluentBit object type CollectorReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme + Log logr.Logger + Scheme *runtime.Scheme + Namespaced bool } // +kubebuilder:rbac:groups=fluentbit.fluent.io,resources=collectors,verbs=get;list;watch;update @@ -49,6 +50,8 @@ type CollectorReconciler struct { // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;patch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=create;get;list;watch;patch // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=create;get;list;watch;patch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;get;list;watch;patch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;delete;get;list;watch;patch // +kubebuilder:rbac:groups=core,resources=pods,verbs=get // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -95,23 +98,21 @@ func (r *CollectorReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, err } - // Install RBAC resources for the filter plugin kubernetes - cr, sa, crb := operator.MakeRBACObjects( + // Reconcile the RBAC the agent needs, scoped to a namespace or the cluster. + role, sa, binding := operator.MakeRBACObjectsForScope( + r.Namespaced, co.Name, co.Namespace, "collector", co.Spec.RBACRules, co.Spec.ServiceAccountAnnotations, ) - // Deploy Fluent Bit Collector ClusterRole - if _, err := controllerutil.CreateOrPatch(ctx, r.Client, cr, r.mutate(cr, &co)); err != nil { + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, role, r.mutate(role, &co)); err != nil { return ctrl.Result{}, err } - // Deploy Fluent Bit Collector ClusterRoleBinding - if _, err := controllerutil.CreateOrPatch(ctx, r.Client, crb, r.mutate(crb, &co)); err != nil { + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, binding, r.mutate(binding, &co)); err != nil { return ctrl.Result{}, err } - // Deploy Fluent Bit Collector ServiceAccount if _, err := controllerutil.CreateOrPatch(ctx, r.Client, sa, r.mutate(sa, &co)); err != nil { return ctrl.Result{}, err } @@ -175,6 +176,36 @@ func (r *CollectorReconciler) mutate(obj client.Object, co *fluentbitv1alpha2.Co o.Subjects = expected.Subjects return nil } + case *rbacv1.Role: + // The Role is shared across all Collector instances in the namespace, so + // no per-instance controller reference is set on it. + expected, _, _ := operator.MakeScopedRBACObjects(co.Name, + co.Namespace, + "collector", + co.Spec.RBACRules, + co.Spec.ServiceAccountAnnotations, + ) + + return func() error { + o.Rules = expected.Rules + return nil + } + case *rbacv1.RoleBinding: + _, _, expected := operator.MakeScopedRBACObjects(co.Name, + co.Namespace, + "collector", + co.Spec.RBACRules, + co.Spec.ServiceAccountAnnotations, + ) + + return func() error { + o.RoleRef = expected.RoleRef + o.Subjects = expected.Subjects + if err := ctrl.SetControllerReference(co, o, r.Scheme); err != nil { + return err + } + return nil + } case *appsv1.StatefulSet: expected := operator.MakefbStatefulset(*co) @@ -214,7 +245,12 @@ func (r *CollectorReconciler) delete(ctx context.Context, co *fluentbitv1alpha2. if err := r.Delete(ctx, &sa); err != nil && !errors.IsNotFound(err) { return err } - // TODO: clusterrole, clusterrolebinding + + if err := operator.DeletePerInstanceBinding( + ctx, r.Client, r.Namespaced, co.Name, co.Namespace, "collector", + ); err != nil { + return err + } sts := appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/fluent_controller_finalizer.go b/controllers/fluent_controller_finalizer.go index b1b0745a9..1bfc188d2 100644 --- a/controllers/fluent_controller_finalizer.go +++ b/controllers/fluent_controller_finalizer.go @@ -76,7 +76,12 @@ func (r *FluentdReconciler) delete(ctx context.Context, fd *fluentdv1alpha1.Flue if err := r.Delete(ctx, &sa); err != nil && !errors.IsNotFound(err) { return err } - // TODO: clusterrole, clusterrolebinding + + if err := operator.DeletePerInstanceBinding( + ctx, r.Client, r.Namespaced, fd.Name, fd.Namespace, "fluentd", + ); err != nil { + return err + } sts := appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ @@ -157,6 +162,38 @@ func (r *FluentdReconciler) mutate(obj client.Object, fd *fluentdv1alpha1.Fluent o.Subjects = expected.Subjects return nil } + case *rbacv1.Role: + // The Role is shared across all Fluentd instances in the namespace, so + // no per-instance controller reference is set on it. + expected, _, _ := operator.MakeScopedRBACObjects( + fd.Name, + fd.Namespace, + "fluentd", + fd.Spec.RBACRules, + fd.Spec.ServiceAccountAnnotations, + ) + + return func() error { + o.Rules = expected.Rules + return nil + } + case *rbacv1.RoleBinding: + _, _, expected := operator.MakeScopedRBACObjects( + fd.Name, + fd.Namespace, + "fluentd", + fd.Spec.RBACRules, + fd.Spec.ServiceAccountAnnotations, + ) + + return func() error { + o.RoleRef = expected.RoleRef + o.Subjects = expected.Subjects + if err := ctrl.SetControllerReference(fd, o, r.Scheme); err != nil { + return err + } + return nil + } case *appsv1.StatefulSet: expected := operator.MakeStatefulSet(*fd) diff --git a/controllers/fluentbit_controller.go b/controllers/fluentbit_controller.go index bf1d2788b..4989d7080 100644 --- a/controllers/fluentbit_controller.go +++ b/controllers/fluentbit_controller.go @@ -53,7 +53,7 @@ type FluentBitReconciler struct { // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;patch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=create;get;list;watch;patch // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=create;get;list;watch;patch -// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;delete;get;list;watch;patch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;get;list;watch;patch // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;delete;get;list;watch;patch // +kubebuilder:rbac:groups=core,resources=pods,verbs=get @@ -100,23 +100,15 @@ func (r *FluentBitReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, err } - // Install RBAC resources for the filter plugin kubernetes - var role, sa, binding client.Object - if r.Namespaced { - role, sa, binding = operator.MakeScopedRBACObjects( - fb.Name, - fb.Namespace, - fb.Spec.ServiceAccountAnnotations, - ) - } else { - role, sa, binding = operator.MakeRBACObjects( - fb.Name, - fb.Namespace, - "fluent-bit", - fb.Spec.RBACRules, - fb.Spec.ServiceAccountAnnotations, - ) - } + // Reconcile the RBAC the agent needs, scoped to a namespace or the cluster. + role, sa, binding := operator.MakeRBACObjectsForScope( + r.Namespaced, + fb.Name, + fb.Namespace, + "fluent-bit", + fb.Spec.RBACRules, + fb.Spec.ServiceAccountAnnotations, + ) if _, err := controllerutil.CreateOrPatch(ctx, r.Client, role, r.mutate(role, &fb)); err != nil { return ctrl.Result{}, err } @@ -197,13 +189,18 @@ func (r *FluentBitReconciler) mutate(obj client.Object, fb *fluentbitv1alpha2.Fl return nil } case *rbacv1.Role: - expected, _, _ := operator.MakeScopedRBACObjects(fb.Name, fb.Namespace, fb.Spec.ServiceAccountAnnotations) + // The Role is shared across all FluentBit instances in the namespace, so + // no per-instance controller reference is set on it. + expected, _, _ := operator.MakeScopedRBACObjects( + fb.Name, + fb.Namespace, + "fluent-bit", + fb.Spec.RBACRules, + fb.Spec.ServiceAccountAnnotations, + ) return func() error { o.Rules = expected.Rules - if err := ctrl.SetControllerReference(fb, o, r.Scheme); err != nil { - return err - } return nil } case *rbacv1.ClusterRole: @@ -218,7 +215,13 @@ func (r *FluentBitReconciler) mutate(obj client.Object, fb *fluentbitv1alpha2.Fl return nil } case *corev1.ServiceAccount: - _, expected, _ := operator.MakeScopedRBACObjects(fb.Name, fb.Namespace, fb.Spec.ServiceAccountAnnotations) + _, expected, _ := operator.MakeScopedRBACObjects( + fb.Name, + fb.Namespace, + "fluent-bit", + fb.Spec.RBACRules, + fb.Spec.ServiceAccountAnnotations, + ) return func() error { o.Annotations = expected.Annotations @@ -228,7 +231,13 @@ func (r *FluentBitReconciler) mutate(obj client.Object, fb *fluentbitv1alpha2.Fl return nil } case *rbacv1.RoleBinding: - _, _, expected := operator.MakeScopedRBACObjects(fb.Name, fb.Namespace, fb.Spec.ServiceAccountAnnotations) + _, _, expected := operator.MakeScopedRBACObjects( + fb.Name, + fb.Namespace, + "fluent-bit", + fb.Spec.RBACRules, + fb.Spec.ServiceAccountAnnotations, + ) return func() error { o.Subjects = expected.Subjects o.RoleRef = expected.RoleRef @@ -267,29 +276,11 @@ func (r *FluentBitReconciler) delete(ctx context.Context, fb *fluentbitv1alpha2. return err } - if r.Namespaced { - roleName, _, roleBindingName := operator.MakeScopedRBACNames(fb.Name) - role := rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: roleName, - Namespace: fb.Namespace, - }, - } - if err := r.Delete(ctx, &role); err != nil && !errors.IsNotFound(err) { - return err - } - - rolebinding := rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: roleBindingName, - Namespace: fb.Namespace, - }, - } - if err := r.Delete(ctx, &rolebinding); err != nil && !errors.IsNotFound(err) { - return err - } + if err := operator.DeletePerInstanceBinding( + ctx, r.Client, r.Namespaced, fb.Name, fb.Namespace, "fluent-bit", + ); err != nil { + return err } - // TODO: clusterrole, clusterrolebinding ds := appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/fluentd_controller.go b/controllers/fluentd_controller.go index 6c99050b1..0c8e140af 100644 --- a/controllers/fluentd_controller.go +++ b/controllers/fluentd_controller.go @@ -43,8 +43,9 @@ const ( // FluentdReconciler reconciles a Fluentd object type FluentdReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme + Log logr.Logger + Scheme *runtime.Scheme + Namespaced bool } // +kubebuilder:rbac:groups=fluentd.fluent.io,resources=fluentds,verbs=get;list;watch;update @@ -54,6 +55,8 @@ type FluentdReconciler struct { // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get // +kubebuilder:rbac:groups=core,resources=serviceaccounts;services,verbs=get;list;watch;create;patch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=create;get;list;watch;patch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;get;list;watch;patch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;delete;get;list;watch;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -99,23 +102,21 @@ func (r *FluentdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } - // Install RBAC resources for the filter plugin kubernetes - cr, sa, crb := operator.MakeRBACObjects( + // Reconcile the RBAC the agent needs, scoped to a namespace or the cluster. + role, sa, binding := operator.MakeRBACObjectsForScope( + r.Namespaced, fd.Name, fd.Namespace, fluentdLowercase, fd.Spec.RBACRules, fd.Spec.ServiceAccountAnnotations, ) - // Deploy Fluentd ClusterRole - if _, err := controllerutil.CreateOrPatch(ctx, r.Client, cr, r.mutate(cr, &fd)); err != nil { + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, role, r.mutate(role, &fd)); err != nil { return ctrl.Result{}, err } - // Deploy Fluentd ClusterRoleBinding - if _, err := controllerutil.CreateOrPatch(ctx, r.Client, crb, r.mutate(crb, &fd)); err != nil { + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, binding, r.mutate(binding, &fd)); err != nil { return ctrl.Result{}, err } - // Deploy Fluentd ServiceAccount if _, err := controllerutil.CreateOrPatch(ctx, r.Client, sa, r.mutate(sa, &fd)); err != nil { return ctrl.Result{}, err } diff --git a/manifests/setup/setup.yaml b/manifests/setup/setup.yaml index fa044f4a2..ac7194c9d 100644 --- a/manifests/setup/setup.yaml +++ b/manifests/setup/setup.yaml @@ -42828,6 +42828,7 @@ rules: resources: - clusterrolebindings - clusterroles + - roles verbs: - create - get @@ -42838,7 +42839,6 @@ rules: - rbac.authorization.k8s.io resources: - rolebindings - - roles verbs: - create - delete diff --git a/pkg/operator/rbac.go b/pkg/operator/rbac.go index c2e1536ea..22e8e87aa 100644 --- a/pkg/operator/rbac.go +++ b/pkg/operator/rbac.go @@ -1,11 +1,14 @@ package operator import ( + "context" "fmt" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) func MakeRBACObjects( @@ -62,10 +65,12 @@ func MakeRBACObjects( func MakeScopedRBACObjects( name, - namespace string, + namespace, + component string, + additionalRules []rbacv1.PolicyRule, saAnnotations map[string]string, ) (*rbacv1.Role, *corev1.ServiceAccount, *rbacv1.RoleBinding) { - rName, saName, rbName := MakeScopedRBACNames(name) + rName, saName, rbName := MakeScopedRBACNames(name, component) r := rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: rName, @@ -80,6 +85,8 @@ func MakeScopedRBACObjects( }, } + r.Rules = append(r.Rules, additionalRules...) + sa := corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: saName, @@ -116,8 +123,49 @@ func MakeRBACNames(name, component string) (string, string, string) { return cr, name, crb } -func MakeScopedRBACNames(name string) (string, string, string) { - r := "fluent:fluent-operator" - rb := fmt.Sprintf("fluent-operator-fluent-bit-%s", name) - return r, name, rb +// MakeScopedRBACNames returns the namespaced Role/ServiceAccount/RoleBinding +// names. They follow the same convention as the cluster-scoped names. +func MakeScopedRBACNames(name, component string) (string, string, string) { + return MakeRBACNames(name, component) +} + +// MakeRBACObjectsForScope builds the RBAC objects an agent needs, returning the +// namespaced (Role/RoleBinding) variants when namespaced is true and the +// cluster-scoped (ClusterRole/ClusterRoleBinding) variants otherwise. +func MakeRBACObjectsForScope( + namespaced bool, + name, namespace, component string, + additionalRules []rbacv1.PolicyRule, + saAnnotations map[string]string, +) (role, sa, binding client.Object) { + if namespaced { + r, s, rb := MakeScopedRBACObjects(name, namespace, component, additionalRules, saAnnotations) + return r, s, rb + } + cr, s, crb := MakeRBACObjects(name, namespace, component, additionalRules, saAnnotations) + return cr, s, crb +} + +// DeletePerInstanceBinding removes the per-instance RoleBinding created for an +// agent when running in namespaced mode. In cluster-scoped mode nothing is +// deleted: the per-instance ClusterRoleBinding references a shared ClusterRole, +// and the operator is intentionally not granted delete on cluster-scoped RBAC +// (keeping its footprint minimal), so the binding is left in place. +func DeletePerInstanceBinding( + ctx context.Context, + c client.Client, + namespaced bool, + name, namespace, component string, +) error { + if !namespaced { + return nil + } + _, _, rbName := MakeScopedRBACNames(name, component) + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: rbName, Namespace: namespace}, + } + if err := c.Delete(ctx, rb); err != nil && !apierrors.IsNotFound(err) { + return err + } + return nil }