Compare commits

...
68 Commits
Author SHA1 Message Date
2martens f5f773a90b Added logging and reformatted, updated drone config
continuous-integration/drone/tag Build is passing
continuous-integration/drone/promote/test Build is passing
2023-10-30 13:30:17 +01:00
2martens 323337ed64 Added quotes around bootstrap server property
continuous-integration/drone/tag Build is passing
2023-10-30 00:32:19 +01:00
2martens b04d6f4389 Added support for passing ports in via environment variables 2023-10-29 18:07:50 +01:00
2martens 517f2b4f1b Fixed kafka errors
continuous-integration/drone/tag Build is passing
2023-10-29 01:07:02 +02:00
2martens f089d70d5f Remove deprecated usage 2023-10-29 00:50:55 +02:00
2martens 2a78b47d83 Added configs to producer and consumer as well
continuous-integration/drone/tag Build is passing
2023-10-29 00:42:47 +02:00
2martens 84baee6ebb Upgrade gradle version 2023-10-28 11:00:50 +02:00
2martens 35a1b49412 Removed chart 2023-08-23 08:28:47 +02:00
2martens 9453e10803 Fixed prefix 2023-08-20 19:55:11 +02:00
2martens a2b85f402d Added environment variables to configure in Docker environment 2023-08-20 19:49:57 +02:00
2martens 3a9c70932d Use alpine base image 2023-08-20 12:14:43 +02:00
2martens 7e87623b0e Created skeleton chart 2023-08-20 10:45:22 +02:00
2martens 1b71ef0614 Use AmazonLinux 2023 2023-08-20 10:44:57 +02:00
2martens 9aa199103d Don't add servers item 2023-08-19 18:34:25 +02:00
2martens 904bc60ed7 Added forwarded header filter as bean 2023-08-19 18:18:28 +02:00
2martens d5691e3081 Use framework handling of forward headers 2023-08-19 17:33:03 +02:00
2martens 538b1badec Add forward headers strategy 2023-08-19 16:59:05 +02:00
2martens d34ca9aff2 Removed not working step 2023-08-19 16:12:09 +02:00
2martens cd79f15d5b Enabled target and removed custom trigger 2023-08-19 03:54:43 +02:00
2martens 37506b6c83 Added promotion step 2023-08-19 03:54:11 +02:00
2martens 406a891849 Improved code style of Kotlin 2023-08-19 03:53:40 +02:00
2martens b411c97f53 Adjusted API to be more in line with resource orientation 2023-08-19 03:52:50 +02:00
2martens 37f57f788b Do not overwrite existing collections
continuous-integration/drone Build is failing
continuous-integration/drone/promote/production Build is passing
2023-08-17 23:56:56 +02:00
2martens d5c7597a04 Add cache to minimize time for installing ansible collections
continuous-integration/drone Build is failing
2023-08-17 23:53:39 +02:00
2martens 8f31d2dd3d Move to private_ssh_key as secret
continuous-integration/drone Build is failing
2023-08-17 23:49:06 +02:00
2martens 5ff47de10a Prevent commit if nothing changed
continuous-integration/drone Build is failing
2023-08-17 23:39:39 +02:00
2martens 794a08c9b0 Only run code quality on pull request
continuous-integration/drone Build is failing
2023-08-17 23:24:41 +02:00
2martens d579004653 Don't limit to target
continuous-integration/drone/push Build was killed
continuous-integration/drone Build was killed
2023-08-17 23:22:30 +02:00
2martens e127d66a1b Remove custom trigger for building docker container
continuous-integration/drone/push Build is failing
2023-08-17 23:21:36 +02:00
2martens 21fa17e62b Added galaxy requirements
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
2023-08-17 23:20:14 +02:00
2martens 8381c1ba7d Use Drone semver
continuous-integration/drone/push Build is failing
2023-08-17 23:07:16 +02:00
2martens 4308130cd1 Fixed usage of image version
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/production Build is failing
2023-08-17 23:03:00 +02:00
2martens 721cdac95d Use latest ansible plugin version
continuous-integration/drone/push Build is failing
2023-08-17 22:59:49 +02:00
2martens 395fa52962 Fixed pipeline
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/production Build is failing
2023-08-17 22:50:20 +02:00
2martens 45e5c473ff Added pipeline to deploy via Ansible
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/production Build is failing
2023-08-17 21:11:44 +02:00
2martens 1c3648f0f4 Update [CI SKIP] 2023-08-17 12:51:18 +02:00
2martens 5b707728b8 Change properties [CI SKIP] 2023-08-17 02:18:08 +02:00
2martens c19c211839 Make properties refreshable
continuous-integration/drone/push Build is failing
2023-08-17 02:15:23 +02:00
2martens aace535914 Change message [CI SKIP] 2023-08-17 02:13:07 +02:00
2martens 59982ef762 Remove refresh scope from Clock
continuous-integration/drone/push Build is failing
2023-08-17 02:08:12 +02:00
2martens 56c6fef178 Remove constructor binding
continuous-integration/drone/push Build is failing
2023-08-17 02:02:19 +02:00
2martens 46b1b32708 Properties must be changeable with setters
continuous-integration/drone/push Build is failing
2023-08-17 01:52:50 +02:00
2martens 519101023c Change message [CI SKIP] 2023-08-17 01:42:01 +02:00
2martens 82b4f51390 Require configserver on prod
continuous-integration/drone/push Build is failing
2023-08-17 01:38:26 +02:00
2martens 3256939ac7 Change message [CI SKIP] 2023-08-17 01:20:17 +02:00
2martens 02e4c5318d Fix bootstrap servers on prod
continuous-integration/drone/push Build is failing
2023-08-17 01:14:03 +02:00
2martens f06abd2838 Add application name
continuous-integration/drone/push Build is failing
2023-08-17 01:12:38 +02:00
2martens f0297f2d2c Changed prod message [CI SKIP] 2023-08-17 01:10:11 +02:00
2martens ec23bd4d32 Connect with kafka using bootstrap servers
continuous-integration/drone/push Build is failing
2023-08-17 00:53:05 +02:00
2martens bab1892673 Added kafka server config
continuous-integration/drone/push Build is failing
2023-08-17 00:42:53 +02:00
2martens 247fe83caa Changed config [CI SKIP] 2023-08-17 00:36:27 +02:00
2martens 5fe6165373 Remove property reload service
continuous-integration/drone/push Build is failing
2023-08-17 00:35:50 +02:00
2martens 64a8f17e6e Change greeting on dev [CI SKIP] 2023-08-17 00:04:56 +02:00
2martens 5eff527c48 Enable config server locally
continuous-integration/drone/push Build is failing
2023-08-17 00:02:57 +02:00
2martens 642713e14c Fixed rest health indicator 2023-08-17 00:02:45 +02:00
2martens ff8b205124 Add Kafka dependency 2023-08-16 23:08:49 +02:00
2martens 8c5f925e36 Change greeting message [CI SKIP] 2023-08-16 22:39:24 +02:00
2martens fe5cd41749 Specify metadata in jar task (likely used by Jib)
continuous-integration/drone/push Build is failing
2023-08-16 22:31:05 +02:00
2martens 3a25e33f3f Prevent NPE 2023-08-16 22:30:50 +02:00
2martens ec625fb7b5 Use service properties to check successful change
continuous-integration/drone/push Build is failing
2023-08-16 22:21:53 +02:00
2martens c8e4474175 Remove configserver import from general configuration
continuous-integration/drone/push Build is failing
2023-08-16 22:09:50 +02:00
2martens 499c85ae49 Load configserver only in production and move refreshable prod properties to wahlrecht-prod
continuous-integration/drone/push Build is failing
2023-08-16 22:08:50 +02:00
2martens e88d98efc1 Dummy change
continuous-integration/drone/push Build is failing
2023-08-16 21:56:57 +02:00
2martens 8d545aa7fb Moved refreshable configuration to config sub-directory
continuous-integration/drone/push Build is failing
2023-08-16 21:54:42 +02:00
2martens a04094bc60 Move configserver import to bootstrap.yaml
continuous-integration/drone/push Build is failing
2023-08-16 21:09:15 +02:00
2martens 7d68612e57 Adjusted path to configserver
continuous-integration/drone/push Build is failing
2023-08-16 20:17:57 +02:00
2martens 64295db0e7 Added environments for http requests
continuous-integration/drone/push Build is failing
2023-08-16 20:08:54 +02:00
2martens 6dcd302ab7 OPTIONS method does not have to be allowed 2023-08-16 20:08:01 +02:00
34 changed files with 381 additions and 285 deletions
+60 -7
View File
@@ -30,7 +30,6 @@ volumes:
trigger: trigger:
event: event:
include: include:
- push
- pull_request - pull_request
- custom - custom
@@ -51,7 +50,7 @@ steps:
image: alpine/git image: alpine/git
environment: environment:
SSH_KEY: SSH_KEY:
from_secret: ssh_key from_secret: private_ssh_key
commands: commands:
- mkdir $HOME/.ssh - mkdir $HOME/.ssh
- echo "$SSH_KEY" > $HOME/.ssh/id_ed25519 - echo "$SSH_KEY" > $HOME/.ssh/id_ed25519
@@ -69,7 +68,7 @@ steps:
PASSWORD: PASSWORD:
from_secret: docker_password from_secret: docker_password
SSH_KEY: SSH_KEY:
from_secret: ssh_key from_secret: private_ssh_key
volumes: volumes:
- name: cache - name: cache
path: /home/gradle/.gradle path: /home/gradle/.gradle
@@ -88,10 +87,64 @@ volumes:
path: /var/lib/drone/cache/.gradle path: /var/lib/drone/cache/.gradle
trigger: trigger:
branches:
- main
event: event:
include: include:
- push - tag
- custom
---
kind: pipeline
name: deploy image
type: docker
platform:
os: linux
arch: arm64
clone:
disable: true
steps:
- name: clone
image: alpine/git
environment:
SSH_KEY:
from_secret: private_ssh_key
commands:
- mkdir $HOME/.ssh
- echo "$SSH_KEY" > $HOME/.ssh/id_ed25519
- chmod 600 $HOME/.ssh/id_ed25519
- touch $HOME/.ssh/known_hosts
- chmod 600 $HOME/.ssh/known_hosts
- ssh-keyscan -H git.2martens.de > $HOME/.ssh/known_hosts 2>/dev/null
- git clone ssh://giteajim@git.2martens.de:22/2martens/cloud-configuration.git .
- git checkout main
- name: update image version
image: alpine
commands:
- mkdir $HOME/bin
- wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64 -O $HOME/bin/yq && chmod +x $HOME/bin/yq
- sed -i -r "s/(tag:).*/\1\ \"${DRONE_SEMVER}\"/" argocd/wahlrecht/${DRONE_DEPLOY_TO}/overwrite_values.yaml
- cd argocd/configserver/${DRONE_DEPLOY_TO} && $HOME/bin/yq '. *= load("overwrite_values.yaml")' default_values.yaml > values.yaml
- name: save modified variable file
image: alpine/git
environment:
SSH_KEY:
from_secret: private_ssh_key
commands:
- mkdir $HOME/.ssh
- echo "$SSH_KEY" > $HOME/.ssh/id_ed25519
- chmod 600 $HOME/.ssh/id_ed25519
- touch $HOME/.ssh/known_hosts
- chmod 600 $HOME/.ssh/known_hosts
- ssh-keyscan -H git.2martens.de > $HOME/.ssh/known_hosts 2>/dev/null
- git add argocd/wahlrecht/${DRONE_DEPLOY_TO}/*
- git diff-index --quiet HEAD || git commit -m "[Drone] Changed wahlrecht_image_version to ${DRONE_SEMVER}"
- git push origin main
trigger:
target:
- test
event:
include:
- promote
- rollback
+3 -3
View File
@@ -543,9 +543,9 @@ ij_html_space_inside_empty_tag = false
ij_html_text_wrap = normal ij_html_text_wrap = normal
[{*.kt,*.kts}] [{*.kt,*.kts}]
indent_size = 4 indent_size = 2
tab_width = 4 tab_width = 2
ij_continuation_indent_size = 8 ij_continuation_indent_size = 4
ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_extends_list = false
-20
View File
@@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="MainApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="wahlrecht.server.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="de.twomartens.wahlrecht.MainApplication" />
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="true" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
<ENTRY IS_ENABLED="true" PARSER="env" IS_EXECUTABLE="false" PATH=".env" />
</ENTRIES>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
@@ -54,7 +54,7 @@ normalization.runtimeClasspath.metaInf {
} }
tasks.register("cleanLibs") { tasks.register("cleanLibs") {
delete("${buildDir}/libs") delete("${layout.buildDirectory}/libs")
} }
tasks.build { tasks.build {
@@ -9,10 +9,10 @@ apply(plugin="com.netflix.nebula.release")
tasks.register("writeVersionProperties") { tasks.register("writeVersionProperties") {
group = "version" group = "version"
mustRunAfter("release") mustRunAfter("release")
outputs.file("$buildDir/version.properties") outputs.file("${layout.buildDirectory}/version.properties")
val directory = buildDir val directory = layout.buildDirectory.asFile
doLast { doLast {
Files.createDirectories(directory.toPath()) Files.createDirectories(directory.get().toPath())
File("$buildDir/version.properties").writeText("VERSION=${project.version}\n") File("${layout.buildDirectory}/version.properties").writeText("VERSION=${project.version}\n")
} }
} }
@@ -62,7 +62,7 @@ tasks.named("buildAll") {
val formatter: DateTimeFormatter = ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") val formatter: DateTimeFormatter = ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
tasks.bootJar { tasks.jar {
manifest { manifest {
attributes["Implementation-Title"] = rootProject.name attributes["Implementation-Title"] = rootProject.name
attributes["Implementation-Version"] = archiveVersion.get() attributes["Implementation-Version"] = archiveVersion.get()
+1
View File
@@ -38,6 +38,7 @@ spring-boot-oauth2-resource-server = { module = "org.springframework.boot:spring
spring-boot-config = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "spring-boot" } spring-boot-config = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "spring-boot" }
spring-cloud = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" } spring-cloud = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" }
spring-cloud-starter = { module = "org.springframework.cloud:spring-cloud-starter" } spring-cloud-starter = { module = "org.springframework.cloud:spring-cloud-starter" }
spring-cloud-starter-bus-kafka = { module = "org.springframework.cloud:spring-cloud-starter-bus-kafka" }
spring-cloud-starter-config = { module = "org.springframework.cloud:spring-cloud-starter-config" } spring-cloud-starter-config = { module = "org.springframework.cloud:spring-cloud-starter-config" }
spring-cloud-config-server = { module = "org.springframework.cloud:spring-cloud-config-server" } spring-cloud-config-server = { module = "org.springframework.cloud:spring-cloud-config-server" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" } spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" }
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+1 -1
View File
@@ -1,4 +1,4 @@
POST http://localhost:12000/wahlrecht/v1/calculate POST {{backend}}/wahlrecht/v1/calculate
Content-Type: application/json Content-Type: application/json
{ {
+2
View File
@@ -0,0 +1,2 @@
GET {{backend}}/wahlrecht/v1/elections
Content-Type: application/json
+8
View File
@@ -0,0 +1,8 @@
{
"dev": {
"backend": "http://localhost:12000"
},
"prod" : {
"backend": "https://api.2martens.de"
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ dependencies {
jib { jib {
from { from {
image = "amazoncorretto:" + properties["projectSourceCompatibility"] image = "amazoncorretto:" + properties["projectSourceCompatibility"] + "-alpine"
platforms { platforms {
platform { platform {
architecture = "amd64" architecture = "amd64"
@@ -1,2 +0,0 @@
server:
port: 12000
+1
View File
@@ -10,4 +10,5 @@ dependencies {
annotationProcessor(libs.mapstruct.processor) annotationProcessor(libs.mapstruct.processor)
kapt(libs.mapstruct.processor) kapt(libs.mapstruct.processor)
implementation(libs.spring.cloud.starter.config) implementation(libs.spring.cloud.starter.config)
implementation(libs.spring.cloud.starter.bus.kafka)
} }
@@ -0,0 +1,13 @@
package de.twomartens.wahlrecht.configuration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.filter.ForwardedHeaderFilter
@Configuration
open class FilterConfiguration {
@Bean
open fun forwardedFilter(): ForwardedHeaderFilter {
return ForwardedHeaderFilter()
}
}
@@ -8,13 +8,13 @@ import java.time.Clock
@Configuration @Configuration
open class InterceptorConfiguration { open class InterceptorConfiguration {
@Bean @Bean
open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest { open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest {
return LoggingInterceptorRest(clock) return LoggingInterceptorRest(clock)
} }
@Bean @Bean
open fun headerInterceptorRest(): HeaderInterceptorRest { open fun headerInterceptorRest(): HeaderInterceptorRest {
return HeaderInterceptorRest() return HeaderInterceptorRest()
} }
} }
@@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.security.OAuthFlows
import io.swagger.v3.oas.annotations.security.SecurityScheme import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.servers.Server
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@@ -36,7 +35,5 @@ open class OpenApiConfiguration {
.version(apiVersion) .version(apiVersion)
.description(apiDescription) .description(apiDescription)
) )
.addServersItem(Server().url("http://localhost:12000"))
.addServersItem(Server().url("https://api.2martens.de"))
} }
} }
@@ -16,8 +16,8 @@ open class WebConfiguration(private val headerInterceptorRest: HeaderInterceptor
override fun addCorsMappings(registry: CorsRegistry) { override fun addCorsMappings(registry: CorsRegistry) {
val registration = registry.addMapping("/**") val registration = registry.addMapping("/**")
registration.allowedMethods( registration.allowedMethods(
HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.GET.name(), HttpMethod.POST.name(),
HttpMethod.PUT.name(), HttpMethod.OPTIONS.name() HttpMethod.PUT.name(), HttpMethod.HEAD.name()
) )
registration.allowCredentials(true) registration.allowCredentials(true)
registration.allowedOrigins( registration.allowedOrigins(
@@ -32,14 +32,16 @@ class VersionHtmlController {
} }
private fun getTitle(): String { private fun getTitle(): String {
val title = VersionHtmlController::class.java.getPackage().implementationTitle val title = VersionHtmlController::class.java.`package`.implementationTitle
?: return "application"
return title.ifBlank { return title.ifBlank {
"application" "application"
} }
} }
private fun getVersion(): String { private fun getVersion(): String {
val version = VersionHtmlController::class.java.getPackage().implementationVersion val version = VersionHtmlController::class.java.`package`.implementationVersion
?: return "DEVELOPER"
return version.ifBlank { return version.ifBlank {
"DEVELOPER" "DEVELOPER"
} }
@@ -12,80 +12,88 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import org.mapstruct.factory.Mappers import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.created
import org.springframework.http.ResponseEntity.ok
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.util.UriComponentsBuilder
@RestController @RestController
@RequestMapping(value = ["/wahlrecht/v1"]) @RequestMapping(value = ["/wahlrecht/v1"])
@Tag(name = "Elections", description = "all requests relating to elections") @Tag(name = "Elections", description = "all requests relating to elections")
class ElectionController(private val service: ElectionService) { class ElectionController(private val service: ElectionService) {
private val mapper = Mappers.getMapper(ElectionMapper::class.java) private val mapper = Mappers.getMapper(ElectionMapper::class.java)
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@GetMapping(value = ["/elections"], produces = [MediaType.APPLICATION_JSON_VALUE]) @GetMapping(value = ["/elections"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation( @Operation(
summary = "Returns all stored elections", summary = "Returns all stored elections",
responses = [ApiResponse( responses = [ApiResponse(
responseCode = "200", responseCode = "200",
description = "Returns all stored elections", description = "Returns all stored elections",
content = [Content( content = [Content(
mediaType = "application/json", mediaType = "application/json",
array = ArraySchema( array = ArraySchema(
schema = Schema( schema = Schema(
implementation = Election::class implementation = Election::class
) )
) )
)] )]
)] )]
) )
fun getElections(): ResponseEntity<Collection<Election>> { fun getElections(): ResponseEntity<Collection<Election>> {
val elections = service.elections.asSequence() val elections = service.elections.asSequence()
.map { mapper.mapToExternal(it.value) } .map { mapper.mapToExternal(it.value) }
.toList() .toList()
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(elections) .body(elections)
} }
@Operation( @Operation(
summary = "Returns election matching provided name", summary = "Returns election matching provided name",
responses = [ApiResponse( responses = [ApiResponse(
responseCode = "200", responseCode = "200",
description = "Returns matching election", description = "Returns matching election",
content = [Content(mediaType = "application/json", schema = Schema(implementation = Election::class))] content = [Content(mediaType = "application/json", schema = Schema(implementation = Election::class))]
)] )]
) )
@GetMapping(value = ["/elections/by-name/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE]) @GetMapping(value = ["/elections/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
fun getElectionByName( fun getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String? @PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String?
): ResponseEntity<Election> { ): ResponseEntity<Election> {
val election = mapper.mapToExternal(service.elections[electionName]!!) val election = mapper.mapToExternal(service.elections[electionName]!!)
return ResponseEntity.ok() return ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(election) .body(election)
} }
@Operation( @Operation(
summary = "Stores a new election", summary = "Stores a new election",
responses = [ApiResponse( responses = [ApiResponse(
responseCode = "201", responseCode = "201",
description = "Election was created" description = "Election was created"
), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse( ), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse(
responseCode = "401", responseCode = "401",
description = "Unauthorized", description = "Unauthorized",
content = [Content(mediaType = "plain/text")] content = [Content(mediaType = "plain/text")]
)] )]
) )
@PutMapping("/election") @PutMapping("/election")
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
fun putElection(@RequestBody election: Election?): ResponseEntity<*> { fun putElection(@RequestBody election: Election?, uriComponentsBuilder: UriComponentsBuilder):
val createdNew = service.storeElection(mapper.mapToDB(election!!)) ResponseEntity<Void> {
return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK) val createdNew = service.storeElection(mapper.mapToDB(election!!))
return if (createdNew) {
created(uriComponentsBuilder.path("/elections/{electionName}")
.buildAndExpand(election.name).toUri())
.build()
} }
else ok().build()
}
} }
@@ -14,10 +14,12 @@ import io.swagger.v3.oas.annotations.tags.Tag
import lombok.RequiredArgsConstructor import lombok.RequiredArgsConstructor
import lombok.extern.slf4j.Slf4j import lombok.extern.slf4j.Slf4j
import org.mapstruct.factory.Mappers import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.created
import org.springframework.http.ResponseEntity.ok
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.util.UriComponentsBuilder
@Slf4j @Slf4j
@RestController @RestController
@@ -40,16 +42,16 @@ class ElectionResultController(private val service: ElectionResultService) {
)] )]
) )
@GetMapping( @GetMapping(
value = ["/electionResult/by-election-name/{electionName}"], value = ["/electionResults/{electionName}"],
produces = [MediaType.APPLICATION_JSON_VALUE] produces = [MediaType.APPLICATION_JSON_VALUE]
) )
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
fun getElectionByName( fun getElectionResultByElectionName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String @PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
): ResponseEntity<ElectionResult> { ): ResponseEntity<ElectionResult> {
val result = mapper.mapToExternal(service.getElectionResult(electionName)) val result = mapper.mapToExternal(service.getElectionResult(electionName))
return ResponseEntity.ok() return ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(result) .body(result)
} }
@@ -68,8 +70,14 @@ class ElectionResultController(private val service: ElectionResultService) {
@PutMapping("/electionResult") @PutMapping("/electionResult")
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
fun putElection(@RequestBody electionResult: ElectionResult?): ResponseEntity<*> { fun putElection(@RequestBody electionResult: ElectionResult?,
uriComponentsBuilder: UriComponentsBuilder): ResponseEntity<Void> {
val createdNew = service.storeResult(mapper.mapToDB(electionResult!!)) val createdNew = service.storeResult(mapper.mapToDB(electionResult!!))
return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK) return if (createdNew) {
created(uriComponentsBuilder.path("/electionResult/{electionName}")
.buildAndExpand(electionResult.electionName).toUri())
.build()
}
else ok().build()
} }
} }
@@ -1,5 +1,6 @@
package de.twomartens.wahlrecht.controller.v1 package de.twomartens.wahlrecht.controller.v1
import de.twomartens.wahlrecht.property.ServiceProperties
import io.swagger.v3.oas.annotations.Hidden import io.swagger.v3.oas.annotations.Hidden
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@@ -7,11 +8,12 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping(value = ["/wahlrecht/v1"]) @RequestMapping(value = ["/wahlrecht/v1"])
class HealthCheckController { class HealthCheckController(private val properties: ServiceProperties) {
@Hidden @Hidden
@GetMapping("/healthCheck") @GetMapping("/healthCheck")
fun healthCheck(message: String): String { fun healthCheck(message: String): String {
return message return properties.greeting.format(message)
} }
} }
@@ -13,65 +13,103 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import org.mapstruct.factory.Mappers import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.created
import org.springframework.http.ResponseEntity.ok
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.util.UriComponentsBuilder
@Controller @Controller
@RequestMapping(value = ["/wahlrecht/v1"]) @RequestMapping(value = ["/wahlrecht/v1"])
@Tag(name = "Parties", description = "all requests relating to parties") @Tag(name = "Parties", description = "all requests relating to parties")
class PartyController(private val service: PartyService) { class PartyController(private val service: PartyService) {
private val mapper = Mappers.getMapper(PartyInElectionMapper::class.java) private val mapper = Mappers.getMapper(PartyInElectionMapper::class.java)
@Operation(
summary = "Returns all stored parties for given election name",
responses = [ApiResponse(
responseCode = "200",
description = "Returns all stored parties",
content = [Content(
mediaType = "application/json",
array = ArraySchema(schema = Schema(implementation = PartyInElection::class))
)]
), ApiResponse(
responseCode = "404",
description = "No parties found",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorMessage::class))]
)]
)
@GetMapping(value = ["/parties/by-election-name/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getPartiesByElectionName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
): ResponseEntity<Collection<PartyInElection>> {
val parties = service.getPartiesByElectionName(electionName).stream()
.map { election: de.twomartens.wahlrecht.model.db.PartyInElection? ->
mapper.mapToExternal(
election!!
)
}
.toList()
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(parties)
}
@Operation( @Operation(
summary = "Stores a new party in an election", summary = "Returns all stored parties for given election name",
responses = [ApiResponse( responses = [ApiResponse(
responseCode = "201", responseCode = "200",
description = "Party was created" description = "Returns all stored parties",
), ApiResponse(responseCode = "200", description = "Party was modified"), ApiResponse( content = [Content(
responseCode = "401", mediaType = "application/json",
description = "Unauthorized" array = ArraySchema(schema = Schema(implementation = PartyInElection::class))
)] )]
), ApiResponse(
responseCode = "404",
description = "No parties found",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorMessage::class))]
)]
)
@GetMapping(value = ["/parties/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getPartiesByElectionName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
): ResponseEntity<Collection<PartyInElection>> {
val parties = service.getPartiesByElectionName(electionName).stream()
.map { election: de.twomartens.wahlrecht.model.db.PartyInElection? ->
mapper.mapToExternal(
election!!
)
}
.toList()
return ok()
.contentType(MediaType.APPLICATION_JSON)
.body(parties)
}
@Operation(
summary = "Returns stored party for given election name and party abbreviation",
responses = [ApiResponse(
responseCode = "200",
description = "Returns stored party",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = PartyInElection::class)
)]
), ApiResponse(
responseCode = "404",
description = "No party found",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorMessage::class))]
)]
)
@GetMapping(value = ["/parties/{electionName}/{abbreviation}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getPartyByElectionNameAndAbbreviation(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String,
@PathVariable @Parameter(description = "the party abbreviation", example = "SPD") abbreviation: String
): ResponseEntity<PartyInElection> {
val party = mapper.mapToExternal(
service.getPartyByElectionNameAndAbbreviation(electionName, abbreviation)
) )
@PutMapping("/party") return ok()
@SecurityRequirement(name = "bearerAuth") .contentType(MediaType.APPLICATION_JSON)
@SecurityRequirement(name = "oauth2") .body(party)
fun putParty(@RequestBody party: PartyInElection?): ResponseEntity<*> { }
val createdNew = service.storeParty(mapper.mapToDB(party!!))
return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK) @Operation(
} summary = "Stores a new party in an election",
responses = [ApiResponse(
responseCode = "201",
description = "Party was created"
), ApiResponse(responseCode = "200", description = "Party was modified"), ApiResponse(
responseCode = "401",
description = "Unauthorized"
)]
)
@PutMapping("/party")
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun putParty(@RequestBody party: PartyInElection?,
uriComponentsBuilder: UriComponentsBuilder): ResponseEntity<Void> {
val createdNew = service.storeParty(mapper.mapToDB(party!!))
return if (createdNew) {
created(uriComponentsBuilder.path("/parties/{electionName}/{abbreviation}")
.buildAndExpand(party.electionName, party.abbreviation).toUri())
.build()
} else ok().build()
}
} }
@@ -1,6 +1,7 @@
package de.twomartens.wahlrecht.monitoring.actuator package de.twomartens.wahlrecht.monitoring.actuator
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest
import de.twomartens.wahlrecht.property.ServiceProperties
import org.springframework.boot.actuate.health.Health import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.boot.actuate.health.Status import org.springframework.boot.actuate.health.Status
@@ -28,8 +29,10 @@ import java.time.Clock
*/ */
@Component @Component
class RestHealthIndicator( class RestHealthIndicator(
clock: Clock, interceptor: HeaderInterceptorRest, clock: Clock, interceptor: HeaderInterceptorRest,
serverProperties: ServerProperties, private val restTemplateRestHealthIndicator: RestTemplate serverProperties: ServerProperties,
private val restTemplateRestHealthIndicator: RestTemplate,
private val serviceProperties: ServiceProperties
) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator { ) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator {
private val randomizer = SecureRandom() private val randomizer = SecureRandom()
private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR
@@ -43,10 +46,10 @@ class RestHealthIndicator(
val random = randomizer.nextInt(100000, 999999).toString() val random = randomizer.nextInt(100000, 999999).toString()
val url = "$urlPrefix{random}" val url = "$urlPrefix{random}"
val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random) val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random)
val status = if (response.body == random) Status.UP else Status.DOWN val status = if (response.body == serviceProperties.greeting.format(random)) Status.UP else Status.DOWN
return Health.status(status) return Health.status(status)
.withDetail(DETAIL_ENDPOINT_KEY, url) .withDetail(DETAIL_ENDPOINT_KEY, url)
.build() .build()
} }
companion object { companion object {
@@ -2,11 +2,13 @@ package de.twomartens.wahlrecht.property
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
import org.springframework.cloud.context.config.annotation.RefreshScope import org.springframework.cloud.context.config.annotation.RefreshScope
import java.time.ZoneId import java.time.ZoneId
@RefreshScope @RefreshScope
@ConfigurationProperties(prefix = "de.twomartens.wahlrecht") @ConfigurationProperties(prefix = "de.twomartens.wahlrecht")
@Schema(description = "Properties, to configure this Application") @Schema(description = "Properties, to configure this Application")
data class ServiceProperties @ConstructorBinding constructor(val defaultTimeZone: ZoneId, val greeting: String) class ServiceProperties {
lateinit var defaultTimeZone: ZoneId
lateinit var greeting: String
}
@@ -5,4 +5,6 @@ import org.springframework.data.mongodb.repository.MongoRepository
interface PartyRepository : MongoRepository<PartyInElection, String> { interface PartyRepository : MongoRepository<PartyInElection, String> {
fun findByElectionName(name: String): Collection<PartyInElection> fun findByElectionName(name: String): Collection<PartyInElection>
fun findByElectionNameAndAbbreviation(electionName: String, abbreviation: String): PartyInElection?
} }
@@ -13,53 +13,54 @@ import org.keycloak.representations.idm.authorization.Permission
class SpringPolicyEnforcer(private val policyEnforcer: PolicyEnforcer, class SpringPolicyEnforcer(private val policyEnforcer: PolicyEnforcer,
private val policyEnforcerConfig: PolicyEnforcerConfig) { private val policyEnforcerConfig: PolicyEnforcerConfig) {
fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext { fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext {
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.uri) log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.uri)
}
val context = authorize(request, response)
if (log.isDebugEnabled) {
log.debug("Policy enforcement result for path [{}] is : {}", request.uri, if (context.isGranted) "GRANTED" else "DENIED")
log.debug("Returning authorization context with permissions:")
for (permission in context.permissions) {
log.debug(permission.toString())
}
}
return context
} }
val context = authorize(request, response)
private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext { if (log.isDebugEnabled) {
val enforcementMode = policyEnforcerConfig.enforcementMode log.debug("Policy enforcement result for path [{}] is : {}", request.uri, if (context.isGranted) "GRANTED" else "DENIED")
return if (EnforcementMode.DISABLED == enforcementMode) { log.debug("Returning authorization context with permissions:")
createAuthorizedContext() for (permission in context.permissions) {
} else policyEnforcer.enforce(request, response) log.debug(permission.toString())
}
} }
return context
}
private fun createAuthorizedContext(): AuthorizationContext { private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext {
return object : ClientAuthorizationContext(policyEnforcer.authzClient) { val enforcementMode = policyEnforcerConfig.enforcementMode
override fun hasPermission(resourceName: String, scopeName: String): Boolean { log.debug("Authorize with enforcement mode: [{}]", enforcementMode)
return true return if (EnforcementMode.DISABLED == enforcementMode) {
} createAuthorizedContext()
} else policyEnforcer.enforce(request, response)
}
override fun hasResourcePermission(resourceName: String): Boolean { private fun createAuthorizedContext(): AuthorizationContext {
return true return object : ClientAuthorizationContext(policyEnforcer.authzClient) {
} override fun hasPermission(resourceName: String, scopeName: String): Boolean {
return true
}
override fun hasScopePermission(scopeName: String): Boolean { override fun hasResourcePermission(resourceName: String): Boolean {
return true return true
} }
override fun getPermissions(): List<Permission> { override fun hasScopePermission(scopeName: String): Boolean {
return emptyList() return true
} }
override fun isGranted(): Boolean { override fun getPermissions(): List<Permission> {
return true return emptyList()
} }
}
override fun isGranted(): Boolean {
return true
}
} }
}
companion object { companion object {
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
} }
} }
@@ -18,6 +18,13 @@ class PartyService(
fetchParties() fetchParties()
} }
fun getPartyByElectionNameAndAbbreviation(electionName: String,
abbreviation: String): PartyInElection {
return partyRepository.findByElectionNameAndAbbreviation(electionName, abbreviation)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND,
"no party found for $electionName and $abbreviation")
}
fun getPartiesByElectionName(electionName: String): Collection<PartyInElection> { fun getPartiesByElectionName(electionName: String): Collection<PartyInElection> {
val parties = partyRepository.findByElectionName(electionName) val parties = partyRepository.findByElectionName(electionName)
if (parties.isEmpty()) { if (parties.isEmpty()) {
@@ -1,41 +0,0 @@
package de.twomartens.wahlrecht.service
import mu.KotlinLogging
import org.apache.logging.log4j.message.StringMapMessage
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.cloud.context.refresh.ContextRefresher
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit
/**
* automatically reloads application*.yaml. reload can also be triggered manually by doing a post
* request on http://localhost:12001/actuator/refresh
*/
@Service
class PropertyReloadService(@Qualifier("configDataContextRefresher") private val contextRefresher: ContextRefresher) {
@Scheduled(
fixedDelay = REFRESH_SECONDS.toLong(),
initialDelay = REFRESH_SECONDS.toLong(),
timeUnit = TimeUnit.SECONDS
)
fun refresh() {
val properties = contextRefresher.refresh()
if (properties.isNotEmpty()) {
log.info(
StringMapMessage()
.with(PARAM_MESSAGE, "properties changed")
.with(PARAM_PROPERTIES, properties.joinToString("\n"))
.asString()
)
}
}
companion object {
const val REFRESH_SECONDS = 60
const val PARAM_MESSAGE = "message"
const val PARAM_PROPERTIES = "labels.properties"
private val log = KotlinLogging.logger {}
}
}
@@ -1,2 +1,5 @@
server.port: 12000 spring:
management.server.port: 12001 config:
import:
- "classpath:config/wahlrecht.yaml"
- "optional:configserver:${CONFIGSERVER_SCHEME:http}://${CONFIGSERVER_HOST:configserver}:${CONFIGSERVER_PORT:8888}${CONFIGSERVER_PREFIX:/config}"
@@ -0,0 +1,7 @@
spring:
config:
import:
- "configserver:${CONFIGSERVER_SCHEME:http}://${CONFIGSERVER_HOST:configserver}:${CONFIGSERVER_PORT:8888}${CONFIGSERVER_PREFIX:/config}"
kafka:
bootstrap-servers:
- "${KAFKA_HOST:kafka}:${KAFKA_PORT:9092}"
@@ -1,11 +1,12 @@
server: server:
port: 12000 port: ${PORT:12000}
shutdown: graceful shutdown: graceful
forward-headers-strategy: framework
### technical configurations ### ### technical configurations ###
management: management:
# for security, don't use same port as application # for security, don't use same port as application
server.port: 12001 server.port: ${ACTUATOR_PORT:12001}
health: health:
livenessState.enabled: true livenessState.enabled: true
readinessState.enabled: true readinessState.enabled: true
@@ -23,8 +24,9 @@ management:
http.server.requests: true http.server.requests: true
spring: spring:
config: cloud:
import: "optional:configserver:http://127.0.0.1:8888/config" config:
name: wahlrecht
main: main:
banner-mode: off banner-mode: off
profiles: profiles:
@@ -36,21 +38,10 @@ spring:
oauth2: oauth2:
resourceserver: resourceserver:
jwt: jwt:
jwk-set-uri: https://id.2martens.de/realms/2martens/protocol/openid-connect/certs jwk-set-uri: ${KEYCLOAK_URL:https://id.2martens.de}/realms/${KEYCLOAK_REALM:2martens}/protocol/openid-connect/certs
application: application:
name: wahlrecht name: wahlrecht
# properties for application
de.twomartens.wahlrecht:
defaultTimeZone: Europe/Berlin
greeting: "Hello %s"
statusprobe:
scheduleDuration: 60
maxKafkaFailureDuration: 5
maxBlobFailureCount: 5
maxFailurePercent: 50
springdoc: springdoc:
swagger-ui: swagger-ui:
display-request-duration: true display-request-duration: true
@@ -0,0 +1,2 @@
de.twomartens.wahlrecht:
greeting: "Moin %s, you are test"
@@ -0,0 +1,8 @@
de.twomartens.wahlrecht:
defaultTimeZone: Europe/Berlin
greeting: "Hello %s, on millenium"
statusprobe:
scheduleDuration: 60
maxKafkaFailureDuration: 5
maxBlobFailureCount: 5
maxFailurePercent: 50