Skip to content

Commit

Permalink
Migrations (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
manelp committed Jun 30, 2023
2 parents c0b1556 + 066941c commit cb46908
Show file tree
Hide file tree
Showing 17 changed files with 190 additions and 27 deletions.
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ test:
.PHONY: deploy
deploy:
./gradlew build -x test -x check
docker build app/. -t agilogy/time-tracking-app:latest
docker build apps/app/. -t agilogy/time-tracking-app:latest
docker build apps/migrations/. -t agilogy/time-tracking-migrations:latest
heroku container:login
docker tag agilogy/time-tracking-app:latest registry.heroku.com/agilogy-time-tracking/web
docker tag agilogy/time-tracking-migrations:latest registry.heroku.com/agilogy-time-tracking/migrations
docker push registry.heroku.com/agilogy-time-tracking/web
heroku container:release web -a agilogy-time-tracking
docker push registry.heroku.com/agilogy-time-tracking/migrations
heroku container:release web migrations -a agilogy-time-tracking
3 changes: 0 additions & 3 deletions app/Dockerfile → apps/app/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
FROM openjdk:17.0.1-jdk-slim

RUN echo pwd

COPY build/libs/app-1.0-SNAPSHOT.jar app.jar


CMD [ "java", "-jar" , "app.jar"]
1 change: 1 addition & 0 deletions app/build.gradle.kts → apps/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {
implementation(project(":domain"))
implementation(project(":postgresdb"))
implementation(project(":httpapi"))
implementation(project(":herokupostgres"))
implementation(suspendApp)
implementation(suspendAppKtor)
implementation(hikariCp)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package com.agilogy.timetracking.app

import arrow.continuations.ktor.server
import com.agilogy.heroku.postgres.loadHerokuPostgresConfig
import com.agilogy.timetracking.driveradapters.httpapi.TimeTrackingApi
import io.ktor.server.netty.Netty
import kotlinx.coroutines.awaitCancellation
import java.net.URI
import kotlin.time.Duration.Companion.seconds

fun main() = app {
val dbUri = URI(System.getenv("DATABASE_URL"))
val port = System.getenv("PORT")?.let { it.toInt() } ?: 8080

val (username, password) = dbUri.userInfo.split(":")
val jdbcUrl = "jdbc:postgresql://" + dbUri.host + ':' + dbUri.port + dbUri.path
val (jdbcUrl, username, password) = loadHerokuPostgresConfig()

val timeTrackingApi = TimeTrackingApi(timeTrackingApp(jdbcUrl, username, password))

Expand Down
File renamed without changes.
5 changes: 5 additions & 0 deletions apps/migrations/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM openjdk:17.0.1-jdk-slim

COPY build/libs/migrations-1.0-SNAPSHOT.jar migrations.jar

CMD [ "java", "-jar" , "migrations.jar"]
42 changes: 42 additions & 0 deletions apps/migrations/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Dependencies.flywayCore
import Dependencies.kotestRunnerJunit
import Dependencies.postgresql
import Dependencies.slf4jProvider
import Dependencies.suspendApp

plugins {
application
id("java")
}

val mainClassCompleteName = "com.agilogy.timetracking.migrations.MigrateDbKt"

tasks.withType<Jar> {
manifest {
attributes["Main-Class"] = mainClassCompleteName
}
// To avoid the duplicate handling strategy error
duplicatesStrategy = DuplicatesStrategy.EXCLUDE

// To add all of the dependencies otherwise a "NoClassDefFoundError" error
from(sourceSets.main.get().output)

dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
application {
mainClass.set(mainClassCompleteName)
}

dependencies {
implementation(slf4jProvider)
implementation(postgresql)
implementation(flywayCore)
implementation(project(":db"))
implementation(project(":postgresdb"))
implementation(project(":herokupostgres"))
implementation(suspendApp)
testImplementation(kotestRunnerJunit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.agilogy.timetracking.migrations

import kotlinx.coroutines.runBlocking
import org.flywaydb.core.api.MigrationVersion
import org.flywaydb.core.api.migration.Context
import org.flywaydb.core.api.migration.JavaMigration
import java.sql.Connection

abstract class KotlinMigration(private val version: String, private val title: String) : JavaMigration {

override fun getVersion(): MigrationVersion = MigrationVersion.fromVersion(version)

override fun getDescription(): String = title

override fun getChecksum(): Int? = null

override fun canExecuteInTransaction(): Boolean = true

final override fun migrate(context: Context) = runBlocking {
context.connection!!.use {
with(it) {
migrate()
}
}
}

context(Connection)
abstract suspend fun migrate()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.agilogy.timetracking.migrations

import arrow.continuations.SuspendApp
import com.agilogy.heroku.postgres.loadHerokuPostgresConfig
import org.flywaydb.core.Flyway
import org.flywaydb.core.api.configuration.FluentConfiguration
import org.slf4j.LoggerFactory

fun main() = SuspendApp {
val (jdbcUrl, username, password) = loadHerokuPostgresConfig()

val flywayConfig: FluentConfiguration = Flyway.configure()
.dataSource(jdbcUrl, username, password)
.group(true)
.outOfOrder(false)
.baselineOnMigrate(true)
.locations("classpath:${this.javaClass.packageName}.scripts")
.loggers("slf4j")

val validated = flywayConfig
.ignoreMigrationPatterns("*:pending")
.load()
.validateWithResult()

if (!validated.validationSuccessful) {
val logger = LoggerFactory.getLogger("RunMigrations")
for (error in validated.invalidMigrations) {
logger.warn(
"""
|Failed to validate migration:
| - version: ${error.version}
| - path: ${error.filepath}
| - description: ${error.description}
| - error code: ${error.errorDetails.errorCode}
| - error message: ${error.errorDetails.errorMessage}
""".trimMargin("|").trim(),
)
}
}
flywayConfig.load().migrate()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.agilogy.timetracking.migrations.scripts

import com.agilogy.db.sql.Sql.update
import com.agilogy.timetracking.migrations.KotlinMigration
import java.sql.Connection

class Migration20230623 : KotlinMigration("20230623", "Initial migration") {

context(Connection)
override suspend fun migrate() {
update(
"""
|create table if not exists time_entries(
|id serial,
|developer text not null,
|project text not null,
|start timestamptz not null,
|"end" timestamptz not null,
|zone_id text not null
|)
""",
)
}
}
13 changes: 8 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,16 @@ kover {
}

dependencies {
File("$rootDir/components/").listFiles()!!.filter { it.isDirectory }.map { it.name }
.forEach { kover(project(":$it")) }

File("$rootDir/libs/").listFiles()!!.filter { it.isDirectory }.map { it.name }
.forEach { kover(project(":$it")) }
fun String.scanApps() =
File("$rootDir/${this@scanApps}/").listFiles()!!.filter { it.isDirectory }.map { it.name }
.forEach { (project(":$it")) }

kover(project(":app"))
"components".scanApps()
"libs".scanApps()
"apps".scanApps()

// kover(project(":time_tracking_api"))
}

koverReport {
Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ object Dependencies {
val suspendApp = "$arrowKt:suspendapp:0.4.1-alpha.5"
val suspendAppKtor = "$arrowKt:suspendapp-ktor:0.4.1-alpha.5"

val flyway = "org.flywaydb"
val flywayVersion = "9.17.0"
val flywayCore = "$flyway:flyway-core:$flywayVersion"

val kotlincsv = "com.github.doyaaaaaken:kotlin-csv-jvm:1.9.1"
val kolinx = "org.jetbrains.kotlinx"
val kotlinxSerializationVersion = "1.5.0"
Expand Down
4 changes: 2 additions & 2 deletions docs/Requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
- [ ] Any needed database change is written as a migration that can be run automatically by our deployment chain
- [ ] If/When security is already implemented, the feature authorization is implemented
- [ ] There are automated tests of all the domain (with test doubles for repositories)
- [ ] There are automated integration tests for any new operation in the driven adapters
- [ ] There are automated tests for any new endpoint in the HTTP API (either using the domain or mocking it)
- [ ] There are automated integration tests for any new or changed operation in the driven adapters
- [ ] There are automated tests for any new or changed endpoint in the HTTP API (either using the domain or mocking it)
- [ ] There are automated tests for the feature authorization (either at the HTTP API level or the domain leve)

### Not in list
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.agilogy.heroku.postgres

import java.net.URI

data class HerokuPostgresConfig(
val jdbcUrl: String,
val username: String,
val password: String,
) {
override fun toString(): String =
"HerokuPostgresConfig($jdbcUrl,$username,\"********\")"
}

fun loadHerokuPostgresConfig(): HerokuPostgresConfig {
val dbUri = URI(System.getenv("DATABASE_URL"))
val (username, password) = dbUri.userInfo.split(":")
val jdbcUrl = "jdbc:postgresql://" + dbUri.host + ':' + dbUri.port + dbUri.path
return HerokuPostgresConfig(jdbcUrl, username, password)
}
18 changes: 8 additions & 10 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ gradleEnterprise {
}

rootProject.name = "timeTrackingApp"
include("app")

val components = File("$rootDir/components/").listFiles()!!.filter { it.isDirectory }.map { it.name }
components.forEach { configureProject(it, "components") }

val libs = File("$rootDir/libs/").listFiles()!!.filter { it.isDirectory }.map { it.name }
libs.forEach { configureProject(it, "libs") }
fun String.configureProjects() =
File("$rootDir/${this@configureProjects}/").listFiles()!!.filter { it.isDirectory }.map { it.name }.forEach {
include(":$it")
project(":$it").projectDir = File("${this@configureProjects}/$it")
}

fun configureProject(name: String, path: String) {
include(":$name")
project(":$name").projectDir = File("$path/$name")
}
"components".configureProjects()
"libs".configureProjects()
"apps".configureProjects()

0 comments on commit cb46908

Please sign in to comment.