Skip to content

Commit 50e9642

Browse files
authored
Merge pull request #1816 from splunk/fix/postgres-controller-roles-removal
fix: propagate role removal to postgres cluster when databases are removed from postgres database spec
2 parents ecf4af6 + bbc6c9c commit 50e9642

File tree

7 files changed

+406
-124
lines changed

7 files changed

+406
-124
lines changed

api/v4/postgrescluster_types.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ type ManagedRole struct {
3636

3737
// Exists controls whether the role should be present (true) or absent (false) in PostgreSQL.
3838
// +kubebuilder:default=true
39-
// +optional
40-
Exists bool `json:"exists,omitempty"`
39+
Exists bool `json:"exists"`
4140
}
4241

4342
// PostgresClusterSpec defines the desired state of PostgresCluster.

internal/controller/postgrescluster_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import (
5151
* PC-09 ignores no-op updates
5252
*/
5353

54-
var _ = Describe("PostgresCluster Controller", func() {
54+
var _ = Describe("PostgresCluster Controller", Label("postgres"), func() {
5555

5656
const (
5757
postgresVersion = "15.10"

internal/controller/postgresdatabase_controller_test.go

Lines changed: 170 additions & 55 deletions
Large diffs are not rendered by default.

pkg/postgresql/cluster/core/cluster.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,6 @@ func reconcileManagedRoles(ctx context.Context, c client.Client, cluster *enterp
606606
Name: role.Name,
607607
Ensure: cnpgv1.EnsureAbsent,
608608
}
609-
// Exists bool replaces the old Ensure string enum ("present"/"absent").
610609
if role.Exists {
611610
r.Ensure = cnpgv1.EnsurePresent
612611
r.Login = true

pkg/postgresql/database/core/database.go

Lines changed: 93 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
369370
func 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

586593
func 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-
769822
func stripOwnerReference(obj metav1.Object, ownerUID types.UID) {
770823
refs := obj.GetOwnerReferences()
771824
filtered := make([]metav1.OwnerReference, 0, len(refs))

0 commit comments

Comments
 (0)