diff --git a/code/tic-tac-tow-service/README.md b/code/tic-tac-tow-service/README.md index 80a56e5..ab139ed 100644 --- a/code/tic-tac-tow-service/README.md +++ b/code/tic-tac-tow-service/README.md @@ -21,4 +21,14 @@ psql -U dbuser -d db * `\h` - show help. * `\d ` - show table. * `select ... ;` - execute query. - * `\q` - quit `psql`. \ No newline at end of file + * `\q` - quit `psql`. + +* Start the full composition +``` +./gradlew composeUp +``` + +* Loop curl +``` +while true; do curl -w "\n" http://localhost:8080/status/hostname; done +``` diff --git a/code/tic-tac-tow-service/build.gradle.kts b/code/tic-tac-tow-service/build.gradle.kts index e176176..7abe3de 100644 --- a/code/tic-tac-tow-service/build.gradle.kts +++ b/code/tic-tac-tow-service/build.gradle.kts @@ -59,6 +59,14 @@ tasks.withType { useJUnitPlatform() } +task("extractUberJar") { + dependsOn("assemble") + // opens the JAR containing everything... + from(zipTree("$buildDir/libs/${rootProject.name}-$version.jar")) + // ... into the 'build/dependency' folder + into("build/dependency") +} + task("dbTestsUp") { commandLine("docker-compose", "up", "-d", "--build", "--force-recreate", "db-tests") } @@ -72,6 +80,11 @@ task("dbTestsDown") { commandLine("docker-compose", "down") } +task("composeUp") { + commandLine("docker-compose", "up", "--build", "--force-recreate") + dependsOn("extractUberJar") +} + // from https://pinterest.github.io/ktlint/install/integrations/#custom-gradle-integration-with-kotlin-dsl val outputDir = "${project.buildDir}/reports/ktlint/" val inputFiles = project.fileTree(mapOf("dir" to "src", "include" to "**/*.kt")) @@ -91,4 +104,4 @@ tasks.named("check") { dependsOn(ktlintCheck) dependsOn("dbTestsWait") finalizedBy("dbTestsDown") -} \ No newline at end of file +} diff --git a/code/tic-tac-tow-service/docker-compose.yml b/code/tic-tac-tow-service/docker-compose.yml index 6e0c501..90f317a 100644 --- a/code/tic-tac-tow-service/docker-compose.yml +++ b/code/tic-tac-tow-service/docker-compose.yml @@ -1,7 +1,9 @@ version: "3.3" services: + # The service running the DB db-tests: container_name: db-tests + hostname: db-tests build: context: . dockerfile: ./tests/Dockerfile-db-test @@ -10,4 +12,62 @@ services: - POSTGRES_PASSWORD=changeit - POSTGRES_DB=db ports: - - 5432:5432 \ No newline at end of file + - 5432:5432 + + # spring-service-1 and spring-service-2 are used to illustrate scenarios with a fixed number of servers + # with static and well know names. + spring-service-1: + container_name: spring-service-1 + hostname: spring-service-1 + build: + context: . + dockerfile: ./tests/Dockerfile-spring + environment: + PORT: 8081 + POSTGRES_URI: "jdbc:postgresql://db-tests:5432/db?user=dbuser&password=changeit" + ports: + - 8081:8081 + + spring-service-2: + container_name: spring-service-2 + hostname: spring-service-2 + build: + context: . + dockerfile: ./tests/Dockerfile-spring + environment: + PORT: 8082 + POSTGRES_URI: "jdbc:postgresql://db-tests:5432/db?user=dbuser&password=changeit" + ports: + - 8082:8082 + + # spring-service is used to illustrate scenario with a dynamic number of servers + # without static and well know names. + # We will use docker compose scaling to create multiple instances of this service + spring-service: + build: + context: . + dockerfile: ./tests/Dockerfile-spring + environment: + PORT: 8080 + POSTGRES_URI: "jdbc:postgresql://db-tests:5432/db?user=dbuser&password=changeit" + + # The service running the load-balancer + nginx: + container_name: nginx + image: nginx + ports: + - 8080:8080 + - 8088:8088 + volumes: + - ./tests/nginx:/etc/nginx + depends_on: + - spring-service-1 + - spring-service-2 + + # Just a machine running ubuntu, with 'dig' installed so that we can observe the docker compose environment. + ubuntu: + container_name: ubuntu + build: + context: . + dockerfile: ./tests/Dockerfile-ubuntu + tty: true diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt index d36c739..365e1e4 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/TicTacTowApplication.kt @@ -1,7 +1,10 @@ package pt.isel.daw.tictactow import org.jdbi.v3.core.Jdbi +import org.postgresql.PGProperty import org.postgresql.ds.PGSimpleDataSource +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean @@ -19,11 +22,24 @@ import java.time.Instant @SpringBootApplication class TicTacTowApplication { @Bean - fun jdbi() = Jdbi.create( - PGSimpleDataSource().apply { - setURL("jdbc:postgresql://localhost:5432/db?user=dbuser&password=changeit") + fun jdbi(): Jdbi { + val postgresUri = + System.getenv("POSTGRES_URI") + ?: "jdbc:postgresql://localhost:5432/db?user=dbuser&password=changeit" + + val dataSource = PGSimpleDataSource().apply { + setURL(postgresUri) } - ).configure() + + // Be careful not to disclose the credentials. + logger.info( + "Using PostgreSQL located at '{}:{}'", + dataSource.getProperty(PGProperty.PG_HOST), + dataSource.getProperty(PGProperty.PG_PORT), + ) + + return Jdbi.create(dataSource).configure() + } @Bean fun passwordEncoder() = BCryptPasswordEncoder() @@ -35,6 +51,10 @@ class TicTacTowApplication { fun clock() = object : Clock { override fun now() = Instant.now() } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(TicTacTowApplication::class.java) + } } // QUESTION: why cannot this be in TicTacTowApplication diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/HomeController.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/HomeController.kt index ba573d7..8af57b5 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/HomeController.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/HomeController.kt @@ -9,7 +9,11 @@ import pt.isel.daw.tictactow.infra.siren class HomeController { @GetMapping(Uris.HOME) - fun getHome() = siren(HomeOutputModel("Made for teaching purposes by P. Félix")) { + fun getHome() = siren( + HomeOutputModel( + credits = "Made for teaching purposes by P. Félix", + ) + ) { link(Uris.home(), Rels.SELF) link(Uris.home(), Rels.HOME) } diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/StatusController.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/StatusController.kt new file mode 100644 index 0000000..d2e6665 --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/StatusController.kt @@ -0,0 +1,32 @@ +package pt.isel.daw.tictactow.http + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import pt.isel.daw.tictactow.http.model.StatusOutputModel +import pt.isel.daw.tictactow.infra.siren +import pt.isel.daw.tictactow.repository.TransactionManager +import java.net.InetAddress + +@RestController +class StatusController( + val transactionManager: TransactionManager +) { + + @GetMapping(Uris.STATUS) + fun getStatus() = transactionManager.run { transaction -> + siren( + StatusOutputModel( + hostname = System.getenv("HOSTNAME"), + gamesCount = transaction.gamesRepository.count() + ) + ) { + // For now, nothing more to add. + } + } + + @GetMapping(Uris.STATUS_HOSTNAME) + fun getStatusHostname(): String = System.getenv("HOSTNAME") + + @GetMapping(Uris.STATUS_IP) + fun getStatusIp(): String = InetAddress.getLocalHost().hostAddress +} \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt index 5724f38..13b0156 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/Uris.kt @@ -7,6 +7,9 @@ import java.net.URI object Uris { const val HOME = "/" + const val STATUS = "/status" + const val STATUS_HOSTNAME = "/status/hostname" + const val STATUS_IP = "/status/ip" const val GAME_BY_ID = "/games/{gid}" fun home(): URI = URI(HOME) diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/StatusOutputModel.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/StatusOutputModel.kt new file mode 100644 index 0000000..669f6c8 --- /dev/null +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/http/model/StatusOutputModel.kt @@ -0,0 +1,6 @@ +package pt.isel.daw.tictactow.http.model + +data class StatusOutputModel( + val hostname: String, + val gamesCount: Int, +) \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/GamesRepository.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/GamesRepository.kt index c1d3f6a..5b2bf97 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/GamesRepository.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/GamesRepository.kt @@ -7,4 +7,5 @@ interface GamesRepository { fun insert(game: Game) fun getById(id: UUID): Game? fun update(game: Game) + fun count(): Int } \ No newline at end of file diff --git a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiGamesRepository.kt b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiGamesRepository.kt index 06a2985..e97b5ff 100644 --- a/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiGamesRepository.kt +++ b/code/tic-tac-tow-service/src/main/kotlin/pt/isel/daw/tictactow/repository/jdbi/JdbiGamesRepository.kt @@ -71,6 +71,11 @@ class JdbiGamesRepository( .execute() } + override fun count(): Int = + handle.createQuery("select count(*) as count from dbo.Games") + .mapTo() + .single() + companion object { private fun Update.bindBoard(name: String, board: Board) = run { bind( diff --git a/code/tic-tac-tow-service/src/main/resources/application.properties b/code/tic-tac-tow-service/src/main/resources/application.properties index 8b13789..50d98ae 100644 --- a/code/tic-tac-tow-service/src/main/resources/application.properties +++ b/code/tic-tac-tow-service/src/main/resources/application.properties @@ -1 +1 @@ - +server.port=${port:8080} diff --git a/code/tic-tac-tow-service/tests/Dockerfile-spring b/code/tic-tac-tow-service/tests/Dockerfile-spring new file mode 100644 index 0000000..b593ace --- /dev/null +++ b/code/tic-tac-tow-service/tests/Dockerfile-spring @@ -0,0 +1,9 @@ +FROM openjdk:17 +ARG DEPENDENCY=build/dependency +# first layer with the external libs (i.e. the files that change the least). +COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib +# second layer with the 'META-INF' contents. +COPY ${DEPENDENCY}/META-INF /app/META-INF +# last layer with the application JARs (i.e. the files that change the most). +COPY ${DEPENDENCY}/BOOT-INF/classes /app +ENTRYPOINT ["java","-cp","app:app/lib/*","pt.isel.daw.tictactow.TicTacTowApplicationKt"] \ No newline at end of file diff --git a/code/tic-tac-tow-service/tests/Dockerfile-ubuntu b/code/tic-tac-tow-service/tests/Dockerfile-ubuntu new file mode 100644 index 0000000..1c9b234 --- /dev/null +++ b/code/tic-tac-tow-service/tests/Dockerfile-ubuntu @@ -0,0 +1,3 @@ +FROM ubuntu +RUN apt -y update +RUN apt -y install dnsutils \ No newline at end of file diff --git a/code/tic-tac-tow-service/tests/nginx/nginx.conf b/code/tic-tac-tow-service/tests/nginx/nginx.conf new file mode 100644 index 0000000..d571136 --- /dev/null +++ b/code/tic-tac-tow-service/tests/nginx/nginx.conf @@ -0,0 +1,35 @@ +events { + worker_connections 1024; +} + +http { + + upstream static-spring-service { + server spring-service-1:8081 max_fails=3 fail_timeout=10s; + server spring-service-2:8082 max_fails=3 fail_timeout=10s; + } + + upstream dynamic-spring-service { + server spring-service:8080 max_fails=3 fail_timeout=10s; + } + + server { + listen 8080; + + location / { + proxy_pass http://static-spring-service; + proxy_connect_timeout 5s; + proxy_next_upstream error timeout http_500; + } + } + + server { + listen 8088; + + location / { + proxy_pass http://dynamic-spring-service; + proxy_connect_timeout 5s; + proxy_next_upstream error timeout http_500; + } + } +} \ No newline at end of file