merge(01-05): server /health + Flyway + HOCON + fail-loud DB boot
This commit is contained in:
@@ -1,20 +1,36 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.netty.Netty
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
embeddedServer(Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module)
|
embeddedServer(Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module)
|
||||||
.start(wait = true)
|
.start(wait = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class Health(val status: String)
|
||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
Database.migrate(this)
|
||||||
|
configureRouting()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Application.configureRouting() {
|
||||||
routing {
|
routing {
|
||||||
get("/") {
|
get("/health") {
|
||||||
call.respondText("Ktor: ${Greeting().greet()}")
|
call.respond(Health(status = "ok"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
31
server/src/main/kotlin/dev/ulfrx/recipe/Database.kt
Normal file
31
server/src/main/kotlin/dev/ulfrx/recipe/Database.kt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
object Database {
|
||||||
|
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||||
|
|
||||||
|
fun migrate(app: Application) {
|
||||||
|
val url = app.environment.config.property("database.url").getString()
|
||||||
|
val user = app.environment.config.property("database.user").getString()
|
||||||
|
val password = app.environment.config.property("database.password").getString()
|
||||||
|
|
||||||
|
log.info("Connecting to {} as {} and running Flyway migrations", url, user)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
Flyway.configure()
|
||||||
|
.dataSource(url, user, password)
|
||||||
|
.locations("classpath:db/migration")
|
||||||
|
.baselineOnMigrate(true)
|
||||||
|
.validateOnMigrate(true)
|
||||||
|
.cleanDisabled(true)
|
||||||
|
.load()
|
||||||
|
.migrate()
|
||||||
|
}.onFailure { ex ->
|
||||||
|
log.error("Flyway migration failed — cannot start server", ex)
|
||||||
|
throw IllegalStateException("Database unreachable or migration failed", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
server/src/main/resources/application.conf
Normal file
18
server/src/main/resources/application.conf
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
ktor {
|
||||||
|
deployment {
|
||||||
|
port = 8080
|
||||||
|
port = ${?PORT}
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
modules = [ dev.ulfrx.recipe.ApplicationKt.module ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
url = "jdbc:postgresql://localhost:5432/recipe"
|
||||||
|
url = ${?DATABASE_URL}
|
||||||
|
user = "recipe"
|
||||||
|
user = ${?DATABASE_USER}
|
||||||
|
password = "recipe"
|
||||||
|
password = ${?DATABASE_PASSWORD}
|
||||||
|
}
|
||||||
0
server/src/main/resources/db/migration/.gitkeep
Normal file
0
server/src/main/resources/db/migration/.gitkeep
Normal file
@@ -1,20 +1,30 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.*
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.testing.*
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlin.test.*
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class ApplicationTest {
|
class ApplicationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRoot() = testApplication {
|
fun `health endpoint returns 200 with status ok`() = testApplication {
|
||||||
application {
|
application {
|
||||||
module()
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureRouting()
|
||||||
}
|
}
|
||||||
val response = client.get("/")
|
val response = client.get("/health")
|
||||||
assertEquals(HttpStatusCode.OK, response.status)
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
assertEquals("Ktor: ${Greeting().greet()}", response.bodyAsText())
|
val body = response.bodyAsText()
|
||||||
|
assertTrue(body.contains("\"status\""), "expected body to contain status field, was: $body")
|
||||||
|
assertTrue(body.contains("\"ok\""), "expected body to contain ok value, was: $body")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user