diff --git a/.dockerignore b/.dockerignore index eb5a316..29815f8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,7 @@ +# .dockerignore target +.git +*.log +*.class +.idea +.vscode diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a735dd3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +name: CI (self-hosted-safe) + +on: + push: + branches: + - 'feature/*' + tags: + - 'v*' + pull_request: + branches: + - 'feature/*' + +permissions: + contents: read + +env: + DOCKER_REPO: brunoe/javahello + LOCAL_REPO_PREFIX: javahello + +jobs: + validate: + name: Validate (Maven, GH-hosted) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + + - name: Cache Maven repo + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-m2- + + - name: Validate + run: ./mvnw -DskipTests validate + + build-docker: + name: Build & smoke-test images (self-hosted, matrix) + needs: validate + runs-on: [self-hosted, Linux, X64] + if: > + github.event_name == 'push' + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository) + + strategy: + matrix: + image: + - { tag: "01", name: "mavenimage", file: "Dockerfile.01.mavenimage" } + - { tag: "02", name: "mavenimagestage", file: "Dockerfile.02.mavenimagestage" } + - { tag: "03", name: "dockercache", file: "Dockerfile.03.dockercache" } + - { tag: "05", name: "manual", file: "Dockerfile.05.manual" } + - { tag: "06", name: "jlink", file: "Dockerfile.06.jlink" } + - { tag: "06b", name: "jlink-alpine", file: "Dockerfile.06b.jlink-alpine" } + - { tag: "07", name: "graalvm", file: "Dockerfile.07.graalVM" } + + steps: + - uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Maven repo + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + + - name: Build image + run: | + docker buildx build \ + --progress=plain \ + --load \ + -t $LOCAL_REPO_PREFIX:${{ matrix.image.tag }} \ + -t $LOCAL_REPO_PREFIX:${{ matrix.image.name }} \ + -f ${{ matrix.image.file }} . + + - name: Smoke test + run: | + NAME=${{ matrix.image.name }} + + # skip smoke-run for heavy native image (graalvm) in CI matrix + if [ "$NAME" = "graalvm" ]; then + echo "Skipping smoke test for graalvm native image" + exit 0 + fi + + docker run --rm $LOCAL_REPO_PREFIX:$NAME + + publish: + name: Publish images (tags only) + runs-on: [self-hosted, Linux, X64] + needs: build-docker + if: startsWith(github.ref, 'refs/tags/') + + steps: + - uses: actions/checkout@v4 + + - name: Login to registry + uses: docker/login-action@v2 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push images + run: | + docker tag $LOCAL_REPO_PREFIX:jlink-alpine $DOCKER_REPO:jlink-alpine-${{ github.sha }} + docker push $DOCKER_REPO:jlink-alpine-${{ github.sha }} + + docker tag $LOCAL_REPO_PREFIX:graalvm $DOCKER_REPO:graalvm-${{ github.sha }} + docker push $DOCKER_REPO:graalvm-${{ github.sha }} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index d58dfb7..8dea6c2 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile.01.mavenimage b/Dockerfile.01.mavenimage index 860dee6..3ff22aa 100644 --- a/Dockerfile.01.mavenimage +++ b/Dockerfile.01.mavenimage @@ -1,30 +1,50 @@ -# Base image with Maven and Java 21 -FROM maven:3.9.6-eclipse-temurin-21-jammy +# ------------------------------------------------------------ +# Stage: build (single-stage) +# Purpose: compile the application using Maven and package the artifact +# Base image: maven:3.9.12-eclipse-temurin-21-noble +# Maven profile: -Pprod +# Artifact: target/hello-world-0.0.1-SNAPSHOT.jar +# Notes: single-stage image includes build tools; consider multi-stage to reduce final image size +# ------------------------------------------------------------ +FROM maven:3.9.12-eclipse-temurin-21-noble -# Set working directory -WORKDIR /app - -# Add metadata LABEL maintainer="Emmanuel Bruno " LABEL description="Java Hello World Application - Full Maven image" +LABEL version="0.1.0-SNAPSHOT" +LABEL license="MIT" + +WORKDIR /app -# Copy POM first to leverage Docker cache for dependencies +# Copy wrapper +COPY mvnw ./ +COPY .mvn .mvn +RUN chmod +x mvnw + +# Copy POM first to leverage Docker cache COPY pom.xml ./ -RUN mvn dependency:resolve +RUN ./mvnw --batch-mode dependency:resolve # Copy source code and build the application COPY src ./src -RUN mvn clean verify +RUN ./mvnw --batch-mode -Pprod -DskipTests clean package + +# Copy entrypoint script and make it executable +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh || true + +# Prepare a canonical runtime artifact path (/app/app.jar) if build produced a jar +# Use wildcard to match the built jar (SNAPSHOT or versioned) +RUN cp target/*.jar /app/app.jar 2>/dev/null || true +# Copy runtime dependency jars (produced by -Pprod) so manifest Class-Path 'libs/' resolves +RUN cp -r target/libs /app/libs 2>/dev/null || true -# Configure Java options for container environment -ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" +# Create non-root user (only for running) +RUN groupadd -r appuser && useradd -r -g appuser -m appuser \ + && chown -R appuser:appuser /app -# Choose one of the following run methods: -# 1. Using Maven exec plugin (development) -CMD ["mvn", "--quiet", "exec:java", "-Dexec.mainClass=fr.univtln.bruno.demos.docker.App"] +USER appuser +ENV HOME=/home/appuser -# 2. Using JAR file directly (production recommended) -# CMD java $JAVA_OPTS -jar target/*-jar-with-dependencies.jar +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75" -# 3. Using classpath (alternative) -# CMD java $JAVA_OPTS -cp target/*-jar-with-dependencies.jar fr.univtln.bruno.demos.docker.App \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/Dockerfile.02.mavenimagestage b/Dockerfile.02.mavenimagestage index 87d9cfd..077bec0 100644 --- a/Dockerfile.02.mavenimagestage +++ b/Dockerfile.02.mavenimagestage @@ -1,31 +1,64 @@ -# Multi-stage build for Java application -# Stage 1: Build environment -FROM maven:3.9.6-eclipse-temurin-21-jammy AS stage-build +# ------------------------------------------------------------ +# Stage: build +# Purpose: compile the application using Maven in the build stage +# Base image: maven:3.9.12-eclipse-temurin-21-noble +# Maven profile: -Pprod +# Artifact: target/hello-world-*-SNAPSHOT.jar +# Notes: use cache mounts for /root/.m2 to speed up dependency resolution +# ------------------------------------------------------------ +FROM maven:3.9.12-eclipse-temurin-21-noble AS stage-build + WORKDIR /app +LABEL maintainer="Emmanuel Bruno " +LABEL description="Java Hello World Application - multi-stage build" +LABEL version="0.1.0-SNAPSHOT" +LABEL license="MIT" + +# Copy wrapper +COPY mvnw ./ +COPY .mvn .mvn +RUN chmod +x mvnw + # Copy POM first to leverage Docker cache for dependencies COPY pom.xml ./ -RUN mvn dependency:resolve +RUN ./mvnw --batch-mode dependency:resolve # Copy source code and build the application COPY src ./src -RUN mvn -P uberjar clean verify +RUN ./mvnw --batch-mode -Pprod -DskipTests clean package +# ------------------------------------------------------------ +# Stage: runtime +# Purpose: provide a minimal runtime image with Temurin JRE +# Base image: eclipse-temurin:21.0.9_10-jre-noble +# Copies: /app/app.jar (from build stage) and /app/libs if provided +# Notes: run the application as a non-root user; keep runtime image minimal +# ------------------------------------------------------------ +FROM eclipse-temurin:21.0.9_10-jre-noble -# Stage 2: Runtime environment -FROM eclipse-temurin:21-jre-jammy LABEL maintainer="Emmanuel Bruno " -LABEL description="Java Hello World Application - multi-stage" +LABEL description="Java Hello World Application - multi-stage runtime" + +WORKDIR /app # Copy only the built jar from the build stage -COPY --from=stage-build /app/target/*-jar-with-dependencies.jar /myapp.jar +COPY --from=stage-build /app/target/hello-world-*-SNAPSHOT.jar /app/app.jar +# Copy the libs directory (containing dependencies) if needed +COPY --from=stage-build /app/target/libs /app/libs + +# Install entrypoint script (exec form) and make it executable +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh || true + +# Create a non-root user for security and ensure app files are owned by that user +RUN groupadd -r appuser && useradd -r -u 1001 -g appuser -m appuser \ + && chown -R appuser:appuser /app -# Create a non-root user for security -RUN useradd -r -u 1001 -g root appuser USER appuser +ENV HOME=/home/appuser -# Configure Java options for container environment +# Configure Java options for container environment (default, overridable) ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" -# Run the application -ENTRYPOINT ["java", "-jar"] -CMD ["/myapp.jar"] \ No newline at end of file +# Use exec-form entrypoint script to preserve signals and expand JAVA_OPTS +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/Dockerfile.03.cache b/Dockerfile.03.cache deleted file mode 100644 index 95179c7..0000000 --- a/Dockerfile.03.cache +++ /dev/null @@ -1,29 +0,0 @@ -# Multi-stage build for Java application with build cache optimization -# Stage 1: Build environment -FROM maven:3.9.6-eclipse-temurin-21-jammy AS stage-build -WORKDIR /app - -# Copy source code -COPY . ./ - -# Build with Maven cache mount to speed up subsequent builds -RUN --mount=type=cache,target=/root/.m2 mvn clean verify - -# Stage 2: Runtime environment -FROM eclipse-temurin:21-jre-jammy -LABEL maintainer="Emmanuel Bruno " -LABEL description="Java Hello World Application - cache optimization" - -# Copy only the built jar from the build stage -COPY --from=stage-build /app/target/*-jar-with-dependencies.jar /myapp.jar - -# Create a non-root user for security -RUN useradd -r -u 1001 -g root appuser -USER appuser - -# Configure Java options for container environment -ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" - -# Run the application -ENTRYPOINT ["java", "-jar"] -CMD ["/myapp.jar"] \ No newline at end of file diff --git a/Dockerfile.03.dockercache b/Dockerfile.03.dockercache new file mode 100644 index 0000000..af183d2 --- /dev/null +++ b/Dockerfile.03.dockercache @@ -0,0 +1,64 @@ +# ------------------------------------------------------------ +# Stage: build +# Purpose: compile the application using Maven with cache mounts enabled +# Base image: maven:3.9.12-eclipse-temurin-21-noble +# Maven profile: -Pprod +# Artifact: target/hello-world-*-SNAPSHOT.jar +# Notes: uses --mount=type=cache for /root/.m2 to speed up builds; cache is not persisted in final image +# ------------------------------------------------------------ +FROM maven:3.9.12-eclipse-temurin-21-noble AS stage-build + +WORKDIR /app + +LABEL maintainer="Emmanuel Bruno " +LABEL description="Java Hello World Application - multi-stage build" +LABEL version="0.1.0-SNAPSHOT" +LABEL license="MIT" + +# Copy wrapper +COPY mvnw ./ +COPY .mvn .mvn +RUN chmod +x mvnw + +# Copy POM first to leverage Docker cache for dependencies +COPY pom.xml ./ +RUN --mount=type=cache,target=/root/.m2 ./mvnw --batch-mode dependency:resolve + +# Copy source code and build the application +COPY src ./src +RUN --mount=type=cache,target=/root/.m2 ./mvnw --batch-mode -Pprod -DskipTests clean package + +# ------------------------------------------------------------ +# Stage: runtime +# Purpose: minimal runtime with Temurin JRE containing the packaged application +# Base image: eclipse-temurin:21.0.9_10-jre-noble +# Copies: /app/app.jar and /app/libs from build stage +# Notes: create a non-root user for security and set JAVA_OPTS appropriately +# ------------------------------------------------------------ +FROM eclipse-temurin:21.0.9_10-jre-noble + +LABEL maintainer="Emmanuel Bruno " +LABEL description="Java Hello World Application - multi-stage runtime" + +WORKDIR /app + +# Copy only the built jar from the build stage +COPY --from=stage-build /app/target/hello-world-*-SNAPSHOT.jar /app/app.jar +# Copy the libs directory (containing dependencies) if needed +COPY --from=stage-build /app/target/libs /app/libs + +# Create a non-root user for security and ensure app files are owned by that user +RUN groupadd -r appuser && useradd -r -u 1001 -g appuser -m appuser \ + && chown -R appuser:appuser /app + +USER appuser +ENV HOME=/home/appuser + +# Configure Java options for container environment +ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" +# Install generic entrypoint script +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh || true + +# Use generic exec-form entrypoint +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/Dockerfile.04.wrapper b/Dockerfile.04.wrapper deleted file mode 100644 index 79aab78..0000000 --- a/Dockerfile.04.wrapper +++ /dev/null @@ -1,35 +0,0 @@ -# Multi-stage build for Java application -# Stage 1: Build environment using Maven wrapper -FROM eclipse-temurin:21-jdk-jammy AS stage-build -WORKDIR /app - -# Copy Maven wrapper files first to leverage Docker cache -COPY .mvn/ .mvn -COPY mvnw pom.xml ./ - -# Download dependencies separately to cache them -RUN chmod +x mvnw && \ - ./mvnw dependency:resolve - -# Copy source code and build the application -COPY src ./src -RUN ./mvnw clean verify - -# Stage 2: Runtime environment -FROM eclipse-temurin:21-jre-jammy -LABEL maintainer="Emmanuel Bruno " -LABEL description="Java Hello World Application - mvn wrapper" - -# Copy only the built jar from the build stage -COPY --from=stage-build /app/target/*-jar-with-dependencies.jar /myapp.jar - -# Create a non-root user for security -RUN useradd -r -u 1001 -g root appuser -USER appuser - -# Configure Java options for container environment -ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" - -# Run the application -ENTRYPOINT ["java", "-jar"] -CMD ["/myapp.jar"] \ No newline at end of file diff --git a/Dockerfile.05.manual b/Dockerfile.05.manual new file mode 100644 index 0000000..229a615 --- /dev/null +++ b/Dockerfile.05.manual @@ -0,0 +1,106 @@ +# ------------------------------------------------------------ +# Stage: build +# Purpose: manual build using SDKMAN to install Java and Maven, then compile the app +# Base image: ubuntu:jammy (SDKMAN will install Java/Maven) +# Maven profile: -Pprod +# Artifact: target/hello-world-*-SNAPSHOT.jar +# Notes: BUILDER_UID/BUILDER_GID control file ownership; use --mount=type=cache for /home/builder/.m2 +# ------------------------------------------------------------ +FROM ubuntu:jammy AS stage-build + +ARG JAVA_VERSION="21.0.2-tem" +ARG MAVEN_VERSION="3.9.6" +ARG BUILDER_UID=2000 +ARG BUILDER_GID=2000 + +LABEL maintainer="Emmanuel Bruno " +LABEL description="Java Hello World Application - multi-stage build (SDKMAN)" +LABEL version="0.1.0-SNAPSHOT" +LABEL license="MIT" + +# Install dependencies (as root) +RUN apt-get update && \ + apt-get install --yes --quiet --no-install-recommends \ + ca-certificates \ + curl \ + unzip \ + zip \ + bash && \ + rm -rf /var/lib/apt/lists/* + +# Create builder user with fixed UID/GID +RUN groupadd -g ${BUILDER_GID} builder \ + && useradd -m -u ${BUILDER_UID} -g builder -s /bin/bash builder + +USER builder +ENV HOME=/home/builder +ENV SDKMAN_DIR="$HOME/.sdkman" +ENV PATH="$SDKMAN_DIR/bin:$SDKMAN_DIR/candidates/java/current/bin:$SDKMAN_DIR/candidates/maven/current/bin:$PATH" + +SHELL ["/bin/bash", "-c"] + +# Install SDKMAN + Java + Maven (as builder) +RUN curl -s "https://get.sdkman.io" | bash && \ + source "$SDKMAN_DIR/bin/sdkman-init.sh" && \ + sdk install java "$JAVA_VERSION" && \ + sdk install maven "$MAVEN_VERSION" && \ + rm -rf "$SDKMAN_DIR/archives/*" "$SDKMAN_DIR/tmp/*" + +WORKDIR /app + +# Copy wrapper +COPY --chown=builder:builder mvnw ./ +COPY --chown=builder:builder .mvn .mvn + +# Copy POM first +COPY --chown=builder:builder pom.xml ./ + +# Resolve dependencies using cache (correct UID/GID) +RUN --mount=type=cache,target=/home/builder/.m2,uid=${BUILDER_UID},gid=${BUILDER_GID} \ + source "$SDKMAN_DIR/bin/sdkman-init.sh" && \ + ./mvnw --batch-mode dependency:resolve + +# Copy source +COPY --chown=builder:builder src ./src + +# Build +RUN --mount=type=cache,target=/home/builder/.m2,uid=${BUILDER_UID},gid=${BUILDER_GID} \ + source "$SDKMAN_DIR/bin/sdkman-init.sh" && \ + ./mvnw --batch-mode -Pprod -DskipTests clean package + +# ------------------------------------------------------------ +# Stage: runtime +# Purpose: provide runtime using Temurin JRE with same UID/GID as build user +# Base image: eclipse-temurin:21.0.9_10-jre-noble +# Copies: /app/app.jar and /app/libs from build stage +# Notes: create runtime user with same UID/GID to avoid permission issues +# ------------------------------------------------------------ +FROM eclipse-temurin:21.0.9_10-jre-noble AS stage-runtime + +ARG BUILDER_UID=2000 +ARG BUILDER_GID=2000 + +LABEL maintainer="Emmanuel Bruno " +LABEL description="Java Hello World Application - multi-stage runtime" + +# Create same user in runtime (same UID/GID) +RUN groupadd -g ${BUILDER_GID} appuser \ + && useradd -m -u ${BUILDER_UID} -g appuser -s /bin/bash appuser + +WORKDIR /app + +COPY --from=stage-build /app/target/hello-world-*-SNAPSHOT.jar /app/app.jar +COPY --from=stage-build /app/target/libs /app/libs + +RUN chown -R appuser:appuser /app + +# Install generic entrypoint script and make executable +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh || true + +USER appuser +ENV HOME=/home/appuser +ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" + +# Use generic exec-form entrypoint +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/Dockerfile.05.scratch b/Dockerfile.05.scratch deleted file mode 100644 index f08b124..0000000 --- a/Dockerfile.05.scratch +++ /dev/null @@ -1,57 +0,0 @@ -# Multi-stage build to create a smaller final image -# Stage 1: Build environment -FROM ubuntu:jammy AS stage-build - -# Specify versions as build arguments for easier updates -ARG JAVA_VERSION="21.0.2-tem" -ARG MAVEN_VERSION="3.9.6" - -# Install required dependencies and SDKMAN! -RUN apt-get update && \ - apt-get install --yes --quiet --no-install-recommends \ - ca-certificates \ - curl \ - unzip \ - zip && \ - curl -s "https://get.sdkman.io" | bash && \ - # Clean up apt cache to reduce image size - rm -rf /var/lib/apt/lists/* - -# Use bash shell for SDKMAN! compatibility -SHELL ["/bin/bash", "-c"] - -# Install Java and Maven using SDKMAN! -RUN source "$HOME"/.sdkman/bin/sdkman-init.sh && \ - yes | sdk install java "$JAVA_VERSION" && \ - yes | sdk install maven "$MAVEN_VERSION" && \ - # Clean up SDKMAN! cache to reduce image size - rm -rf "$HOME"/.sdkman/archives/* && \ - rm -rf "$HOME"/.sdkman/tmp/* - -# Set working directory and build the application -WORKDIR /app -COPY . ./ -# Use Maven cache mount to speed up builds -RUN --mount=type=cache,target=/root/.m2 \ - source "$HOME"/.sdkman/bin/sdkman-init.sh && \ - mvn -P uberjar clean verify - -# Stage 2: Runtime environment -# Use slim JRE image for smaller final image size -FROM eclipse-temurin:21-jre-jammy -LABEL maintainer="Emmanuel Bruno " -LABEL description="Java Hello World Application" - -# Copy only the built jar from the build stage -COPY --from=stage-build /app/target/*-jar-with-dependencies.jar /myapp.jar - -# Create a non-root user for security -RUN useradd -r -u 1001 -g root appuser -USER appuser - -# Configure Java options for container environment -ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" - -# Run the application -ENTRYPOINT ["java", "-jar"] -CMD ["/myapp.jar"] \ No newline at end of file diff --git a/Dockerfile.06.jlink b/Dockerfile.06.jlink index 5190637..77ac9d8 100644 --- a/Dockerfile.06.jlink +++ b/Dockerfile.06.jlink @@ -1,41 +1,73 @@ -# Multi-stage build for Java application with jlink using Maven profile -# Stage 1: Build environment -FROM eclipse-temurin:21-jdk-jammy AS builder -WORKDIR /app +# ------------------------------------------------------------ +# Stage: build +# Purpose: compile the application and produce a custom runtime via jlink +# Base image: maven:3.9.12-eclipse-temurin-21-noble +# Maven profile: -Pjlink +# Artifact: target/maven-jlink/classifiers/runtime-image (copied to /jre) +# Notes: maven-jlink-plugin produces a modular runtime image and a launcher; verify launcher name +# ------------------------------------------------------------ +FROM maven:3.9.12-eclipse-temurin-21-noble AS build + +LABEL maintainer="Emmanuel Bruno " +LABEL description="Java Hello World - jlink build stage" +LABEL license="MIT" -# Copy Maven wrapper files first -COPY .mvn/ .mvn/ -COPY mvnw pom.xml ./ +WORKDIR /app -# Make Maven wrapper executable +# Maven Wrapper +COPY mvnw ./ +COPY .mvn .mvn RUN chmod +x mvnw -# Copy source code -COPY src ./src +# Pré-chargement des dépendances (cache Docker) +COPY pom.xml ./ +RUN --mount=type=cache,target=/root/.m2 \ + ./mvnw --batch-mode dependency:resolve -# Download dependencies and build with jlink profile +# Sources + build jlink +COPY src ./src RUN --mount=type=cache,target=/root/.m2 \ - ./mvnw clean -P jlink package + ./mvnw --batch-mode -Pjlink clean package + +# Ensure jlink output is placed in a stable location inside the build image +# Some maven-jlink-plugin configurations produce the runtime image under +# target/maven-jlink/classifiers/runtime-image; copy its contents to /app/jre +RUN mkdir -p /app/jre \ + && if [ -d target/maven-jlink/classifiers/runtime-image ]; then \ + cp -a target/maven-jlink/classifiers/runtime-image/* /app/jre/ ; \ + fi + + +# ------------------------------------------------------------ +# Stage: runtime +# Purpose: minimal runtime built from the jlink-generated runtime-image +# Base image: debian:bookworm-slim +# Copies: /jre (runtime image) from build stage to /jre in final image +# Notes: jlink runtime is custom and contains only required modules; run the launcher at /jre/bin/hello +# ------------------------------------------------------------ +FROM debian:bookworm-slim -# Stage 2: Runtime image -FROM ubuntu:jammy LABEL maintainer="Emmanuel Bruno " -LABEL description="Java Hello World Application with Custom JRE" +LABEL description="Java Hello World - jlink runtime" LABEL version="0.1.0" -# Copy the custom JRE and application -COPY --from=builder /app/target/maven-jlink/classifiers/runtime-image/ /jre -COPY --from=builder /app/target/*.jar /app/myapp.jar +# Répertoire applicatif +WORKDIR /app -# Create non-root user and set permissions -RUN useradd -r -u 1001 -g root appuser && \ - chown -R appuser:root /app && \ - chmod -R g=u /app +# Runtime Java custom généré par jlink +COPY --from=build /app/jre /jre -USER appuser +# Utilisateur non-root (bonne pratique) +# Create a dedicated group 'appuser' and add the user to it, then set ownership on /jre +RUN groupadd -g 1001 appuser \ + && useradd --system --uid 1001 -g appuser -s /usr/sbin/nologin appuser \ + && chown -R appuser:appuser /jre \ + && chmod -R go-w /jre -# Configure Java options for containers -ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" +USER appuser -# Use module path and module name -ENTRYPOINT ["/jre/bin/java", "-jar", "/app/myapp.jar"] \ No newline at end of file +# Exécution via le launcher jlink +# Prefer generic entrypoint: it will detect and exec the jlink launcher (/jre/bin/hello) +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh || true +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/Dockerfile.06b.jlink-alpine b/Dockerfile.06b.jlink-alpine new file mode 100644 index 0000000..aa202b3 --- /dev/null +++ b/Dockerfile.06b.jlink-alpine @@ -0,0 +1,59 @@ +# ------------------------------------------------------------ +# Stage: build +# Purpose: compile the application and produce a custom runtime via jlink on Alpine/musl +# Base image: eclipse-temurin:21-jdk-alpine-3.23 +# Maven profile: -Pjlink +# Artifact: target/maven-jlink/classifiers/runtime-image (copied to /jre) +# Notes: building for musl; verify jlink output compatibility with Alpine runtime +# ------------------------------------------------------------ +FROM eclipse-temurin:21-jdk-alpine-3.23 AS build + +WORKDIR /app + +COPY mvnw ./ +COPY .mvn .mvn +RUN chmod +x mvnw + +COPY pom.xml ./ +RUN --mount=type=cache,target=/root/.m2 ./mvnw -B dependency:resolve + +COPY src ./src +RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -Pjlink clean package + +# Ensure jlink output is in a stable location inside the build image +# Some configurations place runtime image under target/maven-jlink/classifiers/runtime-image +# Copy contents to /app/jre so the final stage can reliably COPY from /app/jre +RUN mkdir -p /app/jre \ + && if [ -d target/maven-jlink/classifiers/runtime-image ]; then \ + cp -a target/maven-jlink/classifiers/runtime-image/* /app/jre/ ; \ + fi + + +# ------------------------------------------------------------ +# Stage: runtime +# Purpose: minimal Alpine runtime with jlink-generated runtime image +# Base image: alpine:3.23.2 +# Copies: /jre from build stage to /jre in final image +# Notes: ensure TLS CA certificates are installed in Alpine; runtime relies on musl libc +# ------------------------------------------------------------ +FROM alpine:3.23.2 + +WORKDIR /app + +# Certificats TLS indispensables +RUN apk add --no-cache ca-certificates + +# Runtime jlink (use stable path produced in build stage) +COPY --from=build /app/jre /jre +# Sécurité minimale: create user and ensure /jre permissions +RUN adduser -D -u 1001 appuser \ + && chown -R appuser:appuser /jre + +# Install generic entrypoint script and make it executable +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh || true + +USER appuser + +# Use generic entrypoint to run the jlink launcher +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/Dockerfile.07.graalVM b/Dockerfile.07.graalVM index 5003a5b..c0ba366 100644 --- a/Dockerfile.07.graalVM +++ b/Dockerfile.07.graalVM @@ -1,64 +1,48 @@ -# Multi-stage build for Java application with GraalVM native image -# Stage 1: Build environment with GraalVM -FROM ghcr.io/graalvm/native-image-community:21 AS builder +# ------------------------------------------------------------ +# Stage: build +# Purpose: build a static native binary using GraalVM native-image +# Base image: ghcr.io/graalvm/native-image-community:25-muslib +# Maven profile: -Pgraalvm +# Artifact: target/app (native binary) +# Notes: install zlib-static and use musl toolchain; verify binary is truly static before final image +# ------------------------------------------------------------ +FROM ghcr.io/graalvm/native-image-community:25-muslib AS builder + +# Installation de zlib-static (indispensable pour le flag --static) +RUN microdnf install -y gcc make binutils zlib-static && microdnf clean all + WORKDIR /app -# Copy Maven wrapper files first +# IMPORTANT : On force l'utilisation du compilateur musl pour les tests de fonctionnalités +ENV CC=/usr/local/musl/bin/gcc + COPY .mvn/ .mvn/ COPY mvnw pom.xml ./ - -# Make Maven wrapper executable RUN chmod +x mvnw -# Download dependencies separately to leverage Docker cache -RUN --mount=type=cache,target=/root/.m2 ./mvnw dependency:resolve +# On télécharge les dépendances +RUN --mount=type=cache,target=/root/.m2 ./mvnw dependency:go-offline -Pgraalvm -# Copy source code and build the application COPY src ./src -RUN --mount=type=cache,target=/root/.m2 ./mvnw clean package - -# Generate native image with optimizations -RUN native-image \ - --no-fallback \ - --static \ - -H:+ReportExceptionStackTraces \ - -H:Name=app \ - -H:+RemoveSaturatedTypeFlows \ - -H:+PreserveFramePointer \ - -H:+InlineBeforeAnalysis \ - -H:+AddAllCharsets \ - -H:EnableURLProtocols=http,https \ - -H:ConfigurationFileDirectories=/app/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker \ - --initialize-at-build-time=org.slf4j,ch.qos.logback \ - --module-path target/*-jar-with-dependencies.jar \ - --module fr.univtln.bruno.demos.docker - -# Stage 2: Runtime image -FROM scratch -LABEL maintainer="Emmanuel Bruno " -LABEL description="Java Hello World Application with GraalVM Native Image" -LABEL version="0.1.0" -LABEL org.opencontainers.image.source="https://github.com/yourusername/yourrepo" - -# Add SSL certificates for HTTPS support -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ - -# Create non-root user (even though we're using scratch) -COPY --from=builder /etc/passwd /etc/passwd -COPY --from=builder /etc/group /etc/group - -# Copy the native executable -COPY --from=builder /app/app /app - -# Create directory for logs with proper permissions -COPY --from=builder --chown=1001:0 /dev/null /logs/ - -# Set user -USER 1001 - -# Set environment variables -ENV APP_HOME=/app -ENV LOG_DIR=/logs -# Run the native executable -ENTRYPOINT ["/app"] \ No newline at end of file +# On lance la compilation via Maven +# Le flag --libc=musl dans le POM fera le reste +RUN --mount=type=cache,target=/root/.m2 ./mvnw package -Pnative -DskipTests + +# Place any produced native binary in a stable location (/app/app) so final stage can copy it +RUN mkdir -p /app \ + && candidate=$(find target -type f \( -name app -o -name "*app" -o -name "*native*" -o -name "*-runner" -o -name "*.exe" -o -name "*.bin" -o -name "*.so" \) 2>/dev/null | head -n 1 || true) \ + && if [ -n "$candidate" ]; then cp "$candidate" /app/app; chmod +x /app/app || true; else echo "No native artifact found under target/" >&2; fi + +# ------------------------------------------------------------ +# Stage: runtime +# Purpose: minimal final image for the static native binary +# Base image: gcr.io/distroless/static-debian12 +# Copies: /app/app (native binary) from build stage +# Notes: distroless static image contains no shell; ensure user exists or set numeric UID and that binary is static +# ------------------------------------------------------------ +FROM gcr.io/distroless/static-debian12 +COPY --from=builder /app/app /app/app +# Use numeric UID/GID to avoid relying on username existing in distroless +USER 65532:65532 +ENTRYPOINT ["/app/app"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..711d451 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Java Hello World — Docker Samples + +Lightweight repository demonstrating multiple strategies to package a Java application as a container image (single-stage Maven, multi-stage, cache-optimized, jlink and GraalVM native). + +## Contents + +- Dockerfiles: `Dockerfile.01.mavenimage`, `Dockerfile.02.mavenimagestage`, `Dockerfile.03.dockercache`, `Dockerfile.05.manual`, `Dockerfile.06.jlink`, `Dockerfile.06b.jlink-alpine`, `Dockerfile.07.graalVM` +- Helper scripts: `entrypoint.sh`, `build_and_run_all.sh`, `benchmark.sh` +- Maven project with profiles: `prod`, `jlink`, `native` + +## Prerequisites + +- Docker or Podman available on PATH (rootless or rootful) +- Java / Maven (only needed for local builds outside Docker) + +## Quick start + +Build and run all images sequentially (script uses `docker`): + +```bash +./build_and_run_all.sh +``` + +Build a single image manually: + +```bash +# build and run JVM multi-stage image +docker build -t javahello:mavenimagestage -f Dockerfile.02.mavenimagestage . +docker run --rm javahello:mavenimagestage + +# build jlink image +docker build -t javahello:jlink -f Dockerfile.06.jlink . +docker run --rm javahello:jlink +``` + +If you use Podman, replace `docker` by `podman` in the commands above. + +## Benchmark + +Run the included benchmark that measures build/runtime characteristics: + +```bash +./benchmark.sh +``` + +## License +MIT diff --git a/benchmark.sh b/benchmark.sh new file mode 100755 index 0000000..1944f63 --- /dev/null +++ b/benchmark.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +# ------------------------------------------------------------ +# Configuration +# ------------------------------------------------------------ +BASE_NAME="javahello" +PACKAGE_PATH="src/main/java/fr/univtln/bruno/demos/docker" + +declare -A BUILDS=( + ["01.mavenimage"]="mavenimage" + ["02.mavenimagestage"]="mavenimagestage" + ["03.dockercache"]="dockercache" + ["05.manual"]="manual" + ["06.jlink"]="jlink" + ["06b.jlink-alpine"]="jlink-alpine" + ["07.graalVM"]="graalvm" +) + +ORDER=( + "01.mavenimage" + "02.mavenimagestage" + "03.dockercache" + "05.manual" + "06.jlink" + "06b.jlink-alpine" + "07.graalVM" +) + +echo -e "\n🚀 Benchmark : Analyse de l'Efficience (Build vs Runtime)\n" +echo "| Image Tag | Build Cold | Build Warm | Build Incr. | Size | Peak RAM | Footprint Index |" +echo "|:----------|:-----------|:-----------|:------------|:-----|:---------|:----------------|" + +for KEY in "${ORDER[@]}"; do + TAG=${BUILDS[$KEY]} + DOCKERFILE="Dockerfile.$KEY" + FULL_TAG="$BASE_NAME:$TAG" + + [ ! -f "$DOCKERFILE" ] && continue + + # -------------------------------------------------------- + # 1. COLD BUILD (no cache) + # -------------------------------------------------------- + B_START=$(date +%s) + docker build --no-cache -t "$FULL_TAG" -f "$DOCKERFILE" . > /dev/null 2>&1 + COLD_TIME="$(( $(date +%s) - B_START ))s" + + # -------------------------------------------------------- + # 2. WARM BUILD (full cache) + # -------------------------------------------------------- + B_START=$(date +%s) + docker build -t "$FULL_TAG" -f "$DOCKERFILE" . > /dev/null 2>&1 + WARM_TIME="$(( $(date +%s) - B_START ))s" + + # -------------------------------------------------------- + # 3. INCREMENTAL BUILD (ServiceLoader – GraalVM aware) + # -------------------------------------------------------- + FAKE_CLASS="Fake$(date +%s)" + FAKE_JAVA="$PACKAGE_PATH/$FAKE_CLASS.java" + + SPI_DIR="src/main/resources/META-INF/services" + SPI_FILE="$SPI_DIR/fr.univtln.bruno.demos.docker.Marker" + + mkdir -p "$SPI_DIR" + + # Fake provider implémentant Marker + cat > "$FAKE_JAVA" < "$SPI_FILE" + + B_START=$(date +%s) + docker build -t "$FULL_TAG" -f "$DOCKERFILE" . > /dev/null 2>&1 + INCR_TIME="$(( $(date +%s) - B_START ))s" + + # Nettoyage (aucune trace persistante) + rm -f "$FAKE_JAVA" "$SPI_FILE" + rmdir "$SPI_DIR" 2>/dev/null + rmdir "src/main/resources/META-INF" 2>/dev/null + rmdir "src/main/resources" 2>/dev/null + + # -------------------------------------------------------- + # 4. RUNTIME STATS (Peak RAM) + # -------------------------------------------------------- + SIZE_STR=$(docker images --format "{{.Size}}" "$FULL_TAG") + CID=$(docker run -d "$FULL_TAG") + PEAK_MEM_RAW=0 + + while [ "$(docker ps -q -f id=$CID)" ]; do + M_STR=$(docker stats --no-stream --format "{{.MemUsage}}" "$CID" | awk '{print $1}') + VAL=$(echo "$M_STR" | sed 's/[A-Za-z]//g') + + if [[ -n "$VAL" ]] && (( $(echo "$VAL > $PEAK_MEM_RAW" | bc -l 2>/dev/null || echo 0) )); then + PEAK_MEM_RAW=$VAL + fi + sleep 0.05 + done + + docker rm -f "$CID" > /dev/null 2>&1 + + # -------------------------------------------------------- + # 5. FOOTPRINT INDEX + # -------------------------------------------------------- + SIZE_NUM=$(echo "$SIZE_STR" | sed 's/[A-Za-z]//g') + SCORE=$(echo "($PEAK_MEM_RAW * 3) + ($SIZE_NUM / 10)" | bc -l) + + printf "| %-15s | %10s | %10s | %11s | %8s | %8.2f MiB | %15.1f |\n" \ + "$TAG" "$COLD_TIME" "$WARM_TIME" "$INCR_TIME" \ + "$SIZE_STR" "$PEAK_MEM_RAW" "$SCORE" +done + +echo -e "\n*Note : Footprint Index = (RAM × 3) + (Size / 10). Plus l'indice est faible, plus l'image est efficiente.*" diff --git a/build_and_run_all.sh b/build_and_run_all.sh new file mode 100755 index 0000000..ecb23e6 --- /dev/null +++ b/build_and_run_all.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Simple script to build all Dockerfile* images and run them sequentially. +# Usage: ./build_and_run_all.sh +# Optionally set DOCKER command: DOCKER=podman ./build_and_run_all.sh + +DOCKER=${DOCKER:-docker} +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$ROOT_DIR" + +FILES=( + Dockerfile.01.mavenimage + Dockerfile.02.mavenimagestage + Dockerfile.03.dockercache + Dockerfile.05.manual + Dockerfile.06.jlink + Dockerfile.06b.jlink-alpine + Dockerfile.07.graalVM +) + +TAGS=( + javahello:01 + javahello:02 + javahello:03 + javahello:05 + javahello:06 + javahello:06b + javahello:07 +) + +echo "Using docker command: $DOCKER" + +for i in "${!FILES[@]}"; do + file=${FILES[$i]} + tag=${TAGS[$i]} + + if [ ! -f "$file" ]; then + echo "Skipping $file (not found)" + continue + fi + + echo + echo "==============================================" + echo "Building $file -> $tag" + echo "==============================================" + + $DOCKER build -t "$tag" -f "$file" . || { echo "Build failed for $file"; exit 1; } +done + +echo +echo "All builds finished. Running images sequentially." + +for tag in "${TAGS[@]}"; do + echo + echo "--------------------------------" + echo "Running $tag" + echo "--------------------------------" + # Run each container; accept that some may exit quickly. Don't stop the whole script on non-zero exit. + if ! $DOCKER run --rm "$tag"; then + echo "Container $tag exited with non-zero status" + fi +done + +echo +echo "Done." diff --git a/ci/runner-setup.md b/ci/runner-setup.md new file mode 100644 index 0000000..0afb404 --- /dev/null +++ b/ci/runner-setup.md @@ -0,0 +1,58 @@ +# Self-hosted Runner Setup (Ubuntu example) + +This document describes a minimal, secure setup for a self-hosted GitHub Actions runner intended to build Docker images. + +Prerequisites +- Ubuntu 22.04 LTS (or similar Linux) +- Sufficient RAM: 8–32GB (use >=16GB for Graal builds) +- Disk: 50GB+ available + +Steps +1. Create runner on GitHub + - Repository Settings → Actions → Runners → New self-hosted runner + - Choose labels (e.g. `self-hosted`, `Linux`, `X64`, `docker`) + - Copy registration token and instructions. + +2. Install Docker and Buildx + +```bash +sudo apt update +sudo apt install -y docker.io git curl +sudo usermod -aG docker $USER +newgrp docker +# enable buildx +docker buildx create --use +``` + +3. Install and configure the Actions runner + +```bash +# run as a dedicated user (recommended) +sudo adduser --disabled-password --gecos "" gha-runner +sudo su - gha-runner +mkdir actions-runner && cd actions-runner +# download runner archive (replace version as appropriate) +curl -O -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-2.308.0.tar.gz +tar xzf ./actions-runner-linux-x64-2.308.0.tar.gz +# register the runner (replace URL and TOKEN) +./config.sh --url https://github.com/OWNER/REPO --token YOUR_TOKEN --labels self-hosted,Linux,X64,docker +# install as a service +sudo ./svc.sh install +sudo ./svc.sh start +``` + +4. Security recommendations +- Run the runner under a dedicated user with minimal privileges. +- Limit runner usage to trusted repositories or runner groups. +- Do not expose repository secrets to runs triggered by forked PRs. +- Enable automatic OS patching and monitor logs (`/var/log/syslog`, `docker logs`). +- Consider ephemeral runners for heavy/unsafe workloads (e.g., spawn runners on demand). + +5. Maintenance +- Periodically prune Docker images and volumes to free disk: + `docker system prune -a --volumes --force` +- Rotate registration tokens if a runner is compromised. + +Notes +- For Kubernetes: consider using `actions-runner-controller` to manage ephemeral runners. +- For multi-arch builds: enable `binfmt` and use buildx with cache exporters. diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..43821e2 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -e +# Generic entrypoint: detect and run the appropriate artifact +# Priority: /app/app.jar -> any executable in /jre/bin -> /app/app (native) -> first jar in /app or /app/target +# Usage: entrypoint.sh [app-args] + +if [ -f /app/app.jar ]; then + exec java ${JAVA_OPTS} -jar /app/app.jar "$@" +fi + +# If jlink produced a custom runtime, the launcher name varies. +# Look for the first executable file under /jre/bin and run it. +if [ -d /jre/bin ]; then + for f in /jre/bin/*; do + if [ -x "$f" ] && [ ! -d "$f" ]; then + exec "$f" "$@" + fi + done +fi + +if [ -x /app/app ]; then + exec /app/app "$@" +fi + +jar=$(ls /app/*.jar 2>/dev/null | head -n 1) +if [ -z "$jar" ]; then + jar=$(ls /app/target/*.jar 2>/dev/null | head -n 1) +fi +if [ -n "$jar" ]; then + exec java ${JAVA_OPTS} -jar "$jar" "$@" +fi + +echo "No runnable artifact found in /app or /jre" >&2 +exit 1 diff --git a/mvnw b/mvnw index 19529dd..bd8896b 100755 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index b150b91..5761d94 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/pom copy.xml b/pom copy.xml new file mode 100644 index 0000000..d7e9861 --- /dev/null +++ b/pom copy.xml @@ -0,0 +1,351 @@ + + + + + + + + 4.0.0 + + + fr.univtln.bruno.demos.docker + hello-world + 0.1.0-SNAPSHOT + + Hello World + + Application Java "Hello World" utilisée pour démontrer Maven et Docker + + + + + + + + + + UTF-8 + UTF-8 + + + 21 + + + fr.univtln.bruno.demos.docker.App + + + 5.11.4 + 2.0.16 + 1.5.16 + + + 3.13.0 + 3.5.2 + 3.4.2 + 3.6.1 + 2.21.0 + + 0.11.0 + + + + + + + + + + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + + + + + + + + + + + + maven-compiler-plugin + ${maven.compiler.version} + + + ${java.version} + + + true + + + + + + + + maven-surefire-plugin + ${maven.surefire.version} + + + + + + + + org.codehaus.mojo + versions-maven-plugin + ${versions.maven.version} + + + + https://bruno.univ-tln.fr/rules.xml + + + + + + + + + + + + + + + + + + + prod + + + + + + + + + + maven-dependency-plugin + ${maven.dependency.version} + + + copy-dependencies + package + + copy-dependencies + + + + ${project.build.directory}/libs + + runtime + + + + + + + + + + + maven-jar-plugin + ${maven.jar.version} + + + + ${main.class} + true + libs/ + + + + + ${user.name} + ${java.version} + ${maven.build.timestamp} + + + + + + + + + + + + jlink + + + + + + org.apache.maven.plugins + maven-jlink-plugin + 3.2.0 + + + + jlink + + jlink + + + + + + + runtime-image + + ALL-MODULE-PATH,java.naming + + hello=fr.univtln.bruno.demos.docker/fr.univtln.bruno.demos.docker.App + + + 2 + true + true + true + + + + + + + + + + + + + + graalvm + + + + + + maven-dependency-plugin + ${maven.dependency.version} + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/dependency + runtime + + + + + + + + org.graalvm.buildtools + native-maven-plugin + ${native.image.plugin.version} + + + native-image + package + + compile-no-fork + + + + + + app + ${main.class} + + + --no-fallback + -H:+ReportExceptionStackTraces + -H:+AddAllCharsets + -H:EnableURLProtocols=http,https + --initialize-at-build-time=org.slf4j,ch.qos.logback + --add-modules=java.naming + --static + --libc=musl -H:+ReportExceptionStackTraces + -H:IncludeResources="logback.*" + + -H:ConfigurationFileDirectories=src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index e9cea7d..6aa1571 100644 --- a/pom.xml +++ b/pom.xml @@ -1,54 +1,86 @@ + + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + 4.0.0 + + + + fr.univtln.bruno.demos.docker - helloworld + hello-world 0.1.0-SNAPSHOT + Hello World - Java Hello World Application for Docker Demo + + Application Java "Hello World" utilisée pour démontrer Maven et Docker + + https://github.com/ebpro/notebook-containers-intro-sample-java-helloworld - + - + + UTF-8 UTF-8 - + 21 - fr.univtln.bruno.demos.docker.App - fr.univtln.bruno.demos.docker - ${java.version} - ${java.version} - ${java.version} - - - 5.11.4 - 1.5.16 - 2.0.16 - - - 3.7.1 - 3.13.0 - 3.5.2 - 2.18.0 - 3.2.0 - 1.1.3 - 3.4.0 - 3.4.0 - 3.3.1 - 3.4.2 - 3.1.3 - 3.1.3 + + + fr.univtln.bruno.demos.docker.App + + + 6.0.2 + 2.0.17 + 1.5.25 + + + 3.14.1 + 3.5.4 + 3.5.0 + 3.9.0 + 2.21.0 + 0.11.4 + 3.2.0 + + + 3.6.2 + + 3.5.0 + 3.1.4 3.21.0 + 3.4.0 + 3.1.4 + + + 2024-06-01T00:00:00Z + - + + org.junit.jupiter junit-jupiter @@ -56,196 +88,267 @@ test - + org.slf4j slf4j-api - ${sl4j.version} + ${slf4j.version} + ch.qos.logback logback-classic ${logback.version} + runtime + + - - - - - - org.codehaus.mojo - versions-maven-plugin - ${versions.maven.version} - - https://bruno.univ-tln.fr/rules.xml - - - - - - maven-compiler-plugin - ${maven.compiler.version} - - ${maven.compiler.release} - true - - - - - - maven-surefire-plugin - ${maven.surefire.version} - - - - + org.apache.maven.plugins maven-clean-plugin ${maven.clean.version} - maven-resources-plugin - ${maven.resources.version} + org.apache.maven.plugins + maven-install-plugin + ${maven.install.version} - maven-jar-plugin - ${maven.jar.version} - - - - ${app.main.class} - - - ${user.name} - ${maven.build.timestamp} - - - + org.apache.maven.plugins + maven-site-plugin + ${maven.site.version} - maven-install-plugin - ${maven.install.version} + org.apache.maven.plugins + maven-resources-plugin + ${maven.resources.version} + org.apache.maven.plugins maven-deploy-plugin ${maven.deploy.version} - - maven-site-plugin - ${maven.site.version} - + + + + + maven-compiler-plugin + ${maven.compiler.version} + + + ${java.version} + + true + + + + + + maven-surefire-plugin + ${maven.surefire.version} + + + + + org.codehaus.mojo + versions-maven-plugin + ${versions.maven.version} + + + https://bruno.univ-tln.fr/rules.xml + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven.enforcer.version} + + + enforce + + enforce + + + + + + [3.6.3,) + + + + + [21,) + + + + + + + + + + + + true + + + + + + + + - uberjar + prod + + - org.apache.maven.plugins - maven-assembly-plugin - ${maven.assembly.version} - - - - fr.univtln.bruno.demos.docker.App - - - - all-permissions - true - - - - jar-with-dependencies - - + maven-dependency-plugin + ${maven.dependency.version} - make-assembly + copy-dependencies package - single + copy-dependencies + + ${project.build.directory}/libs + runtime + + + + + maven-jar-plugin + ${maven.jar.version} + + + + + ${main.class} + + true + libs/ + + + + + + jlink + - + - maven-dependency-plugin - ${maven.dependency.version} + org.apache.maven.plugins + maven-jlink-plugin + ${maven.jlink.version} + - copy-dependencies - prepare-package + jlink - copy-dependencies + jlink - - ${project.build.directory}/modules - - - - - org.apache.maven.plugins - maven-jlink-plugin - ${maven.jlink.plugin.version} + runtime-image - true - - ${project.module} - - - ${project.build.directory}/modules - - app=${project.module}/${app.main.class} + + ALL-MODULE-PATH,java.naming + + hello=fr.univtln.bruno.demos.docker/fr.univtln.bruno.demos.docker.App + + + 2 true true true - true + + + + + + + + + + native + + + + + org.graalvm.buildtools + native-maven-plugin + ${native.image.plugin.version} + true - create-runtime-image + build-native package - jlink + compile-no-fork - - - - org.apache.maven.plugins - maven-install-plugin + - true + app + ${main.class} + + + --no-fallback + + --static + + --libc=musl + + + --initialize-at-build-time=org.slf4j,ch.qos.logback + + + + - \ No newline at end of file + diff --git a/src/main/java/fr/univtln/bruno/demos/docker/App.java b/src/main/java/fr/univtln/bruno/demos/docker/App.java index eb0e3a5..3c10e4d 100644 --- a/src/main/java/fr/univtln/bruno/demos/docker/App.java +++ b/src/main/java/fr/univtln/bruno/demos/docker/App.java @@ -3,17 +3,77 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.ServiceLoader; + /** - * Hello world! + * Sample Java application to demonstrate resource usage in a containerized + * environment. + * Refactored for observability, resilience, and testability. * + * @author Emmanuel Bruno + * @version 0.2.0 */ -public class App -{ - private static Logger logger = LoggerFactory.getLogger(App.class.getName()); - - public static void main( String[] args ) - { - logger.info("Hello world maven + docker."); - System.out.println( "Hello World!" ); +public class App { + private static final Logger logger = LoggerFactory.getLogger(App.class); + + /** + * Main entry point. Orchestrates resource simulation and monitoring. + */ + public static void main(String[] args) { + // Log environment context for better container debugging + logger.info("Java Vendor: {} | Version: {}", + System.getProperty("java.vendor"), + System.getProperty("java.version")); + + // Demonstrate service loading (if any implementations are provided) + // Enable to add reachable services via SPI + ServiceLoader.load(Marker.class ).forEach(Marker::touch); + + // 0. Configuration with safe parsing + int iterations = getEnvInt("APP_ITERATIONS", 10000); + long sleepMs = getEnvLong("APP_SLEEP_MS", 1500L); + + ResourceProcessor processor = new ResourceProcessor(); + + Instant start = Instant.now(); + logger.info("Démarrage de l'application (Iterations: {})...", iterations); + + // 1. Memory allocation + List data = processor.generateData(iterations); + + // 2. CPU activity + long count = processor.processData(data); + + Duration duration = Duration.between(start, Instant.now()); + logger.info("Traitement terminé. Éléments filtrés : {} | Temps : {} ms", + count, duration.toMillis()); + + // 3. Pause to allow resource monitoring (e.g., docker stats) + performGracefulSleep(sleepMs); + + // 4. Final output (prevents JIT from optimizing away the 'count' variable) + System.out.printf("Fin du programme. (Éléments traités: %d)%n", count); + } + + private static void performGracefulSleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + logger.warn("Pause interrupted."); + Thread.currentThread().interrupt(); + } + } + + private static int getEnvInt(String key, int def) { + String val = System.getenv(key); + return (val != null && val.matches("\\d+")) ? Integer.parseInt(val) : def; + } + + private static long getEnvLong(String key, long def) { + String val = System.getenv(key); + return (val != null && val.matches("\\d+")) ? Long.parseLong(val) : def; } } diff --git a/src/main/java/fr/univtln/bruno/demos/docker/Marker.java b/src/main/java/fr/univtln/bruno/demos/docker/Marker.java new file mode 100644 index 0000000..7db81ab --- /dev/null +++ b/src/main/java/fr/univtln/bruno/demos/docker/Marker.java @@ -0,0 +1,33 @@ +package fr.univtln.bruno.demos.docker; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Marker SPI interface for ServiceLoader / GraalVM demonstrations. + * + *

+ * Implementations of this interface can be discovered at runtime using + * {@link java.util.ServiceLoader}. Each discovered service may be invoked + * to demonstrate dynamic reachability under GraalVM native-image. + *

+ * + *

+ * This interface is intentionally minimal and side-effect free. + *

+ * + * @author Emmanuel Bruno + * @version 0.1.0 + */ +public interface Marker { + + Logger LOGGER = LoggerFactory.getLogger(Marker.class); + + /** + * Invoked when the service is loaded. + * Default implementation logs the concrete service class. + */ + default void touch() { + LOGGER.info("Marker service invoked: {}", getClass().getName()); + } +} diff --git a/src/main/java/fr/univtln/bruno/demos/docker/ResourceProcessor.java b/src/main/java/fr/univtln/bruno/demos/docker/ResourceProcessor.java new file mode 100644 index 0000000..4398396 --- /dev/null +++ b/src/main/java/fr/univtln/bruno/demos/docker/ResourceProcessor.java @@ -0,0 +1,37 @@ +package fr.univtln.bruno.demos.docker; + +import java.util.List; +import java.util.UUID; +import java.util.stream.IntStream; + +/** + * Handles the resource-intensive tasks of the application. + */ +class ResourceProcessor { + + /** + * Allocates memory by generating a list of UUID strings. + * * @param count Number of elements to generate. + * + * @return A list of UUID strings. + */ + public List generateData(int count) { + return IntStream.range(0, count) + .mapToObj(i -> UUID.randomUUID().toString()) + .toList(); + } + + /** + * Simulates CPU activity by filtering a list. + * * @param data The list of strings to process. + * + * @return The count of elements containing the character 'a'. + */ + public long processData(List data) { + if (data == null) + return 0; + return data.parallelStream() + .filter(s -> s.contains("a")) + .count(); + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 1a66256..8727020 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,14 +1,11 @@ module fr.univtln.bruno.demos.docker { - // Required modules - requires java.base; + + // Required modules (used directly in the code) requires org.slf4j; - requires ch.qos.logback.classic; - requires ch.qos.logback.core; - requires java.logging; - - // Exports + + // Declare SPI usage so ServiceLoader works when running on the module path + uses fr.univtln.bruno.demos.docker.Marker; + + // Public API of the module exports fr.univtln.bruno.demos.docker; - - // Opens for logging configuration - opens fr.univtln.bruno.demos.docker to ch.qos.logback.classic, ch.qos.logback.core; -} \ No newline at end of file +} diff --git a/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/jni-config.json b/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/jni-config.json index 3f0f809..5d48ff7 100644 --- a/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/jni-config.json +++ b/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/jni-config.json @@ -1,3 +1,5 @@ -{ - "name": "fr.univtln.bruno.demos.docker.App" -} \ No newline at end of file +[ + { + "name": "fr.univtln.bruno.demos.docker.App" + } +] diff --git a/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/reflect-config.json b/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/reflect-config.json index bf0e94d..75729e5 100644 --- a/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/reflect-config.json +++ b/src/main/resources/META-INF/native-image/fr.univtln.bruno.demos.docker/reflect-config.json @@ -1,7 +1,9 @@ -{ - "name": "fr.univtln.bruno.demos.docker.App", - "allDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredMethods": true, - "allPublicMethods": true -} \ No newline at end of file +[ + { + "name": "fr.univtln.bruno.demos.docker.App", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true + } +] diff --git a/src/test/java/fr/univtln/bruno/demos/docker/AppTest.java b/src/test/java/fr/univtln/bruno/demos/docker/AppTest.java index 5eed290..e248e02 100644 --- a/src/test/java/fr/univtln/bruno/demos/docker/AppTest.java +++ b/src/test/java/fr/univtln/bruno/demos/docker/AppTest.java @@ -1,27 +1,27 @@ package fr.univtln.bruno.demos.docker; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertNotNull; - import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Unit test for simple App. + * Unit tests for ResourceProcessor to ensure logic remains correct + * across refactors. */ -class AppTest { - +class ResourceProcessorTest { + private final ResourceProcessor processor = new ResourceProcessor(); + @Test - @DisplayName("Application can be instantiated") - void shouldCreateAppInstance() { - App app = new App(); - assertNotNull(app, "App instance should not be null"); + void testProcessData() { + List mockData = List.of("apple", "berry", "cherry"); + // "apple" and "berry" contain 'a' (in some locales/logic) or just 'apple' here + // Based on the code s.contains("a"): apple=yes, berry=no, cherry=no + long result = processor.processData(mockData); + assertEquals(1, result, "Should find exactly 1 string containing 'a'"); } @Test - @DisplayName("Main method should run without errors") - void shouldRunMainWithoutErrors() { - App.main(new String[]{}); - assertTrue(true, "Main method executed successfully"); + void testGenerateDataSize() { + assertEquals(10, processor.generateData(10).size()); } -} \ No newline at end of file +}