From 26b9f5389432b7d36eb1985cc6126c7e95a3426c Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Fri, 4 Sep 2026 11:38:02 +0200 Subject: [PATCH 1/5] fix doc, line feed and ignore extra properties for the `FileCredentials` fileRef --- docs/cluster-connection.md | 2 +- .../it/aboutbits/postgresql/core/FileRef.java | 66 +- .../postgresql/core/KubernetesService.java | 4 + .../core/KubernetesServiceTest.java | 839 +++++++++--------- 4 files changed, 471 insertions(+), 440 deletions(-) diff --git a/docs/cluster-connection.md b/docs/cluster-connection.md index 86905b9..8f1b922 100644 --- a/docs/cluster-connection.md +++ b/docs/cluster-connection.md @@ -35,7 +35,7 @@ The referenced secret must be of type `kubernetes.io/basic-auth` and contain the Use this option when the credentials are mounted as a file instead of a Kubernetes Secret. -### File format +#### File format The file must contain JSON with the following fields: diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java b/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java index ab73258..c111415 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java @@ -1,33 +1,33 @@ -package it.aboutbits.postgresql.core; - -import io.fabric8.generator.annotation.Required; -import io.fabric8.generator.annotation.ValidationRule; -import lombok.Getter; -import lombok.Setter; -import org.jspecify.annotations.NullMarked; - -/// A reference to a file inside the operator container. -/// -/// This class is used wherever a CRD spec needs to point to a specific file -/// The [#path] field identifies the file location within the container. -/// -/// ### Example usage in a CR manifest -/// -/// ```yaml -/// spec: -/// adminSecretFileRef: -/// path: "/mnt/secrets/db-credentials.json" -/// ``` -@Getter -@Setter -@NullMarked -public class FileRef { - /// The path to the file. - /// Must not be blank. - @Required - @ValidationRule( - value = "self.trim().size() > 0", - message = "The path must not be empty." - ) - private String path = ""; -} +package it.aboutbits.postgresql.core; + +import io.fabric8.generator.annotation.Required; +import io.fabric8.generator.annotation.ValidationRule; +import lombok.Getter; +import lombok.Setter; +import org.jspecify.annotations.NullMarked; + +/// A reference to a file inside the operator container. +/// +/// This class is used wherever a CRD spec needs to point to a specific file +/// The [#path] field identifies the file location within the container. +/// +/// ### Example usage in a CR manifest +/// +/// ```yaml +/// spec: +/// adminSecretFileRef: +/// path: "/mnt/secrets/db-credentials.json" +/// ``` +@Getter +@Setter +@NullMarked +public class FileRef { + /// The path to the file. + /// Must not be blank. + @Required + @ValidationRule( + value = "self.trim().size() > 0", + message = "The path must not be empty." + ) + private String path = ""; +} diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java b/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java index 33a4d78..0a36cee 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java @@ -1,5 +1,6 @@ package it.aboutbits.postgresql.core; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection; @@ -21,6 +22,9 @@ public final class KubernetesService { private final ObjectMapper objectMapper; + /// The JSON file may carry more keys than we need, for example, the AWS Secrets Manager + /// format also has `engine`, `host`, `port` and `dbname`. Unknown keys are ignored. + @JsonIgnoreProperties(ignoreUnknown = true) private record FileCredentials( @Nullable String username, @Nullable String password diff --git a/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java b/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java index 481133d..dd40ecd 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java @@ -1,406 +1,433 @@ -package it.aboutbits.postgresql.core; - -import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; -import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection; -import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnectionSpec; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@NullMarked -@EnableKubernetesMockClient(crud = true) -class KubernetesServiceTest { - private final KubernetesService service = new KubernetesService(new ObjectMapper()); - - @SuppressWarnings("NullAway.Init") - static KubernetesClient client; - - @TempDir - Path tempDir; - - @BeforeEach - void clearSecrets() { - client.secrets().inAnyNamespace().delete(); - } - - @Nested - class GetSecretFileRefCredentials { - @Test - @DisplayName("when both username and password present, should return credentials") - void whenBothUsernameAndPassword_shouldReturnCredentials() throws IOException { - // given - var file = tempDir.resolve("secret.json"); - Files.writeString(file, """ - {"username": "admin", "password": "s3cret"} - """); - - var fileRef = new FileRef(); - fileRef.setPath(file.toString()); - - // when - var result = service.getSecretFileRefCredentials(fileRef); - - // then - assertThat(result.username()).isEqualTo("admin"); - assertThat(result.password()).isEqualTo("s3cret"); - } - - @ParameterizedTest(name = "when username {0}, should throw") - @ValueSource(strings = { - "{\"password\": \"s3cret\"}", - "{\"username\": null, \"password\": \"s3cret\"}" - }) - void whenUsernameMissingOrNull_shouldThrow(String json) throws IOException { - // given - var file = tempDir.resolve("secret.json"); - Files.writeString(file, json); - - var fileRef = new FileRef(); - fileRef.setPath(file.toString()); - - // when / then - assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing required field 'username'"); - } - - @ParameterizedTest(name = "when password {0}, should throw") - @ValueSource(strings = { - "{\"username\": \"admin\"}", - "{\"username\": \"admin\", \"password\": null}" - }) - void whenPasswordMissingOrNull_shouldThrow(String json) throws IOException { - // given - var file = tempDir.resolve("secret.json"); - Files.writeString(file, json); - - var fileRef = new FileRef(); - fileRef.setPath(file.toString()); - - // when / then - assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing required field 'password'"); - } - - @ParameterizedTest(name = "when field has wrong type {0}, should throw") - @ValueSource(strings = { - "{\"username\": {}, \"password\": \"s3cret\"}", - "{\"username\": [], \"password\": \"s3cret\"}", - "{\"username\": \"admin\", \"password\": {}}", - "{\"username\": \"admin\", \"password\": []}" - }) - void whenFieldHasWrongType_shouldThrow(String json) throws IOException { - // given - var file = tempDir.resolve("secret.json"); - Files.writeString(file, json); - - var fileRef = new FileRef(); - fileRef.setPath(file.toString()); - - // when / then - assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Failed to read"); - } - - @Test - @DisplayName("when file not found, should throw") - void whenFileNotFound_shouldThrow() { - // given - var fileRef = new FileRef(); - fileRef.setPath(tempDir.resolve("nonexistent.json").toString()); - - // when / then - assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Credentials file not found"); - } - - @Test - @DisplayName("when invalid JSON, should throw") - void whenInvalidJson_shouldThrow() throws IOException { - // given - var file = tempDir.resolve("secret.json"); - Files.writeString(file, "not valid json {{{"); - - var fileRef = new FileRef(); - fileRef.setPath(file.toString()); - - // when / then - assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Failed to read"); - } - } - - @Nested - class GetSecretRefCredentials { - @Test - @DisplayName("when secret exists, should return decoded credentials") - void whenSecretExists_shouldReturnDecodedCredentials() { - // given - var secret = basicAuthSecret("my-ns", "my-secret", "admin", "s3cret"); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - // when - var result = service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns"); - - // then - assertThat(result.username()).isEqualTo("admin"); - assertThat(result.password()).isEqualTo("s3cret"); - } - - @Test - @DisplayName("when secret not found, should throw") - void whenSecretNotFound_shouldThrow() { - // given — no secret created - - // when / then - assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Secret reference not found"); - } - - @Test - @DisplayName("when secret wrong type, should throw") - void whenSecretWrongType_shouldThrow() { - // given - var secret = new SecretBuilder() - .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() - .withType("Opaque") - .addToData("password", base64("s3cret")) - .build(); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - // when / then - assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("wrong type"); - } - - @Test - @DisplayName("when secret has no data, should throw") - void whenSecretHasNoData_shouldThrow() { - // given - var secret = new SecretBuilder() - .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() - .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) - .build(); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - // when / then - assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("has no data set"); - } - - @Test - @DisplayName("when namespace null, should use default namespace") - void whenNamespaceNull_shouldUseDefaultNamespace() { - // given - var secret = basicAuthSecret("default-ns", "my-secret", "admin", "s3cret"); - client.secrets().inNamespace("default-ns").resource(secret).create(); - - // when - var result = service.getSecretRefCredentials(client, secretRef(null, "my-secret"), "default-ns"); - - // then - assertThat(result.username()).isEqualTo("admin"); - assertThat(result.password()).isEqualTo("s3cret"); - } - - @Test - @DisplayName("when secret has empty data map, should throw") - void whenSecretHasEmptyDataMap_shouldThrow() { - // given - var secret = new SecretBuilder() - .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() - .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) - .withData(Map.of()) - .build(); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - // when / then - assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("has no data set"); - } - - @Test - @DisplayName("when secret has username only (no password), should throw") - void whenSecretHasUsernameOnly_shouldThrow() { - // given - var secret = new SecretBuilder() - .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() - .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) - .addToData("username", base64("admin")) - .build(); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - // when / then - assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing required data password"); - } - - @Test - @DisplayName("when secret has password only (no username), should return null username") - void whenSecretHasPasswordOnly_shouldReturnNullUsername() { - // given - var secret = new SecretBuilder() - .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() - .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) - .addToData("password", base64("s3cret")) - .build(); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - // when - var result = service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns"); - - // then - assertThat(result.username()).isNull(); - assertThat(result.password()).isEqualTo("s3cret"); - } - } - - @Nested - class GetAdminCredentials { - @Test - @DisplayName("when only adminSecretRef set, should delegate to secret ref") - void whenOnlyAdminSecretRefSet_shouldDelegateToSecretRef() { - // given - var secret = basicAuthSecret("my-ns", "my-secret", "admin", "s3cret"); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - var spec = new ClusterConnectionSpec(); - spec.setAdminSecretRef(secretRef("my-ns", "my-secret")); - - var clusterConnection = buildClusterConnection(spec, "cr-ns"); - - // when - var result = service.getAdminCredentials(client, clusterConnection); - - // then - assertThat(result.username()).isEqualTo("admin"); - assertThat(result.password()).isEqualTo("s3cret"); - } - - @Test - @DisplayName("when only adminSecretFileRef set, should delegate to file ref") - void whenOnlyAdminSecretFileRefSet_shouldDelegateToFileRef() throws IOException { - // given - var file = tempDir.resolve("secret.json"); - Files.writeString(file, """ - {"username": "file-admin", "password": "file-s3cret"} - """); - - var fileRef = new FileRef(); - fileRef.setPath(file.toString()); - - var spec = new ClusterConnectionSpec(); - spec.setAdminSecretFileRef(fileRef); - - var clusterConnection = buildClusterConnection(spec, "cr-ns"); - - // when - var result = service.getAdminCredentials(client, clusterConnection); - - // then - assertThat(result.username()).isEqualTo("file-admin"); - assertThat(result.password()).isEqualTo("file-s3cret"); - } - - @Test - @DisplayName("when adminSecretRef missing username, should throw with message not NPE") - void whenAdminSecretRefMissingUsername_shouldThrowWithMessage() { - // given - var secret = new SecretBuilder() - .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() - .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) - .addToData("password", base64("s3cret")) - .build(); - client.secrets().inNamespace("my-ns").resource(secret).create(); - - var spec = new ClusterConnectionSpec(); - spec.setAdminSecretRef(secretRef("my-ns", "my-secret")); - - var clusterConnection = buildClusterConnection(spec, "cr-ns"); - - // when / then - assertThatThrownBy(() -> service.getAdminCredentials(client, clusterConnection)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("missing required data username"); - } - - @Test - @DisplayName("when neither ref set, should throw") - void whenNeitherRefSet_shouldThrow() { - // given - var spec = new ClusterConnectionSpec(); - - var clusterConnection = buildClusterConnection(spec, "cr-ns"); - - // when / then - assertThatThrownBy(() -> service.getAdminCredentials(client, clusterConnection)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Exactly one of"); - } - } - - private static ClusterConnection buildClusterConnection(ClusterConnectionSpec spec, String namespace) { - var meta = new ObjectMeta(); - meta.setNamespace(namespace); - - var clusterConnection = new ClusterConnection(); - clusterConnection.setSpec(spec); - clusterConnection.setMetadata(meta); - - return clusterConnection; - } - - private static ResourceRef secretRef(@Nullable String namespace, String name) { - var ref = new ResourceRef(); - ref.setNamespace(namespace); - ref.setName(name); - return ref; - } - - private static Secret basicAuthSecret(String namespace, String name, - @Nullable String username, String password) { - var builder = new SecretBuilder() - .withNewMetadata().withNamespace(namespace).withName(name).endMetadata() - .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) - .addToData("password", base64(password)); - if (username != null) { - builder.addToData("username", base64(username)); - } - return builder.build(); - } - - private static String base64(String value) { - return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset())); - } -} +package it.aboutbits.postgresql.core; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection; +import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnectionSpec; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@NullMarked +@EnableKubernetesMockClient(crud = true) +class KubernetesServiceTest { + private final KubernetesService service = new KubernetesService(new ObjectMapper()); + + @SuppressWarnings("NullAway.Init") + static KubernetesClient client; + + @TempDir + Path tempDir; + + @BeforeEach + void clearSecrets() { + client.secrets().inAnyNamespace().delete(); + } + + @Nested + class GetSecretFileRefCredentials { + @Test + @DisplayName("when both username and password present, should return credentials") + void whenBothUsernameAndPassword_shouldReturnCredentials() throws IOException { + // given + var file = tempDir.resolve("secret.json"); + Files.writeString(file, """ + {"username": "admin", "password": "s3cret"} + """); + + var fileRef = new FileRef(); + fileRef.setPath(file.toString()); + + // when + var result = service.getSecretFileRefCredentials(fileRef); + + // then + assertThat(result.username()).isEqualTo("admin"); + assertThat(result.password()).isEqualTo("s3cret"); + } + + @Test + @DisplayName("when file has extra keys (AWS Secrets Manager format), should ignore them") + void whenFileHasExtraKeys_shouldIgnoreThem() throws IOException { + // given + var file = tempDir.resolve("secret.json"); + Files.writeString(file, """ + { + "engine": "postgres", + "host": "db.example.com", + "username": "admin", + "password": "s3cret", + "dbname": "postgres", + "port": 5432 + } + """); + + var fileRef = new FileRef(); + fileRef.setPath(file.toString()); + + // when + var result = service.getSecretFileRefCredentials(fileRef); + + // then + assertThat(result.username()).isEqualTo("admin"); + assertThat(result.password()).isEqualTo("s3cret"); + } + + @ParameterizedTest(name = "when username {0}, should throw") + @ValueSource(strings = { + "{\"password\": \"s3cret\"}", + "{\"username\": null, \"password\": \"s3cret\"}" + }) + void whenUsernameMissingOrNull_shouldThrow(String json) throws IOException { + // given + var file = tempDir.resolve("secret.json"); + Files.writeString(file, json); + + var fileRef = new FileRef(); + fileRef.setPath(file.toString()); + + // when / then + assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("missing required field 'username'"); + } + + @ParameterizedTest(name = "when password {0}, should throw") + @ValueSource(strings = { + "{\"username\": \"admin\"}", + "{\"username\": \"admin\", \"password\": null}" + }) + void whenPasswordMissingOrNull_shouldThrow(String json) throws IOException { + // given + var file = tempDir.resolve("secret.json"); + Files.writeString(file, json); + + var fileRef = new FileRef(); + fileRef.setPath(file.toString()); + + // when / then + assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("missing required field 'password'"); + } + + @ParameterizedTest(name = "when field has wrong type {0}, should throw") + @ValueSource(strings = { + "{\"username\": {}, \"password\": \"s3cret\"}", + "{\"username\": [], \"password\": \"s3cret\"}", + "{\"username\": \"admin\", \"password\": {}}", + "{\"username\": \"admin\", \"password\": []}" + }) + void whenFieldHasWrongType_shouldThrow(String json) throws IOException { + // given + var file = tempDir.resolve("secret.json"); + Files.writeString(file, json); + + var fileRef = new FileRef(); + fileRef.setPath(file.toString()); + + // when / then + assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to read"); + } + + @Test + @DisplayName("when file not found, should throw") + void whenFileNotFound_shouldThrow() { + // given + var fileRef = new FileRef(); + fileRef.setPath(tempDir.resolve("nonexistent.json").toString()); + + // when / then + assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Credentials file not found"); + } + + @Test + @DisplayName("when invalid JSON, should throw") + void whenInvalidJson_shouldThrow() throws IOException { + // given + var file = tempDir.resolve("secret.json"); + Files.writeString(file, "not valid json {{{"); + + var fileRef = new FileRef(); + fileRef.setPath(file.toString()); + + // when / then + assertThatThrownBy(() -> service.getSecretFileRefCredentials(fileRef)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to read"); + } + } + + @Nested + class GetSecretRefCredentials { + @Test + @DisplayName("when secret exists, should return decoded credentials") + void whenSecretExists_shouldReturnDecodedCredentials() { + // given + var secret = basicAuthSecret("my-ns", "my-secret", "admin", "s3cret"); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + // when + var result = service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns"); + + // then + assertThat(result.username()).isEqualTo("admin"); + assertThat(result.password()).isEqualTo("s3cret"); + } + + @Test + @DisplayName("when secret not found, should throw") + void whenSecretNotFound_shouldThrow() { + // given — no secret created + + // when / then + assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Secret reference not found"); + } + + @Test + @DisplayName("when secret wrong type, should throw") + void whenSecretWrongType_shouldThrow() { + // given + var secret = new SecretBuilder() + .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() + .withType("Opaque") + .addToData("password", base64("s3cret")) + .build(); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + // when / then + assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("wrong type"); + } + + @Test + @DisplayName("when secret has no data, should throw") + void whenSecretHasNoData_shouldThrow() { + // given + var secret = new SecretBuilder() + .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() + .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) + .build(); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + // when / then + assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("has no data set"); + } + + @Test + @DisplayName("when namespace null, should use default namespace") + void whenNamespaceNull_shouldUseDefaultNamespace() { + // given + var secret = basicAuthSecret("default-ns", "my-secret", "admin", "s3cret"); + client.secrets().inNamespace("default-ns").resource(secret).create(); + + // when + var result = service.getSecretRefCredentials(client, secretRef(null, "my-secret"), "default-ns"); + + // then + assertThat(result.username()).isEqualTo("admin"); + assertThat(result.password()).isEqualTo("s3cret"); + } + + @Test + @DisplayName("when secret has empty data map, should throw") + void whenSecretHasEmptyDataMap_shouldThrow() { + // given + var secret = new SecretBuilder() + .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() + .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) + .withData(Map.of()) + .build(); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + // when / then + assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("has no data set"); + } + + @Test + @DisplayName("when secret has username only (no password), should throw") + void whenSecretHasUsernameOnly_shouldThrow() { + // given + var secret = new SecretBuilder() + .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() + .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) + .addToData("username", base64("admin")) + .build(); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + // when / then + assertThatThrownBy(() -> service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("missing required data password"); + } + + @Test + @DisplayName("when secret has password only (no username), should return null username") + void whenSecretHasPasswordOnly_shouldReturnNullUsername() { + // given + var secret = new SecretBuilder() + .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() + .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) + .addToData("password", base64("s3cret")) + .build(); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + // when + var result = service.getSecretRefCredentials(client, secretRef("my-ns", "my-secret"), "default-ns"); + + // then + assertThat(result.username()).isNull(); + assertThat(result.password()).isEqualTo("s3cret"); + } + } + + @Nested + class GetAdminCredentials { + @Test + @DisplayName("when only adminSecretRef set, should delegate to secret ref") + void whenOnlyAdminSecretRefSet_shouldDelegateToSecretRef() { + // given + var secret = basicAuthSecret("my-ns", "my-secret", "admin", "s3cret"); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + var spec = new ClusterConnectionSpec(); + spec.setAdminSecretRef(secretRef("my-ns", "my-secret")); + + var clusterConnection = buildClusterConnection(spec, "cr-ns"); + + // when + var result = service.getAdminCredentials(client, clusterConnection); + + // then + assertThat(result.username()).isEqualTo("admin"); + assertThat(result.password()).isEqualTo("s3cret"); + } + + @Test + @DisplayName("when only adminSecretFileRef set, should delegate to file ref") + void whenOnlyAdminSecretFileRefSet_shouldDelegateToFileRef() throws IOException { + // given + var file = tempDir.resolve("secret.json"); + Files.writeString(file, """ + {"username": "file-admin", "password": "file-s3cret"} + """); + + var fileRef = new FileRef(); + fileRef.setPath(file.toString()); + + var spec = new ClusterConnectionSpec(); + spec.setAdminSecretFileRef(fileRef); + + var clusterConnection = buildClusterConnection(spec, "cr-ns"); + + // when + var result = service.getAdminCredentials(client, clusterConnection); + + // then + assertThat(result.username()).isEqualTo("file-admin"); + assertThat(result.password()).isEqualTo("file-s3cret"); + } + + @Test + @DisplayName("when adminSecretRef missing username, should throw with message not NPE") + void whenAdminSecretRefMissingUsername_shouldThrowWithMessage() { + // given + var secret = new SecretBuilder() + .withNewMetadata().withNamespace("my-ns").withName("my-secret").endMetadata() + .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) + .addToData("password", base64("s3cret")) + .build(); + client.secrets().inNamespace("my-ns").resource(secret).create(); + + var spec = new ClusterConnectionSpec(); + spec.setAdminSecretRef(secretRef("my-ns", "my-secret")); + + var clusterConnection = buildClusterConnection(spec, "cr-ns"); + + // when / then + assertThatThrownBy(() -> service.getAdminCredentials(client, clusterConnection)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("missing required data username"); + } + + @Test + @DisplayName("when neither ref set, should throw") + void whenNeitherRefSet_shouldThrow() { + // given + var spec = new ClusterConnectionSpec(); + + var clusterConnection = buildClusterConnection(spec, "cr-ns"); + + // when / then + assertThatThrownBy(() -> service.getAdminCredentials(client, clusterConnection)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Exactly one of"); + } + } + + private static ClusterConnection buildClusterConnection(ClusterConnectionSpec spec, String namespace) { + var meta = new ObjectMeta(); + meta.setNamespace(namespace); + + var clusterConnection = new ClusterConnection(); + clusterConnection.setSpec(spec); + clusterConnection.setMetadata(meta); + + return clusterConnection; + } + + private static ResourceRef secretRef(@Nullable String namespace, String name) { + var ref = new ResourceRef(); + ref.setNamespace(namespace); + ref.setName(name); + return ref; + } + + private static Secret basicAuthSecret(String namespace, String name, + @Nullable String username, String password) { + var builder = new SecretBuilder() + .withNewMetadata().withNamespace(namespace).withName(name).endMetadata() + .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) + .addToData("password", base64(password)); + if (username != null) { + builder.addToData("username", base64(username)); + } + return builder.build(); + } + + private static String base64(String value) { + return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset())); + } +} From 9f5d13c7d732581869388d47cedd4d1bd91ed5c2 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Fri, 4 Sep 2026 11:44:39 +0200 Subject: [PATCH 2/5] reorder members and formatting --- .../postgresql/core/KubernetesService.java | 42 ++++---- .../core/KubernetesServiceTest.java | 34 ++++--- .../ClusterConnectionReconcilerTest.java | 95 ++++++++++--------- 3 files changed, 93 insertions(+), 78 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java b/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java index 0a36cee..0ca6bd1 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java @@ -20,21 +20,12 @@ @RequiredArgsConstructor @NullMarked public final class KubernetesService { - private final ObjectMapper objectMapper; - - /// The JSON file may carry more keys than we need, for example, the AWS Secrets Manager - /// format also has `engine`, `host`, `port` and `dbname`. Unknown keys are ignored. - @JsonIgnoreProperties(ignoreUnknown = true) - private record FileCredentials( - @Nullable String username, - @Nullable String password - ) { - } - public static final String SECRET_TYPE_BASIC_AUTH = "kubernetes.io/basic-auth"; public static final String SECRET_DATA_BASIC_AUTH_USERNAME_KEY = "username"; public static final String SECRET_DATA_BASIC_AUTH_PASSWORD_KEY = "password"; + private final ObjectMapper objectMapper; + public Credentials getAdminCredentials( KubernetesClient kubernetesClient, ClusterConnection clusterConnection @@ -43,6 +34,7 @@ public Credentials getAdminCredentials( if (spec.getAdminSecretRef() != null) { var secretRef = spec.getAdminSecretRef(); var defaultNamespace = clusterConnection.getMetadata().getNamespace(); + var credentials = getSecretRefCredentials(kubernetesClient, secretRef, defaultNamespace); if (credentials.username() == null) { var secretNamespace = getSecretNamespace(secretRef, defaultNamespace); @@ -50,6 +42,7 @@ public Credentials getAdminCredentials( "The Secret reference is missing required data username [secret.namespace=%s, secret.name=%s]".formatted( secretNamespace, secretRef.getName())); } + return credentials; } else if (spec.getAdminSecretFileRef() != null) { return getSecretFileRefCredentials(spec.getAdminSecretFileRef()); @@ -64,20 +57,17 @@ public Credentials getSecretFileRefCredentials(FileRef fileRef) { try (var in = Files.newInputStream(path)) { var file = objectMapper.readValue(in, FileCredentials.class); if (file.username() == null) { - throw new IllegalStateException( - "Credentials file is missing required field 'username' [path=%s]".formatted(path)); + throw new IllegalStateException("Credentials file is missing required field 'username' [path=%s]".formatted(path)); } if (file.password() == null) { - throw new IllegalStateException( - "Credentials file is missing required field 'password' [path=%s]".formatted(path)); + throw new IllegalStateException("Credentials file is missing required field 'password' [path=%s]".formatted(path)); } + return new Credentials(file.username(), file.password()); } catch (NoSuchFileException e) { - throw new IllegalStateException( - "Credentials file not found [path=%s]".formatted(path), e); + throw new IllegalStateException("Credentials file not found [path=%s]".formatted(path), e); } catch (IOException e) { - throw new IllegalStateException( - "Failed to read the credentials file [path=%s]".formatted(path), e); + throw new IllegalStateException("Failed to read the credentials file [path=%s]".formatted(path), e); } } @@ -145,9 +135,21 @@ public Credentials getSecretRefCredentials( ); } - private String getSecretNamespace(ResourceRef secretRef, String defaultNamespace) { + private String getSecretNamespace( + ResourceRef secretRef, + String defaultNamespace + ) { return secretRef.getNamespace() != null ? secretRef.getNamespace() : defaultNamespace; } + + /// The JSON file may carry more keys than we need, for example, the AWS Secrets Manager + /// format also has `engine`, `host`, `port` and `dbname`. Unknown keys are ignored. + @JsonIgnoreProperties(ignoreUnknown = true) + private record FileCredentials( + @Nullable String username, + @Nullable String password + ) { + } } diff --git a/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java b/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java index dd40ecd..050e080 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/core/KubernetesServiceTest.java @@ -1,5 +1,6 @@ package it.aboutbits.postgresql.core; +import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; @@ -17,8 +18,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import com.fasterxml.jackson.databind.ObjectMapper; - import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; @@ -32,14 +31,14 @@ @NullMarked @EnableKubernetesMockClient(crud = true) class KubernetesServiceTest { - private final KubernetesService service = new KubernetesService(new ObjectMapper()); - @SuppressWarnings("NullAway.Init") static KubernetesClient client; @TempDir Path tempDir; + private final KubernetesService service = new KubernetesService(new ObjectMapper()); + @BeforeEach void clearSecrets() { client.secrets().inAnyNamespace().delete(); @@ -52,9 +51,12 @@ class GetSecretFileRefCredentials { void whenBothUsernameAndPassword_shouldReturnCredentials() throws IOException { // given var file = tempDir.resolve("secret.json"); - Files.writeString(file, """ + Files.writeString( + file, + """ {"username": "admin", "password": "s3cret"} - """); + """ + ); var fileRef = new FileRef(); fileRef.setPath(file.toString()); @@ -72,7 +74,9 @@ void whenBothUsernameAndPassword_shouldReturnCredentials() throws IOException { void whenFileHasExtraKeys_shouldIgnoreThem() throws IOException { // given var file = tempDir.resolve("secret.json"); - Files.writeString(file, """ + Files.writeString( + file, + """ { "engine": "postgres", "host": "db.example.com", @@ -81,7 +85,8 @@ void whenFileHasExtraKeys_shouldIgnoreThem() throws IOException { "dbname": "postgres", "port": 5432 } - """); + """ + ); var fileRef = new FileRef(); fileRef.setPath(file.toString()); @@ -340,9 +345,12 @@ void whenOnlyAdminSecretRefSet_shouldDelegateToSecretRef() { void whenOnlyAdminSecretFileRefSet_shouldDelegateToFileRef() throws IOException { // given var file = tempDir.resolve("secret.json"); - Files.writeString(file, """ + Files.writeString( + file, + """ {"username": "file-admin", "password": "file-s3cret"} - """); + """ + ); var fileRef = new FileRef(); fileRef.setPath(file.toString()); @@ -415,8 +423,10 @@ private static ResourceRef secretRef(@Nullable String namespace, String name) { return ref; } - private static Secret basicAuthSecret(String namespace, String name, - @Nullable String username, String password) { + private static Secret basicAuthSecret( + String namespace, String name, + @Nullable String username, String password + ) { var builder = new SecretBuilder() .withNewMetadata().withNamespace(namespace).withName(name).endMetadata() .withType(KubernetesService.SECRET_TYPE_BASIC_AUTH) diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java index e5b29dd..12315ee 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java @@ -57,6 +57,50 @@ void resetEnvironment() { TestUtil.resetEnvironment(kubernetesClient); } + @Nested + class CRDValidation { + @Test + @DisplayName("when both adminSecretRef and adminSecretFileRef set, should reject") + void whenBothSet_shouldReject() { + var fileRef = new FileRef(); + fileRef.setPath("/mnt/secrets/db-credentials.json"); + + assertThatThrownBy(() -> given.one() + .clusterConnection() + .withAdminSecretFileRef(fileRef) + .returnFirst() + ).isInstanceOf(KubernetesClientException.class) + .hasMessageContaining("Exactly one of"); + } + + @Test + @DisplayName("when neither adminSecretRef nor adminSecretFileRef set, should reject") + void whenNeitherSet_shouldReject() { + assertThatThrownBy(() -> given.one() + .clusterConnection() + .withoutAdminSecret() + .returnFirst() + ).isInstanceOf(KubernetesClientException.class) + .hasMessageContaining("Exactly one of"); + } + + @ParameterizedTest + @BlankSource + @DisplayName("when adminSecretFileRef has blank path, should reject") + void whenFileRefBlankPath_shouldReject(String blankOrEmptyString) { + var fileRef = new FileRef(); + fileRef.setPath(blankOrEmptyString); + + assertThatThrownBy(() -> given.one() + .clusterConnection() + .withoutAdminSecret() + .withAdminSecretFileRef(fileRef) + .returnFirst() + ).isInstanceOf(KubernetesClientException.class) + .hasMessageContaining("must not be empty"); + } + } + @Test @DisplayName("When a ClusterConnection is created, the status should be ready") void createsCustomResource_andReconcilerStatusIsReady() { @@ -92,9 +136,12 @@ void createsCustomResourceWithFileRef_andReconcilerStatusIsReady() throws IOExce // given var credentialsFile = Files.createTempFile("db-credentials", ".json"); try { - Files.writeString(credentialsFile, """ + Files.writeString( + credentialsFile, + """ {"username": "%s", "password": "%s"} - """.formatted(dbUsername, dbPassword)); + """.formatted(dbUsername, dbPassword) + ); var fileRef = new FileRef(); fileRef.setPath(credentialsFile.toAbsolutePath().toString()); @@ -130,50 +177,6 @@ void createsCustomResourceWithFileRef_andReconcilerStatusIsReady() throws IOExce } } - @Nested - class CRDValidation { - @Test - @DisplayName("when both adminSecretRef and adminSecretFileRef set, should reject") - void whenBothSet_shouldReject() { - var fileRef = new FileRef(); - fileRef.setPath("/mnt/secrets/db-credentials.json"); - - assertThatThrownBy(() -> given.one() - .clusterConnection() - .withAdminSecretFileRef(fileRef) - .returnFirst() - ).isInstanceOf(KubernetesClientException.class) - .hasMessageContaining("Exactly one of"); - } - - @Test - @DisplayName("when neither adminSecretRef nor adminSecretFileRef set, should reject") - void whenNeitherSet_shouldReject() { - assertThatThrownBy(() -> given.one() - .clusterConnection() - .withoutAdminSecret() - .returnFirst() - ).isInstanceOf(KubernetesClientException.class) - .hasMessageContaining("Exactly one of"); - } - - @ParameterizedTest - @BlankSource - @DisplayName("when adminSecretFileRef has blank path, should reject") - void whenFileRefBlankPath_shouldReject(String blankOrEmptyString) { - var fileRef = new FileRef(); - fileRef.setPath(blankOrEmptyString); - - assertThatThrownBy(() -> given.one() - .clusterConnection() - .withoutAdminSecret() - .withAdminSecretFileRef(fileRef) - .returnFirst() - ).isInstanceOf(KubernetesClientException.class) - .hasMessageContaining("must not be empty"); - } - } - private static void assertThatClusterConnectionHasExpectedStatus( ClusterConnection clusterConnection, CRStatus expectedStatus, From 6779106b69299b3f574ca11d231712450e932a10 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Fri, 4 Sep 2026 12:05:32 +0200 Subject: [PATCH 3/5] simplify the ClusterConnectionSpec validation rule --- .../postgresql/crd/clusterconnection/ClusterConnectionSpec.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java index ebde156..1961bff 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java @@ -21,7 +21,7 @@ @SchemaCustomizer(value = HostCustomizer.class, input = "host") @NullMarked @ValidationRule( - value = "(has(self.adminSecretRef) ? 1 : 0) + (has(self.adminSecretFileRef) ? 1 : 0) == 1", + value = "has(self.adminSecretRef) != has(self.adminSecretFileRef)", message = "Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided" ) public class ClusterConnectionSpec { From 5deb1c673589223fb767a6db2cf038855b7dad3d Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Fri, 4 Sep 2026 12:06:29 +0200 Subject: [PATCH 4/5] remove forward-looking usages from the FileRef javadoc --- .../main/java/it/aboutbits/postgresql/core/FileRef.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java b/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java index c111415..11520ef 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java @@ -10,14 +10,6 @@ /// /// This class is used wherever a CRD spec needs to point to a specific file /// The [#path] field identifies the file location within the container. -/// -/// ### Example usage in a CR manifest -/// -/// ```yaml -/// spec: -/// adminSecretFileRef: -/// path: "/mnt/secrets/db-credentials.json" -/// ``` @Getter @Setter @NullMarked From d3e6902a07fbb7545af66af868cbdbaea4a4aeda Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Fri, 4 Sep 2026 12:06:57 +0200 Subject: [PATCH 5/5] use `var` for AtomicReference --- .../clusterconnection/ClusterConnectionReconcilerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java index 12315ee..34d4fcf 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconcilerTest.java @@ -111,7 +111,7 @@ void createsCustomResource_andReconcilerStatusIsReady() { .returnFirst(); // then - AtomicReference<@Nullable DSLContext> dslAtomic = new AtomicReference<>(); + var dslAtomic = new AtomicReference<@Nullable DSLContext>(); assertThatNoException().isThrownBy( () -> dslAtomic.set(postgreSQLContextFactory.getDSLContext(customResource)) ); @@ -155,7 +155,7 @@ void createsCustomResourceWithFileRef_andReconcilerStatusIsReady() throws IOExce .returnFirst(); // then - AtomicReference<@Nullable DSLContext> dslAtomic = new AtomicReference<>(); + var dslAtomic = new AtomicReference<@Nullable DSLContext>(); assertThatNoException().isThrownBy( () -> dslAtomic.set(postgreSQLContextFactory.getDSLContext(customResource)) );