diff --git a/naisful-postgres/README.md b/naisful-postgres/README.md new file mode 100644 index 0000000..8794dc7 --- /dev/null +++ b/naisful-postgres/README.md @@ -0,0 +1,40 @@ +Naisful Postgres +================ + +Enkel måte å komme kjapt i gang med en databasetilkobling. + +Auto-detecter miljøvariabler og lager JDBC-url for deg. + +## Fra miljøvariabler +```kotlin + +// lager jdbc-url fra env +val jdbcUrl = defaultJdbcUrl() +``` + +Miljøvariabler som slutter på `_HOST`, `_USERNAME` osv. vil bli brukt. +Hvis appen har flere slike miljøvariabler så kan de skilles ved å spesifisere prefix: +```kotlin +ConnectionConfigFactory.Env(envVarPrefix = "DB") +``` +Da vil det søkes etter `DB_HOST`, `DB_USERNAME`, osv. + +## Fra mount path +Hvis du vil laste sql secrets fra mount path så kan de konfigureres slik i nais.yml. +```yml + filesFrom: + - secret: google-sql-APP + mountPath: /var/run/secrets/sql/APP +``` +og så gjøre dette i kotlin: +```kotlin +// lager jdbc-url fra mount path +val jdbcUrl = defaultJdbcUrl(ConnectionConfigFactory.MountPath("/var/run/secrets/sql/APP")) +``` + +## Google SocketFactory + +Det er også en variant som vil konfe opp `com.google.cloud.sql.postgres.SocketFactory` i JDBC-url: +```kotlin +val jdbcUrl = jdbcUrlWithGoogleSocketFactory("dbinstance", ConnectionConfigFactory.Env()) +``` \ No newline at end of file diff --git a/naisful-postgres/build.gradle.kts b/naisful-postgres/build.gradle.kts new file mode 100644 index 0000000..e69de29 diff --git a/naisful-postgres/src/main/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfig.kt b/naisful-postgres/src/main/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfig.kt new file mode 100644 index 0000000..de47275 --- /dev/null +++ b/naisful-postgres/src/main/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfig.kt @@ -0,0 +1,92 @@ +package com.github.navikt.tbd_libs.naisful.postgres + +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.plus +import kotlin.io.path.Path +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.readText + + +/** + * bruker _JDBC_URL hvis den er satt, ellers bygges opp en jdbc-url: + * @see nais doc + */ +fun defaultJdbcUrl(metode: ConnectionConfigFactory = ConnectionConfigFactory.Env(), options: Map = emptyMap()): String? { + return metode.buildJdbcUrl(options) +} + +/** + * bruker _JDBC_URL hvis den er satt, ellers bygges opp en jdbc-url med gitte options. + * det betyr at socketFactory kun brukes hvis _JDBC_URL ikke finnes + * + * @see nais doc + */ +fun jdbcUrlWithGoogleSocketFactory(databaseInstance: String, metode: ConnectionConfigFactory, gcpTeamProjectId: String = System.getenv("GCP_TEAM_PROJECT_ID"), databaseRegion: String = "europe-north1"): String? { + return defaultJdbcUrl(metode, mapOf( + "socketFactory" to "com.google.cloud.sql.postgres.SocketFactory", + "cloudSqlInstance" to "$gcpTeamProjectId:$databaseRegion:$databaseInstance" + )) +} + +sealed class ConnectionConfigFactory { + abstract fun buildJdbcUrl(options: Map): String? + + + data class Env(val env: Map = System.getenv(), val envVarPrefix: String? = null) : ConnectionConfigFactory() { + override fun buildJdbcUrl(options: Map): String? { + return buildJdbcUrl(options) { key -> + env.getKeySuffixOrNull(envVarPrefix, key) + } + } + } + data class MountPath(val path: String) : ConnectionConfigFactory() { + override fun buildJdbcUrl(options: Map): String? { + val secretsPath = Path(path).listDirectoryEntries() + return buildJdbcUrl(options) { key -> + secretsPath.firstOrNull { it.name.endsWith(key) }?.readText() + } + } + } + + private companion object { + private fun buildJdbcUrl(options: Map, strategi: (String) -> String?): String? { + val jdbcUrlFromPlatform = strategi("_JDBC_URL") + if (jdbcUrlFromPlatform != null) return jdbcUrlFromPlatform + + val hostname = strategi("_HOST") ?: return null + val port = strategi("_PORT")?.toInt() ?: return null + val databaseName = strategi("_DATABASE") ?: return null + val username = strategi("_USERNAME") ?: return null + val password = strategi("_PASSWORD") ?: return null + + val sslOptions = buildMap { + strategi("_SSLCERT")?.also { this["sslcert"] = it } + strategi("_SSLROOTCERT")?.also { this["sslrootcert"] = it } + strategi("_SSLKEY_PK8")?.also { this["sslkey"] = it } + strategi("_SSLMODE")?.also { this["sslmode"] = it } + } + + return buildPostgresCompliantJdbcUrl(hostname, port, databaseName, username, password, options + sslOptions) + } + } +} + +fun Map.getKeySuffixOrNull(prefix: String?, suffix: String): String? { + val searchKey = "${prefix ?: ""}$suffix" + return entries.firstOrNull { (k, _) -> k.endsWith(searchKey) }?.value +} + +private fun buildPostgresCompliantJdbcUrl(hostname: String, port: Int, databaseName: String, username: String, password: String, options: Map = emptyMap()): String { + val defaultOptions = mapOf( + "user" to username, + "password" to password + ) + val optionsString = optionsString(defaultOptions + options) + return "jdbc:postgresql://$hostname:$port/$databaseName?$optionsString" +} + +private fun optionsString(options: Map): String { + return (options).entries.joinToString(separator = "&") { (k, v) -> "$k=$v" } +} \ No newline at end of file diff --git a/naisful-postgres/src/test/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfigEnvTest.kt b/naisful-postgres/src/test/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfigEnvTest.kt new file mode 100644 index 0000000..7c3f410 --- /dev/null +++ b/naisful-postgres/src/test/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfigEnvTest.kt @@ -0,0 +1,80 @@ +package com.github.navikt.tbd_libs.naisful.postgres + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class DataSourceConfigEnvTest { + + @Test + fun `default jdbc url`() { + val fakeEnv = mapOf( + "DB_HOST" to "localhost", + "DB_PORT" to "5432", + "DB_DATABASE" to "postgres", + "DB_USERNAME" to "username", + "DB_PASSWORD" to "secret", + ) + val jdbcUrl = defaultJdbcUrl(ConnectionConfigFactory.Env(fakeEnv)) + assertEquals("jdbc:postgresql://localhost:5432/postgres?user=username&password=secret", jdbcUrl) + } + + @Test + fun `default jdbc url - jdbc_url set`() { + val fakeEnv = mapOf( + "DB_HOST" to "localhost", + "DB_PORT" to "5432", + "DB_DATABASE" to "postgres", + "DB_USERNAME" to "username", + "DB_PASSWORD" to "secret", + "DB_JDBC_URL" to "jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar", + ) + val jdbcUrl = defaultJdbcUrl(ConnectionConfigFactory.Env(fakeEnv)) + assertEquals("jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar", jdbcUrl) + } + + @Test + fun `default jdbc url - env with prefix`() { + val fakeEnv = mapOf( + "CONFLICTING_HOST" to "remote_ip", + "CONFLICTING_PORT" to "2345", + "CONFLICTING_DATABASE" to "dev-db", + "CONFLICTING_USERNAME" to "willy", + "CONFLICTING_PASSWORD" to "wonka", + + "DB_HOST" to "localhost", + "DB_PORT" to "5432", + "DB_DATABASE" to "postgres", + "DB_USERNAME" to "username", + "DB_PASSWORD" to "secret", + ) + val jdbcUrl = defaultJdbcUrl(ConnectionConfigFactory.Env(fakeEnv, envVarPrefix = "DB")) + assertEquals("jdbc:postgresql://localhost:5432/postgres?user=username&password=secret", jdbcUrl) + } + + @Test + fun `default jdbc url - google factory`() { + val fakeEnv = mapOf( + "DB_HOST" to "localhost", + "DB_PORT" to "5432", + "DB_DATABASE" to "postgres", + "DB_USERNAME" to "username", + "DB_PASSWORD" to "secret", + ) + val jdbcUrl = jdbcUrlWithGoogleSocketFactory("dbinstance", ConnectionConfigFactory.Env(fakeEnv), gcpTeamProjectId = "project_id") + assertEquals("jdbc:postgresql://localhost:5432/postgres?user=username&password=secret&socketFactory=com.google.cloud.sql.postgres.SocketFactory&cloudSqlInstance=project_id:europe-north1:dbinstance", jdbcUrl) + } + + @Test + fun `default jdbc url - google factory - with jdbc_url set`() { + val fakeEnv = mapOf( + "DB_HOST" to "localhost", + "DB_PORT" to "5432", + "DB_DATABASE" to "postgres", + "DB_USERNAME" to "username", + "DB_PASSWORD" to "secret", + "DB_JDBC_URL" to "jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar", + ) + val jdbcUrl = jdbcUrlWithGoogleSocketFactory("dbinstance", ConnectionConfigFactory.Env(fakeEnv), gcpTeamProjectId = "project_id") + assertEquals("jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar", jdbcUrl) + } +} \ No newline at end of file diff --git a/naisful-postgres/src/test/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfigMountPathTest.kt b/naisful-postgres/src/test/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfigMountPathTest.kt new file mode 100644 index 0000000..2722350 --- /dev/null +++ b/naisful-postgres/src/test/kotlin/com/github/navikt/tbd_libs/naisful/postgres/DataSourceConfigMountPathTest.kt @@ -0,0 +1,56 @@ +package com.github.navikt.tbd_libs.naisful.postgres + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.writeText + +class DataSourceConfigMountPathTest { + + @Test + fun `default jdbc url - env`(@TempDir tempDir: Path) { + tempDir.resolve("DB_HOST").writeText("localhost") + tempDir.resolve("DB_PORT").writeText("5432") + tempDir.resolve("DB_DATABASE").writeText("postgres") + tempDir.resolve("DB_USERNAME").writeText("username") + tempDir.resolve("DB_PASSWORD").writeText("secret") + val jdbcUrl = defaultJdbcUrl(ConnectionConfigFactory.MountPath(tempDir.toString())) + assertEquals("jdbc:postgresql://localhost:5432/postgres?user=username&password=secret", jdbcUrl) + } + + @Test + fun `default jdbc url - jdbc_url set`(@TempDir tempDir: Path) { + tempDir.resolve("DB_HOST").writeText("localhost") + tempDir.resolve("DB_PORT").writeText("5432") + tempDir.resolve("DB_DATABASE").writeText("postgres") + tempDir.resolve("DB_USERNAME").writeText("username") + tempDir.resolve("DB_PASSWORD").writeText("secret") + tempDir.resolve("DB_JDBC_URL").writeText("jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar") + val jdbcUrl = defaultJdbcUrl(ConnectionConfigFactory.MountPath(tempDir.toString())) + assertEquals("jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar", jdbcUrl) + } + + @Test + fun `default jdbc url - google factory`(@TempDir tempDir: Path) { + tempDir.resolve("DB_HOST").writeText("localhost") + tempDir.resolve("DB_PORT").writeText("5432") + tempDir.resolve("DB_DATABASE").writeText("postgres") + tempDir.resolve("DB_USERNAME").writeText("username") + tempDir.resolve("DB_PASSWORD").writeText("secret") + val jdbcUrl = jdbcUrlWithGoogleSocketFactory("dbinstance", ConnectionConfigFactory.MountPath(tempDir.toString()), gcpTeamProjectId = "project_id") + assertEquals("jdbc:postgresql://localhost:5432/postgres?user=username&password=secret&socketFactory=com.google.cloud.sql.postgres.SocketFactory&cloudSqlInstance=project_id:europe-north1:dbinstance", jdbcUrl) + } + + @Test + fun `default jdbc url - google factory - with jdbc_url set`(@TempDir tempDir: Path) { + tempDir.resolve("DB_HOST").writeText("localhost") + tempDir.resolve("DB_PORT").writeText("5432") + tempDir.resolve("DB_DATABASE").writeText("postgres") + tempDir.resolve("DB_USERNAME").writeText("username") + tempDir.resolve("DB_PASSWORD").writeText("secret") + tempDir.resolve("DB_JDBC_URL").writeText("jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar") + val jdbcUrl = jdbcUrlWithGoogleSocketFactory("dbinstance", ConnectionConfigFactory.MountPath(tempDir.toString()), gcpTeamProjectId = "project_id") + assertEquals("jdbc:postgresql://remote_ip:5432/testdb?user=foo&password=bar", jdbcUrl) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c3e0579..8e63ca6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ include( "minimal-soap-client", "naisful-app", "naisful-test-app", + "naisful-postgres", "postgres-testdatabaser", "retry", "rapids-and-rivers-api",