From c7e7bdac37a73ca449362e70e6f8bf06400b9d4b Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:03:31 +0100 Subject: [PATCH 1/2] set the Role status to ERROR when its password Secret is missing Assisted-by: Claude:claude-opus-5-5 --- .../postgresql/crd/role/RoleReconciler.java | 19 ++++---- .../crd/role/RoleReconcilerTest.java | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java index 875374e..d4f2d76 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java @@ -101,20 +101,17 @@ public UpdateControl reconcile( var passwordSecretRef = spec.getPasswordSecretRef(); - String password; - if (passwordSecretRef != null) { - password = kubernetesService.getSecretRefCredentials( - kubernetesClient, - passwordSecretRef, - namespace - ).password(); - } else { - password = null; - } - UpdateControl updateControl; try (var dsl = contextFactory.getDSLContext(clusterConnection)) { + // Read the Secret inside the try block, so that a missing or invalid Secret sets the phase to ERROR + var password = passwordSecretRef != null + ? kubernetesService.getSecretRefCredentials( + kubernetesClient, + passwordSecretRef, + namespace + ).password() + : null; var passwordEncryption = spec.getPasswordEncryption(); // The password hash in pg_authid is readable by superusers only. diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java index bb63af3..76b4df6 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java @@ -525,6 +525,52 @@ void upToDateRole_afterPendingClusterConnection_setsReady() { assertThat(updatedRole.getStatus().getMessage()).isNull(); } + @Test + @DisplayName( + "When the password Secret of a Role does not exist, the status should be ERROR until the Secret is back" + ) + void missingPasswordSecret_setsErrorUntilSecretIsBack() { + // given: a reconciled login Role + var clusterConnection = given.one() + .clusterConnection() + .withName("test-connection-role-missing-secret") + .returnFirst(); + + var secretRef = given.one() + .secretRef() + .returnFirst(); + + var role = given.one() + .role() + .withName("test-role-missing-secret") + .withClusterConnectionName(clusterConnection.getMetadata().getName()) + .withPasswordSecretRef(secretRef) + .returnFirst(); + + assertThat(role.getStatus().getPhase()).isEqualTo(CRPhase.READY); + + // when: the Role references a Secret that does not exist + var missingSecretRef = new ResourceRef(); + missingSecretRef.setName("non-existing-secret"); + + role.getSpec().setPasswordSecretRef(missingSecretRef); + + var erroredRole = applyRole(role); + + // then + assertThat(erroredRole.getStatus().getPhase()).isEqualTo(CRPhase.ERROR); + assertThat(erroredRole.getStatus().getMessage()).startsWith("Secret reference not found"); + + // when: the Role references the existing Secret again + role.getSpec().setPasswordSecretRef(secretRef); + + var updatedRole = applyRole(role); + + // then + assertThat(updatedRole.getStatus().getPhase()).isEqualTo(CRPhase.READY); + assertThat(updatedRole.getStatus().getMessage()).isNull(); + } + @Test @DisplayName("When the Secret already contains a SCRAM-SHA-256 verifier, it should be stored verbatim") void preHashedPassword_isStoredVerbatim() { From fa7aaf4f5a7b5fb94979a4464545aaffa0766208 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:18:32 +0100 Subject: [PATCH 2/2] test that a created password Secret brings an errored Role back to READY Assisted-by: Claude:claude-opus-5-5 --- .../crd/role/RoleReconcilerTest.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java index 76b4df6..f9705d3 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java @@ -32,6 +32,7 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import static it.aboutbits.postgresql._support.testdata.base.TestDataCreator.randomKubernetesNameSuffix; import static it.aboutbits.postgresql.core.KubernetesService.SECRET_DATA_BASIC_AUTH_PASSWORD_KEY; import static it.aboutbits.postgresql.core.infrastructure.persistence.Tables.PG_ROLES; import static org.assertj.core.api.Assertions.assertThat; @@ -527,9 +528,9 @@ void upToDateRole_afterPendingClusterConnection_setsReady() { @Test @DisplayName( - "When the password Secret of a Role does not exist, the status should be ERROR until the Secret is back" + "When the password Secret of a Role does not exist, the status should be ERROR until the Secret exists" ) - void missingPasswordSecret_setsErrorUntilSecretIsBack() { + void missingPasswordSecret_setsErrorUntilSecretExists() { // given: a reconciled login Role var clusterConnection = given.one() .clusterConnection() @@ -551,7 +552,7 @@ void missingPasswordSecret_setsErrorUntilSecretIsBack() { // when: the Role references a Secret that does not exist var missingSecretRef = new ResourceRef(); - missingSecretRef.setName("non-existing-secret"); + missingSecretRef.setName(randomKubernetesNameSuffix("test-missing-secret")); role.getSpec().setPasswordSecretRef(missingSecretRef); @@ -561,13 +562,22 @@ void missingPasswordSecret_setsErrorUntilSecretIsBack() { assertThat(erroredRole.getStatus().getPhase()).isEqualTo(CRPhase.ERROR); assertThat(erroredRole.getStatus().getMessage()).startsWith("Secret reference not found"); - // when: the Role references the existing Secret again - role.getSpec().setPasswordSecretRef(secretRef); + // when: the missing Secret is created + given.one() + .secretRef() + .withName(missingSecretRef.getName()) + .returnFirst(); - var updatedRole = applyRole(role); + // then: the Secret event source reconciles the Role without a spec change + var updatedRole = kubernetesClient.resources(Role.class) + .inNamespace(kubernetesClient.getNamespace()) + .withName(role.getMetadata().getName()) + .waitUntilCondition( + r -> r.getStatus() != null && r.getStatus().getPhase() == CRPhase.READY, + 5, + TimeUnit.SECONDS + ); - // then - assertThat(updatedRole.getStatus().getPhase()).isEqualTo(CRPhase.READY); assertThat(updatedRole.getStatus().getMessage()).isNull(); }