Compare commits

..

9 Commits

Author SHA1 Message Date
Florian THIERRY
d23320b218 Edit gitea action workflow to build and deploy kotlin app on production server.
Some checks failed
Build and Deploy Kotlin gradle Application / build-and-deploy (push) Has been cancelled
2025-05-11 17:20:22 +02:00
Florian THIERRY
4497891788 First workflow.
Some checks failed
Build and Deploy Kotlin gradle Application / build-and-deploy (push) Has been cancelled
2025-05-11 13:07:32 +02:00
Florian THIERRY
458e72d6aa Dockerisation. 2025-05-11 12:39:40 +02:00
Florian THIERRY
f98e1227e8 Implementation of getAll and create products endpoints. 2025-04-23 23:13:48 +02:00
Florian THIERRY
ed0acfc5dc Enable coroutines. 2025-04-23 21:56:14 +02:00
Florian THIERRY
ee26a56627 Cleaning in gradle configurations. 2025-04-15 08:58:51 +02:00
Florian THIERRY
2a5bbe5b57 Add all layers implementation. 2025-04-15 08:52:24 +02:00
Florian THIERRY
8fa13103f3 Fix dependency management. 2025-04-14 14:28:55 +02:00
Florian THIERRY
2ca85aab88 Add product classes into domain. 2025-04-14 13:52:20 +02:00
34 changed files with 460 additions and 48 deletions

9
.gitea/gitea-runner.yml Normal file
View File

@@ -0,0 +1,9 @@
services:
runner:
image: docker.io/gitea/act_runner:latest
environment:
GITEA_INSTANCE_URL: "http://192.168.0.25:3000"
GITEA_RUNNER_REGISTRATION_TOKEN: "IfowioCs4PedXyRJMnedDVCXkRHjyDrZv1sdvaif"
GITEA_RUNNER_NAME: "runner-1"
volumes:
- /var/run/docker.sock:/var/run/docker.sock

View File

@@ -0,0 +1,50 @@
name: Build and Deploy Kotlin gradle Application
on:
push:
branches:
- working-poc
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build backend docker image
run: |
sudo /usr/bin/docker build -t gitea-workshop-app -f ./Dockerfile-backend . --no-cache
- name: Extract backend docker image into archive
run: |
sudo /usr/bin/docker save gitea-workshop-app:latest -o ./gitea-workshop-app.tar
- name: Transfer artifacts to remote server
env:
SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.PROD_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.PROD_REMOTE_USER }}
REMOTE_PORT: ${{ secrets.PROD_REMOTE_PORT }}
REMOTE_PATH: ${{ secrets.PROD_REMOTE_PATH }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 400 ~/.ssh/id_rsa
scp -o StrictHostKeyChecking=no -P $REMOTE_PORT ./gitea-workshop-app.tar $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH
scp -o StrictHostKeyChecking=no -P $REMOTE_PORT ./docker-compose.yml $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH
- name: Launch application onto remote server
env:
SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.PROD_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.PROD_REMOTE_USER }}
REMOTE_PORT: ${{ secrets.PROD_REMOTE_PORT }}
REMOTE_PATH: ${{ secrets.PROD_REMOTE_PATH }}
run: |
ssh -o StrictHostKeyChecking=no $REMOTE_HOST -l $REMOTE_USER -p $REMOTE_PORT << EOC
cd $REMOTE_PATH
sudo /usr/bin/docker load < $REMOTE_PATH/gitea-workshop-app.tar
sudo /usr/bin/docker compose down
sudo /usr/bin/docker compose up --detach
EOC

15
Dockerfile-backend Normal file
View File

@@ -0,0 +1,15 @@
FROM gradle:8.12.1-jdk21 AS builder
COPY ./gradlew /app/
COPY ./build.gradle.kts /app/
COPY ./settings.gradle.kts /app/
COPY ./demo-application /app/demo-application
COPY ./demo-domain /app/demo-domain
COPY ./demo-exposition /app/demo-exposition
COPY ./demo-infrastructure /app/demo-infrastructure
COPY ./demo-launcher /app/demo-launcher
WORKDIR /app
RUN gradle build jar
FROM eclipse-temurin:21-jre-alpine AS final
COPY --from=builder /app/demo-launcher/build/libs/demo-launcher.jar /app/demo-launcher.jar
CMD ["java", "-jar", "/app/demo-launcher.jar"]

View File

@@ -0,0 +1,11 @@
meta {
name: Debug
type: http
seq: 3
}
get {
url: {{url}}/api/debug/info
body: none
auth: none
}

View File

@@ -0,0 +1,11 @@
meta {
name: Get all products
type: http
seq: 2
}
get {
url: {{url}}/api/products
body: none
auth: none
}

View File

