Skip to content

Commit

Permalink
lager et lib for å lage jdbc-url
Browse files Browse the repository at this point in the history
baserer seg i all hovedgrad på defaults på nais-plattformen.
  • Loading branch information
davidsteinsland committed Nov 24, 2024
1 parent 047802e commit 42fdc22
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 0 deletions.
40 changes: 40 additions & 0 deletions naisful-postgres/README.md
Original file line number Diff line number Diff line change
@@ -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())
```
Empty file.
Original file line number Diff line number Diff line change
@@ -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 <a href="https://doc.nais.io/persistence/postgres/reference/?h=jdbc#database-connnection">nais doc</a>
*/
fun defaultJdbcUrl(metode: ConnectionConfigFactory = ConnectionConfigFactory.Env(), options: Map<String, String> = 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 <a href="https://doc.nais.io/persistence/postgres/reference/?h=jdbc#database-connnection">nais doc</a>
*/
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, String>): String?


data class Env(val env: Map<String, String> = System.getenv(), val envVarPrefix: String? = null) : ConnectionConfigFactory() {
override fun buildJdbcUrl(options: Map<String, String>): String? {
return buildJdbcUrl(options) { key ->
env.getKeySuffixOrNull(envVarPrefix, key)
}
}
}
data class MountPath(val path: String) : ConnectionConfigFactory() {
override fun buildJdbcUrl(options: Map<String, String>): 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<String, String>, 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<String, String>.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<String, String> = 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, String>): String {
return (options).entries.joinToString(separator = "&") { (k, v) -> "$k=$v" }
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ include(
"minimal-soap-client",
"naisful-app",
"naisful-test-app",
"naisful-postgres",
"postgres-testdatabaser",
"retry",
"rapids-and-rivers-api",
Expand Down

0 comments on commit 42fdc22

Please sign in to comment.