Skip to content

Conversation

@sago2k8
Copy link
Contributor

@sago2k8 sago2k8 commented Dec 1, 2025

Summary

Implements proper reconciliation of role membership changes by adding status tracking and ensuring the operator updates principals when the spec changes. This follows the same pattern as ACL management, where membership is strictly managed based on the spec.

Key Changes

  • Status Tracking: Added ManagedPrincipals bool field to RoleStatus to track whether principals are being managed
  • Controller Logic: Updated role controller to reconcile membership changes:
    • Creates roles with principals → ManagedPrincipals = true
    • Updates principals when spec changes
    • Clears all principals when spec becomes empty/nil → ManagedPrincipals = false
  • Client Enhancement: Added ClearPrincipals() method for transitioning from managed to unmanaged mode
  • Helper Methods: Added ShouldManagePrincipals() and HasManagedPrincipals() for clean API

Testing

Unit Tests

  • TestRoleMembershipReconciliation - New comprehensive test covering:
    • Adding members
    • Removing members
    • Replacing all members
    • Clearing all members (transition to unmanaged)
  • All existing role tests updated to verify ManagedPrincipals tracking

Acceptance Tests

Four new scenarios following PR #1146 naming conventions:

  1. Add managed principals to the role - Verifies adding members triggers reconciliation
  2. Remove managed principals from the role - Verifies removing specific members
  3. Stop managing principals - Verifies transition from managed to unmanaged
  4. Replace all managed principals - Verifies complete membership replacement

Behavior

  • Strict Management: When spec.principals is specified, operator manages ALL membership
  • Unmanaged Mode: When spec.principals is empty/nil, operator doesn't touch membership
  • Status Tracking: ManagedPrincipals flag allows proper cleanup on deletion

Relates to #1146
Relates to https://redpandadata.atlassian.net/browse/UX-430

- Add ManagedPrincipals status field to track membership management
- Update controller to reconcile membership changes (add/remove/clear)
- Add ClearPrincipals method for transitioning to unmanaged mode
- Add comprehensive unit tests for membership updates
- Add acceptance tests for principals management scenarios

Implements strict membership management where operator reconciles
all members in spec. When spec.principals is empty/nil, operator
stops managing membership (managedPrincipals=false).
The operator always takes full ownership of roles it manages,
regardless of whether they pre-existed. Updated the authorization-only
test to expect role deletion when the CRD is removed.

Also updated controller to set managedRole=true when managing
pre-existing roles to ensure proper cleanup.
@sago2k8 sago2k8 force-pushed the sj/operator/manage-role-membership branch from 3e8a838 to e321a0b Compare December 1, 2025 13:10
@sago2k8 sago2k8 added v25.1.x and removed v25.1.x labels Dec 1, 2025
Copy link
Contributor

@RafalKorepta RafalKorepta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines 110 to 116
if hasRole && !shouldManageRole {
if err := rolesClient.Delete(ctx, role); err != nil {
return createPatch(err)
}
hasManagedRole = false
hasManagedPrincipals = false
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can see this is dead code as shouldManageRole will always be set to true

require.True(t, role.ShouldManagePrincipals())
require.True(t, role.Status.ManagedRole)
require.False(t, role.Status.ManagedACLs)
require.True(t, role.Status.ManagedPrincipals)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: I feel like on line 154 it is compering desire state with actual status.

Suggested change
require.True(t, role.Status.ManagedPrincipals)
require.Equal(t, role.ShouldManagePrincipals(), role.Status.ManagedPrincipals)

require.True(t, role.ShouldManagePrincipals())
require.True(t, role.Status.ManagedRole)
require.True(t, role.Status.ManagedACLs)
require.True(t, role.Status.ManagedPrincipals)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Ditto

Suggested change
require.True(t, role.Status.ManagedPrincipals)
require.Equal(t, role.ShouldManagePrincipals(), role.Status.ManagedPrincipals)

require.True(t, role.ShouldManagePrincipals())
require.True(t, role.Status.ManagedRole)
require.False(t, role.Status.ManagedACLs)
require.True(t, role.Status.ManagedPrincipals)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: DITTO

Suggested change
require.True(t, role.Status.ManagedPrincipals)
require.Equal(t, role.ShouldManagePrincipals(), role.Status.ManagedPrincipals)

}

