Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ ij_java_continuation_indent_size = 4
ij_java_class_count_to_use_import_on_demand = 999
ij_java_names_count_to_use_import_on_demand = 999
# Import order to match Checkstyle groups: java, javax, org, com
ij_java_imports_layout = import java.*; import javax.*; import org.*; import com.*; blank_line; import all other imports; blank_line; import static all other static imports
ij_java_imports_layout = $*,java.**,javax.**,org.**,com.**,*

[*.{yml,yaml}]
indent_size = 2

30 changes: 30 additions & 0 deletions .github/workflows/deploy-application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: deploy-to-k8s

on:
push:
branches:
- "**"
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5

- name: Build & Publish Docker image
id: build
uses: Agile-Software-Engineering-25/build-and-publish-image@v1.0.0
with:
# optional: set extra tags if needed
extra_tags: ${{ github.sha }}

- name: Deploy to Namespace
uses: Agile-Software-Engineering-25/deploy-to-k8s@v1.0.1
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
namespace: ${{ vars.K8S_NAMESPACE }}
deployment: grading-service
image: ghcr.io/agile-software-engineering-25/team-12-backend-exagrad-lecturer-service
tag: ${{ github.sha }}
65 changes: 65 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: CI - Test

on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test (Java ${{ matrix.java }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java: [ '21', '22' ]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Temurin JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
cache: maven

- name: Show Java and Maven versions
run: |
java -version
mvn -v

- name: Build and run tests
run: mvn -B -ntp clean verify

- name: Upload test reports (Surefire)
if: always()
uses: actions/upload-artifact@v4
with:
name: surefire-reports-java-${{ matrix.java }}
path: |
**/target/surefire-reports/**
if-no-files-found: ignore
retention-days: 14

- name: Upload integration test reports (Failsafe)
if: always()
uses: actions/upload-artifact@v4
with:
name: failsafe-reports-java-${{ matrix.java }}
path: |
**/target/failsafe-reports/**
if-no-files-found: ignore
retention-days: 14
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ build/

### VS Code ###
.vscode/

/data/

### MacOs ###
*/.DS_STORE
*/.DS_STORE
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Stage 1: build the jar
FROM maven:3.9.9-eclipse-temurin-21-alpine AS builder
WORKDIR /app

COPY pom.xml .
RUN mvn dependency:go-offline -B

COPY src src
RUN mvn clean package -DskipTests -B

# Stage 2: run the jar
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

# Copy the fat jar (Spring Boot creates a runnable jar under target/)
COPY --from=builder /app/target/*.jar app.jar
RUN chown -R 1000:1000 /app

# Run as non-root
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]
6 changes: 3 additions & 3 deletions checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
<property name="caseIndent" value="2"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="tabWidth" value="2"/>
<property name="forceStrictCondition" value="true"/>
<property name="forceStrictCondition" value="false"/>
</module>


Expand Down Expand Up @@ -143,9 +143,9 @@

</module>

<!-- Line Length: max 80 characters -->
<!-- Line Length: max 100 characters -->
<module name="LineLength">
<property name="max" value="80"/>
<property name="max" value="100"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http(s)?://.*"/>
</module>

Expand Down
95 changes: 95 additions & 0 deletions k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: grading-service
namespace: ase-12
labels:
app.kubernetes.io/name: grading-service
app.kubernetes.io/instance: grading-service
spec:
replicas: 2
revisionHistoryLimit: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: grading-service
app.kubernetes.io/instance: grading-service
template:
metadata:
labels:
app.kubernetes.io/name: grading-service
app.kubernetes.io/instance: grading-service
spec:
serviceAccountName: default
terminationGracePeriodSeconds: 30
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: app
image: ghcr.io/agile-software-engineering-25/team-12-backend-exagrad-lecturer-service:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
# Enable graceful shutdown at app level (align with terminationGracePeriodSeconds)
- name: SERVER_SERVLET_CONTEXT_PATH
value: "/exa-grad/grading-service"
- name: SERVER_SHUTDOWN
value: "graceful"

readinessProbe:
httpGet:
path: /exa-grad/grading-service/actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 6
timeoutSeconds: 1
livenessProbe:
httpGet:
path: /exa-grad/grading-service/actuator/health/liveness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 1
startupProbe:
httpGet:
path: /exa-grad/grading-service/actuator/health
port: 8080
periodSeconds: 5
failureThreshold: 30

resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1024Mi"

securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # Immutable root FS → mount /tmp as writable
capabilities:
drop: [ "ALL" ]

volumeMounts:
- name: data
mountPath: /app/data
- name: tmp
mountPath: /tmp

volumes:
- name: tmp
emptyDir: { }
- name: data
emptyDir: { }
23 changes: 23 additions & 0 deletions k8s/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grading-service
namespace: ase-12
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure # send traffic over HTTPS
spec:
ingressClassName: traefik
tls:
- hosts: ["sau-portal.de"]
secretName: tls-to-come
rules:
- host: sau-portal.de
http:
paths:
- path: /exa-grad/grading-service # Public URL prefix for this service
pathType: Prefix
backend:
service:
name: grading-service
port:
number: 80
7 changes: 7 additions & 0 deletions k8s/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ase-12 #e.g ase-1
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
12 changes: 12 additions & 0 deletions k8s/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: grading-service
namespace: ase-12
spec:
selector:
app.kubernetes.io/name: grading-service # Must match Deployment template labels
ports:
- name: http
port: 80
targetPort: 8080
25 changes: 20 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,13 @@
</properties>
<dependencies>
<dependency>
<groupId>ch.bildspur</groupId>
<artifactId>artnet4j</artifactId>
<version>0.6.2</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand All @@ -65,4 +62,22 @@
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.ase.lecturerservice.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ase.userservice;
package com.ase.lecturerservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/com/ase/lecturerservice/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.ase.lecturerservice.config;

import java.util.Arrays;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(CorsConfig.CorsConfigurationProperties.class)
public class CorsConfig {

private final CorsConfigurationProperties corsConfigurationProperties;

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
Arrays.stream(corsConfigurationProperties.allowedOrigins)
.forEach(corsConfiguration::addAllowedOrigin);
log.info("Allowed origins:");
Arrays.stream(corsConfigurationProperties.allowedOrigins).forEach(System.out::println);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}

@Bean
CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}

@ConfigurationProperties(prefix = "app.cors")
record CorsConfigurationProperties(String[] allowedOrigins) {

}
}
Loading