From 0e6f6d138f95fd36b052ef51a2b7674b30d90276 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:23:11 +0100 Subject: [PATCH 01/11] log the result of a Grant and DefaultPrivilege reconcile A Grant in ALL mode on tables or sequences runs its bulk grant on each reconcile, so it always logs "Updated Grant", never "Grant up-to-date". Assisted-by: Claude:claude-opus-5-5 --- .../postgresql/core/BaseReconciler.java | 17 ++++++ .../DefaultPrivilegeReconciler.java | 41 +++++++++---- .../postgresql/crd/grant/GrantReconciler.java | 60 ++++++++++++++----- .../postgresql/crd/grant/GrantService.java | 10 +++- .../DefaultPrivilegeReconcilerTest.java | 7 ++- .../crd/grant/GrantReconcilerTest.java | 7 ++- 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java index fe7515f..3630a19 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java @@ -86,6 +86,23 @@ public Optional getReferencedClusterConnection( return Optional.of(clusterConnection); } + /// Sets the phase to [CRPhase#ERROR] for a failure without an exception, and logs the message. + public void setErrorPhase( + CR resource, + S status, + String message + ) { + log.warn( + "Failed to reconcile resource [resource={}/{}, message={}]", + resource.getMetadata().getNamespace(), + resource.getMetadata().getName(), + message + ); + + status.setPhase(CRPhase.ERROR) + .setMessage(message); + } + public UpdateControl handleError( CR resource, S status, diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java index 5c331c0..cee7770 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java @@ -68,14 +68,17 @@ public UpdateControl reconcile( invalid.removeAll(allowedPrivilegesForObjectType); - status.setPhase(CRPhase.ERROR) - .setMessage("DefaultPrivilege contains invalid privileges for the specified objectType. [resource=%s/%s, objectType=%s, invalidPrivileges=%s, allowedPrivilegesForObjectType=%s]".formatted( - getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()), - clusterRef.getName(), + setErrorPhase( + resource, + status, + "DefaultPrivilege contains invalid privileges for the specified objectType. [resource=%s/%s, objectType=%s, invalidPrivileges=%s, allowedPrivilegesForObjectType=%s]".formatted( + namespace, + name, objectType, invalid, objectType.privileges() - )); + ) + ); return UpdateControl.patchStatus(resource); } @@ -188,7 +191,8 @@ public DeleteControl cleanup( "Failed to delete DefaultPrivilege [resource={}/{}, status.phase={}]", namespace, name, - status.getPhase() + status.getPhase(), + e ); status.setMessage("Deletion failed: %s".formatted(e.getMessage())); @@ -232,13 +236,16 @@ private UpdateControl reconcileInTransaction( )); if (!unsupportedPrivileges.isEmpty()) { - status.setPhase(CRPhase.ERROR) - .setMessage("The following privileges require a newer PostgreSQL version (current: %d): %s [resource=%s/%s]".formatted( + setErrorPhase( + resource, + status, + "The following privileges require a newer PostgreSQL version (current: %d): %s [resource=%s/%s]".formatted( databaseMajorVersion, unsupportedPrivileges, - getResourceNamespaceOrOwn(resource, namespace), + namespace, name - )); + ) + ); return UpdateControl.patchStatus(resource); } @@ -269,6 +276,20 @@ private UpdateControl reconcileInTransaction( ); } + if (privilegesToRevoke.isEmpty() && privilegesToGrant.isEmpty()) { + log.info( + "DefaultPrivilege up-to-date [resource={}/{}]", + namespace, + name + ); + } else { + log.info( + "Updated DefaultPrivilege [resource={}/{}]", + namespace, + name + ); + } + status.setPhase(CRPhase.READY) .setMessage(null); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index 5024420..d3924ff 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -73,14 +73,17 @@ public UpdateControl reconcile( invalid.removeAll(allowedPrivilegesForObjectType); - status.setPhase(CRPhase.ERROR) - .setMessage("Grant contains invalid privileges for the specified objectType. [resource=%s/%s, objectType=%s, invalidPrivileges=%s, allowedPrivilegesForObjectType=%s]".formatted( - getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()), - clusterRef.getName(), + setErrorPhase( + resource, + status, + "Grant contains invalid privileges for the specified objectType. [resource=%s/%s, objectType=%s, invalidPrivileges=%s, allowedPrivilegesForObjectType=%s]".formatted( + namespace, + name, objectType, invalid, objectType.privileges() - )); + ) + ); return UpdateControl.patchStatus(resource); } @@ -197,7 +200,8 @@ public DeleteControl cleanup( "Failed to delete Grant [resource={}/{}, status.phase={}]", namespace, name, - status.getPhase() + status.getPhase(), + e ); status.setMessage("Deletion failed: %s".formatted(e.getMessage())); @@ -252,13 +256,16 @@ private UpdateControl reconcileInTransaction( )); if (!unsupportedPrivileges.isEmpty()) { - status.setPhase(CRPhase.ERROR) - .setMessage("The following privileges require a newer PostgreSQL version (current: %d): %s [resource=%s/%s]".formatted( + setErrorPhase( + resource, + status, + "The following privileges require a newer PostgreSQL version (current: %d): %s [resource=%s/%s]".formatted( databaseMajorVersion, unsupportedPrivileges, - getResourceNamespaceOrOwn(resource, namespace), + namespace, name - )); + ) + ); return UpdateControl.patchStatus(resource); } @@ -284,18 +291,24 @@ private UpdateControl reconcileInTransaction( }); if (!missingObjects.isEmpty()) { - status.setPhase(CRPhase.ERROR) - .setMessage("Did not grant or revoke any privileges as the listed %s objects do not exist [resource=%s/%s]%n%s".formatted( + setErrorPhase( + resource, + status, + "Did not grant or revoke any privileges as the listed %s objects do not exist [resource=%s/%s]%n%s".formatted( objectType, - getResourceNamespaceOrOwn(resource, namespace), + namespace, name, String.join("\n • ", missingObjects) - )); + ) + ); return UpdateControl.patchStatus(resource) .rescheduleAfter(Duration.ofSeconds(60)); } + // Whether any grant or revoke statement was executed, only used for the log + var updated = false; + // 1. Reconcile objects explicitly listed in the Spec (processObjects). // We know these are NOT owned (filtered above) and ARE in the spec. for (var object : processObjects) { @@ -312,6 +325,7 @@ private UpdateControl reconcileInTransaction( object, privilegesToRevoke ); + updated = true; } // If we are not in the "ALL" mode, e.g. objects is an empty List, do explicit grants @@ -331,13 +345,14 @@ private UpdateControl reconcileInTransaction( object, privilegesToGrant ); + updated = true; } } } // 2. Bulk grant ("ALL" mode only) if (isAllMode) { - grantService.grantOnAll( + updated |= grantService.grantOnAll( tx, spec, expectedPrivileges @@ -367,9 +382,24 @@ private UpdateControl reconcileInTransaction( object, privilegesToRevoke ); + updated = true; } } + if (updated) { + log.info( + "Updated Grant [resource={}/{}]", + namespace, + name + ); + } else { + log.info( + "Grant up-to-date [resource={}/{}]", + namespace, + name + ); + } + String message = null; if (!ownedObjects.isEmpty()) { message = "The role is the owner of the listed %s objects and thus we did not need to grant or revoke any privileges from them. [resource=%s/%s]%n%s".formatted( diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantService.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantService.java index 8c59e17..3b1c0d4 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantService.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantService.java @@ -420,7 +420,11 @@ public void grant( tx.execute(statement); } - public void grantOnAll( + /// Grants the privileges on all objects of the objectType in the schema. + /// Only `TABLE` and `SEQUENCE` support this, for all other objectTypes nothing is executed. + /// + /// @return `true` if the statement was executed + public boolean grantOnAll( DSLContext tx, GrantSpec spec, Set privilegesToGrant @@ -432,7 +436,7 @@ public void grantOnAll( // grant on all s in schema to ; // is only supported by TABLE, SEQUENCE, FUNCTION, PROCEDURE and ROUTINE if (objectType != TABLE && objectType != SEQUENCE) { - return; + return false; } var privileges = privilegesToGrant.stream() @@ -448,6 +452,8 @@ public void grantOnAll( ); tx.execute(statement); + + return true; } public void revoke( diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconcilerTest.java index 5d92980..0451b7e 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconcilerTest.java @@ -295,7 +295,12 @@ void errorWhenInvalidPrivileges() { .extracting(DefaultPrivilege::getStatus) .satisfies(status -> { assertThat(status.getPhase()).isEqualTo(CRPhase.ERROR); - assertThat(status.getMessage()).startsWith("DefaultPrivilege contains invalid privileges for the specified objectType"); + assertThat(status.getMessage()) + .startsWith("DefaultPrivilege contains invalid privileges for the specified objectType") + .contains("[resource=%s/%s,".formatted( + defaultPrivilege.getMetadata().getNamespace(), + defaultPrivilege.getMetadata().getName() + )); }); } diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java index 29cc12d..c5a6a98 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java @@ -290,7 +290,12 @@ void errorWhenInvalidPrivileges() { .extracting(Grant::getStatus) .satisfies(status -> { assertThat(status.getPhase()).isEqualTo(CRPhase.ERROR); - assertThat(status.getMessage()).startsWith("Grant contains invalid privileges for the specified objectType"); + assertThat(status.getMessage()) + .startsWith("Grant contains invalid privileges for the specified objectType") + .contains("[resource=%s/%s,".formatted( + grant.getMetadata().getNamespace(), + grant.getMetadata().getName() + )); }); } From ddf2d65d9fc19a138b231c9bed0c36760ebf85d2 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:34:40 +0100 Subject: [PATCH 02/11] add an empty line before each updated flag in GrantReconciler Assisted-by: Claude:claude-opus-5-5 --- .../it/aboutbits/postgresql/crd/grant/GrantReconciler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index d3924ff..ed149a9 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -325,6 +325,7 @@ private UpdateControl reconcileInTransaction( object, privilegesToRevoke ); + updated = true; } @@ -345,6 +346,7 @@ private UpdateControl reconcileInTransaction( object, privilegesToGrant ); + updated = true; } } @@ -382,6 +384,7 @@ private UpdateControl reconcileInTransaction( object, privilegesToRevoke ); + updated = true; } } From 6957b870fa327145bf79545d1623695e668dafff Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:45:47 +0100 Subject: [PATCH 03/11] log Updated Grant in ALL mode only when a privilege is missing Assisted-by: Claude:claude-opus-5-5 --- .../postgresql/crd/grant/GrantReconciler.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index ed149a9..8640080 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -354,11 +354,19 @@ private UpdateControl reconcileInTransaction( // 2. Bulk grant ("ALL" mode only) if (isAllMode) { - updated |= grantService.grantOnAll( + var executed = grantService.grantOnAll( tx, spec, expectedPrivileges ); + + // The bulk grant changes something only if a non-owned object misses an expected privilege + if (executed) { + updated |= processObjects.stream() + .anyMatch(object -> !currentObjectPrivileges.getOrDefault(object, Set.of()) + .containsAll(expectedPrivileges) + ); + } } // 2. Revoke orphaned object privileges (Objects with privileges but not in Spec) From b2c96415e494683691af34a939d3ff02354a722f Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:46:13 +0100 Subject: [PATCH 04/11] add the kind of the resource to the reconcile error log Assisted-by: Claude:claude-opus-5-5 --- .../main/java/it/aboutbits/postgresql/core/BaseReconciler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java index 3630a19..4036158 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java @@ -93,7 +93,8 @@ public void setErrorPhase( String message ) { log.warn( - "Failed to reconcile resource [resource={}/{}, message={}]", + "Failed to reconcile {} [resource={}/{}, message={}]", + resource.getKind(), resource.getMetadata().getNamespace(), resource.getMetadata().getName(), message From 8d45b1e8a49bf58f17da69fa9559834660e775f7 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:55:00 +0100 Subject: [PATCH 05/11] log Updating instead of Updated for a Grant and DefaultPrivilege The other reconcilers log in the present tense, and the transaction commits only after the log line. Assisted-by: Claude:claude-opus-5-5 --- .../crd/defaultprivilege/DefaultPrivilegeReconciler.java | 2 +- .../java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java index cee7770..f2becae 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java @@ -284,7 +284,7 @@ private UpdateControl reconcileInTransaction( ); } else { log.info( - "Updated DefaultPrivilege [resource={}/{}]", + "Updating DefaultPrivilege [resource={}/{}]", namespace, name ); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index 8640080..104e3e7 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -399,7 +399,7 @@ private UpdateControl reconcileInTransaction( if (updated) { log.info( - "Updated Grant [resource={}/{}]", + "Updating Grant [resource={}/{}]", namespace, name ); From aeaa4266481741c588c2578c88a84ba25614967e Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:55:04 +0100 Subject: [PATCH 06/11] add the kind and the namespace to the reconcile exception log Assisted-by: Claude:claude-opus-5-5 --- .../java/it/aboutbits/postgresql/core/BaseReconciler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java index 4036158..6acd76c 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java @@ -110,7 +110,9 @@ public UpdateControl handleError( E exception ) { log.error( - "Failed to reconcile resource [resource={}]", + "Failed to reconcile {} [resource={}/{}]", + resource.getKind(), + resource.getMetadata().getNamespace(), resource.getMetadata().getName(), exception ); From 7ee3c81e1a3d1448d9f4165cc30829c9008e92a4 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 16:58:49 +0100 Subject: [PATCH 07/11] remove the duplicate error log of a failed ClusterConnection check handleError already logs the exception with the kind, the namespace and the name of the resource. Assisted-by: Claude:claude-opus-5-5 --- .../crd/clusterconnection/ClusterConnectionReconciler.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java index 529af3d..d958019 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java @@ -43,8 +43,6 @@ public UpdateControl reconcile( return UpdateControl.patchStatus(resource); } catch (RuntimeException e) { - log.error("Failed to check database connectivity", e); - return handleError( resource, status, From a95b0cd5ff41393681e3a7d39bee9e6ec70bfe1c Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 17:04:46 +0100 Subject: [PATCH 08/11] fix the missing bullet before the first object in the Grant status message Assisted-by: Claude:claude-opus-5-5 --- .../it/aboutbits/postgresql/crd/grant/GrantReconciler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index 104e3e7..471626c 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -294,7 +294,7 @@ private UpdateControl reconcileInTransaction( setErrorPhase( resource, status, - "Did not grant or revoke any privileges as the listed %s objects do not exist [resource=%s/%s]%n%s".formatted( + "Did not grant or revoke any privileges as the listed %s objects do not exist [resource=%s/%s]%n • %s".formatted( objectType, namespace, name, @@ -413,7 +413,7 @@ private UpdateControl reconcileInTransaction( String message = null; if (!ownedObjects.isEmpty()) { - message = "The role is the owner of the listed %s objects and thus we did not need to grant or revoke any privileges from them. [resource=%s/%s]%n%s".formatted( + message = "The role is the owner of the listed %s objects and thus we did not need to grant or revoke any privileges from them. [resource=%s/%s]%n • %s".formatted( objectType, getResourceNamespaceOrOwn(resource, namespace), name, From a8cb2f2e4af57118980a64fd8bd67d36d5469c82 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 17:04:49 +0100 Subject: [PATCH 09/11] fix the comment of the updated flag in GrantReconciler Assisted-by: Claude:claude-opus-5-5 --- .../java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index 471626c..a0d1df7 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -306,7 +306,7 @@ private UpdateControl reconcileInTransaction( .rescheduleAfter(Duration.ofSeconds(60)); } - // Whether any grant or revoke statement was executed, only used for the log + // Whether any grant or revoke statement changes a privilege, only used for the log var updated = false; // 1. Reconcile objects explicitly listed in the Spec (processObjects). From b4744d7a33b236da39b510ecd46139b667203d80 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 17:15:10 +0100 Subject: [PATCH 10/11] test the missing and owned table messages of a Grant Assisted-by: Claude:claude-opus-5-5 --- .../crd/grant/GrantReconcilerTest.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java index c5a6a98..f09ed14 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/grant/GrantReconcilerTest.java @@ -913,6 +913,177 @@ void grantOnAllTables( ); } + @Test + @DisplayName("Should reconcile to ERROR and list the missing tables when a listed table does not exist") + void errorWhenTableDoesNotExist() { + // given + var clusterConnectionMain = given.one() + .clusterConnection() + .returnFirst(); + + var database = given.one() + .database() + .withClusterConnectionName(clusterConnectionMain.getMetadata().getName()) + .withReclaimPolicy(DELETE) + .returnFirst(); + + var clusterConnectionDb = given.one() + .clusterConnection() + .withDatabase(database.getSpec().getName()) + .returnFirst(); + + var schema = given.one() + .schema() + .withClusterConnectionName(clusterConnectionDb.getMetadata().getName()) + .withDatabase(database.getSpec().getName()) + .withReclaimPolicy(DELETE) + .returnFirst(); + + var role = given.one() + .role() + .withClusterConnectionName(clusterConnectionMain.getMetadata().getName()) + .returnFirst(); + + var tableName = "test_table"; + var missingTableName = "missing_table"; + createTable( + clusterConnectionDb, + database.getSpec().getName(), + schema.getSpec().getName(), + tableName + ); + + // when + var grant = given.one() + .grant() + .withClusterConnectionName(clusterConnectionDb.getMetadata().getName()) + .withDatabase(database.getSpec().getName()) + .withSchema(schema.getSpec().getName()) + .withRole(role.getSpec().getName()) + .withObjectType(TABLE) + .withObjects(tableName, missingTableName) + .withPrivileges(SELECT) + .returnFirst(); + + // then + assertThat(grant) + .isNotNull() + .extracting(Grant::getStatus) + .satisfies(status -> { + assertThat(status.getPhase()).isEqualTo(CRPhase.ERROR); + assertThat(status.getMessage()) + .startsWith("Did not grant or revoke any privileges") + .contains("[resource=%s/%s]".formatted( + grant.getMetadata().getNamespace(), + grant.getMetadata().getName() + )) + .endsWith("%n • \"%s\".\"%s\"".formatted( + schema.getSpec().getName(), + missingTableName + )) + .doesNotContain("\"%s\"".formatted(tableName)); + }); + + assertThatNoPrivileges( + clusterConnectionDb, + grant, + tableName + ); + + // cleanup + deleteTable( + clusterConnectionDb, + database.getSpec().getName(), + schema.getSpec().getName(), + tableName + ); + } + + @Test + @DisplayName("Should reconcile to READY and list the owned tables when the role is the owner of a listed table") + void readyWhenRoleOwnsTable() { + // given + var clusterConnectionMain = given.one() + .clusterConnection() + .returnFirst(); + + var database = given.one() + .database() + .withClusterConnectionName(clusterConnectionMain.getMetadata().getName()) + .withReclaimPolicy(DELETE) + .returnFirst(); + + var clusterConnectionDb = given.one() + .clusterConnection() + .withDatabase(database.getSpec().getName()) + .returnFirst(); + + var schema = given.one() + .schema() + .withClusterConnectionName(clusterConnectionDb.getMetadata().getName()) + .withDatabase(database.getSpec().getName()) + .withReclaimPolicy(DELETE) + .returnFirst(); + + var role = given.one() + .role() + .withClusterConnectionName(clusterConnectionMain.getMetadata().getName()) + .returnFirst(); + + var tableName = "test_table"; + createTable( + clusterConnectionDb, + database.getSpec().getName(), + schema.getSpec().getName(), + tableName + ); + changeTableOwner( + clusterConnectionDb, + database.getSpec().getName(), + schema.getSpec().getName(), + tableName, + role.getSpec().getName() + ); + + // when + var grant = given.one() + .grant() + .withClusterConnectionName(clusterConnectionDb.getMetadata().getName()) + .withDatabase(database.getSpec().getName()) + .withSchema(schema.getSpec().getName()) + .withRole(role.getSpec().getName()) + .withObjectType(TABLE) + .withObjects(tableName) + .withPrivileges(SELECT) + .returnFirst(); + + // then + assertThat(grant) + .isNotNull() + .extracting(Grant::getStatus) + .satisfies(status -> { + assertThat(status.getPhase()).isEqualTo(CRPhase.READY); + assertThat(status.getMessage()) + .startsWith("The role is the owner of the listed") + .contains("[resource=%s/%s]".formatted( + grant.getMetadata().getNamespace(), + grant.getMetadata().getName() + )) + .endsWith("%n • \"%s\".\"%s\"".formatted( + schema.getSpec().getName(), + tableName + )); + }); + + // cleanup: the role cannot be dropped while it owns the table + deleteTable( + clusterConnectionDb, + database.getSpec().getName(), + schema.getSpec().getName(), + tableName + ); + } + static Stream> provideAllSupportedPrivileges() { int version = ConfigProvider.getConfig().getValue("test.postgresql-major-version", Integer.class); @@ -1355,6 +1526,22 @@ private void createTableWithSerial( } } + private void changeTableOwner( + ClusterConnection clusterConnection, + String databaseName, + String schemaName, + String tableName, + String owner + ) { + try (var dsl = postgreSQLContextFactory.getDSLContext(clusterConnection, databaseName)) { + dsl.execute( + "alter table {0} owner to {1}", + quotedName(schemaName, tableName), + quotedName(owner) + ); + } + } + private void deleteTable( ClusterConnection clusterConnection, String databaseName, From dd7db4486a1187cc82d1ef223a8f900c5f14c03d Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Sun, 25 Oct 2026 17:16:18 +0100 Subject: [PATCH 11/11] use the namespace of the Grant in the owned objects message Assisted-by: Claude:claude-opus-5-5 --- .../java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index a0d1df7..fb8a594 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -415,7 +415,7 @@ private UpdateControl reconcileInTransaction( if (!ownedObjects.isEmpty()) { message = "The role is the owner of the listed %s objects and thus we did not need to grant or revoke any privileges from them. [resource=%s/%s]%n • %s".formatted( objectType, - getResourceNamespaceOrOwn(resource, namespace), + namespace, name, String.join("\n • ", ownedObjects) );