@@ -172,31 +172,29 @@ func PostgresDatabaseService(
172172 }
173173
174174 // Phase: RoleProvisioning
175- desiredUsers := getDesiredUsers (postgresDB )
176- actualRoles := getUsersInClusterSpec (cluster )
177- var missing []string
178- for _ , role := range desiredUsers {
179- if ! slices .Contains (actualRoles , role ) {
180- missing = append (missing , role )
181- }
182- }
183-
184- if len (missing ) > 0 {
185- logger .Info ("CNPG Cluster patch started, missing roles detected" , "missing" , missing )
186- if err := patchManagedRoles (ctx , c , postgresDB , cluster ); err != nil {
175+ fieldManager := fieldManagerName (postgresDB .Name )
176+ desired := buildDesiredRoles (postgresDB .Name , postgresDB .Spec .Databases )
177+ rolesToAdd := findAddedRoleNames (cluster , desired )
178+ rolesToRemove := absentRolesByName (findRemovedRoleNames (cluster , fieldManager , desired ))
179+ allRoles := append (desired , rolesToRemove ... )
180+
181+ if len (rolesToAdd ) > 0 || len (rolesToRemove ) > 0 {
182+ logger .Info ("CNPG Cluster patch started, role drift detected" , "toAdd" , len (rolesToAdd ), "toRemove" , len (rolesToRemove ))
183+ if err := patchManagedRoles (ctx , c , fieldManager , cluster , allRoles ); err != nil {
187184 logger .Error (err , "Failed to patch users in CNPG Cluster" )
188185 rc .emitWarning (postgresDB , EventManagedRolesPatchFailed , fmt .Sprintf ("Failed to patch managed roles: %v" , err ))
189186 return ctrl.Result {}, err
190187 }
191- rc .emitNormal (postgresDB , EventRoleReconciliationStarted , fmt .Sprintf ("Patched managed roles, waiting for %d roles to reconcile " , len (desiredUsers )))
188+ rc .emitNormal (postgresDB , EventRoleReconciliationStarted , fmt .Sprintf ("Patched managed roles: %d to add, %d to remove " , len (rolesToAdd ), len ( rolesToRemove )))
192189 if err := updateStatus (rolesReady , metav1 .ConditionFalse , reasonWaitingForCNPG ,
193- fmt .Sprintf ("Waiting for %d roles to be reconciled" , len (desiredUsers )), provisioningDBPhase ); err != nil {
190+ fmt .Sprintf ("Waiting for roles to be reconciled: %d to add, %d to remove " , len (rolesToAdd ), len ( rolesToRemove )), provisioningDBPhase ); err != nil {
194191 return ctrl.Result {}, err
195192 }
196193 return ctrl.Result {RequeueAfter : retryDelay }, nil
197194 }
198195
199- notReadyRoles , err := verifyRolesReady (ctx , desiredUsers , cnpgCluster )
196+ roleNames := getDesiredUsers (postgresDB )
197+ notReadyRoles , err := verifyRolesReady (ctx , roleNames , cnpgCluster )
200198 if err != nil {
201199 rc .emitWarning (postgresDB , EventRoleFailed , fmt .Sprintf ("Role reconciliation failed: %v" , err ))
202200 if statusErr := updateStatus (rolesReady , metav1 .ConditionFalse , reasonUsersCreationFailed ,
@@ -212,9 +210,9 @@ func PostgresDatabaseService(
212210 }
213211 return ctrl.Result {RequeueAfter : retryDelay }, nil
214212 }
215- rc .emitOnConditionTransition (postgresDB , postgresDB .Status .Conditions , rolesReady , EventRolesReady , fmt .Sprintf ("All %d roles reconciled " , len (desiredUsers )))
213+ rc .emitOnConditionTransition (postgresDB , postgresDB .Status .Conditions , rolesReady , EventRolesReady , fmt .Sprintf ("Roles reconciled: %d active, %d removed " , len (rolesToAdd ), len ( rolesToRemove )))
216214 if err := updateStatus (rolesReady , metav1 .ConditionTrue , reasonUsersAvailable ,
217- fmt .Sprintf ("All %d users in PostgreSQL " , len (desiredUsers )), provisioningDBPhase ); err != nil {
215+ fmt .Sprintf ("Roles reconciled: %d active, %d removed " , len (rolesToAdd ), len ( rolesToRemove )), provisioningDBPhase ); err != nil {
218216 return ctrl.Result {}, err
219217 }
220218
@@ -366,6 +364,9 @@ func getUsersInClusterSpec(cluster *enterprisev4.PostgresCluster) []string {
366364 return users
367365}
368366
367+ // rolesMatchClusterSpec returns true if desired and actual contain the same roles
368+ // (by name and Exists state), regardless of order.
369+
369370func getRoleConflicts (postgresDB * enterprisev4.PostgresDatabase , cluster * enterprisev4.PostgresCluster ) []string {
370371 myManager := fieldManagerName (postgresDB .Name )
371372 desired := make (map [string ]struct {}, len (postgresDB .Spec .Databases )* 2 )
@@ -413,18 +414,16 @@ func parseRoleNames(raw []byte) []string {
413414 return names
414415}
415416
416- func patchManagedRoles (ctx context.Context , c client.Client , postgresDB * enterprisev4. PostgresDatabase , cluster * enterprisev4.PostgresCluster ) error {
417+ func patchManagedRoles (ctx context.Context , c client.Client , fieldManager string , cluster * enterprisev4.PostgresCluster , roles []enterprisev4. ManagedRole ) error {
417418 logger := log .FromContext (ctx )
418- allRoles := buildManagedRoles (postgresDB .Name , postgresDB .Spec .Databases )
419- rolePatch , err := buildManagedRolesPatch (cluster , allRoles , c .Scheme ())
419+ rolePatch , err := buildManagedRolesPatch (cluster , roles , c .Scheme ())
420420 if err != nil {
421- return fmt .Errorf ("building managed roles patch for PostgresDatabase %s : %w" , postgresDB . Name , err )
421+ return fmt .Errorf ("building managed roles patch: %w" , err )
422422 }
423- fieldManager := fieldManagerName (postgresDB .Name )
424423 if err := c .Patch (ctx , rolePatch , client .Apply , client .FieldOwner (fieldManager )); err != nil {
425- return fmt .Errorf ("patching managed roles for PostgresDatabase %s : %w" , postgresDB . Name , err )
424+ return fmt .Errorf ("patching managed roles: %w" , err )
426425 }
427- logger .Info ("Users added to PostgresCluster via SSA " , "roleCount " , len (allRoles ))
426+ logger .Info ("Managed roles patched " , "count " , len (roles ))
428427 return nil
429428}
430429
@@ -580,7 +579,15 @@ func cleanupManagedRoles(ctx context.Context, c client.Client, postgresDB *enter
580579 logger .Info ("PostgresCluster already deleted, skipping role cleanup" )
581580 return nil
582581 }
583- return patchManagedRolesOnDeletion (ctx , c , postgresDB , cluster , plan .retained )
582+ fieldManager := fieldManagerName (postgresDB .Name )
583+ retainedRoles := buildDesiredRoles (postgresDB .Name , plan .retained )
584+ rolesToRemove := buildRolesToRemove (plan .deleted )
585+ allRoles := append (retainedRoles , rolesToRemove ... )
586+ if err := patchManagedRoles (ctx , c , fieldManager , cluster , allRoles ); err != nil {
587+ return err
588+ }
589+ logger .Info ("Managed roles patched on deletion" , "retained" , len (retainedRoles ), "removed" , len (rolesToRemove ))
590+ return nil
584591}
585592
586593func orphanCNPGDatabases (ctx context.Context , c client.Client , postgresDB * enterprisev4.PostgresDatabase , databases []enterprisev4.DatabaseDefinition ) error {
@@ -716,7 +723,67 @@ func deleteSecrets(ctx context.Context, c client.Client, postgresDB *enterprisev
716723 return nil
717724}
718725
719- func buildManagedRoles (postgresDBName string , databases []enterprisev4.DatabaseDefinition ) []enterprisev4.ManagedRole {
726+ // buildRolesToRemove produces Exists:false entries for the given databases so CNPG drops their roles.
727+ func buildRolesToRemove (databases []enterprisev4.DatabaseDefinition ) []enterprisev4.ManagedRole {
728+ roles := make ([]enterprisev4.ManagedRole , 0 , len (databases )* 2 )
729+ for _ , dbSpec := range databases {
730+ roles = append (roles ,
731+ enterprisev4.ManagedRole {Name : adminRoleName (dbSpec .Name ), Exists : false },
732+ enterprisev4.ManagedRole {Name : rwRoleName (dbSpec .Name ), Exists : false },
733+ )
734+ }
735+ return roles
736+ }
737+
738+ // absentRolesByName produces Exists:false entries from a list of raw role names.
739+ // Used by the normal reconcile path where names come from SSA field manager parsing.
740+ func absentRolesByName (names []string ) []enterprisev4.ManagedRole {
741+ roles := make ([]enterprisev4.ManagedRole , 0 , len (names ))
742+ for _ , name := range names {
743+ roles = append (roles , enterprisev4.ManagedRole {Name : name , Exists : false })
744+ }
745+ return roles
746+ }
747+
748+ // findAddedRoleNames returns role names from the desired list that are missing
749+ // from the cluster spec or currently marked absent.
750+ func findAddedRoleNames (cluster * enterprisev4.PostgresCluster , desired []enterprisev4.ManagedRole ) []string {
751+ current := make (map [string ]bool , len (cluster .Spec .ManagedRoles ))
752+ for _ , r := range cluster .Spec .ManagedRoles {
753+ current [r .Name ] = r .Exists
754+ }
755+ var toAdd []string
756+ for _ , r := range desired {
757+ exists , found := current [r .Name ]
758+ if ! found || ! exists {
759+ toAdd = append (toAdd , r .Name )
760+ }
761+ }
762+ return toAdd
763+ }
764+
765+ // findRemovedRoleNames returns role names currently owned by this field manager
766+ // in the cluster spec that are absent from the desired list.
767+ func findRemovedRoleNames (cluster * enterprisev4.PostgresCluster , manager string , desired []enterprisev4.ManagedRole ) []string {
768+ desiredSet := make (map [string ]struct {}, len (desired ))
769+ for _ , r := range desired {
770+ desiredSet [r .Name ] = struct {}{}
771+ }
772+ owners := managedRoleOwners (cluster .ManagedFields )
773+ var toRemove []string
774+ for name , owner := range owners {
775+ if owner == manager {
776+ if _ , ok := desiredSet [name ]; ! ok {
777+ toRemove = append (toRemove , name )
778+ }
779+ }
780+ }
781+ return toRemove
782+ }
783+
784+ // buildDesiredRoles builds the full set of roles that should be present for the given databases.
785+ // This is the input to findAddedRoleNames and findRemovedRoleNames.
786+ func buildDesiredRoles (postgresDBName string , databases []enterprisev4.DatabaseDefinition ) []enterprisev4.ManagedRole {
720787 roles := make ([]enterprisev4.ManagedRole , 0 , len (databases )* 2 )
721788 for _ , dbSpec := range databases {
722789 roles = append (roles ,
@@ -752,20 +819,6 @@ func buildManagedRolesPatch(cluster *enterprisev4.PostgresCluster, roles []enter
752819 }, nil
753820}
754821
755- func patchManagedRolesOnDeletion (ctx context.Context , c client.Client , postgresDB * enterprisev4.PostgresDatabase , cluster * enterprisev4.PostgresCluster , retained []enterprisev4.DatabaseDefinition ) error {
756- logger := log .FromContext (ctx )
757- roles := buildManagedRoles (postgresDB .Name , retained )
758- rolePatch , err := buildManagedRolesPatch (cluster , roles , c .Scheme ())
759- if err != nil {
760- return fmt .Errorf ("building managed roles patch: %w" , err )
761- }
762- if err := c .Patch (ctx , rolePatch , client .Apply , client .FieldOwner (fieldManagerName (postgresDB .Name ))); err != nil {
763- return fmt .Errorf ("patching managed roles on deletion: %w" , err )
764- }
765- logger .Info ("Managed roles patched on deletion" , "retainedRoles" , len (roles ))
766- return nil
767- }
768-
769822func stripOwnerReference (obj metav1.Object , ownerUID types.UID ) {
770823 refs := obj .GetOwnerReferences ()
771824 filtered := make ([]metav1.OwnerReference , 0 , len (refs ))
0 commit comments