Skip to content

Commit 5618891

Browse files
committed
Write tests
1 parent ecf4af6 commit 5618891

File tree

7 files changed

+304
-70
lines changed

7 files changed

+304
-70
lines changed

api/v4/postgrescluster_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type ManagedRole struct {
3737
// Exists controls whether the role should be present (true) or absent (false) in PostgreSQL.
3838
// +kubebuilder:default=true
3939
// +optional
40-
Exists bool `json:"exists,omitempty"`
40+
Exists bool `json:"exists"`
4141
}
4242

4343
// 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: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ func seedOwnedDatabaseArtifacts(ctx context.Context, namespace, resourceName, cl
309309
}
310310
}
311311

312+
func expectManagedRoleExists(cluster *enterprisev4.PostgresCluster, roleName string, exists bool) {
313+
rolesByName := make(map[string]enterprisev4.ManagedRole, len(cluster.Spec.ManagedRoles))
314+
for _, r := range cluster.Spec.ManagedRoles {
315+
rolesByName[r.Name] = r
316+
}
317+
Expect(rolesByName).To(HaveKey(roleName))
318+
Expect(rolesByName[roleName].Exists).To(Equal(exists), "role %s: expected Exists=%v", roleName, exists)
319+
}
320+
312321
func expectRetainedArtifact(ctx context.Context, name, namespace, resourceName string, obj client.Object) {
313322
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, obj)).To(Succeed())
314323
Expect(obj.GetAnnotations()).To(HaveKeyWithValue("enterprise.splunk.com/retained-from", resourceName))
@@ -344,7 +353,7 @@ func expectReadyStatus(current *enterprisev4.PostgresDatabase, generation int64,
344353
Expect(current.Status.Databases[0].ConfigMapRef).NotTo(BeNil())
345354
}
346355

347-
var _ = Describe("PostgresDatabase Controller", func() {
356+
var _ = Describe("PostgresDatabase Controller", Label("postgres"), func() {
348357
var (
349358
ctx context.Context
350359
namespace string
@@ -493,6 +502,59 @@ var _ = Describe("PostgresDatabase Controller", func() {
493502
})
494503
})
495504

505+
When("a database is removed from spec.databases while the CR stays alive", func() {
506+
It("marks the removed database roles as absent in postgres cluster and keeps the retained roles present", func() {
507+
resourceName := "live-db-removal"
508+
clusterName := "live-db-removal-postgres"
509+
cnpgClusterName := "live-db-removal-cnpg"
510+
requestName := types.NamespacedName{Name: resourceName, Namespace: namespace}
511+
512+
postgresDB := createPostgresDatabaseResource(ctx, namespace, resourceName, clusterName, []enterprisev4.DatabaseDefinition{
513+
{Name: "keepdb"},
514+
{Name: "dropdb"},
515+
}, postgresDatabaseFinalizer)
516+
Expect(k8sClient.Get(ctx, requestName, postgresDB)).To(Succeed())
517+
518+
postgresCluster := createPostgresClusterResource(ctx, namespace, clusterName)
519+
markPostgresClusterReady(ctx, postgresCluster, cnpgClusterName, namespace, false)
520+
cnpgCluster := createCNPGClusterResource(ctx, namespace, cnpgClusterName)
521+
markCNPGClusterReady(ctx, cnpgCluster, []string{"keepdb_admin", "keepdb_rw", "dropdb_admin", "dropdb_rw"}, "tenant-rw", "tenant-ro")
522+
523+
initialRolesPatch := &unstructured.Unstructured{
524+
Object: map[string]any{
525+
"apiVersion": enterprisev4.GroupVersion.String(),
526+
"kind": "PostgresCluster",
527+
"metadata": map[string]any{"name": clusterName, "namespace": namespace},
528+
"spec": map[string]any{
529+
"managedRoles": []map[string]any{
530+
{"name": "keepdb_admin", "exists": true, "passwordSecretRef": map[string]any{"name": resourceName + "-keepdb-admin", "key": "password"}},
531+
{"name": "keepdb_rw", "exists": true, "passwordSecretRef": map[string]any{"name": resourceName + "-keepdb-rw", "key": "password"}},
532+
{"name": "dropdb_admin", "exists": true, "passwordSecretRef": map[string]any{"name": resourceName + "-dropdb-admin", "key": "password"}},
533+
{"name": "dropdb_rw", "exists": true, "passwordSecretRef": map[string]any{"name": resourceName + "-dropdb-rw", "key": "password"}},
534+
},
535+
},
536+
},
537+
}
538+
Expect(k8sClient.Patch(ctx, initialRolesPatch, client.Apply, client.FieldOwner("postgresdatabase-"+resourceName))).To(Succeed())
539+
540+
seedOwnedDatabaseArtifacts(ctx, namespace, resourceName, clusterName, postgresDB, "keepdb", "dropdb")
541+
542+
postgresDB.Spec.Databases = []enterprisev4.DatabaseDefinition{{Name: "keepdb"}}
543+
Expect(k8sClient.Update(ctx, postgresDB)).To(Succeed())
544+
545+
result, err := reconcilePostgresDatabase(ctx, requestName)
546+
expectReconcileResult(result, err, 15*time.Second)
547+
548+
updatedCluster := &enterprisev4.PostgresCluster{}
549+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, updatedCluster)).To(Succeed())
550+
551+
expectManagedRoleExists(updatedCluster, "keepdb_admin", true)
552+
expectManagedRoleExists(updatedCluster, "keepdb_rw", true)
553+
expectManagedRoleExists(updatedCluster, "dropdb_admin", false)
554+
expectManagedRoleExists(updatedCluster, "dropdb_rw", false)
555+
})
556+
})
557+
496558
When("the PostgresDatabase is being deleted", func() {
497559
Context("with retained and deleted databases", func() {
498560
It("orphans retained resources, removes deleted resources, and patches managed roles", func() {
@@ -547,7 +609,11 @@ var _ = Describe("PostgresDatabase Controller", func() {
547609

548610
updatedCluster := &enterprisev4.PostgresCluster{}
549611
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, updatedCluster)).To(Succeed())
550-
Expect(managedRoleNames(updatedCluster.Spec.ManagedRoles)).To(ConsistOf("keepdb_admin", "keepdb_rw"))
612+
613+
expectManagedRoleExists(updatedCluster, "keepdb_admin", true)
614+
expectManagedRoleExists(updatedCluster, "keepdb_rw", true)
615+
expectManagedRoleExists(updatedCluster, "dropdb_admin", false)
616+
expectManagedRoleExists(updatedCluster, "dropdb_rw", false)
551617

552618
current := &enterprisev4.PostgresDatabase{}
553619
err = k8sClient.Get(ctx, requestName, current)

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)