func TestRoleMembershipReconciliation(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
ctx, cancel := context.WithTimeout(t.Context(), time.Minute*2)

require.NoError(t, environment.Factory.Get(ctx, key, role))
require.True(t, role.Status.ManagedRole)
require.True(t, role.Status.ManagedPrincipals)
require.Equal(t, []string{"User:alice", "User:charlie"}, role.Spec.Principals)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like comparison with role.Spec.Principals does not test reconciler, but rather client. Shouldn't you get principals from Redpanda and compare?

Comment on lines +697 to +702
t.Run("cleanup", func(t *testing.T) {
require.NoError(t, environment.Factory.Delete(ctx, role))
_, err := environment.Reconciler.Reconcile(ctx, req)
require.NoError(t, err)
require.True(t, apierrors.IsNotFound(environment.Factory.Get(ctx, key, role)))
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: I'm not sure why cleanup is implemented in subset.

I would suggest to implement it within Cleanup function as follows:

Suggested change
t.Run("cleanup", func(t *testing.T) {
require.NoError(t, environment.Factory.Delete(ctx, role))
_, err := environment.Reconciler.Reconcile(ctx, req)
require.NoError(t, err)
require.True(t, apierrors.IsNotFound(environment.Factory.Get(ctx, key, role)))
})
t.Cleanup(func() {
require.NoError(t, environment.Factory.Delete(ctx, role))
_, err := environment.Reconciler.Reconcile(ctx, req)
require.NoError(t, err)
require.True(t, apierrors.IsNotFound(environment.Factory.Get(ctx, key, role)))
})

@sago2k8 sago2k8 merged commit 7b1c1df into main Dec 2, 2025
16 checks passed
github-actions bot pushed a commit that referenced this pull request Dec 2, 2025
* feat(operator): implement role membership reconciliation

- Add ManagedPrincipals status field to track membership management
- Update controller to reconcile membership changes (add/remove/clear)
- Add ClearPrincipals method for transitioning to unmanaged mode
- Add comprehensive unit tests for membership updates
- Add acceptance tests for principals management scenarios

Implements strict membership management where operator reconciles
all members in spec. When spec.principals is empty/nil, operator
stops managing membership (managedPrincipals=false).

* chore: add changelog entry for role membership reconciliation

* fix(acceptance): update test for always-manage-role pattern

The operator always takes full ownership of roles it manages,
regardless of whether they pre-existed. Updated the authorization-only
test to expect role deletion when the CRD is removed.

Also updated controller to set managedRole=true when managing
pre-existing roles to ensure proper cleanup.

(cherry picked from commit 7b1c1df)

# Conflicts:
#	acceptance/features/role-crds.feature
#	acceptance/steps/register.go
#	acceptance/steps/roles.go
#	operator/api/applyconfiguration/redpanda/v1alpha2/rolestatus.go
#	operator/api/redpanda/v1alpha2/role_types.go
#	operator/api/redpanda/v1alpha2/testdata/crd-docs.adoc
#	operator/config/crd/bases/cluster.redpanda.com_redpandaroles.yaml
#	operator/internal/controller/redpanda/role_controller.go
#	operator/internal/controller/redpanda/role_controller_test.go
#	operator/pkg/client/roles/client.go
@github-actions
Copy link

github-actions bot commented Dec 2, 2025

💚 All backports created successfully

Status Branch Result
release/v25.1.x

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation and see the Github Action logs for details

@RafalKorepta RafalKorepta deleted the sj/operator/manage-role-membership branch December 2, 2025 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants