diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..699ec32b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.vscode +**/.idea +**/*.iml +**/target +**/hs_err_pid* +**/replay_pid* diff --git a/.github/workflows/cd-auditservice.yml b/.github/workflows/cd-auditservice.yml index 7a21b995..beb75195 100644 --- a/.github/workflows/cd-auditservice.yml +++ b/.github/workflows/cd-auditservice.yml @@ -7,12 +7,15 @@ on: - main paths: - '.github/workflows/cd-auditservice.yml' + - '.dockerignore' + - 'pom.xml' - 'auditlog/**' - 'shiftcontrol-lib/**' env: IMAGE_NAME: shiftcontrol/auditservice DOCKERFILE_PATH: ./auditlog/Dockerfile + NATIVE_DOCKERFILE_PATH: ./auditlog/Dockerfile.native jobs: build-and-publish: @@ -20,19 +23,31 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image - uses: docker/build-push-action@v2 + - name: Build and push JVM Docker image + uses: docker/build-push-action@v6 with: context: ./ file: ${{ env.DOCKERFILE_PATH }} push: true - tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest \ No newline at end of file + tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha,scope=auditservice-jvm + cache-to: type=gha,mode=max,scope=auditservice-jvm + + - name: Build and push native Docker image + uses: docker/build-push-action@v6 + with: + context: ./ + file: ${{ env.NATIVE_DOCKERFILE_PATH }} + push: true + tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest-native + cache-from: type=gha,scope=auditservice-native + cache-to: type=gha,mode=max,scope=auditservice-native diff --git a/.github/workflows/cd-shiftservice.yml b/.github/workflows/cd-shiftservice.yml index e747f7e5..bf3e320e 100644 --- a/.github/workflows/cd-shiftservice.yml +++ b/.github/workflows/cd-shiftservice.yml @@ -7,12 +7,16 @@ on: - main paths: - '.github/workflows/cd-shiftservice.yml' + - '.dockerignore' + - 'pom.xml' + - 'pretalx-client/**' - 'shiftservice/**' - 'shiftcontrol-lib/**' env: IMAGE_NAME: shiftcontrol/shiftservice DOCKERFILE_PATH: ./shiftservice/Dockerfile + NATIVE_DOCKERFILE_PATH: ./shiftservice/Dockerfile.native jobs: build-and-publish: @@ -20,19 +24,31 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image - uses: docker/build-push-action@v2 + - name: Build and push JVM Docker image + uses: docker/build-push-action@v6 with: context: ./ file: ${{ env.DOCKERFILE_PATH }} push: true - tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest \ No newline at end of file + tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha,scope=shiftservice-jvm + cache-to: type=gha,mode=max,scope=shiftservice-jvm + + - name: Build and push native Docker image + uses: docker/build-push-action@v6 + with: + context: ./ + file: ${{ env.NATIVE_DOCKERFILE_PATH }} + push: true + tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest-native + cache-from: type=gha,scope=shiftservice-native + cache-to: type=gha,mode=max,scope=shiftservice-native diff --git a/.github/workflows/cd-trustservice.yml b/.github/workflows/cd-trustservice.yml index 397ca709..24408d93 100644 --- a/.github/workflows/cd-trustservice.yml +++ b/.github/workflows/cd-trustservice.yml @@ -7,12 +7,15 @@ on: - main paths: - '.github/workflows/cd-trustservice.yml' + - '.dockerignore' + - 'pom.xml' - 'trustservice/**' - 'shiftcontrol-lib/**' env: IMAGE_NAME: shiftcontrol/trustservice DOCKERFILE_PATH: ./trustservice/Dockerfile + NATIVE_DOCKERFILE_PATH: ./trustservice/Dockerfile.native jobs: build-and-publish: @@ -20,19 +23,31 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image - uses: docker/build-push-action@v2 + - name: Build and push JVM Docker image + uses: docker/build-push-action@v6 with: context: ./ file: ${{ env.DOCKERFILE_PATH }} push: true - tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest \ No newline at end of file + tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha,scope=trustservice-jvm + cache-to: type=gha,mode=max,scope=trustservice-jvm + + - name: Build and push native Docker image + uses: docker/build-push-action@v6 + with: + context: ./ + file: ${{ env.NATIVE_DOCKERFILE_PATH }} + push: true + tags: ghcr.io/pilotwhale-os/${{ env.IMAGE_NAME }}:latest-native + cache-from: type=gha,scope=trustservice-native + cache-to: type=gha,mode=max,scope=trustservice-native diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de745ff0..5d08477f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,3 +85,95 @@ jobs: path: | **/target/surefire-reports/*.xml **/target/failsafe-reports/*.xml + + shiftservice-native: + name: Shiftservice Native Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build native shiftservice image + run: docker build -f shiftservice/Dockerfile.native -t shiftservice-native:ci . + + - name: Smoke test native shiftservice image + run: | + set -euo pipefail + success=0 + container_id=$(docker run -d -p 18080:8080 -e SPRING_PROFILES_ACTIVE=openapi shiftservice-native:ci) + cleanup() { + if [ "$success" -ne 1 ]; then + docker logs "$container_id" || true + fi + docker rm -f "$container_id" >/dev/null 2>&1 || true + } + trap cleanup EXIT + + for i in {1..90}; do + if curl --silent --fail http://127.0.0.1:18080/v3/api-docs >/dev/null; then + success=1 + exit 0 + fi + sleep 1 + done + + echo "Native shiftservice did not become ready in time." + exit 1 + + trustservice-native: + name: Trustservice Native Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build native trustservice image + run: docker build -f trustservice/Dockerfile.native -t trustservice-native:ci . + + - name: Smoke test native trustservice image + run: | + set -euo pipefail + log_file=$(mktemp) + timeout 120s docker run --rm \ + -e SPRING_RABBITMQ_LISTENER_SIMPLE_AUTO_STARTUP=false \ + -v "$PWD/trustservice/config/application.yml:/app/application.yml:ro" \ + trustservice-native:ci 2>&1 | tee "$log_file" + + grep -q "Started TrustServiceApplication" "$log_file" + + auditservice-native: + name: Auditservice Native Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build native auditservice image + run: docker build -f auditlog/Dockerfile.native -t auditservice-native:ci . + + - name: Smoke test native auditservice image + run: | + set -euo pipefail + success=0 + container_id=$(docker run -d -p 18089:8080 -e SPRING_PROFILES_ACTIVE=openapi auditservice-native:ci) + cleanup() { + if [ "$success" -ne 1 ]; then + docker logs "$container_id" || true + fi + docker rm -f "$container_id" >/dev/null 2>&1 || true + } + trap cleanup EXIT + + for i in {1..90}; do + if curl --silent --fail http://127.0.0.1:18089/v3/api-docs >/dev/null; then + success=1 + exit 0 + fi + sleep 1 + done + + echo "Native auditservice did not become ready in time." + exit 1 diff --git a/.gitignore b/.gitignore index 72e79dac..20458cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,144 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,maven,intellij+iml +# Edit at https://www.toptal.com/developers/gitignore?templates=java,maven,intellij+iml + +### Intellij+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# End of https://www.toptal.com/developers/gitignore/api/java,maven,intellij+iml + + + .idea notificationservice/ -*/**/target \ No newline at end of file +*/**/target + + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d53ecaf3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/auditlog/Dockerfile.native b/auditlog/Dockerfile.native new file mode 100644 index 00000000..f5731ebe --- /dev/null +++ b/auditlog/Dockerfile.native @@ -0,0 +1,39 @@ +FROM docker.io/maven:3.9.11-eclipse-temurin-25 AS maven + +FROM ghcr.io/graalvm/native-image-community:25 AS builder +WORKDIR /app + +COPY --from=maven /usr/share/maven /usr/share/maven +RUN ln -s /usr/share/maven/bin/mvn /usr/bin/mvn + +ENV MAVEN_HOME=/usr/share/maven +ENV MAVEN_CONFIG=/root/.m2 + +COPY pom.xml . +COPY shiftcontrol-lib/pom.xml shiftcontrol-lib/ +COPY shiftservice/pom.xml shiftservice/ +COPY pretalx-client/pom.xml pretalx-client/ +COPY trustservice/pom.xml trustservice/ +COPY auditlog/pom.xml auditlog/ + +RUN mvn -pl auditlog -am -DskipTests -DskipITs dependency:go-offline + +COPY shiftcontrol-lib shiftcontrol-lib +COPY shiftservice shiftservice +COPY pretalx-client pretalx-client +COPY trustservice trustservice +COPY auditlog auditlog + +RUN mvn -pl auditlog -am -DskipTests -DskipITs install +RUN mvn -f auditlog/pom.xml -Pnative -DskipTests -DskipITs package native:compile-no-fork + +FROM ghcr.io/graalvm/native-image-community:25 AS server +WORKDIR /app + +COPY --from=builder /app/auditlog/target /app/target + +ENV LD_LIBRARY_PATH=/app/target + +EXPOSE 8089 +USER 1000 +ENTRYPOINT ["/app/target/auditlog"] diff --git a/auditlog/compose.native.yml b/auditlog/compose.native.yml new file mode 100644 index 00000000..10cac22e --- /dev/null +++ b/auditlog/compose.native.yml @@ -0,0 +1,25 @@ +services: + auditservice-native: + image: ${AUDITSERVICE_NATIVE_IMAGE:-auditservice-native:latest} + build: + context: ../ + dockerfile: auditlog/Dockerfile.native + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/shiftservice + SPRING_DATASOURCE_USERNAME: shiftcontrol + SPRING_DATASOURCE_PASSWORD: password + volumes: + - ./config/application.yml:/app/application.yml + networks: + - shiftcontrol + ports: + - "8089:8089" + labels: + - traefik.enable=true + - traefik.http.routers.auditservice-native.entrypoints=web + - traefik.http.routers.auditservice-native.rule=Host(`auditservice-native.127.0.0.1.nip.io`) + - traefik.http.services.auditservice-native.loadbalancer.server.port=8089 + +networks: + shiftcontrol: + external: true diff --git a/auditlog/src/main/resources/META-INF/native-image/at.shiftcontrol/auditlog/reflect-config.json b/auditlog/src/main/resources/META-INF/native-image/at.shiftcontrol/auditlog/reflect-config.json new file mode 100644 index 00000000..95662a79 --- /dev/null +++ b/auditlog/src/main/resources/META-INF/native-image/at.shiftcontrol/auditlog/reflect-config.json @@ -0,0 +1,20 @@ +[ + { + "name": "org.hibernate.type.format.jackson.JacksonJsonFormatMapper", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.hibernate.type.format.jakartajson.JsonBJsonFormatMapper", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + } +] diff --git a/auditlog/src/main/resources/application-openapi.yml b/auditlog/src/main/resources/application-openapi.yml index 35760609..2dd597b4 100644 --- a/auditlog/src/main/resources/application-openapi.yml +++ b/auditlog/src/main/resources/application-openapi.yml @@ -5,6 +5,11 @@ spring: username: sa password: "" + rabbitmq: + listener: + simple: + auto-startup: false + liquibase: enabled: false diff --git a/auditlog/src/main/resources/application.yml b/auditlog/src/main/resources/application.yml index f583a64e..96d0867e 100644 --- a/auditlog/src/main/resources/application.yml +++ b/auditlog/src/main/resources/application.yml @@ -18,7 +18,7 @@ spring: properties: hibernate: type: - json_format_mapper: jsonb + json_format_mapper: jackson oidc: provider: issuer-uri: http://keycloak.127.0.0.1.nip.io/realms/dev diff --git a/shiftservice/Dockerfile b/shiftservice/Dockerfile index 15903189..ac502a2e 100644 --- a/shiftservice/Dockerfile +++ b/shiftservice/Dockerfile @@ -25,5 +25,6 @@ FROM docker.io/eclipse-temurin:25-jre-alpine AS server WORKDIR /app COPY --from=builder /app/shiftservice/target/shiftservice-*.jar /app/app.jar +USER 1000 EXPOSE 8080 CMD ["java", "-jar", "app.jar"] diff --git a/shiftservice/Dockerfile.native b/shiftservice/Dockerfile.native new file mode 100644 index 00000000..d6d6398e --- /dev/null +++ b/shiftservice/Dockerfile.native @@ -0,0 +1,39 @@ +FROM docker.io/maven:3.9.11-eclipse-temurin-25 AS maven + +FROM ghcr.io/graalvm/native-image-community:25 AS builder +WORKDIR /app + +COPY --from=maven /usr/share/maven /usr/share/maven +RUN ln -s /usr/share/maven/bin/mvn /usr/bin/mvn + +ENV MAVEN_HOME=/usr/share/maven +ENV MAVEN_CONFIG=/root/.m2 + +COPY pom.xml . +COPY shiftcontrol-lib/pom.xml shiftcontrol-lib/ +COPY shiftservice/pom.xml shiftservice/ +COPY pretalx-client/pom.xml pretalx-client/ +COPY trustservice/pom.xml trustservice/ +COPY auditlog/pom.xml auditlog/ + +RUN mvn -pl shiftservice -am -DskipTests -DskipITs dependency:go-offline + +COPY shiftcontrol-lib shiftcontrol-lib +COPY shiftservice shiftservice +COPY pretalx-client pretalx-client +COPY trustservice trustservice +COPY auditlog auditlog + +RUN mvn -pl shiftservice -am -DskipTests -DskipITs install +RUN mvn -f shiftservice/pom.xml -Pnative -DskipTests -DskipITs package native:compile-no-fork + +FROM ghcr.io/graalvm/native-image-community:25 AS server +WORKDIR /app + +COPY --from=builder /app/shiftservice/target /app/target + +ENV LD_LIBRARY_PATH=/app/target + +EXPOSE 8080 +USER 1000 +ENTRYPOINT ["/app/target/shiftservice"] diff --git a/shiftservice/README.Docker.md b/shiftservice/README.Docker.md index 8716f34b..6a1b8d6d 100644 --- a/shiftservice/README.Docker.md +++ b/shiftservice/README.Docker.md @@ -16,4 +16,24 @@ you'll want to build the image for that platform, e.g.: Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) -docs for more detail on building and pushing. \ No newline at end of file +docs for more detail on building and pushing. + +### Native image variant + +`shiftservice` also has a separate native Dockerfile that keeps the normal JVM Dockerfile intact. + +Build it with: + +```bash +docker build --network host -f shiftservice/Dockerfile.native -t shiftservice-native:latest . +``` + +Run it with: + +```bash +docker compose -f shiftservice/compose.native.yml up --build -d +``` + +The published registry tag for the native image is: + +- `ghcr.io/pilotwhale-os/shiftcontrol/shiftservice:latest-native` diff --git a/shiftservice/README.md b/shiftservice/README.md index 3afad6d8..32f2df8d 100644 --- a/shiftservice/README.md +++ b/shiftservice/README.md @@ -86,3 +86,37 @@ If you are running from the Java multimodule root instead of the `shiftservice` ```bash mvn verify -pl shiftservice -am -Popenapi ``` + +## Native container image + +`shiftservice` keeps the regular JVM jar and Dockerfile flow, and also has a second Docker-based build path that compiles a native executable inside a GraalVM 25 container. + +### Build the native image + +From the `shiftcontrol-java-backend` directory run: + +```bash +docker build --network host -f shiftservice/Dockerfile.native -t shiftservice-native:latest . +``` + +This does not require a local GraalVM installation. The native compile happens inside the build container. + +If your Docker setup supports bridge networking normally, you can omit `--network host`. It is needed on this machine because standard Docker bridge networking is not available. + +The published registry tag for the native image uses the normal image name with a `-native` suffix on the tag: + +- `ghcr.io/pilotwhale-os/shiftcontrol/shiftservice:latest-native` + +### Run the native image + +Use the dedicated compose file if you want the native variant alongside the normal JVM one: + +```bash +docker compose -f shiftservice/compose.native.yml up --build -d +``` + +The native service is exposed separately at: + +- `http://shiftservice-native.127.0.0.1.nip.io` + +The standard `shiftservice/compose.yml` remains the JVM-based version. diff --git a/shiftservice/compose.native.yml b/shiftservice/compose.native.yml new file mode 100644 index 00000000..4ae98ecd --- /dev/null +++ b/shiftservice/compose.native.yml @@ -0,0 +1,25 @@ +services: + + shiftservice-native: + image: ${SHIFTSERVICE_NATIVE_IMAGE:-shiftservice-native:latest} + build: + context: ../ + dockerfile: shiftservice/Dockerfile.native + network: host + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/shiftservice + SPRING_DATASOURCE_USERNAME: shiftcontrol + SPRING_DATASOURCE_PASSWORD: password + labels: + - traefik.enable=true + - traefik.http.routers.shiftservice-native.entrypoints=web + - traefik.http.routers.shiftservice-native.rule=Host(`shiftservice-native.127.0.0.1.nip.io`) + - traefik.http.services.shiftservice-native.loadbalancer.server.port=8080 + volumes: + - ./config/docker.application.yml:/app/application.yml + networks: + - shiftcontrol + +networks: + shiftcontrol: + external: true diff --git a/shiftservice/src/main/java/at/shiftcontrol/shiftservice/event/RabbitEventPublisher.java b/shiftservice/src/main/java/at/shiftcontrol/shiftservice/event/RabbitEventPublisher.java index 27190f35..c404858c 100644 --- a/shiftservice/src/main/java/at/shiftcontrol/shiftservice/event/RabbitEventPublisher.java +++ b/shiftservice/src/main/java/at/shiftcontrol/shiftservice/event/RabbitEventPublisher.java @@ -32,7 +32,7 @@ public class RabbitEventPublisher { ) private void publishEvent(BaseEvent event) { event.setTraceId(getTraceId()); - if (event.getActingUserId() != null) { + if (event.getActingUserId() == null) { event.setActingUserId(getCurrentUserId()); } var routingKey = event.getRoutingKey(); diff --git a/shiftservice/src/main/resources/META-INF/native-image/at.shiftcontrol/shiftservice/reflect-config.json b/shiftservice/src/main/resources/META-INF/native-image/at.shiftcontrol/shiftservice/reflect-config.json new file mode 100644 index 00000000..ea70fcf5 --- /dev/null +++ b/shiftservice/src/main/resources/META-INF/native-image/at.shiftcontrol/shiftservice/reflect-config.json @@ -0,0 +1,164 @@ +[ + { + "name": "com.github.benmanes.caffeine.cache.BLCHeader$DrainStatusRef", + "fields": [ + { + "name": "drainStatus" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.BaseMpscLinkedArrayQueueColdProducerFields", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.BaseMpscLinkedArrayQueueConsumerFields", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.BaseMpscLinkedArrayQueueProducerFields", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.BoundedLocalCache", + "fields": [ + { + "name": "refreshes" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.CacheLoader" + }, + { + "name": "com.github.benmanes.caffeine.cache.PS", + "fields": [ + { + "name": "key" + }, + { + "name": "value" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.PSA", + "fields": [ + { + "name": "accessTime" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.PSAMS", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.PSW", + "fields": [ + { + "name": "writeTime" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.PSWMS", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.SSMS", + "fields": [ + { + "name": "maximum" + }, + { + "name": "weightedSize" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.SSMSA", + "fields": [ + { + "name": "FACTORY" + }, + { + "name": "expiresAfterAccessNanos" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.SSMSW", + "fields": [ + { + "name": "FACTORY" + }, + { + "name": "expiresAfterWriteNanos" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.SSSMS", + "fields": [ + { + "name": "maximum" + }, + { + "name": "weightedSize" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.SSSMSA", + "fields": [ + { + "name": "FACTORY" + }, + { + "name": "expiresAfterAccessNanos" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.SSSMSW", + "fields": [ + { + "name": "FACTORY" + }, + { + "name": "expiresAfterWriteNanos" + } + ] + }, + { + "name": "com.github.benmanes.caffeine.cache.StripedBuffer", + "fields": [ + { + "name": "tableBusy" + } + ] + } +] diff --git a/shiftservice/src/test/java/at/shiftcontrol/shiftservice/event/RabbitEventPublisherTest.java b/shiftservice/src/test/java/at/shiftcontrol/shiftservice/event/RabbitEventPublisherTest.java new file mode 100644 index 00000000..aa170f1a --- /dev/null +++ b/shiftservice/src/test/java/at/shiftcontrol/shiftservice/event/RabbitEventPublisherTest.java @@ -0,0 +1,87 @@ +package at.shiftcontrol.shiftservice.event; + +import java.util.Optional; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import io.micrometer.tracing.CurrentTraceContext; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import at.shiftcontrol.lib.event.BaseEvent; +import at.shiftcontrol.lib.event.EventType; +import at.shiftcontrol.shiftservice.auth.ApplicationUserProvider; +import at.shiftcontrol.shiftservice.auth.user.ServiceUser; +import at.shiftcontrol.shiftservice.config.RabbitMqConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RabbitEventPublisherTest { + @Mock + private RabbitTemplate rabbitTemplate; + + @Mock + private ApplicationUserProvider userProvider; + + @Mock + private Tracer tracer; + + @Mock + private CurrentTraceContext currentTraceContext; + + @Mock + private TraceContext traceContext; + + @Test + void publishEvent_backfillsMissingActingUserIdFromCurrentUser() { + var event = new TestEvent(); + var currentUser = new ServiceUser(java.util.List.of(), "alice", "user-123"); + var publisher = new RabbitEventPublisher(rabbitTemplate, userProvider, tracer); + + when(userProvider.getNullableApplicationUser()).thenReturn(Optional.of(currentUser)); + when(tracer.currentTraceContext()).thenReturn(currentTraceContext); + when(currentTraceContext.context()).thenReturn(traceContext); + when(traceContext.traceId()).thenReturn("trace-123"); + + org.springframework.test.util.ReflectionTestUtils.invokeMethod(publisher, "publishEvent", event); + + assertThat(event.getActingUserId()).isEqualTo("user-123"); + assertThat(event.getTraceId()).isEqualTo("trace-123"); + verify(rabbitTemplate).convertAndSend( + RabbitMqConfig.EXCHANGE_NAME, + "shiftcontrol.test.event", + event + ); + } + + @Test + void publishEvent_preservesExplicitActingUserId() { + var event = (TestEvent) new TestEvent().withActingUserId("pretalx-sync"); + var publisher = new RabbitEventPublisher(rabbitTemplate, userProvider, tracer); + + when(tracer.currentTraceContext()).thenReturn(currentTraceContext); + when(currentTraceContext.context()).thenReturn(traceContext); + when(traceContext.traceId()).thenReturn("trace-456"); + + org.springframework.test.util.ReflectionTestUtils.invokeMethod(publisher, "publishEvent", event); + + assertThat(event.getActingUserId()).isEqualTo("pretalx-sync"); + assertThat(event.getTraceId()).isEqualTo("trace-456"); + verify(rabbitTemplate).convertAndSend( + RabbitMqConfig.EXCHANGE_NAME, + "shiftcontrol.test.event", + event + ); + } + + private static final class TestEvent extends BaseEvent { + private TestEvent() { + super(EventType.EVENT_CREATED, "test.event"); + } + } +} diff --git a/trustservice/Dockerfile.native b/trustservice/Dockerfile.native new file mode 100644 index 00000000..94ae69ca --- /dev/null +++ b/trustservice/Dockerfile.native @@ -0,0 +1,39 @@ +FROM docker.io/maven:3.9.11-eclipse-temurin-25 AS maven + +FROM ghcr.io/graalvm/native-image-community:25 AS builder +WORKDIR /app + +COPY --from=maven /usr/share/maven /usr/share/maven +RUN ln -s /usr/share/maven/bin/mvn /usr/bin/mvn + +ENV MAVEN_HOME=/usr/share/maven +ENV MAVEN_CONFIG=/root/.m2 + +COPY pom.xml . +COPY shiftcontrol-lib/pom.xml shiftcontrol-lib/ +COPY shiftservice/pom.xml shiftservice/ +COPY pretalx-client/pom.xml pretalx-client/ +COPY trustservice/pom.xml trustservice/ +COPY auditlog/pom.xml auditlog/ + +RUN mvn -pl trustservice -am -DskipTests -DskipITs dependency:go-offline + +COPY shiftcontrol-lib shiftcontrol-lib +COPY shiftservice shiftservice +COPY pretalx-client pretalx-client +COPY trustservice trustservice +COPY auditlog auditlog + +RUN mvn -pl trustservice -am -DskipTests -DskipITs install +RUN mvn -f trustservice/pom.xml -Pnative -DskipTests -DskipITs package native:compile-no-fork + +FROM ghcr.io/graalvm/native-image-community:25 AS server +WORKDIR /app + +COPY --from=builder /app/trustservice/target /app/target + +ENV LD_LIBRARY_PATH=/app/target + +EXPOSE 8080 +USER 1000 +ENTRYPOINT ["/app/target/trustservice"] diff --git a/trustservice/compose.native.yml b/trustservice/compose.native.yml new file mode 100644 index 00000000..830522d6 --- /dev/null +++ b/trustservice/compose.native.yml @@ -0,0 +1,29 @@ +services: + + redis: + image: redis:7-alpine + container_name: trustservice-native-redis + networks: + - shiftcontrol + + trustservice-native: + image: ${TRUSTSERVICE_NATIVE_IMAGE:-trustservice-native:latest} + build: + context: ../ + dockerfile: trustservice/Dockerfile.native + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/shiftservice + SPRING_DATASOURCE_USERNAME: shiftcontrol + SPRING_DATASOURCE_PASSWORD: password + SPRING_DATA_REDIS_HOST: redis + SPRING_DATA_REDIS_PORT: 6379 + depends_on: + - redis + volumes: + - ./config/application.yml:/app/application.yml + networks: + - shiftcontrol + +networks: + shiftcontrol: + external: true