Compare commits

..
70 Commits
Author SHA1 Message Date
2martens f2c1d1ea49 Revert "fix: Remove kafka"
continuous-integration/drone/tag Build is passing
This reverts commit 0082305a5a.
2024-03-08 22:21:27 +01:00
2martens aaec4e940a fix: Prevent memory leak 2024-03-08 22:19:55 +01:00
2martens a02dfa100c chore: Version update
continuous-integration/drone/tag Build is passing
2024-03-06 15:19:53 +01:00
2martens 0082305a5a fix: Remove kafka
continuous-integration/drone/tag Build is passing
2024-03-06 15:06:32 +01:00
2martens 11345c94a5 fix: Update values in correct repository 2024-01-06 15:41:51 +01:00
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
31 changed files with 383 additions and 300 deletions
+60 -7
View File
@@ -30,7 +30,6 @@ volumes:
trigger:
event:
include:
- push
- pull_request
- custom
@@ -51,7 +50,7 @@ steps:
image: alpine/git
environment:
SSH_KEY:
from_secret: ssh_key
from_secret: private_ssh_key
commands:
- mkdir $HOME/.ssh
- echo "$SSH_KEY" > $HOME/.ssh/id_ed25519
@@ -69,7 +68,7 @@ steps:
PASSWORD:
from_secret: docker_password
SSH_KEY:
from_secret: ssh_key
from_secret: private_ssh_key
volumes:
- name: cache
path: /home/gradle/.gradle
@@ -88,10 +87,64 @@ volumes:
path: /var/lib/drone/cache/.gradle
trigger:
branches:
- main
event:
include:
- push
- custom
- tag
---
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/wahlrecht/${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
[{*.kt,*.kts}]
indent_size = 4
tab_width = 4
ij_continuation_indent_size = 8
indent_size = 2
tab_width = 2
ij_continuation_indent_size = 4
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = 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") {
delete("${buildDir}/libs")
delete("${layout.buildDirectory}/libs")
}
tasks.build {
@@ -9,10 +9,10 @@ apply(plugin="com.netflix.nebula.release")
tasks.register("writeVersionProperties") {
group = "version"
mustRunAfter("release")
outputs.file("$buildDir/version.properties")
val directory = buildDir
outputs.file("${layout.buildDirectory}/version.properties")
val directory = layout.buildDirectory.asFile
doLast {
Files.createDirectories(directory.toPath())
File("$buildDir/version.properties").writeText("VERSION=${project.version}\n")
Files.createDirectories(directory.get().toPath())
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")
tasks.bootJar {
tasks.jar {
manifest {
attributes["Implementation-Title"] = rootProject.name
attributes["Implementation-Version"] = archiveVersion.get()
+13 -12
View File
@@ -1,21 +1,21 @@
[versions]
spring-boot = "3.1.2"
spring-doc = "2.1.0"
spring-cloud = "2022.0.4"
spring-boot = "3.2.3"
spring-doc = "2.3.0"
spring-cloud = "2023.0.0"
spring-grpc = "2.14.0.RELEASE"
grpc = "1.57.0"
tomcat-annotations = "6.0.53"
httpclient = "5.2.1"
slf4j = "2.0.7"
log4j = "2.20.0"
log4j-ecs = "1.5.0"
httpclient = "5.3.1"
slf4j = "2.0.12"
log4j = "2.23.0"
log4j-ecs = "1.6.0"
mapstruct = "1.5.5.Final"
junit = "5.10.0"
assertj = "3.24.2"
mockito = "5.4.0"
keycloak = "22.0.1"
junit = "5.10.2"
assertj = "3.25.3"
mockito = "5.11.0"
keycloak = "24.0.1"
kotlin-logging = "3.0.5"
kotlin-reflect = "1.9.0"
kotlin-reflect = "1.9.22"
kotlin-lombok = "1.9.0"
plugin-nebula-release = "17.2.2"
plugin-lombok = "8.0.1"
@@ -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-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-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-config-server = { module = "org.springframework.cloud:spring-cloud-config-server" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" }
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists
+1 -1
View File
@@ -8,7 +8,7 @@ dependencies {
jib {
from {
image = "amazoncorretto:" + properties["projectSourceCompatibility"]
image = "amazoncorretto:" + properties["projectSourceCompatibility"] + "-alpine"
platforms {
platform {
architecture = "amd64"
@@ -1,2 +0,0 @@
server:
port: 12000
+1
View File
@@ -10,4 +10,5 @@ dependencies {
annotationProcessor(libs.mapstruct.processor)
kapt(libs.mapstruct.processor)
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
open class InterceptorConfiguration {
@Bean
open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest {
return LoggingInterceptorRest(clock)
}
@Bean
open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest {
return LoggingInterceptorRest(clock)
}
@Bean
open fun headerInterceptorRest(): HeaderInterceptorRest {
return HeaderInterceptorRest()
}
@Bean
open fun headerInterceptorRest(): 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.models.OpenAPI
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.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -36,7 +35,5 @@ open class OpenApiConfiguration {
.version(apiVersion)
.description(apiDescription)
)
.addServersItem(Server().url("http://localhost:12000"))
.addServersItem(Server().url("https://api.2martens.de"))
}
}
@@ -32,14 +32,16 @@ class VersionHtmlController {
}
private fun getTitle(): String {
val title = VersionHtmlController::class.java.getPackage().implementationTitle
val title = VersionHtmlController::class.java.`package`.implementationTitle
?: return "application"
return title.ifBlank {
"application"
}
}
private fun getVersion(): String {
val version = VersionHtmlController::class.java.getPackage().implementationVersion
val version = VersionHtmlController::class.java.`package`.implementationVersion
?: return "DEVELOPER"
return version.ifBlank {
"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.tags.Tag
import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
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.util.UriComponentsBuilder
@RestController
@RequestMapping(value = ["/wahlrecht/v1"])
@Tag(name = "Elections", description = "all requests relating to elections")
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 = "bearerAuth")
@GetMapping(value = ["/elections"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation(
summary = "Returns all stored elections",
responses = [ApiResponse(
responseCode = "200",
description = "Returns all stored elections",
content = [Content(
mediaType = "application/json",
array = ArraySchema(
schema = Schema(
implementation = Election::class
)
)
)]
)]
)
fun getElections(): ResponseEntity<Collection<Election>> {
val elections = service.elections.asSequence()
.map { mapper.mapToExternal(it.value) }
.toList()
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(elections)
}
@SecurityRequirement(name = "oauth2")
@SecurityRequirement(name = "bearerAuth")
@GetMapping(value = ["/elections"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation(
summary = "Returns all stored elections",
responses = [ApiResponse(
responseCode = "200",
description = "Returns all stored elections",
content = [Content(
mediaType = "application/json",
array = ArraySchema(
schema = Schema(
implementation = Election::class
)
)
)]
)]
)
fun getElections(): ResponseEntity<Collection<Election>> {
val elections = service.elections.asSequence()
.map { mapper.mapToExternal(it.value) }
.toList()
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(elections)
}
@Operation(
summary = "Returns election matching provided name",
responses = [ApiResponse(
responseCode = "200",
description = "Returns matching election",
content = [Content(mediaType = "application/json", schema = Schema(implementation = Election::class))]
)]
)
@GetMapping(value = ["/elections/by-name/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String?
): ResponseEntity<Election> {
val election = mapper.mapToExternal(service.elections[electionName]!!)
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(election)
}
@Operation(
summary = "Stores a new election",
responses = [ApiResponse(
responseCode = "201",
description = "Election was created"
), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse(
responseCode = "401",
description = "Unauthorized",
content = [Content(mediaType = "plain/text")]
)]
)
@PutMapping("/election")
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun putElection(@RequestBody election: Election?): ResponseEntity<*> {
val createdNew = service.storeElection(mapper.mapToDB(election!!))
return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
@Operation(
summary = "Returns election matching provided name",
responses = [ApiResponse(
responseCode = "200",
description = "Returns matching election",
content = [Content(mediaType = "application/json", schema = Schema(implementation = Election::class))]
)]
)
@GetMapping(value = ["/elections/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String?
): ResponseEntity<Election> {
val election = mapper.mapToExternal(service.elections[electionName]!!)
return ok()
.contentType(MediaType.APPLICATION_JSON)
.body(election)
}
@Operation(
summary = "Stores a new election",
responses = [ApiResponse(
responseCode = "201",
description = "Election was created"
), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse(
responseCode = "401",
description = "Unauthorized",
content = [Content(mediaType = "plain/text")]
)]
)
@PutMapping("/election")
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun putElection(@RequestBody election: Election?, uriComponentsBuilder: UriComponentsBuilder):
ResponseEntity<Void> {
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.extern.slf4j.Slf4j
import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
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.util.UriComponentsBuilder
@Slf4j
@RestController
@@ -40,16 +42,16 @@ class ElectionResultController(private val service: ElectionResultService) {
)]
)
@GetMapping(
value = ["/electionResult/by-election-name/{electionName}"],
value = ["/electionResults/{electionName}"],
produces = [MediaType.APPLICATION_JSON_VALUE]
)
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getElectionByName(
fun getElectionResultByElectionName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
): ResponseEntity<ElectionResult> {
val result = mapper.mapToExternal(service.getElectionResult(electionName))
return ResponseEntity.ok()
return ok()
.contentType(MediaType.APPLICATION_JSON)
.body(result)
}
@@ -68,8 +70,14 @@ class ElectionResultController(private val service: ElectionResultService) {
@PutMapping("/electionResult")
@SecurityRequirement(name = "bearerAuth")
@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!!))
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
import de.twomartens.wahlrecht.property.ServiceProperties
import io.swagger.v3.oas.annotations.Hidden
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
@@ -7,11 +8,12 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping(value = ["/wahlrecht/v1"])
class HealthCheckController {
class HealthCheckController(private val properties: ServiceProperties) {
@Hidden
@GetMapping("/healthCheck")
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.tags.Tag
import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
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.web.bind.annotation.*
import org.springframework.web.util.UriComponentsBuilder
@Controller
@RequestMapping(value = ["/wahlrecht/v1"])
@Tag(name = "Parties", description = "all requests relating to parties")
class PartyController(private val service: PartyService) {
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)
}
private val mapper = Mappers.getMapper(PartyInElectionMapper::class.java)
@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"
)]
@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/{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")
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
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)
}
return ok()
.contentType(MediaType.APPLICATION_JSON)
.body(party)
}
@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
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.HealthIndicator
import org.springframework.boot.actuate.health.Status
@@ -28,8 +29,10 @@ import java.time.Clock
*/
@Component
class RestHealthIndicator(
clock: Clock, interceptor: HeaderInterceptorRest,
serverProperties: ServerProperties, private val restTemplateRestHealthIndicator: RestTemplate
clock: Clock, interceptor: HeaderInterceptorRest,
serverProperties: ServerProperties,
private val restTemplateRestHealthIndicator: RestTemplate,
private val serviceProperties: ServiceProperties
) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator {
private val randomizer = SecureRandom()
private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR
@@ -43,10 +46,10 @@ class RestHealthIndicator(
val random = randomizer.nextInt(100000, 999999).toString()
val url = "$urlPrefix{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)
.withDetail(DETAIL_ENDPOINT_KEY, url)
.build()
.withDetail(DETAIL_ENDPOINT_KEY, url)
.build()
}
companion object {
@@ -2,11 +2,13 @@ package de.twomartens.wahlrecht.property
import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
import org.springframework.cloud.context.config.annotation.RefreshScope
import java.time.ZoneId
@RefreshScope
@ConfigurationProperties(prefix = "de.twomartens.wahlrecht")
@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> {
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,
private val policyEnforcerConfig: PolicyEnforcerConfig) {
fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext {
if (log.isDebugEnabled) {
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
fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext {
if (log.isDebugEnabled) {
log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.uri)
}
private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext {
val enforcementMode = policyEnforcerConfig.enforcementMode
return if (EnforcementMode.DISABLED == enforcementMode) {
createAuthorizedContext()
} else policyEnforcer.enforce(request, response)
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
}
private fun createAuthorizedContext(): AuthorizationContext {
return object : ClientAuthorizationContext(policyEnforcer.authzClient) {
override fun hasPermission(resourceName: String, scopeName: String): Boolean {
return true
}
private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext {
val enforcementMode = policyEnforcerConfig.enforcementMode
log.debug("Authorize with enforcement mode: [{}]", enforcementMode)
return if (EnforcementMode.DISABLED == enforcementMode) {
createAuthorizedContext()
} else policyEnforcer.enforce(request, response)
}
override fun hasResourcePermission(resourceName: String): Boolean {
return true
}
private fun createAuthorizedContext(): AuthorizationContext {
return object : ClientAuthorizationContext(policyEnforcer.authzClient) {
override fun hasPermission(resourceName: String, scopeName: String): Boolean {
return true
}
override fun hasScopePermission(scopeName: String): Boolean {
return true
}
override fun hasResourcePermission(resourceName: String): Boolean {
return true
}
override fun getPermissions(): List<Permission> {
return emptyList()
}
override fun hasScopePermission(scopeName: String): Boolean {
return true
}
override fun isGranted(): Boolean {
return true
}
}
override fun getPermissions(): List<Permission> {
return emptyList()
}
override fun isGranted(): Boolean {
return true
}
}
}
companion object {
private val log = KotlinLogging.logger {}
}
companion object {
private val log = KotlinLogging.logger {}
}
}
@@ -12,18 +12,15 @@ import org.keycloak.adapters.authorization.spi.ConfigurationResolver
import org.keycloak.adapters.authorization.spi.HttpRequest
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver) : Filter {
private val policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = ConcurrentHashMap()
@Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) {
val request = servletRequest as HttpServletRequest
val response = servletResponse as HttpServletResponse?
val httpRequest = ServletHttpRequest(request) { extractBearerToken(request) }
val policyEnforcer = getOrCreatePolicyEnforcer(httpRequest)
val policyEnforcer = createPolicyEnforcer(httpRequest)
val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response))
request.setAttribute(AuthorizationContext::class.java.name, authzContext)
if (authzContext.isGranted) {
@@ -53,8 +50,8 @@ class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolv
return null
}
private fun getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
return policyEnforcer.computeIfAbsent(configResolver.resolve(request)) { createPolicyEnforcer(it) }
private fun createPolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
return createPolicyEnforcer(configResolver.resolve(request))
}
private fun createPolicyEnforcer(enforcerConfig: PolicyEnforcerConfig): SpringPolicyEnforcer {
@@ -18,6 +18,13 @@ class PartyService(
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> {
val parties = partyRepository.findByElectionName(electionName)
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
management.server.port: 12001
spring:
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:
port: 12000
port: ${PORT:12000}
shutdown: graceful
forward-headers-strategy: framework
### technical configurations ###
management:
# for security, don't use same port as application
server.port: 12001
server.port: ${ACTUATOR_PORT:12001}
health:
livenessState.enabled: true
readinessState.enabled: true
@@ -23,8 +24,9 @@ management:
http.server.requests: true
spring:
config:
import: "optional:configserver:http://configserver:8888/config"
cloud:
config:
name: wahlrecht
main:
banner-mode: off
profiles:
@@ -36,21 +38,10 @@ spring:
oauth2:
resourceserver:
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:
name: wahlrecht
# properties for application
de.twomartens.wahlrecht:
defaultTimeZone: Europe/Berlin
greeting: "Hello %s"
statusprobe:
scheduleDuration: 60
maxKafkaFailureDuration: 5
maxBlobFailureCount: 5
maxFailurePercent: 50
springdoc:
swagger-ui:
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