diff --git a/.github/workflows/build-java-image.yaml b/.github/workflows/build-java-image.yaml new file mode 100644 index 0000000..77cdbb4 --- /dev/null +++ b/.github/workflows/build-java-image.yaml @@ -0,0 +1,84 @@ +name: Build & Push Java App + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +env: + IMAGE_NAME: techiescamp/java-image + DOCKERFILE: ./Dockerfile + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Java 17 and Maven cache + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + cache: maven + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build with Maven (tests on) + working-directory: ./java-app + run: ./mvnw clean package + + - name: Lint Dockerfile (hadolint) + if: ${{ github.event_name == 'pull_request' }} + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ${{ env.DOCKERFILE }} + failure-threshold: error + + - name: Docker build (no push) for PR + if: ${{ github.event_name == 'pull_request' }} + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ env.DOCKERFILE }} + platforms: linux/amd64 + tags: ${{ env.IMAGE_NAME }}:pr-${{ github.event.number }} + load: true + push: false + + - name: Trivy scan (image) + if: ${{ github.event_name == 'pull_request' }} + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: image + image-ref: ${{ env.IMAGE_NAME }}:pr-${{ github.event.number }} + severity: HIGH,CRITICAL + ignore-unfixed: true + format: table + exit-code: '0' + + - name: Login to Docker Hub + if: ${{ github.event_name == 'push' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Docker build & push (multi-arch) + if: ${{ github.event_name == 'push' }} + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ env.DOCKERFILE }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ env.IMAGE_NAME }}:1.0.${{ github.run_number }} + ${{ env.IMAGE_NAME }}:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef3af02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!mvnw +!mvnw.cmd +.mvn/wrapper/maven-wrapper.jar + +.idea/ +*.iml + +.classpath +.project +.settings/ + +.DS_Store +Thumbs.db + +*.log +*.tmp +*.bak +*.swp + +*.class + +*.war +*.jar +application-*.properties +application-*.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1475f00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Use the distroless Java base image +FROM gcr.io/distroless/java21-debian12 + +# Set the working directory in the container +WORKDIR /app + +# Copy the JAR file into the container at /app +COPY java-app/target/*.jar java.jar + +# Expose the port your app runs on +EXPOSE 8080 + +# Run the jar file +CMD ["java.jar"] \ No newline at end of file diff --git a/java-app/.mvn/wrapper/maven-wrapper.properties b/java-app/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..1eec1fb --- /dev/null +++ b/java-app/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,4 @@ +# Maven to use +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +# Wrapper bootstrap JAR to download when missing +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar diff --git a/java-app/mvnw b/java-app/mvnw new file mode 100755 index 0000000..4df6a10 --- /dev/null +++ b/java-app/mvnw @@ -0,0 +1,53 @@ +#!/bin/sh +set -e + +# Resolve script dir +PRG="$0" +while [ -h "$PRG" ]; do + ls=$(ls -ld "$PRG"); link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' >/dev/null; then PRG="$link"; else PRG=$(dirname "$PRG")"/$link"; fi +done +BASEDIR=$(cd "$(dirname "$PRG")" >/dev/null 2>&1 && pwd -P) +APP_HOME="$BASEDIR" + +WRAPPER_DIR="$APP_HOME/.mvn/wrapper" +WRAPPER_JAR="$WRAPPER_DIR/maven-wrapper.jar" +WRAPPER_PROPS="$WRAPPER_DIR/maven-wrapper.properties" + +# Pick Java +if [ -n "$JAVA_HOME" ]; then JAVA_CMD="$JAVA_HOME/bin/java"; else JAVA_CMD="java"; fi + +# Find project base dir (folder that has pom.xml) +if [ -z "$MAVEN_PROJECTBASEDIR" ]; then + CUR="$APP_HOME" + while [ "$CUR" != "/" ]; do + if [ -f "$CUR/pom.xml" ]; then MAVEN_PROJECTBASEDIR="$CUR"; break; fi + CUR=$(cd "$CUR/.." && pwd -P) + done + [ -n "$MAVEN_PROJECTBASEDIR" ] || MAVEN_PROJECTBASEDIR="$APP_HOME" +fi + +# Download wrapper JAR if missing +if [ ! -f "$WRAPPER_JAR" ]; then + mkdir -p "$WRAPPER_DIR" + if [ ! -f "$WRAPPER_PROPS" ]; then + echo "Missing $WRAPPER_PROPS" >&2; exit 1 + fi + WRAPPER_URL=$(grep -E '^wrapperUrl=' "$WRAPPER_PROPS" | cut -d'=' -f2-) + if [ -z "$WRAPPER_URL" ]; then + echo "wrapperUrl not found in $WRAPPER_PROPS" >&2; exit 1 + fi + echo "Downloading Maven Wrapper JAR from $WRAPPER_URL ..." + if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$WRAPPER_JAR" "$WRAPPER_URL" + elif command -v wget >/dev/null 2>&1; then + wget -q -O "$WRAPPER_JAR" "$WRAPPER_URL" + else + echo "Need curl or wget to download maven-wrapper.jar" >&2; exit 1 + fi +fi + +exec "$JAVA_CMD" \ + -Dmaven.wrapper.log.level=INFO \ + "-Dmaven.multiModuleProjectDirectory=$MAVEN_PROJECTBASEDIR" \ + -cp "$WRAPPER_JAR" org.apache.maven.wrapper.MavenWrapperMain "$@" diff --git a/java-app/mvnw.cmd b/java-app/mvnw.cmd new file mode 100644 index 0000000..65e4b3e --- /dev/null +++ b/java-app/mvnw.cmd @@ -0,0 +1,46 @@ +@ECHO OFF +SETLOCAL + +SET "BASEDIR=%~dp0" +SET "WRAPPER_DIR=%BASEDIR%.mvn\wrapper" +SET "WRAPPER_JAR=%WRAPPER_DIR%\maven-wrapper.jar" +SET "WRAPPER_PROPS=%WRAPPER_DIR%\maven-wrapper.properties" + +IF EXIST "%JAVA_HOME%\bin\java.exe" ( + SET "JAVA_EXE=%JAVA_HOME%\bin\java.exe" +) ELSE ( + SET "JAVA_EXE=java" +) + +IF NOT EXIST "%WRAPPER_JAR%" ( + IF NOT EXIST "%WRAPPER_DIR%" MKDIR "%WRAPPER_DIR%" + FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%WRAPPER_PROPS%") DO ( + IF "%%A"=="wrapperUrl" SET "WRAPPER_URL=%%B" + ) + IF "%WRAPPER_URL%"=="" ( + ECHO wrapperUrl not found in %WRAPPER_PROPS% + EXIT /B 1 + ) + ECHO Downloading Maven Wrapper JAR from %WRAPPER_URL% + POWERSHELL -NoProfile -ExecutionPolicy Bypass -Command ^ + "Invoke-WebRequest -Uri '%WRAPPER_URL%' -OutFile '%WRAPPER_JAR%'" +) + +REM Find project base dir +SET "MAVEN_PROJECTBASEDIR=" +SET "CUR=%CD%" +:findPom +IF EXIST "%CUR%\pom.xml" ( + SET "MAVEN_PROJECTBASEDIR=%CUR%" +) ELSE ( + CD /D "%CUR%\.." + SET "CUR=%CD%" + IF "%CUR%"=="%SystemDrive%\" GOTO afterFind + GOTO findPom +) +:afterFind +IF "%MAVEN_PROJECTBASEDIR%"=="" SET "MAVEN_PROJECTBASEDIR=%BASEDIR%" + +"%JAVA_EXE%" -Dmaven.wrapper.log.level=INFO ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + -cp "%WRAPPER_JAR%" org.apache.maven.wrapper.MavenWrapperMain %* diff --git a/java-app/pom.xml b/java-app/pom.xml new file mode 100644 index 0000000..286385f --- /dev/null +++ b/java-app/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + + + com.techiescamp + java-app + 1.0.0 + java-app + Spring Boot application + + + 21 + + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/java-app/src/main/java/springboot/Application.java b/java-app/src/main/java/springboot/Application.java new file mode 100644 index 0000000..f9b7bc4 --- /dev/null +++ b/java-app/src/main/java/springboot/Application.java @@ -0,0 +1,20 @@ +package springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@RestController +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @GetMapping("/") + public String index() { + return "This is a demo Spring Boot application"; + } +} diff --git a/java-app/src/test/java/springboot/ApplicationTest.java b/java-app/src/test/java/springboot/ApplicationTest.java new file mode 100644 index 0000000..e82c32a --- /dev/null +++ b/java-app/src/test/java/springboot/ApplicationTest.java @@ -0,0 +1,26 @@ +package springboot; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class ApplicationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void indexEndpointReturnsExpectedMessage() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(content().string("This is a demo Spring Boot application")); + } +}