[RoleTemplate Aggregation] Use RoleBindings for Management Plane Rules (#52650)

* Change from using clusterrolebinding to rolebinding

* Remove references to clusterrolebindings

* Use CreateOrUpdate helper function instead of just create

* Fix tests
This commit is contained in:
Jonathan Crowther
2025-11-12 11:15:22 -05:00
committed by GitHub
parent 265cbbdc1b
commit 67b0d930cc
9 changed files with 213 additions and 181 deletions

View File

@@ -23,26 +23,26 @@ const (
const (
// Statuses
clusterRoleTemplateBindingDelete = "ClusterRoleTemplateBindingDelete"
removeClusterRoleBindings = "RemoveClusterRoleBindings"
removeRoleBindings = "RemoveRoleBindings"
reconcileSubject = "ReconcileSubject"
reconcileMembershipBindings = "ReconcileMembershipBindings"
reconcileBindings = "ReconcileBindings"
// Reasons
clusterRoleBindingDeleted = "ClusterRoleBindingDeleted"
roleBindingDeleted = "roleBindingDeleted"
bindingsExists = "BindingsExists"
membershipBindingExists = "MembershipBindingExists"
subjectExists = "SubjectExists"
crtbHasNoSubject = "CRTBHasNoSubject"
clusterMembershipBindingDeleted = "ClusterMembershipBindingDeleted"
authv2ProvisioningBindingDeleted = "AuthV2ProvisioningBindingDeleted"
failedToCreateClusterRoleBinding = "FailedToCreateClusterRoleBinding"
failedToCreateRoleBinding = "FailedToCreateRoleBinding"
failedToCreateOrUpdateMembershipBinding = "FailedToCreateOrUpdateMembershipBinding"
failedToCreateUser = "FailedToCreateUser"
failedToDeleteClusterRoleBinding = "FailedToDeleteClusterRoleBinding"
failedToGetDesiredClusterRoleBindings = "FailedToGetDesiredClusterRoleBindings"
failedToDeleteRoleBinding = "FailedToDeleteRoleBinding"
failedToGetDesiredRoleBindings = "FailedToGetDesiredRoleBindings"
failedToGetRoleTemplate = "FailedToGetRoleTemplate"
failedToGetUser = "FailedToGetUser"
failedToListExistingClusterRoleBindings = "FailedToGetExistingClusterRoleBindings"
failedToListExistingRoleBindings = "FailedToGetExistingRoleBindings"
)
// createOrUpdateClusterMembershipBinding ensures that the user specified by a CRTB or PRTB has membership to the cluster referenced by the CRTB or PRTB.
@@ -174,7 +174,7 @@ func createOrUpdateProjectMembershipBinding(prtb *v3.ProjectRoleTemplateBinding,
return err
}
if !rbac.AreRoleBindingContentsSame(wantedRB, existingRB) {
if ok, _ := rbac.AreRoleBindingContentsSame(wantedRB, existingRB); !ok {
if err := rbController.Delete(wantedRB.Namespace, wantedRB.Name, &metav1.DeleteOptions{}); err != nil {
return err
}

View File

@@ -132,40 +132,40 @@ func (c *crtbHandler) reconcileMembershipBindings(crtb *v3.ClusterRoleTemplateBi
}
// reconcileBindings Ensures that all bindings required to provide access to the CRTB either exist or get created.
// It deletes any of the existing CRBs that are not needed.
// It deletes any existing role bindings with the CRTB owner label that are not needed.
func (c *crtbHandler) reconcileBindings(crtb *v3.ClusterRoleTemplateBinding, localConditions *[]metav1.Condition) error {
condition := metav1.Condition{Type: reconcileBindings}
desiredCRBs, err := c.getDesiredClusterRoleBindings(crtb)
desiredRBs, err := c.getDesiredRoleBindings(crtb)
if err != nil {
c.s.AddCondition(localConditions, condition, failedToGetDesiredClusterRoleBindings, err)
c.s.AddCondition(localConditions, condition, failedToGetDesiredRoleBindings, err)
return err
}
currentCRBs, err := c.crbController.List(metav1.ListOptions{LabelSelector: rbac.GetCRTBOwnerLabel(crtb.Name)})
currentRBs, err := c.rbController.List(crtb.Namespace, metav1.ListOptions{LabelSelector: rbac.GetCRTBOwnerLabel(crtb.Name)})
if err != nil {
c.s.AddCondition(localConditions, condition, failedToListExistingClusterRoleBindings, err)
c.s.AddCondition(localConditions, condition, failedToListExistingRoleBindings, err)
return err
}
for _, currentCRB := range currentCRBs.Items {
if crb, ok := desiredCRBs[currentCRB.Name]; ok {
if rbac.AreClusterRoleBindingContentsSame(&currentCRB, crb) {
// If the cluster role binding already exists with the right contents, we can skip creating it.
delete(desiredCRBs, crb.Name)
for _, currentRB := range currentRBs.Items {
if rb, ok := desiredRBs[currentRB.Name]; ok {
if ok, _ := rbac.AreRoleBindingContentsSame(&currentRB, rb); ok {
// If the role binding already exists with the right contents, we can skip creating it.
delete(desiredRBs, rb.Name)
continue
}
}
// If the CRB is not a member of the desired CRBs or has different contents, delete it.
if err := c.crbController.Delete(currentCRB.Name, &metav1.DeleteOptions{}); err != nil {
c.s.AddCondition(localConditions, condition, failedToDeleteClusterRoleBinding, err)
// If the role binding is not a member of the desired role bindings or has different contents, delete it.
if err := c.rbController.Delete(currentRB.Namespace, currentRB.Name, &metav1.DeleteOptions{}); err != nil {
c.s.AddCondition(localConditions, condition, failedToDeleteRoleBinding, err)
return err
}
}
// For any CRBs that don't exist, create them
for _, crb := range desiredCRBs {
if _, err := c.crbController.Create(crb); err != nil {
c.s.AddCondition(localConditions, condition, failedToCreateClusterRoleBinding, err)
// For any role bindings that don't exist, create them
for _, rb := range desiredRBs {
if err := rbac.CreateOrUpdateNamespacedResource(rb, c.rbController, rbac.AreRoleBindingContentsSame); err != nil {
c.s.AddCondition(localConditions, condition, failedToCreateRoleBinding, err)
return err
}
}
@@ -174,18 +174,18 @@ func (c *crtbHandler) reconcileBindings(crtb *v3.ClusterRoleTemplateBinding, loc
return nil
}
// getDesiredClusterRoleBindings checks for project and cluster management roles, and if they exist, builds and returns the needed ClusterRoleBindings
func (c *crtbHandler) getDesiredClusterRoleBindings(crtb *v3.ClusterRoleTemplateBinding) (map[string]*rbacv1.ClusterRoleBinding, error) {
desiredCRBs := map[string]*rbacv1.ClusterRoleBinding{}
// getDesiredRoleBindings checks for project and cluster management roles, and if they exist, builds and returns the needed RoleBindings
func (c *crtbHandler) getDesiredRoleBindings(crtb *v3.ClusterRoleTemplateBinding) (map[string]*rbacv1.RoleBinding, error) {
desiredRBs := map[string]*rbacv1.RoleBinding{}
// Check if there is a project management role to bind to
projectManagementRoleName := rbac.ProjectManagementPlaneClusterRoleNameFor(crtb.RoleTemplateName)
cr, err := c.crController.Get(rbac.AggregatedClusterRoleNameFor(projectManagementRoleName), metav1.GetOptions{})
if err == nil && cr != nil {
crb, err := rbac.BuildAggregatingClusterRoleBindingFromRTB(crtb, projectManagementRoleName)
rb, err := rbac.BuildAggregatingRoleBindingFromRTB(crtb, projectManagementRoleName)
if err != nil {
return nil, err
}
desiredCRBs[crb.Name] = crb
desiredRBs[rb.Name] = rb
} else if !apierrors.IsNotFound(err) {
return nil, err
}
@@ -194,16 +194,16 @@ func (c *crtbHandler) getDesiredClusterRoleBindings(crtb *v3.ClusterRoleTemplate
clusterManagementRoleName := rbac.ClusterManagementPlaneClusterRoleNameFor(crtb.RoleTemplateName)
cr, err = c.crController.Get(rbac.AggregatedClusterRoleNameFor(clusterManagementRoleName), metav1.GetOptions{})
if err == nil && cr != nil {
crb, err := rbac.BuildAggregatingClusterRoleBindingFromRTB(crtb, clusterManagementRoleName)
rb, err := rbac.BuildAggregatingRoleBindingFromRTB(crtb, clusterManagementRoleName)
if err != nil {
return nil, err
}
desiredCRBs[crb.Name] = crb
desiredRBs[rb.Name] = rb
} else if !apierrors.IsNotFound(err) {
return nil, err
}
return desiredCRBs, nil
return desiredRBs, nil
}
// OnRemove deletes Cluster Role Bindings that are owned by the CRTB and the membership binding if no other CRTBs give membership access.
@@ -220,28 +220,28 @@ func (c *crtbHandler) OnRemove(_ string, crtb *v3.ClusterRoleTemplateBinding) (*
err = removeAuthV2Permissions(crtb, c.rbController)
c.s.AddCondition(&crtb.Status.LocalConditions, condition, authv2ProvisioningBindingDeleted, err)
return crtb, errors.Join(err, c.removeClusterRoleBindings(crtb))
return crtb, errors.Join(err, c.removeRoleBindings(crtb))
}
// removeClusterRoleBindings removes all bindings owned by the CRTB
func (c *crtbHandler) removeClusterRoleBindings(crtb *v3.ClusterRoleTemplateBinding) error {
condition := metav1.Condition{Type: removeClusterRoleBindings}
currentCRBs, err := c.crbController.List(metav1.ListOptions{LabelSelector: rbac.GetCRTBOwnerLabel(crtb.Name)})
func (c *crtbHandler) removeRoleBindings(crtb *v3.ClusterRoleTemplateBinding) error {
condition := metav1.Condition{Type: removeRoleBindings}
currentRBs, err := c.rbController.List(crtb.Namespace, metav1.ListOptions{LabelSelector: rbac.GetCRTBOwnerLabel(crtb.Name)})
if err != nil {
c.s.AddCondition(&crtb.Status.LocalConditions, condition, failedToListExistingClusterRoleBindings, err)
c.s.AddCondition(&crtb.Status.LocalConditions, condition, failedToListExistingRoleBindings, err)
return err
}
var returnErr error
for _, crb := range currentCRBs.Items {
err = rbac.DeleteResource(crb.Name, c.crbController)
for _, rb := range currentRBs.Items {
err = rbac.DeleteNamespacedResource(crtb.Namespace, rb.Name, c.rbController)
if err != nil {
c.s.AddCondition(&crtb.Status.LocalConditions, condition, failedToDeleteClusterRoleBinding, err)
c.s.AddCondition(&crtb.Status.LocalConditions, condition, failedToDeleteRoleBinding, err)
returnErr = errors.Join(returnErr, err)
}
}
c.s.AddCondition(&crtb.Status.LocalConditions, condition, clusterRoleBindingDeleted, returnErr)
c.s.AddCondition(&crtb.Status.LocalConditions, condition, roleBindingDeleted, returnErr)
return returnErr
}

View File

@@ -299,12 +299,12 @@ const (
clusterMGMT = "test-rt-cluster-mgmt-aggregator"
)
func Test_crtbHandler_getDesiredClusterRoleBindings(t *testing.T) {
func Test_crtbHandler_getDesiredRoleBindings(t *testing.T) {
tests := []struct {
name string
crtb *v3.ClusterRoleTemplateBinding
setupCRBController func(*fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList])
want map[string]*rbacv1.ClusterRoleBinding
want map[string]*rbacv1.RoleBinding
wantErr bool
}{
{
@@ -331,7 +331,7 @@ func Test_crtbHandler_getDesiredClusterRoleBindings(t *testing.T) {
m.EXPECT().Get(projectMGMT, metav1.GetOptions{}).Return(nil, errNotFound)
m.EXPECT().Get(clusterMGMT, metav1.GetOptions{}).Return(nil, errNotFound)
},
want: map[string]*rbacv1.ClusterRoleBinding{},
want: map[string]*rbacv1.RoleBinding{},
},
{
name: "found project management plane role",
@@ -340,18 +340,19 @@ func Test_crtbHandler_getDesiredClusterRoleBindings(t *testing.T) {
m.EXPECT().Get(projectMGMT, metav1.GetOptions{}).Return(&rbacv1.ClusterRole{}, nil)
m.EXPECT().Get(clusterMGMT, metav1.GetOptions{}).Return(nil, errNotFound)
},
want: map[string]*rbacv1.ClusterRoleBinding{
"crb-5x2rfzlbvz": {
want: map[string]*rbacv1.RoleBinding{
"rb-visjzlqzqw": {
ObjectMeta: metav1.ObjectMeta{
Name: "crb-5x2rfzlbvz",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
Name: "rb-visjzlqzqw",
Namespace: "test-namespace",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: projectMGMT,
},
Subjects: defaultCRB.Subjects,
Subjects: defaultRB.Subjects,
},
},
},
@@ -362,18 +363,19 @@ func Test_crtbHandler_getDesiredClusterRoleBindings(t *testing.T) {
m.EXPECT().Get(projectMGMT, metav1.GetOptions{}).Return(nil, errNotFound)
m.EXPECT().Get(clusterMGMT, metav1.GetOptions{}).Return(&rbacv1.ClusterRole{}, nil)
},
want: map[string]*rbacv1.ClusterRoleBinding{
"crb-meemnnklov": {
want: map[string]*rbacv1.RoleBinding{
"rb-lhchhtbxqn": {
ObjectMeta: metav1.ObjectMeta{
Name: "crb-meemnnklov",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
Name: "rb-lhchhtbxqn",
Namespace: "test-namespace",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: clusterMGMT,
},
Subjects: defaultCRB.Subjects,
Subjects: defaultRB.Subjects,
},
},
},
@@ -384,30 +386,32 @@ func Test_crtbHandler_getDesiredClusterRoleBindings(t *testing.T) {
m.EXPECT().Get(projectMGMT, metav1.GetOptions{}).Return(&rbacv1.ClusterRole{}, nil)
m.EXPECT().Get(clusterMGMT, metav1.GetOptions{}).Return(&rbacv1.ClusterRole{}, nil)
},
want: map[string]*rbacv1.ClusterRoleBinding{
"crb-meemnnklov": {
want: map[string]*rbacv1.RoleBinding{
"rb-lhchhtbxqn": {
ObjectMeta: metav1.ObjectMeta{
Name: "crb-meemnnklov",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
Name: "rb-lhchhtbxqn",
Namespace: "test-namespace",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: clusterMGMT,
},
Subjects: defaultCRB.Subjects,
Subjects: defaultRB.Subjects,
},
"crb-5x2rfzlbvz": {
"rb-visjzlqzqw": {
ObjectMeta: metav1.ObjectMeta{
Name: "crb-5x2rfzlbvz",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
Name: "rb-visjzlqzqw",
Namespace: "test-namespace",
Labels: map[string]string{"authz.cluster.cattle.io/crtb-owner-test-crtb": "true"},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: projectMGMT,
},
Subjects: defaultCRB.Subjects,
Subjects: defaultRB.Subjects,
},
},
},
@@ -423,13 +427,13 @@ func Test_crtbHandler_getDesiredClusterRoleBindings(t *testing.T) {
s: status.NewStatus(),
crController: crController,
}
got, err := c.getDesiredClusterRoleBindings(tt.crtb)
got, err := c.getDesiredRoleBindings(tt.crtb)
if (err != nil) != tt.wantErr {
t.Errorf("crtbHandler.getDesiredClusterRoleBindings() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("crtbHandler.getDesiredRoleBindings() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("crtbHandler.getDesiredClusterRoleBindings() = %v, want %v", got, tt.want)
t.Errorf("crtbHandler.getDesiredRoleBindings() = %v, want %v", got, tt.want)
}
})
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/rancher/rancher/pkg/types/config"
"github.com/rancher/rancher/pkg/user"
crbacv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -67,13 +66,13 @@ func (p *prtbHandler) OnRemove(_ string, prtb *v3.ProjectRoleTemplateBinding) (*
deleteProjectMembershipBinding(prtb, p.rbController),
removeAuthV2Permissions(prtb, p.rbController))
currentCRBs, err := p.crbController.List(metav1.ListOptions{LabelSelector: rbac.GetPRTBOwnerLabel(prtb.Name)})
currentRBs, err := p.rbController.List(prtb.Namespace, metav1.ListOptions{LabelSelector: rbac.GetPRTBOwnerLabel(prtb.Name)})
if err != nil {
return nil, errors.Join(returnErr, err)
}
for _, crb := range currentCRBs.Items {
returnErr = errors.Join(returnErr, rbac.DeleteResource(crb.Name, p.crbController))
for _, rb := range currentRBs.Items {
returnErr = errors.Join(returnErr, rbac.DeleteNamespacedResource(rb.Namespace, rb.Name, p.rbController))
}
return prtb, returnErr
@@ -126,10 +125,8 @@ func (p *prtbHandler) reconcileMembershipBindings(prtb *v3.ProjectRoleTemplateBi
createOrUpdateProjectMembershipBinding(prtb, rt, p.rbController))
}
// reconcileBindings ensures the right CRB exists for the project management plane role. It deletes any additional unwanted CRBs.
// reconcileBindings ensures the right Role Binding exists for the project management plane role. It deletes any additional unwanted Role Bindings.
func (p *prtbHandler) reconcileBindings(prtb *v3.ProjectRoleTemplateBinding) error {
var crb *rbacv1.ClusterRoleBinding
projectManagementRoleName := rbac.ProjectManagementPlaneClusterRoleNameFor(prtb.RoleTemplateName)
// If there is no project management plane role, no need to create a binding for it
@@ -141,29 +138,29 @@ func (p *prtbHandler) reconcileBindings(prtb *v3.ProjectRoleTemplateBinding) err
return err
}
crb, err = rbac.BuildAggregatingClusterRoleBindingFromRTB(prtb, projectManagementRoleName)
rb, err := rbac.BuildAggregatingRoleBindingFromRTB(prtb, projectManagementRoleName)
if err != nil {
return err
}
currentCRBs, err := p.crbController.List(metav1.ListOptions{LabelSelector: rbac.GetPRTBOwnerLabel(prtb.Name)})
currentRBs, err := p.rbController.List(prtb.Namespace, metav1.ListOptions{LabelSelector: rbac.GetPRTBOwnerLabel(prtb.Name)})
if err != nil {
return err
}
var prtbHasBinding bool
for _, currentCRB := range currentCRBs.Items {
if rbac.AreClusterRoleBindingContentsSame(&currentCRB, crb) {
for _, currentRB := range currentRBs.Items {
if ok, _ := rbac.AreRoleBindingContentsSame(&currentRB, rb); ok {
prtbHasBinding = true
continue
}
if err := rbac.DeleteResource(currentCRB.Name, p.crbController); err != nil {
if err := rbac.DeleteNamespacedResource(currentRB.Namespace, currentRB.Name, p.rbController); err != nil {
return err
}
}
if !prtbHasBinding {
if _, err := p.crbController.Create(crb); err != nil {
if err := rbac.CreateOrUpdateNamespacedResource(rb, p.rbController, rbac.AreRoleBindingContentsSame); err != nil {
return err
}
}

View File

@@ -179,10 +179,11 @@ func Test_reconcileSubject(t *testing.T) {
var (
ownerLabel = "authz.cluster.cattle.io/prtb-owner-test-prtb"
defaultCRB = rbacv1.ClusterRoleBinding{
defaultRB = rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "crb-5x2rfzlbvz",
Labels: map[string]string{"authz.cluster.cattle.io/prtb-owner-test-prtb": "true"},
Name: "rb-visjzlqzqw",
Namespace: "test-namespace",
Labels: map[string]string{"authz.cluster.cattle.io/prtb-owner-test-prtb": "true"},
},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
@@ -198,16 +199,22 @@ var (
},
},
}
badRB = rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-crb",
Namespace: "test-namespace",
},
}
)
func Test_reconcileBindings(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setupCRController func(*fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList])
setupCRBController func(*fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList])
prtb *v3.ProjectRoleTemplateBinding
wantErr bool
name string
setupCRController func(*fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList])
setupRBController func(*fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList])
prtb *v3.ProjectRoleTemplateBinding
wantErr bool
}{
{
name: "error getting cluster role",
@@ -230,7 +237,7 @@ func Test_reconcileBindings(t *testing.T) {
wantErr: false,
},
{
name: "error building clusterrolebinding",
name: "error building rolebinding",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
@@ -240,128 +247,107 @@ func Test_reconcileBindings(t *testing.T) {
wantErr: true,
},
{
name: "error listing clusterrolebindings",
name: "error listing rolebindings",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupCRBController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]) {
m.EXPECT().List(metav1.ListOptions{LabelSelector: ownerLabel}).Return(nil, errDefault)
},
prtb: &v3.ProjectRoleTemplateBinding{
ObjectMeta: metav1.ObjectMeta{Name: "test-prtb"},
UserName: "test-user",
RoleTemplateName: "test-rt",
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(nil, errDefault)
},
prtb: defaultPRTB.DeepCopy(),
wantErr: true,
},
{
name: "error listing clusterrolebindings",
name: "error listing rolebindings",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupCRBController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]) {
m.EXPECT().List(metav1.ListOptions{LabelSelector: ownerLabel}).Return(nil, errDefault)
},
prtb: &v3.ProjectRoleTemplateBinding{
ObjectMeta: metav1.ObjectMeta{Name: "test-prtb"},
UserName: "test-user",
RoleTemplateName: "test-rt",
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(nil, errDefault)
},
prtb: defaultPRTB.DeepCopy(),
wantErr: true,
},
{
name: "error deleting unwanted clusterrolebindings",
name: "error deleting unwanted rolebindings",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupCRBController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]) {
m.EXPECT().List(metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.ClusterRoleBindingList{
Items: []rbacv1.ClusterRoleBinding{
{
ObjectMeta: metav1.ObjectMeta{Name: "bad-crb"},
},
},
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.RoleBindingList{
Items: []rbacv1.RoleBinding{badRB},
}, nil)
m.EXPECT().Delete("bad-crb", &metav1.DeleteOptions{}).Return(errDefault)
},
prtb: &v3.ProjectRoleTemplateBinding{
ObjectMeta: metav1.ObjectMeta{Name: "test-prtb"},
UserName: "test-user",
RoleTemplateName: "test-rt",
m.EXPECT().Delete("test-namespace", "bad-crb", &metav1.DeleteOptions{}).Return(errDefault)
},
prtb: defaultPRTB.DeepCopy(),
wantErr: true,
},
{
name: "CRB already exists",
name: "RB already exists",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupCRBController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]) {
m.EXPECT().List(metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.ClusterRoleBindingList{
Items: []rbacv1.ClusterRoleBinding{defaultCRB},
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.RoleBindingList{
Items: []rbacv1.RoleBinding{defaultRB},
}, nil)
},
prtb: &v3.ProjectRoleTemplateBinding{
ObjectMeta: metav1.ObjectMeta{Name: "test-prtb"},
UserName: "test-user",
RoleTemplateName: "test-rt",
},
prtb: defaultPRTB.DeepCopy(),
},
{
name: "CRB already exists with extra bad CRBs",
name: "RB already exists with extra bad RBs",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupCRBController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]) {
m.EXPECT().List(metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.ClusterRoleBindingList{
Items: []rbacv1.ClusterRoleBinding{
defaultCRB,
{
ObjectMeta: metav1.ObjectMeta{Name: "bad-crb"},
},
},
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.RoleBindingList{
Items: []rbacv1.RoleBinding{defaultRB, badRB},
}, nil)
m.EXPECT().Delete("bad-crb", &metav1.DeleteOptions{}).Return(nil)
},
prtb: &v3.ProjectRoleTemplateBinding{
ObjectMeta: metav1.ObjectMeta{Name: "test-prtb"},
UserName: "test-user",
RoleTemplateName: "test-rt",
m.EXPECT().Delete("test-namespace", "bad-crb", &metav1.DeleteOptions{}).Return(nil)
},
prtb: defaultPRTB.DeepCopy(),
},
{
name: "CRB needs to be created",
name: "RB needs to be created",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupCRBController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]) {
m.EXPECT().List(metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.ClusterRoleBindingList{
Items: []rbacv1.ClusterRoleBinding{},
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.RoleBindingList{
Items: []rbacv1.RoleBinding{},
}, nil)
m.EXPECT().Create(defaultCRB.DeepCopy()).Return(nil, nil)
},
prtb: &v3.ProjectRoleTemplateBinding{
ObjectMeta: metav1.ObjectMeta{Name: "test-prtb"},
UserName: "test-user",
RoleTemplateName: "test-rt",
m.EXPECT().Get("test-namespace", defaultRB.Name, metav1.GetOptions{}).Return(nil, errNotFound)
m.EXPECT().Create(defaultRB.DeepCopy()).Return(nil, nil)
},
prtb: defaultPRTB.DeepCopy(),
},
{
name: "error creating CRB",
name: "error creating RB",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupCRBController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]) {
m.EXPECT().List(metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.ClusterRoleBindingList{
Items: []rbacv1.ClusterRoleBinding{},
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.RoleBindingList{
Items: []rbacv1.RoleBinding{},
}, nil)
m.EXPECT().Create(defaultCRB.DeepCopy()).Return(nil, errDefault)
m.EXPECT().Get("test-namespace", defaultRB.Name, metav1.GetOptions{}).Return(nil, errNotFound)
m.EXPECT().Create(defaultRB.DeepCopy()).Return(nil, errDefault)
},
prtb: &v3.ProjectRoleTemplateBinding{
ObjectMeta: metav1.ObjectMeta{Name: "test-prtb"},
UserName: "test-user",
RoleTemplateName: "test-rt",
prtb: defaultPRTB.DeepCopy(),
wantErr: true,
},
{
name: "error getting RB",
setupCRController: func(m *fake.MockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]) {
m.EXPECT().Get("test-rt-project-mgmt-aggregator", metav1.GetOptions{}).Return(nil, nil)
},
setupRBController: func(m *fake.MockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]) {
m.EXPECT().List("test-namespace", metav1.ListOptions{LabelSelector: ownerLabel}).Return(&rbacv1.RoleBindingList{
Items: []rbacv1.RoleBinding{},
}, nil)
m.EXPECT().Get("test-namespace", defaultRB.Name, metav1.GetOptions{}).Return(nil, errDefault)
},
prtb: defaultPRTB.DeepCopy(),
wantErr: true,
},
}
@@ -369,9 +355,9 @@ func Test_reconcileBindings(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
crbController := fake.NewMockNonNamespacedControllerInterface[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList](ctrl)
if tt.setupCRBController != nil {
tt.setupCRBController(crbController)
rbController := fake.NewMockControllerInterface[*rbacv1.RoleBinding, *rbacv1.RoleBindingList](ctrl)
if tt.setupRBController != nil {
tt.setupRBController(rbController)
}
crController := fake.NewMockNonNamespacedControllerInterface[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList](ctrl)
if tt.setupCRController != nil {
@@ -379,8 +365,8 @@ func Test_reconcileBindings(t *testing.T) {
}
p := &prtbHandler{
crbController: crbController,
crController: crController,
crController: crController,
rbController: rbController,
}
if err := p.reconcileBindings(tt.prtb); (err != nil) != tt.wantErr {
t.Errorf("prtbHandler.reconcileBindings() error = %v, wantErr %v", err, tt.wantErr)

View File

@@ -28,7 +28,6 @@ var (
"etcdsnapshots": "rke.cattle.io",
}
projectManagementPlaneResources = map[string]string{
"apps": "project.cattle.io",
"projectroletemplatebindings": "management.cattle.io",
"secrets": "",
}

View File

@@ -755,11 +755,6 @@ func Test_getManagementPlaneRules(t *testing.T) {
},
managementResources: projectManagementPlaneResources,
want: []rbacv1.PolicyRule{
{
Resources: []string{"apps"},
APIGroups: []string{"project.cattle.io"},
Verbs: []string{"*"},
},
{
Resources: []string{"projectroletemplatebindings"},
APIGroups: []string{"management.cattle.io"},

View File

@@ -95,8 +95,8 @@ func (n *namespaceHandler) OnChange(_ string, namespace *corev1.Namespace) (*cor
for _, secret := range secrets {
secretCopy := getNamespacedSecret(secret, namespace.Name)
s, err := rbac.CreateOrUpdateNamespacedResource(secretCopy, n.secretClient, areSecretsSame)
desiredSecrets.Insert(client.ObjectKeyFromObject(s))
err := rbac.CreateOrUpdateNamespacedResource(secretCopy, n.secretClient, areSecretsSame)
desiredSecrets.Insert(client.ObjectKeyFromObject(secretCopy))
errs = errors.Join(errs, err)
}
if errs != nil {

View File

@@ -389,23 +389,25 @@ func CreateOrUpdateResource[T generic.RuntimeMetaObject, TList runtime.Object](o
// - client is the Wrangler client to use to get/create/update resource.
// - areResourcesTheSame is a func that compares two resources and returns (true, nil) if they are equal, and (false, T) when not the same.
// T is an updated version of the resource.
func CreateOrUpdateNamespacedResource[T generic.RuntimeMetaObject, TList runtime.Object](obj T, client generic.ClientInterface[T, TList], areResourcesTheSame func(T, T) (bool, T)) (T, error) {
func CreateOrUpdateNamespacedResource[T generic.RuntimeMetaObject, TList runtime.Object](obj T, client generic.ClientInterface[T, TList], areResourcesTheSame func(T, T) (bool, T)) error {
resource, err := client.Get(obj.GetNamespace(), obj.GetName(), metav1.GetOptions{})
if err != nil {
if !apierrors.IsNotFound(err) {
return obj, err
return err
}
// resource doesn't exist, create it
logrus.Infof("%T %s being created in namespace %s", obj, obj.GetName(), obj.GetNamespace())
return client.Create(obj)
_, err := client.Create(obj)
return err
}
if same, updatedResource := areResourcesTheSame(resource, obj); !same {
logrus.Infof("%T %s in namespace %s needs to be updated", obj, obj.GetName(), obj.GetNamespace())
return client.Update(updatedResource)
_, err := client.Update(updatedResource)
return err
}
return obj, nil
return nil
}
// AreClusterRolesSame returns true if the current ClusterRole has the same fields present in the desired ClusterRole.
@@ -458,6 +460,16 @@ func DeleteResource[T generic.RuntimeMetaObject, TList runtime.Object](name stri
return err
}
// DeleteResource deletes a non namespaced resource
func DeleteNamespacedResource[T generic.RuntimeMetaObject, TList runtime.Object](namespace, name string, client generic.ClientInterface[T, TList]) error {
err := client.Delete(namespace, name, &metav1.DeleteOptions{})
// If the resource is already gone, don't treat it as an error
if apierrors.IsNotFound(err) {
return nil
}
return err
}
// BuildClusterRole creates a cluster role with an aggregation label
// - name: name of the cluster role
// - ownerName: name of the creator of this cluster role
@@ -510,6 +522,45 @@ func BuildAggregatingClusterRole(rt *v3.RoleTemplate, nameTransformer func(strin
}
}
// BuildAggregatingRoleBindingFromRTB returns a RoleBinding for a RTB. It is bound to the Aggregating ClusterRole.
func BuildAggregatingRoleBindingFromRTB(rtb metav1.Object, roleRefName string) (*rbacv1.RoleBinding, error) {
return BuildRoleBindingFromRTB(rtb, AggregatedClusterRoleNameFor(roleRefName))
}
// BuildRoleBindingFromRTB returns a RoleBinding for a RTB. It is bound to the ClusterRole specified by roleRefName.
func BuildRoleBindingFromRTB(rtb metav1.Object, roleRefName string) (*rbacv1.RoleBinding, error) {
roleRef := rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: roleRefName,
}
subject, err := BuildSubjectFromRTB(rtb)
if err != nil {
return nil, err
}
var ownerLabel string
switch rtb.(type) {
case *v3.ProjectRoleTemplateBinding:
ownerLabel = GetPRTBOwnerLabel(rtb.GetName())
case *v3.ClusterRoleTemplateBinding:
ownerLabel = GetCRTBOwnerLabel(rtb.GetName())
default:
return nil, fmt.Errorf("unrecognized roleTemplateBinding type: %T", rtb)
}
return &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: NameForRoleBinding(rtb.GetNamespace(), roleRef, subject),
Namespace: rtb.GetNamespace(),
Labels: map[string]string{ownerLabel: "true"},
},
RoleRef: roleRef,
Subjects: []rbacv1.Subject{subject},
}, nil
}
// BuildAggregatingClusterRoleBindingFromRTB returns the ClusterRoleBinding needed for a RTB. It is bound to the Aggregating ClusterRole.
func BuildAggregatingClusterRoleBindingFromRTB(rtb metav1.Object, roleRefName string) (*rbacv1.ClusterRoleBinding, error) {
return BuildClusterRoleBindingFromRTB(rtb, AggregatedClusterRoleNameFor(roleRefName))
@@ -555,9 +606,9 @@ func AreClusterRoleBindingContentsSame(crb1, crb2 *rbacv1.ClusterRoleBinding) bo
}
// AreRoleBindingsSame compares the Subjects and RoleRef fields of two Cluster Role Bindings.
func AreRoleBindingContentsSame(rb1, rb2 *rbacv1.RoleBinding) bool {
func AreRoleBindingContentsSame(rb1, rb2 *rbacv1.RoleBinding) (bool, *rbacv1.RoleBinding) {
return equality.Semantic.DeepEqual(rb1.Subjects, rb2.Subjects) &&
equality.Semantic.DeepEqual(rb1.RoleRef, rb2.RoleRef)
equality.Semantic.DeepEqual(rb1.RoleRef, rb2.RoleRef), rb2
}
// ClusterRoleNameFor returns safe version of a string to be used for a clusterRoleName