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
31 changes: 31 additions & 0 deletions .github/workflows/deploy-application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: deploy-to-k8s

on:
push:
branches:
- main
- edit-exam-ASE-436
- create-exam-ASE-435

permissions:
contents: read
packages: write

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

- name: Upload image
uses: Agile-Software-Engineering-25/build-and-publish-image@v1
with:
push: true
extra_tags: ${{ github.sha }}

- name: Deploy to Namespace
uses: Agile-Software-Engineering-25/deploy-to-k8s@v1
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
namespace: ${{ vars.K8S_NAMESPACE }}
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# ---- Build stage
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn -q -DskipTests clean package

# ---- Run stage
FROM eclipse-temurin:21-jre
WORKDIR /app
ENV JAVA_OPTS="-Xms256m -Xmx512m"
EXPOSE 8080
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /app/app.jar"]
36 changes: 36 additions & 0 deletions k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: exam-service
spec:
replicas: 1
selector:
matchLabels:
app: exam-service
template:
metadata:
labels:
app: exam-service
spec:
containers:
- name: app
image: ghcr.io/agile-software-engineering-25/team-14-backend-examination-office:latest
ports:
- containerPort: 8080
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 20
periodSeconds: 20
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
9 changes: 9 additions & 0 deletions k8s/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml

images:
- name: ghcr.io/agile-software-engineering-25/team-14-backend-examination-office
newTag: latest
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: exam-service
spec:
type: ClusterIP
selector:
app: exam-service
ports:
- name: http
port: 80
targetPort: 8080
39 changes: 39 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,44 @@
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Web + JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Bean Validation (jakarta.validation.*) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- DBs -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Optional -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
59 changes: 59 additions & 0 deletions src/main/java/com/ase/userservice/controllers/ExamController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.ase.userservice.controllers;

import com.ase.userservice.dto.CreateExamRequest;
import com.ase.userservice.dto.ExamResponse;
import com.ase.userservice.services.ExamService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.List;

@CrossOrigin(
origins = "http://localhost:5173",
allowedHeaders = "*",
allowCredentials = "true",
maxAge = 3600,
methods = { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS }
)
@RestController
@RequestMapping("/api/exams")
public class ExamController {
private final ExamService service;

public ExamController(ExamService service) {
this.service = service;
}

@PostMapping
public ResponseEntity<ExamResponse> createExam(@Valid @RequestBody CreateExamRequest req,
UriComponentsBuilder uri) {
ExamResponse created = service.create(req);
return ResponseEntity
.created(uri.path("/api/exams/{id}").buildAndExpand(created.id()).toUri())
.body(created);
}

@PutMapping("/{id}")
public ResponseEntity<ExamResponse> updateExam(@PathVariable Long id,
@Valid @RequestBody CreateExamRequest req) {
return ResponseEntity.ok(service.update(id, req));
}

@GetMapping("/{id}")
public ResponseEntity<ExamResponse> getExam(@PathVariable Long id) {
return ResponseEntity.ok(service.get(id));
}

@GetMapping
public ResponseEntity<List<ExamResponse>> listExams() {
return ResponseEntity.ok(service.list());
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteExam(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.ase.userservice.controllers;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.Map;
import java.util.stream.Collectors;

@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidation(MethodArgumentNotValidException ex) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
fe -> fe.getField(),
fe -> fe.getDefaultMessage(),
(a, b) -> a
));
return ResponseEntity.badRequest().body(Map.of("message", "Validation failed", "errors", errors));
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<?> handleIllegalArg(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage()));
}

@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<?> handleIllegalState(IllegalStateException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("message", ex.getMessage()));
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<?> handleNotFound(NotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", ex.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.ase.userservice.controllers;

public class NotFoundException extends RuntimeException {
public NotFoundException(String message) { super(message); }
}
29 changes: 29 additions & 0 deletions src/main/java/com/ase/userservice/dto/CreateExamRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ase.userservice.dto;

import jakarta.validation.constraints.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Duration;
import java.util.List;

public record CreateExamRequest(
@NotBlank String title,
@NotBlank String moduleCode,
@NotNull LocalDateTime examDate,
@NotBlank String room,
@NotBlank String examType,
@NotBlank String semester,
@NotNull @PositiveOrZero Integer ects,
@NotNull @Positive Integer maxPoints,
@NotNull @Positive Integer duration, // Minuten
@NotNull @Positive Integer attemptNumber, // 1..n
boolean fileUploadRequired,
@NotNull @Size(max = 20) List<@NotBlank String> tools
) {
public void validateBusinessRules() {
if (tools != null && tools.stream().anyMatch(s -> s == null || s.isBlank())) {
throw new IllegalArgumentException("tools must not contain blank items");
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/ase/userservice/dto/ExamResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ase.userservice.dto;

import java.time.LocalDateTime;
import java.util.List;

public record ExamResponse(
Long id,
String title,
String moduleCode,
LocalDateTime examDate,
String room,
String examType,
String semester,
Integer ects,
Integer maxPoints,
Integer duration,
Integer attemptNumber,
boolean fileUploadRequired,
List<String> tools
) {}
Loading
Loading