diff --git a/Dockerfile b/Dockerfile index 49fa992..9f26464 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,8 @@ RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so FROM alpine +RUN apk add --no-cache curl + ENV APPLICATION_USER simplenotes RUN adduser -D -g '' $APPLICATION_USER diff --git a/app/src/main/kotlin/controllers/HealthCheckController.kt b/app/src/main/kotlin/controllers/HealthCheckController.kt new file mode 100644 index 0000000..ca8c388 --- /dev/null +++ b/app/src/main/kotlin/controllers/HealthCheckController.kt @@ -0,0 +1,12 @@ +package be.simplenotes.app.controllers + +import be.simplenotes.persistance.DbHealthCheck +import org.http4k.core.Request +import org.http4k.core.Response +import org.http4k.core.Status.Companion.OK +import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE + +class HealthCheckController(private val dbHealthCheck: DbHealthCheck) { + fun healthCheck(request: Request) = + if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE) +} diff --git a/app/src/main/kotlin/modules/CoreModules.kt b/app/src/main/kotlin/modules/CoreModules.kt index 10040b6..8961883 100644 --- a/app/src/main/kotlin/modules/CoreModules.kt +++ b/app/src/main/kotlin/modules/CoreModules.kt @@ -1,9 +1,6 @@ package be.simplenotes.app.modules -import be.simplenotes.app.controllers.BaseController -import be.simplenotes.app.controllers.NoteController -import be.simplenotes.app.controllers.SettingsController -import be.simplenotes.app.controllers.UserController +import be.simplenotes.app.controllers.* import be.simplenotes.app.views.BaseView import be.simplenotes.app.views.NoteView import be.simplenotes.app.views.SettingView @@ -16,6 +13,7 @@ val userModule = module { } val baseModule = module { + single { HealthCheckController(get()) } single { BaseController(get()) } single { BaseView(get()) } } diff --git a/app/src/main/kotlin/modules/ServerModule.kt b/app/src/main/kotlin/modules/ServerModule.kt index dd91d9a..0c732ed 100644 --- a/app/src/main/kotlin/modules/ServerModule.kt +++ b/app/src/main/kotlin/modules/ServerModule.kt @@ -45,6 +45,7 @@ val serverModule = module { get(), get(), get(), + get(), requiredAuth = get(AuthType.Required.qualifier), optionalAuth = get(AuthType.Optional.qualifier), apiAuth = get(named("apiAuthFilter")), diff --git a/app/src/main/kotlin/routes/Router.kt b/app/src/main/kotlin/routes/Router.kt index 4d27e91..4caf064 100644 --- a/app/src/main/kotlin/routes/Router.kt +++ b/app/src/main/kotlin/routes/Router.kt @@ -2,10 +2,7 @@ package be.simplenotes.app.routes import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.api.ApiUserController -import be.simplenotes.app.controllers.BaseController -import be.simplenotes.app.controllers.NoteController -import be.simplenotes.app.controllers.SettingsController -import be.simplenotes.app.controllers.UserController +import be.simplenotes.app.controllers.* import be.simplenotes.app.filters.* import be.simplenotes.domain.security.JwtPayload import org.http4k.core.* @@ -22,6 +19,7 @@ class Router( private val settingsController: SettingsController, private val apiUserController: ApiUserController, private val apiNoteController: ApiNoteController, + private val healthCheckController: HealthCheckController, private val requiredAuth: Filter, private val optionalAuth: Filter, private val apiAuth: Filter, @@ -31,7 +29,11 @@ class Router( ) { operator fun invoke(): RoutingHttpHandler { - val basicRoutes = ImmutableFilter.then(static(Classpath("/static"), "woff2" to ContentType("font/woff2"))) + val basicRoutes = + routes( + "/health" bind GET to healthCheckController::healthCheck, + ImmutableFilter.then(static(Classpath("/static"), "woff2" to ContentType("font/woff2"))) + ) val publicRoutes = routes( "/" bind GET public baseController::index, diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 3c520e6..ffd7b45 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -11,7 +11,11 @@ simplenotes.be { import strict-transport header -Server - reverse_proxy http://localhost:8080 + reverse_proxy http://localhost:8080 { + health_path /health + health_interval 5s + health_timeout 200ms + } } dev.simplenotes.be { diff --git a/docker-compose.yml b/docker-compose.yml index 80118b1..ef6f360 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,10 @@ services: volumes: - notes-db-volume:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] - timeout: 10s + test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'" + interval: 5s + timeout: 1s + start_period: 2s retries: 10 simplenotes: @@ -39,6 +41,12 @@ services: # - PASSWORD ports: - 127.0.0.1:8080:8080 + healthcheck: + test: "curl --fail -s http://localhost:8080/health" + interval: 5s + timeout: 1s + start_period: 2s + retries: 3 depends_on: db: condition: service_healthy diff --git a/persistance/src/main/kotlin/HealthCheck.kt b/persistance/src/main/kotlin/HealthCheck.kt new file mode 100644 index 0000000..063646a --- /dev/null +++ b/persistance/src/main/kotlin/HealthCheck.kt @@ -0,0 +1,28 @@ +package be.simplenotes.persistance + +import be.simplenotes.persistance.utils.DbType +import be.simplenotes.persistance.utils.type +import be.simplenotes.shared.config.DataSourceConfig +import me.liuwj.ktorm.database.Database +import me.liuwj.ktorm.database.asIterable +import java.sql.SQLTransientException + +interface DbHealthCheck { + fun isOk(): Boolean +} + +internal class DbHealthCheckImpl( + private val db: Database, + private val dataSourceConfig: DataSourceConfig, +) : DbHealthCheck { + override fun isOk() = if (dataSourceConfig.type() == DbType.H2) true + else try { + db.useConnection { connection -> + connection.prepareStatement("""SHOW DATABASES""").use { + it.executeQuery().asIterable().map { it.getString(1) } + } + }.any { it in dataSourceConfig.jdbcUrl } + } catch (e: SQLTransientException) { + false + } +} diff --git a/persistance/src/main/kotlin/DbMigrationsImpl.kt b/persistance/src/main/kotlin/Migrations.kt similarity index 55% rename from persistance/src/main/kotlin/DbMigrationsImpl.kt rename to persistance/src/main/kotlin/Migrations.kt index 30f41e1..3938ded 100644 --- a/persistance/src/main/kotlin/DbMigrationsImpl.kt +++ b/persistance/src/main/kotlin/Migrations.kt @@ -1,18 +1,24 @@ package be.simplenotes.persistance +import be.simplenotes.persistance.utils.DbType +import be.simplenotes.persistance.utils.type import be.simplenotes.shared.config.DataSourceConfig import org.flywaydb.core.Flyway import javax.sql.DataSource +interface DbMigrations { + fun migrate() +} + internal class DbMigrationsImpl( private val dataSource: DataSource, - private val dataSourceConfig: DataSourceConfig + private val dataSourceConfig: DataSourceConfig, ) : DbMigrations { override fun migrate() { - val migrationDir = when { - dataSourceConfig.jdbcUrl.contains("mariadb") -> "db/migration/mariadb" - else -> "db/migration/other" + val migrationDir = when (dataSourceConfig.type()) { + DbType.H2 -> "db/migration/other" + DbType.MariaDb -> "db/migration/mariadb" } Flyway.configure() diff --git a/persistance/src/main/kotlin/PersistanceModule.kt b/persistance/src/main/kotlin/PersistanceModule.kt index 91be1a8..1d6a334 100644 --- a/persistance/src/main/kotlin/PersistanceModule.kt +++ b/persistance/src/main/kotlin/PersistanceModule.kt @@ -13,10 +13,6 @@ import org.koin.dsl.module import org.koin.dsl.onClose import javax.sql.DataSource -interface DbMigrations { - fun migrate() -} - private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource { val hikariConfig = HikariConfig().also { it.jdbcUrl = conf.jdbcUrl @@ -41,4 +37,5 @@ val persistanceModule = module { get().migrate() Database.connect(get()) } + single { DbHealthCheckImpl(get(), get()) } } diff --git a/persistance/src/main/kotlin/utils/DataSourceConfigUtils.kt b/persistance/src/main/kotlin/utils/DataSourceConfigUtils.kt new file mode 100644 index 0000000..c683027 --- /dev/null +++ b/persistance/src/main/kotlin/utils/DataSourceConfigUtils.kt @@ -0,0 +1,8 @@ +package be.simplenotes.persistance.utils + +import be.simplenotes.shared.config.DataSourceConfig + +enum class DbType { H2, MariaDb } + +fun DataSourceConfig.type(): DbType = if (jdbcUrl.contains("mariadb")) DbType.MariaDb +else DbType.H2