@@ -0,0 +1,9 @@
{
"version": "1",
"name": "GiteaActionsWorkshop",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -0,0 +1,3 @@
vars {
url: http://localhost:8080
}

View File

@@ -1,7 +1,6 @@
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.4.4"
kotlin("jvm") version "2.1.20"
kotlin("plugin.spring") version "2.1.20"
id("io.spring.dependency-management") version "1.1.7"
}
@@ -18,13 +17,24 @@ repositories {
mavenCentral()
}
subprojects {
apply(plugin = "java")
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.4"))
implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.1")
}
}
dependencyManagement {
imports {
mavenBom("org.springframework.boot:spring-boot-dependencies:3.4.4")
}
}
kotlin {

View File

@@ -0,0 +1,12 @@
plugins {
kotlin("jvm")
id("io.spring.dependency-management") version "1.1.7"
}
dependencies {
implementation(kotlin("stdlib"))
implementation(project(":demo-domain"))
implementation("org.springframework:spring-context")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.0.1")
}

View File

@@ -0,0 +1,45 @@
package com.example.demo.application.product
import com.example.demo.domain.core.error.FunctionalError
import com.example.demo.domain.product.inputport.ProductInputPort
import com.example.demo.domain.product.model.Product
import com.example.demo.domain.product.model.ProductType
import com.example.demo.domain.product.outputport.ProductOutputPort
import com.github.michaelbull.result.*
import com.github.michaelbull.result.coroutines.coroutineBinding
import org.springframework.stereotype.Service
import java.util.*
@Service
class ProductUseCases(
private val productOutputPort: ProductOutputPort
) : ProductInputPort {
override suspend fun getAll(): Result<List<Product>, FunctionalError> = productOutputPort.getAll()
.mapError { FunctionalError(it.message) }
// override suspend fun create(name: String, type: ProductType): Result<Product, FunctionalError> {
// if (name.isBlank()) {
// return Err(FunctionalError("Product name should be set."))
// }
//
// val newProduct = Product(id = UUID.randomUUID(), name, type)
// return productOutputPort.save(newProduct)
// .mapError { FunctionalError(it.message) }
// .map { newProduct }
// }
override suspend fun create(name: String, type: ProductType): Result<Product, FunctionalError> = coroutineBinding {
validateName(name).bind()
val newProduct = Product(id = UUID.randomUUID(), name, type)
productOutputPort.save(newProduct)
.mapError { FunctionalError(it.message) }
.map { newProduct }
.bind()
}
private fun validateName(name: String): Result<Unit, FunctionalError> = when {
name.isBlank() -> Err(FunctionalError("Product name should be set."))
else -> Ok(Unit)
}
}

View File

@@ -0,0 +1,7 @@
plugins {
kotlin("jvm")
}
dependencies {
implementation(kotlin("stdlib"))
}

View File

@@ -0,0 +1,3 @@
package com.example.demo.domain.core.error
open class DomainError(val message: String)

View File

@@ -0,0 +1,3 @@
package com.example.demo.domain.core.error
class FunctionalError(message: String) : DomainError(message)

View File

@@ -0,0 +1,3 @@
package com.example.demo.domain.core.error
class TechnicalError(message: String) : DomainError(message)

View File

@@ -0,0 +1,11 @@
package com.example.demo.domain.product.inputport
import com.example.demo.domain.core.error.FunctionalError
import com.example.demo.domain.product.model.Product
import com.example.demo.domain.product.model.ProductType
import com.github.michaelbull.result.Result
interface ProductInputPort {
suspend fun create(name: String, type: ProductType): Result<Product, FunctionalError>
suspend fun getAll(): Result<List<Product>, FunctionalError>
}

View File

@@ -0,0 +1,9 @@
package com.example.demo.domain.product.model
import java.util.UUID
data class Product(
val id: UUID,
val name: String,
val type: ProductType
)

View File

@@ -0,0 +1,7 @@
package com.example.demo.domain.product.model
enum class ProductType {
SOLID,
LIQUID,
GAS
}

View File

@@ -0,0 +1,16 @@
package com.example.demo.domain.product.outputport
import com.example.demo.domain.core.error.TechnicalError
import com.example.demo.domain.product.model.Product
import com.github.michaelbull.result.Result
import java.util.UUID
interface ProductOutputPort {
fun getById(id: UUID): Product?
suspend fun getAll(): Result<List<Product>, TechnicalError>
fun save(product: Product): Result<Unit, TechnicalError>
fun deleteById(id: UUID)
}

View File

@@ -0,0 +1,15 @@
plugins {
kotlin("jvm")
id("io.spring.dependency-management") version "1.1.7"
}
dependencies {
implementation(kotlin("stdlib"))
implementation(project(":demo-application"))
implementation(project(":demo-domain"))
implementation("org.springframework:spring-context")
implementation("org.springframework:spring-web")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.0.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.3")
}

View File

@@ -0,0 +1,16 @@
package com.example.demo.exposition.debug
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/debug")
class DebugController {
@GetMapping("/info")
fun debug(): String = """
{
"message": "Hello world!"
}
""".trimIndent()
}

View File

@@ -0,0 +1,28 @@
package com.example.demo.exposition.product
import com.example.demo.domain.core.error.FunctionalError
import com.example.demo.domain.product.inputport.ProductInputPort
import com.example.demo.domain.product.model.Product
import com.example.demo.exposition.product.model.ProductCreationRequest
import com.example.demo.exposition.product.model.ProductDto
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.map
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/products")
class ProductController(
private val productInputPort: ProductInputPort
) {
@GetMapping
suspend fun getAll(): Result<List<ProductDto>, FunctionalError> = productInputPort.getAll()
.map { products -> products.map(::ProductDto) }
@PostMapping
suspend fun create(@RequestBody request: ProductCreationRequest): Result<Product, FunctionalError> =
productInputPort.create(request.name, request.type)
}

View File

@@ -0,0 +1,8 @@
package com.example.demo.exposition.product.core.advice
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class ResultControllerAdvice {
}

View File

@@ -0,0 +1,8 @@
package com.example.demo.exposition.product.model
import com.example.demo.domain.product.model.ProductType
data class ProductCreationRequest(
val name: String,
val type: ProductType
)

View File

@@ -0,0 +1,22 @@
package com.example.demo.exposition.product.model
import com.example.demo.domain.product.model.Product
import java.util.*
data class ProductDto(
val id: UUID,
val name: String,
val type: ProductTypeDto
) {
constructor(product: Product): this(
id = product.id,
name = product.name,
type = ProductTypeDto.fromDomain(product.type)
)
fun toDomain() = Product(
id,
name,
type = type.toDomain()
)
}

View File

@@ -0,0 +1,25 @@
package com.example.demo.exposition.product.model
import com.example.demo.domain.product.model.ProductType
enum class ProductTypeDto {
SOLID,
LIQUID,
GAS;
fun toDomain(): ProductType {
return when (this) {
SOLID -> ProductType.SOLID
LIQUID -> ProductType.LIQUID
GAS -> ProductType.GAS
}
}
companion object {
fun fromDomain(productType: ProductType) = when (productType) {
ProductType.SOLID -> SOLID
ProductType.LIQUID -> LIQUID
ProductType.GAS -> GAS
}
}
}

View File

@@ -0,0 +1,13 @@
plugins {
kotlin("jvm")
id("io.spring.dependency-management") version "1.1.7"
}
dependencies {
implementation(kotlin("stdlib"))
implementation(project(":demo-application"))
implementation(project(":demo-domain"))
implementation("org.springframework:spring-context")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.0.1")
}

View File

@@ -0,0 +1,40 @@
package com.example.demo.infrastructure.product
import com.example.demo.domain.core.error.TechnicalError
import com.example.demo.domain.product.model.Product
import com.example.demo.domain.product.model.ProductType.LIQUID
import com.example.demo.domain.product.model.ProductType.SOLID
import com.example.demo.domain.product.outputport.ProductOutputPort
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import org.springframework.stereotype.Component
import java.util.*
@Component
class ProductInMemoryAdapter : ProductOutputPort {
private val products = mutableListOf(
Product(
id = UUID.randomUUID(),
name = "Banana x5",
type = SOLID
),
Product(
id = UUID.randomUUID(),
name = "Banana juice 1L",
type = LIQUID
)
)
override fun getById(id: UUID): Product? = products.find { product -> product.id == id }
override suspend fun getAll(): Result<List<Product>, TechnicalError> = Ok(products)
override fun save(product: Product): Result<Unit, TechnicalError> {
products += product
return Ok(Unit)
}
override fun deleteById(id: UUID) {
products.removeIf { product -> product.id == id }
}
}

View File

@@ -1,19 +1,22 @@
plugins {
kotlin("jvm")
id("org.springframework.boot")
id("io.spring.dependency-management")
id("java")
}
repositories {
mavenCentral()
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect"))
implementation(project(":demo-domain"))
implementation(project(":demo-application"))
implementation(project(":demo-infrastructure"))
implementation(project(":demo-exposition"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-webflux")
}
springBoot {
mainClass = "com.example.demo.launcher.ApplicationLauncher"
mainClass = "com.example.demo.launcher.ApplicationLauncherKt"
}

View File

@@ -1,10 +1,19 @@
package com.example.demo.launcher
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class DemoApplication
@SpringBootApplication(
scanBasePackages = [
"com.example.demo.domain",
"com.example.demo.application",
"com.example.demo.infrastructure",
"com.example.demo.exposition"
]
)
@EnableAutoConfiguration
open class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)

12
docker-compose.yml Normal file
View File

@@ -0,0 +1,12 @@
services:
backend:
image: gitea-workshop-app:latest
container_name: gitea-workshop-app-backend
ports:
- "50000:8080"
restart: always
networks:
- "gitea-workshop"
networks:
gitea-workshop:

View File

@@ -1,2 +1,6 @@
rootProject.name = "demo"
include(":demo-domain")
include(":demo-application")
include(":demo-infrastructure")
include(":demo-exposition")
include(":demo-launcher")

View File

@@ -1,11 +0,0 @@
package com.example.demo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}

View File

@@ -1 +0,0 @@
spring.application.name=demo

View File

@@ -1,13 +0,0 @@
package com.example.demo
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class DemoApplicationTests {
@Test
fun contextLoads() {
}
}