@@ -11,11 +11,13 @@ package redpanda_test
1111
1212import (
1313 "context"
14+ _ "embed"
1415 "encoding/json"
1516 "fmt"
1617 "math/rand"
1718 "sort"
1819 "strings"
20+ "slices"
1921 "testing"
2022 "time"
2123
@@ -36,6 +38,7 @@ import (
3638 "github.com/stretchr/testify/suite"
3739 appsv1 "k8s.io/api/apps/v1"
3840 corev1 "k8s.io/api/core/v1"
41+ rbacv1 "k8s.io/api/rbac/v1"
3942 apierrors "k8s.io/apimachinery/pkg/api/errors"
4043 apimeta "k8s.io/apimachinery/pkg/api/meta"
4144 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -47,6 +50,12 @@ import (
4750 "sigs.k8s.io/controller-runtime/pkg/client"
4851)
4952
53+ // operatorRBAC is the ClusterRole and Role generated via controller-gen and
54+ // goembeded so it can be used for tests.
55+ //
56+ //go:embed role.yaml
57+ var operatorRBAC []byte
58+
5059// NB: This test setup is largely incompatible with webhooks. Though we might
5160// be able to figure something freaky out.
5261func TestRedpandaController (t * testing.T ) {
@@ -254,7 +263,7 @@ func (s *RedpandaControllerSuite) TestManagedDecommission() {
254263 "operator.redpanda.com/managed-decommission" : "2999-12-31T00:00:00Z" ,
255264 }
256265
257- s .applyAndWaitFor (rp , func (o client.Object ) bool {
266+ s .applyAndWaitFor (func (o client.Object ) bool {
258267 rp := o .(* redpandav1alpha2.Redpanda )
259268
260269 for _ , cond := range rp .Status .Conditions {
@@ -263,7 +272,7 @@ func (s *RedpandaControllerSuite) TestManagedDecommission() {
263272 }
264273 }
265274 return false
266- })
275+ }, rp )
267276
268277 s .waitFor (rp , func (o client.Object ) bool {
269278 rp := o .(* redpandav1alpha2.Redpanda )
@@ -318,15 +327,15 @@ func (s *RedpandaControllerSuite) TestClusterSettings() {
318327
319328 rp .Spec .ClusterSpec .Config .Cluster = & runtime.RawExtension {Raw : asJson }
320329 s .applyAndWait (rp )
321- s .applyAndWaitFor (rp , func (o client.Object ) bool {
330+ s .applyAndWaitFor (func (o client.Object ) bool {
322331 rp := o .(* redpandav1alpha2.Redpanda )
323332 for _ , cond := range rp .Status .Conditions {
324333 if cond .Type == redpandav1alpha2 .ClusterConfigSynced {
325334 return cond .ObservedGeneration == rp .Generation && cond .Status == metav1 .ConditionTrue
326335 }
327336 }
328337 return false
329- })
338+ }, rp )
330339 }
331340 s .applyAndWait (& corev1.Secret {
332341 ObjectMeta : metav1.ObjectMeta {
@@ -426,14 +435,14 @@ func (s *RedpandaControllerSuite) SetupSuite() {
426435 // rest config given to the manager.
427436 s .ctx = context .Background ()
428437 s .env = testenv .New (t , testenv.Options {
429- Scheme : controller .UnifiedScheme ,
438+ Scheme : controller .V2Scheme ,
430439 CRDs : crds .All (),
431440 Logger : testr .New (t ),
432441 })
433442
434443 s .client = s .env .Client ()
435444
436- s .env .SetupManager (func (mgr ctrl.Manager ) error {
445+ s .env .SetupManager (s . setupRBAC (), func (mgr ctrl.Manager ) error {
437446 controllers := flux .NewFluxControllers (mgr , fluxclient.Options {}, fluxclient.KubeConfigOptions {})
438447 for _ , controller := range controllers {
439448 if err := controller .SetupWithManager (s .ctx , mgr ); err != nil {
@@ -462,18 +471,81 @@ func (s *RedpandaControllerSuite) SetupSuite() {
462471 })
463472}
464473
465- func (s * RedpandaControllerSuite ) minimalRP (useFlux bool ) * redpandav1alpha2.Redpanda {
474+ func (s * RedpandaControllerSuite ) setupRBAC () string {
475+ roles , err := kube .DecodeYAML (operatorRBAC , s .client .Scheme ())
476+ s .Require ().NoError (err )
477+
478+ role := roles [1 ].(* rbacv1.Role )
479+ clusterRole := roles [0 ].(* rbacv1.ClusterRole )
480+
481+ // Inject additional permissions required for running in testenv.
482+ role .Rules = append (role .Rules , rbacv1.PolicyRule {
483+ APIGroups : []string {"" },
484+ Resources : []string {"pods/portforward" },
485+ Verbs : []string {"*" },
486+ })
487+
488+ name := "testenv-" + s .randString (6 )
489+
490+ role .Name = name
491+ role .Namespace = s .env .Namespace ()
492+ clusterRole .Name = name
493+ clusterRole .Namespace = s .env .Namespace ()
494+
495+ s .applyAndWait (roles ... )
496+ s .applyAndWait (
497+ & corev1.ServiceAccount {
498+ ObjectMeta : metav1.ObjectMeta {
499+ Name : name ,
500+ },
501+ },
502+ & rbacv1.RoleBinding {
503+ ObjectMeta : metav1.ObjectMeta {
504+ Name : name ,
505+ },
506+ Subjects : []rbacv1.Subject {
507+ {Kind : "ServiceAccount" , Namespace : s .env .Namespace (), Name : name },
508+ },
509+ RoleRef : rbacv1.RoleRef {
510+ APIGroup : "rbac.authorization.k8s.io" ,
511+ Kind : "Role" ,
512+ Name : role .Name ,
513+ },
514+ },
515+ & rbacv1.ClusterRoleBinding {
516+ ObjectMeta : metav1.ObjectMeta {
517+ Name : name ,
518+ },
519+ Subjects : []rbacv1.Subject {
520+ {Kind : "ServiceAccount" , Namespace : s .env .Namespace (), Name : name },
521+ },
522+ RoleRef : rbacv1.RoleRef {
523+ APIGroup : "rbac.authorization.k8s.io" ,
524+ Kind : "ClusterRole" ,
525+ Name : clusterRole .Name ,
526+ },
527+ },
528+ )
529+
530+ return name
531+ }
532+
533+ func (s * RedpandaControllerSuite ) randString (length int ) string {
466534 const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
467535
468- name := "rp- "
536+ name := ""
469537 for i := 0 ; i < 6 ; i ++ {
470538 //nolint:gosec // not meant to be a secure random string.
471539 name += string (alphabet [rand .Intn (len (alphabet ))])
472540 }
473541
542+ return name
543+ }
544+
545+ func (s * RedpandaControllerSuite ) minimalRP (useFlux bool ) * redpandav1alpha2.Redpanda {
474546 return & redpandav1alpha2.Redpanda {
475547 ObjectMeta : metav1.ObjectMeta {
476- Name : name , // GenerateName doesn't play nice with SSA.
548+ Name : "rp-" + s . randString ( 6 ) , // GenerateName doesn't play nice with SSA.
477549 },
478550 Spec : redpandav1alpha2.RedpandaSpec {
479551 ChartRef : redpandav1alpha2.ChartRef {
@@ -541,45 +613,50 @@ func (s *RedpandaControllerSuite) deleteAndWait(obj client.Object) {
541613 }))
542614}
543615
544- func (s * RedpandaControllerSuite ) applyAndWait (obj client.Object ) {
545- s .applyAndWaitFor (obj , func (o client.Object ) bool {
616+ func (s * RedpandaControllerSuite ) applyAndWait (objs ... client.Object ) {
617+ s .applyAndWaitFor (func (obj client.Object ) bool {
546618 switch obj := obj .(type ) {
547619 case * redpandav1alpha2.Redpanda :
548620 ready := apimeta .IsStatusConditionTrue (obj .Status .Conditions , "Ready" )
549621 upToDate := obj .Generation != 0 && obj .Generation == obj .Status .ObservedGeneration
550622 return upToDate && ready
551623
552- case * corev1.Secret , * corev1.ConfigMap :
624+ case * corev1.Secret , * corev1.ConfigMap , * corev1.ServiceAccount ,
625+ * rbacv1.ClusterRole , * rbacv1.Role , * rbacv1.RoleBinding , * rbacv1.ClusterRoleBinding :
553626 return true
554627
555628 default :
556629 s .T ().Fatalf ("unhandled object %T in applyAndWait" , obj )
557630 panic ("unreachable" )
558631 }
559- })
632+ }, objs ... )
560633}
561634
562- func (s * RedpandaControllerSuite ) applyAndWaitFor (obj client.Object , cond func (client.Object ) bool ) {
563- gvk , err := s .client .GroupVersionKindFor (obj )
564- s .NoError (err )
635+ func (s * RedpandaControllerSuite ) applyAndWaitFor (cond func (client.Object ) bool , objs ... client.Object ) {
636+ for _ , obj := range objs {
637+ gvk , err := s .client .GroupVersionKindFor (obj )
638+ s .NoError (err )
565639
566- obj .SetManagedFields (nil )
567- obj .GetObjectKind ().SetGroupVersionKind (gvk )
640+ obj .SetManagedFields (nil )
641+ obj .GetObjectKind ().SetGroupVersionKind (gvk )
568642
569- s .Require ().NoError (s .client .Patch (s .ctx , obj , client .Apply , client .ForceOwnership , client .FieldOwner ("tests" )))
643+ s .Require ().NoError (s .client .Patch (s .ctx , obj , client .Apply , client .ForceOwnership , client .FieldOwner ("tests" )))
644+ }
570645
571- s .NoError (wait .PollUntilContextTimeout (s .ctx , 5 * time .Second , 5 * time .Minute , false , func (ctx context.Context ) (done bool , err error ) {
572- if err := s .client .Get (ctx , client .ObjectKeyFromObject (obj ), obj ); err != nil {
573- return false , err
574- }
646+ for _ , obj := range objs {
647+ s .NoError (wait .PollUntilContextTimeout (s .ctx , 5 * time .Second , 5 * time .Minute , false , func (ctx context.Context ) (done bool , err error ) {
648+ if err := s .client .Get (ctx , client .ObjectKeyFromObject (obj ), obj ); err != nil {
649+ return false , err
650+ }
575651
576- if cond (obj ) {
577- return true , nil
578- }
652+ if cond (obj ) {
653+ return true , nil
654+ }
579655
580- s .T ().Logf ("waiting for %T %q to be ready" , obj , obj .GetName ())
581- return false , nil
582- }))
656+ s .T ().Logf ("waiting for %T %q to be ready" , obj , obj .GetName ())
657+ return false , nil
658+ }))
659+ }
583660}
584661
585662func (s * RedpandaControllerSuite ) waitFor (obj client.Object , cond func (client.Object ) bool ) {
@@ -686,3 +763,59 @@ func TestPostInstallUpgradeJobIndex(t *testing.T) {
686763 // `clusterConfigfor` utilizes.
687764 require .Equal (t , "bootstrap-yaml-envsubst" , job .Spec .Template .Spec .InitContainers [0 ].Name )
688765}
766+
767+ // TestControllerRBAC asserts that the declared Roles and ClusterRoles of the
768+ // RedpandaReconciler line up with all the resource types it needs to manage.
769+ func TestControllerRBAC (t * testing.T ) {
770+ scheme := controller .V2Scheme
771+
772+ expectedVerbs := []string {"create" , "delete" , "get" , "list" , "patch" , "update" , "watch" }
773+
774+ roles , err := kube .DecodeYAML (operatorRBAC , scheme )
775+ require .NoError (t , err )
776+
777+ role := roles [1 ].(* rbacv1.Role )
778+ clusterRole := roles [0 ].(* rbacv1.ClusterRole )
779+
780+ for _ , typ := range redpandachart .Types () {
781+ gkvs , _ , err := scheme .ObjectKinds (typ )
782+ require .NoError (t , err )
783+
784+ require .Len (t , gkvs , 1 )
785+ gvk := gkvs [0 ]
786+
787+ rules := role .Rules
788+ if ! isNamespaced (typ ) {
789+ rules = clusterRole .Rules
790+ }
791+
792+ group := gvk .Group
793+ kind := pluralize (gvk .Kind )
794+
795+ idx := slices .IndexFunc (rules , func (rule rbacv1.PolicyRule ) bool {
796+ return slices .Contains (rule .APIGroups , group ) && slices .Contains (rule .Resources , kind )
797+ })
798+
799+ require .NotEqual (t , - 1 , idx , "missing rules for %s %s" , gvk .Group , kind )
800+ require .EqualValues (t , expectedVerbs , rules [idx ].Verbs , "incorrect verbs for %s %s" , gvk .Group , kind )
801+ }
802+ }
803+
804+ func isNamespaced (obj client.Object ) bool {
805+ switch obj .(type ) {
806+ case * corev1.Namespace , * rbacv1.ClusterRole , * rbacv1.ClusterRoleBinding :
807+ return false
808+ default :
809+ return true
810+ }
811+ }
812+
813+ func pluralize (kind string ) string {
814+ switch kind [len (kind )- 1 ] {
815+ case 's' :
816+ return strings .ToLower (kind ) + "es"
817+
818+ default :
819+ return strings .ToLower (kind ) + "s"
820+ }
821+ }
0 commit comments