diff --git a/src/main/java/fr/insee/genesis/controller/rest/RundeckExecutionController.java b/src/main/java/fr/insee/genesis/controller/rest/RundeckExecutionController.java new file mode 100644 index 00000000..4c89857e --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/rest/RundeckExecutionController.java @@ -0,0 +1,42 @@ +package fr.insee.genesis.controller.rest; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.domain.ports.api.RundeckExecutionApiPort; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping(path = "/rundeck-execution") +@Controller +@Slf4j +public class RundeckExecutionController { + + private final RundeckExecutionApiPort rundeckExecutionApiPort; + + @Autowired + public RundeckExecutionController(RundeckExecutionApiPort rundeckExecutionApiPort) { + this.rundeckExecutionApiPort = rundeckExecutionApiPort; + } + + @Operation(summary = "Register a Rundeck execution") + @PostMapping(path = "/save") + public ResponseEntity addRundeckExecution( + @Parameter(description = "JSON response from Rundeck API /run endpoint") @RequestBody RundeckExecution rundeckExecution + ){ + try{ + rundeckExecutionApiPort.addExecution(rundeckExecution); + log.info("{} job saved", rundeckExecution.getJob().getName()); + } catch(Exception e){ + log.info("Rundeck execution was not saved in database"); + return ResponseEntity.internalServerError().build(); + } + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/fr/insee/genesis/domain/model/rundeck/DateStarted.java b/src/main/java/fr/insee/genesis/domain/model/rundeck/DateStarted.java new file mode 100644 index 00000000..e0386b5d --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/model/rundeck/DateStarted.java @@ -0,0 +1,13 @@ +package fr.insee.genesis.domain.model.rundeck; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DateStarted { + + private long unixtime; + private String date; + +} diff --git a/src/main/java/fr/insee/genesis/domain/model/rundeck/Job.java b/src/main/java/fr/insee/genesis/domain/model/rundeck/Job.java new file mode 100644 index 00000000..47da4844 --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/model/rundeck/Job.java @@ -0,0 +1,20 @@ +package fr.insee.genesis.domain.model.rundeck; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Job { + + @JsonProperty("id") + private String idJob; + private long averageDuration; + private String name; + private String group; + private String project; + private String description; + private String href; + private String permalink; +} diff --git a/src/main/java/fr/insee/genesis/domain/model/rundeck/RundeckExecution.java b/src/main/java/fr/insee/genesis/domain/model/rundeck/RundeckExecution.java new file mode 100644 index 00000000..2736762c --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/model/rundeck/RundeckExecution.java @@ -0,0 +1,27 @@ +package fr.insee.genesis.domain.model.rundeck; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RundeckExecution { + + @JsonProperty("id") + private long idExecution; + private String href; + private String permalink; + private String status; + private String project; + private String executionType; + private String user; + + @JsonProperty("date-started") + private DateStarted dateStarted; + + private Job job; + private String description; + private String argstring; + private String serverUUID; +} diff --git a/src/main/java/fr/insee/genesis/domain/ports/api/RundeckExecutionApiPort.java b/src/main/java/fr/insee/genesis/domain/ports/api/RundeckExecutionApiPort.java new file mode 100644 index 00000000..91246858 --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/ports/api/RundeckExecutionApiPort.java @@ -0,0 +1,9 @@ +package fr.insee.genesis.domain.ports.api; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; + +public interface RundeckExecutionApiPort { + + void addExecution(RundeckExecution rundeckExecution); + +} diff --git a/src/main/java/fr/insee/genesis/domain/ports/spi/RundeckExecutionPersistencePort.java b/src/main/java/fr/insee/genesis/domain/ports/spi/RundeckExecutionPersistencePort.java new file mode 100644 index 00000000..bbdbdb85 --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/ports/spi/RundeckExecutionPersistencePort.java @@ -0,0 +1,8 @@ +package fr.insee.genesis.domain.ports.spi; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; + +public interface RundeckExecutionPersistencePort { + + void save(RundeckExecution rundeckExecution); +} diff --git a/src/main/java/fr/insee/genesis/domain/service/rundeck/RundeckExecutionService.java b/src/main/java/fr/insee/genesis/domain/service/rundeck/RundeckExecutionService.java new file mode 100644 index 00000000..06d84560 --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/service/rundeck/RundeckExecutionService.java @@ -0,0 +1,26 @@ +package fr.insee.genesis.domain.service.rundeck; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.domain.ports.api.RundeckExecutionApiPort; +import fr.insee.genesis.domain.ports.spi.RundeckExecutionPersistencePort; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class RundeckExecutionService implements RundeckExecutionApiPort { + @Qualifier("rundeckExecutionMongoAdapter") + private final RundeckExecutionPersistencePort rundeckExecutionPersistencePort; + + @Autowired + public RundeckExecutionService(RundeckExecutionPersistencePort rundeckExecutionPersistencePort) { + this.rundeckExecutionPersistencePort = rundeckExecutionPersistencePort; + } + + @Override + public void addExecution(RundeckExecution rundeckExecution) { + rundeckExecutionPersistencePort.save(rundeckExecution); + } +} diff --git a/src/main/java/fr/insee/genesis/infrastructure/adapter/RundeckExecutionMongoAdapter.java b/src/main/java/fr/insee/genesis/infrastructure/adapter/RundeckExecutionMongoAdapter.java new file mode 100644 index 00000000..9903e33d --- /dev/null +++ b/src/main/java/fr/insee/genesis/infrastructure/adapter/RundeckExecutionMongoAdapter.java @@ -0,0 +1,30 @@ +package fr.insee.genesis.infrastructure.adapter; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.domain.ports.spi.RundeckExecutionPersistencePort; +import fr.insee.genesis.infrastructure.mappers.RundeckExecutionDocumentMapper; +import fr.insee.genesis.infrastructure.repository.RundeckExecutionDBRepository; +import fr.insee.genesis.infrastructure.repository.ScheduleMongoDBRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.stereotype.Service; + +@Service +@Qualifier("rundeckExecutionMongoAdapter") +@Slf4j +public class RundeckExecutionMongoAdapter implements RundeckExecutionPersistencePort { + + private final RundeckExecutionDBRepository rundeckExecutionDBRepository; + + @Autowired + public RundeckExecutionMongoAdapter(RundeckExecutionDBRepository rundeckExecutionDBRepository) { + this.rundeckExecutionDBRepository = rundeckExecutionDBRepository; + } + + @Override + public void save(RundeckExecution rundeckExecution) { + rundeckExecutionDBRepository.insert(RundeckExecutionDocumentMapper.INSTANCE.modelToDocument(rundeckExecution)); + } +} diff --git a/src/main/java/fr/insee/genesis/infrastructure/document/rundeck/Job.java b/src/main/java/fr/insee/genesis/infrastructure/document/rundeck/Job.java new file mode 100644 index 00000000..4f4a2df6 --- /dev/null +++ b/src/main/java/fr/insee/genesis/infrastructure/document/rundeck/Job.java @@ -0,0 +1,12 @@ +package fr.insee.genesis.infrastructure.document.rundeck; + +import lombok.Data; + +@Data +public class Job { + + private String idJob; + private long averageDuration; + private String name; + private String project; +} diff --git a/src/main/java/fr/insee/genesis/infrastructure/document/rundeck/RundeckExecutionDocument.java b/src/main/java/fr/insee/genesis/infrastructure/document/rundeck/RundeckExecutionDocument.java new file mode 100644 index 00000000..8e2b8ad5 --- /dev/null +++ b/src/main/java/fr/insee/genesis/infrastructure/document/rundeck/RundeckExecutionDocument.java @@ -0,0 +1,23 @@ +package fr.insee.genesis.infrastructure.document.rundeck; + +import com.fasterxml.jackson.annotation.JsonProperty; +import fr.insee.genesis.domain.model.rundeck.DateStarted; +import lombok.*; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Document(collection= "rundeckExecutions") +public class RundeckExecutionDocument { + + private long idExecution; + private String status; + private String project; + private String user; + + @JsonProperty("date-started") + private DateStarted dateStarted; + + private Job job; +} diff --git a/src/main/java/fr/insee/genesis/infrastructure/mappers/RundeckExecutionDocumentMapper.java b/src/main/java/fr/insee/genesis/infrastructure/mappers/RundeckExecutionDocumentMapper.java new file mode 100644 index 00000000..f0847f0e --- /dev/null +++ b/src/main/java/fr/insee/genesis/infrastructure/mappers/RundeckExecutionDocumentMapper.java @@ -0,0 +1,16 @@ +package fr.insee.genesis.infrastructure.mappers; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.infrastructure.document.rundeck.RundeckExecutionDocument; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface RundeckExecutionDocumentMapper { + + RundeckExecutionDocumentMapper INSTANCE = Mappers.getMapper(RundeckExecutionDocumentMapper.class); + + RundeckExecution documentToModel(RundeckExecutionDocument rundeckExecutionDocument); + + RundeckExecutionDocument modelToDocument(RundeckExecution rundeckExecution); +} diff --git a/src/main/java/fr/insee/genesis/infrastructure/repository/RundeckExecutionDBRepository.java b/src/main/java/fr/insee/genesis/infrastructure/repository/RundeckExecutionDBRepository.java new file mode 100644 index 00000000..29835686 --- /dev/null +++ b/src/main/java/fr/insee/genesis/infrastructure/repository/RundeckExecutionDBRepository.java @@ -0,0 +1,9 @@ +package fr.insee.genesis.infrastructure.repository; + +import fr.insee.genesis.infrastructure.document.rundeck.RundeckExecutionDocument; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RundeckExecutionDBRepository extends MongoRepository { +} diff --git a/src/test/java/fr/insee/genesis/controller/rest/RundeckExecutionControllerTest.java b/src/test/java/fr/insee/genesis/controller/rest/RundeckExecutionControllerTest.java new file mode 100644 index 00000000..35eca5c2 --- /dev/null +++ b/src/test/java/fr/insee/genesis/controller/rest/RundeckExecutionControllerTest.java @@ -0,0 +1,56 @@ +package fr.insee.genesis.controller.rest; + +import fr.insee.genesis.domain.model.rundeck.Job; +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.stubs.RundeckExecutionApiPortStub; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RundeckExecutionControllerTest { + + private static RundeckExecutionController rundeckExecutionController; + private static RundeckExecutionApiPortStub rundeckExecutionApiPortStub; + + @BeforeAll + static void init(){ + rundeckExecutionApiPortStub = new RundeckExecutionApiPortStub(); + rundeckExecutionController = new RundeckExecutionController(rundeckExecutionApiPortStub); + } + + @Test + void testAddRundeckExecution_Success() { + + RundeckExecution rundeckExecution = new RundeckExecution(); + Job job = new Job(); + job.setName("TEST"); + rundeckExecution.setJob(job); + + // WHEN + ResponseEntity response = rundeckExecutionController.addRundeckExecution(rundeckExecution); + + // THEN + assertEquals(ResponseEntity.ok().build(), response); + } + + @Test + void testAddRundeckExecution_Failure() { + // GIVEN + rundeckExecutionApiPortStub.setShouldThrowException(true); + + RundeckExecution rundeckExecution = new RundeckExecution(); + Job job = new Job(); + job.setName("TEST"); + + rundeckExecution.setJob(job); + + // WHEN + ResponseEntity response = rundeckExecutionController.addRundeckExecution(rundeckExecution); + + // THEN + assertEquals(ResponseEntity.internalServerError().build(), response); + } + +} \ No newline at end of file diff --git a/src/test/java/fr/insee/genesis/domain/service/rundeck/RundeckExecutionServiceTest.java b/src/test/java/fr/insee/genesis/domain/service/rundeck/RundeckExecutionServiceTest.java new file mode 100644 index 00000000..a24c98d0 --- /dev/null +++ b/src/test/java/fr/insee/genesis/domain/service/rundeck/RundeckExecutionServiceTest.java @@ -0,0 +1,71 @@ +package fr.insee.genesis.domain.service.rundeck; + +import fr.insee.genesis.domain.model.rundeck.DateStarted; +import fr.insee.genesis.domain.model.rundeck.Job; +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.stubs.RundeckExecutionPersistencePortStub; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class RundeckExecutionServiceTest { + + static RundeckExecutionPersistencePortStub rundeckPersistencePortStub; + + static RundeckExecutionService rundeckExecutionService; + + @BeforeAll + static void init(){ + rundeckPersistencePortStub = new RundeckExecutionPersistencePortStub(); + rundeckExecutionService = new RundeckExecutionService(rundeckPersistencePortStub); + } + + @Test + void addRundeckExecution_test() { + //GIVEN + DateStarted dateStarted = new DateStarted(); + dateStarted.setUnixtime(1737643070029L); + dateStarted.setDate("2025-01-23T14:37:50Z"); + + Job job = new Job(); + job.setIdJob("9506a45d-79e3-42d9-afb4-c8f59652c676"); + job.setAverageDuration(8817); + job.setName("TEST"); + job.setGroup(""); + job.setProject("project-test"); + job.setDescription("job de test qui attend 8 s et retourne OK"); + job.setHref("https://example-url"); + job.setPermalink("https://example-url"); + + // Création de l'objet RundeckExecution + RundeckExecution execution = new RundeckExecution(); + execution.setIdExecution(1747563); + execution.setHref("https://example-url"); + execution.setPermalink("https://example-url"); + execution.setStatus("running"); + execution.setProject("project-test"); + execution.setExecutionType("user"); + execution.setUser("project-test"); + execution.setDateStarted(dateStarted); + execution.setJob(job); + execution.setDescription("A very usefull description"); + execution.setArgstring(null); + execution.setServerUUID("e9ff62c2-b621-4b31-827e-9d47f0c34310"); + + //WHEN + rundeckExecutionService.addExecution(execution); + + //THEN + Assertions.assertThat(rundeckPersistencePortStub.getMongoStub()).hasSize(1); + Assertions.assertThat(rundeckPersistencePortStub.getMongoStub().getFirst().getIdExecution()).isEqualTo(1747563); + Assertions.assertThat(rundeckPersistencePortStub.getMongoStub().getFirst().getStatus()).isEqualTo("running"); + + Assertions.assertThat(rundeckPersistencePortStub.getMongoStub().getFirst().getUser()).isEqualTo("project-test"); + Assertions.assertThat(rundeckPersistencePortStub.getMongoStub().getFirst().getProject()).isEqualTo("project-test"); + Assertions.assertThat(rundeckPersistencePortStub.getMongoStub().getFirst().getDateStarted().getUnixtime()).isEqualTo(1737643070029L); + Assertions.assertThat(rundeckPersistencePortStub.getMongoStub().getFirst().getJob().getIdJob()).isEqualTo("9506a45d-79e3-42d9-afb4-c8f59652c676"); + + } + + +} \ No newline at end of file diff --git a/src/test/java/fr/insee/genesis/infrastructure/mapper/RundeckExecutionDocumentMapperImplTest.java b/src/test/java/fr/insee/genesis/infrastructure/mapper/RundeckExecutionDocumentMapperImplTest.java new file mode 100644 index 00000000..0c551dca --- /dev/null +++ b/src/test/java/fr/insee/genesis/infrastructure/mapper/RundeckExecutionDocumentMapperImplTest.java @@ -0,0 +1,56 @@ +package fr.insee.genesis.infrastructure.mapper; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.infrastructure.document.rundeck.RundeckExecutionDocument; +import fr.insee.genesis.infrastructure.mappers.RundeckExecutionDocumentMapper; +import fr.insee.genesis.infrastructure.mappers.RundeckExecutionDocumentMapperImpl; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RundeckExecutionDocumentMapperImplTest { + + //Given + static RundeckExecutionDocumentMapper rundeckExecutionDocumentMapperImplStatic; + static RundeckExecutionDocument rundeckExecutionDocument; + static RundeckExecution rundeckExecution; + + @BeforeAll + static void init(){ + rundeckExecutionDocumentMapperImplStatic = new RundeckExecutionDocumentMapperImpl(); + + rundeckExecutionDocument = new RundeckExecutionDocument(); + rundeckExecutionDocument.setIdExecution(1236589); + + rundeckExecution = new RundeckExecution(); + rundeckExecution.setIdExecution(1236589); + + } + + @Test + @DisplayName("Should return null if null parameter") + void shouldReturnNull(){ + Assertions.assertThat(rundeckExecutionDocumentMapperImplStatic.documentToModel(null)).isNull(); + Assertions.assertThat(rundeckExecutionDocumentMapperImplStatic.modelToDocument(null)).isNull(); + } + + @Test + @DisplayName("Should convert document to DTO") + void shouldReturnDocumentDtoFromDocument(){ + RundeckExecution rundeckModel = rundeckExecutionDocumentMapperImplStatic.documentToModel(rundeckExecutionDocument); + + Assertions.assertThat(rundeckModel.getIdExecution()).isEqualTo(1236589); + } + + @Test + @DisplayName("Should convert DTO to document") + void shouldReturnDocumentFromDocumentDto(){ + RundeckExecutionDocument rundeckDocument = rundeckExecutionDocumentMapperImplStatic.modelToDocument(rundeckExecution); + + Assertions.assertThat(rundeckDocument.getIdExecution()).isEqualTo(1236589); + + } + + +} diff --git a/src/test/java/fr/insee/genesis/stubs/RundeckExecutionApiPortStub.java b/src/test/java/fr/insee/genesis/stubs/RundeckExecutionApiPortStub.java new file mode 100644 index 00000000..1293e8f0 --- /dev/null +++ b/src/test/java/fr/insee/genesis/stubs/RundeckExecutionApiPortStub.java @@ -0,0 +1,20 @@ +package fr.insee.genesis.stubs; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.domain.ports.api.RundeckExecutionApiPort; +import lombok.Setter; + +@Setter +public class RundeckExecutionApiPortStub implements RundeckExecutionApiPort { + + private boolean shouldThrowException = false; + + @Override + public void addExecution(RundeckExecution rundeckExecution) { + if (shouldThrowException) { + throw new RuntimeException("Simulated exception"); + } + // Otherwise, do nothing + System.out.println("Execution saved: " + rundeckExecution.getJob().getName()); + } +} diff --git a/src/test/java/fr/insee/genesis/stubs/RundeckExecutionPersistencePortStub.java b/src/test/java/fr/insee/genesis/stubs/RundeckExecutionPersistencePortStub.java new file mode 100644 index 00000000..b312c1c3 --- /dev/null +++ b/src/test/java/fr/insee/genesis/stubs/RundeckExecutionPersistencePortStub.java @@ -0,0 +1,21 @@ +package fr.insee.genesis.stubs; + +import fr.insee.genesis.domain.model.rundeck.RundeckExecution; +import fr.insee.genesis.domain.ports.spi.RundeckExecutionPersistencePort; +import fr.insee.genesis.infrastructure.document.rundeck.RundeckExecutionDocument; +import fr.insee.genesis.infrastructure.mappers.RundeckExecutionDocumentMapper; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class RundeckExecutionPersistencePortStub implements RundeckExecutionPersistencePort { + + List mongoStub = new ArrayList<>(); + + @Override + public void save(RundeckExecution rundeckExecution) { + mongoStub.add(RundeckExecutionDocumentMapper.INSTANCE.modelToDocument(rundeckExecution)); + } +}