From ef04d3dbea3d33f1a2a6d9afcd4ebb0ad3aba3fb Mon Sep 17 00:00:00 2001 From: Florian THIERRY Date: Mon, 26 Jul 2021 23:40:03 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 33 +++++ pom.xml | 66 +++++++++ .../cerberusapp/CerberusApplication.java | 13 ++ .../controller/ApplicationController.java | 83 ++++++++++++ .../exception/BadRequestException.java | 15 +++ .../exception/BusinessException.java | 26 ++++ .../exception/ForbiddenException.java | 23 ++++ .../InternalServerErrorException.java | 19 +++ .../exception/NoContentException.java | 17 +++ .../exception/NotFoundException.java | 17 +++ .../exception/TechnicalException.java | 23 ++++ .../exception/UnauthorizedException.java | 22 +++ .../cerberusapp/model/Application.java | 73 ++++++++++ .../cerberusapp/model/ServiceStatus.java | 7 + .../cerberusapp/model/ServiceType.java | 7 + .../ApplicationJpaRepositoryAdapter.java | 58 ++++++++ .../mapper/ApplicationEntityMapper.java | 29 ++++ .../persistence/model/ApplicationEntity.java | 63 +++++++++ .../repository/ApplicationRepository.java | 12 ++ .../service/ApplicationRepositoryPort.java | 15 +++ .../service/ApplicationService.java | 97 +++++++++++++ .../exception/ValidationException.java | 7 + .../servicemanager/DockerServiceManager.java | 28 ++++ .../servicemanager/FakeServiceManager.java | 39 ++++++ .../servicemanager/LinuxServiceManager.java | 28 ++++ .../servicemanager/ServiceManager.java | 11 ++ .../ServiceManagerProvider.java | 42 ++++++ .../validator/ApplicationValidator.java | 24 ++++ src/main/resources/application.yml | 15 +++ src/main/sql/ddl.sql | 9 ++ .../cerberusapp/CerberusApplicationTests.java | 13 ++ .../service/ApplicationServiceTest.java | 127 ++++++++++++++++++ .../validator/ApplicationValidatorTest.java | 96 +++++++++++++ 33 files changed, 1157 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/CerberusApplication.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/controller/ApplicationController.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BadRequestException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BusinessException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/ForbiddenException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/InternalServerErrorException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NoContentException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NotFoundException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/TechnicalException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/exception/UnauthorizedException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/model/Application.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceStatus.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceType.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/adapter/ApplicationJpaRepositoryAdapter.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/mapper/ApplicationEntityMapper.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/model/ApplicationEntity.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/repository/ApplicationRepository.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationRepositoryPort.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationService.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/exception/ValidationException.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/DockerServiceManager.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/FakeServiceManager.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/LinuxServiceManager.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManager.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManagerProvider.java create mode 100644 src/main/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidator.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/sql/ddl.sql create mode 100644 src/test/java/org/takiguchi/cerberus/cerberusapp/CerberusApplicationTests.java create mode 100644 src/test/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationServiceTest.java create mode 100644 src/test/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidatorTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e972e75 --- /dev/null +++ b/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.3 + + + org.takiguchi.cerberus + cerberus + 0.0.1-SNAPSHOT + Cerberus + Project to mangage services on a private server. + + 11 + 42.2.23 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.postgresql + postgresql + ${postgresql.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/CerberusApplication.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/CerberusApplication.java new file mode 100644 index 0000000..10c387e --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/CerberusApplication.java @@ -0,0 +1,13 @@ +package org.takiguchi.cerberus.cerberusapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CerberusApplication { + + public static void main(String[] args) { + SpringApplication.run(CerberusApplication.class, args); + } + +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/controller/ApplicationController.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/controller/ApplicationController.java new file mode 100644 index 0000000..8fcaa86 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/controller/ApplicationController.java @@ -0,0 +1,83 @@ +package org.takiguchi.cerberus.cerberusapp.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.model.ServiceStatus; +import org.takiguchi.cerberus.cerberusapp.service.ApplicationService; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.takiguchi.cerberus.cerberusapp.model.Application.anApplication; + +@RestController +@RequestMapping("/api/applications") +public class ApplicationController { + private final ApplicationService service; + + public ApplicationController(ApplicationService service) { + this.service = service; + } + + @GetMapping("/{applicationId}") + public Optional getById(@PathVariable("applicationId") UUID applicationId) { + return service.getById(applicationId); + } + + @GetMapping + public List getAll() { + return service.getAll(); + } + + @PostMapping + public Application add(@RequestBody Application application) { + Application applicationToAdd = anApplication() + .withName(application.getName()) + .withServiceName(application.getServiceName()) + .withServiceType(application.getServiceType()) + .build(); + return service.add(applicationToAdd); + } + + @PutMapping("/{applicationId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@PathVariable("applicationId") UUID applicationId, @RequestBody Application application) { + Application applicationToUpdate = anApplication() + .withId(applicationId) + .withName(application.getName()) + .withServiceName(application.getServiceName()) + .withServiceType(application.getServiceType()) + .build(); + service.update(applicationToUpdate); + } + + @DeleteMapping("/{applicationId}") + public void remove(@PathVariable("applicationId") UUID applicationId) { + service.remove(applicationId); + } + + @GetMapping("/{applicationId}/status") + public ServiceStatus getStatus(@PathVariable("applicationId") UUID applicationId) { + return service.checkStatus(applicationId); + } + + @PostMapping("/{applicationId}/start") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void start(@PathVariable("applicationId") UUID applicationId) { + service.start(applicationId); + } + + @PostMapping("/{applicationId}/stop") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void stop(@PathVariable("applicationId") UUID applicationId) { + service.stop(applicationId); + } + + @PostMapping("/{applicationId}/restart") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void restart(@PathVariable("applicationId") UUID applicationId) { + service.restart(applicationId); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BadRequestException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BadRequestException.java new file mode 100644 index 0000000..ac4380f --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BadRequestException.java @@ -0,0 +1,15 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.BAD_REQUEST) +public class BadRequestException extends BusinessException { + public BadRequestException(String message) { + super(message); + } + + public BadRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BusinessException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BusinessException.java new file mode 100644 index 0000000..ab60ce5 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/BusinessException.java @@ -0,0 +1,26 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +/** + * Business exception. + */ +public class BusinessException extends RuntimeException { + + public BusinessException() {} + + /** + * Constructs an exception with a message. + * @param message The description of the error met. + */ + public BusinessException(final String message) { + super(message); + } + + /** + * Constructs an exception with a message and a code. + * @param message The description of the error met. + * @param cause The cause of the exception. + */ + public BusinessException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/ForbiddenException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/ForbiddenException.java new file mode 100644 index 0000000..f1cd1e9 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/ForbiddenException.java @@ -0,0 +1,23 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Exception thrown when user attempt to access a resource that he has not rights. + */ +@ResponseStatus(value = HttpStatus.FORBIDDEN) +public class ForbiddenException extends BusinessException { + + public ForbiddenException() { + super(); + } + + public ForbiddenException(String message) { + super(message); + } + + public ForbiddenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/InternalServerErrorException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/InternalServerErrorException.java new file mode 100644 index 0000000..3727f66 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/InternalServerErrorException.java @@ -0,0 +1,19 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) +public class InternalServerErrorException extends TechnicalException { + public InternalServerErrorException() { + super(""); + } + + public InternalServerErrorException(String message) { + super(message); + } + + public InternalServerErrorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NoContentException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NoContentException.java new file mode 100644 index 0000000..cdad31d --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NoContentException.java @@ -0,0 +1,17 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NO_CONTENT) +public class NoContentException extends BusinessException { + public NoContentException() {} + + public NoContentException(String message) { + super(message); + } + + public NoContentException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NotFoundException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NotFoundException.java new file mode 100644 index 0000000..2a2e553 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/NotFoundException.java @@ -0,0 +1,17 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class NotFoundException extends BusinessException { + public NotFoundException() {} + + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/TechnicalException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/TechnicalException.java new file mode 100644 index 0000000..26eb499 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/TechnicalException.java @@ -0,0 +1,23 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +/** + * Technical exception. + */ +public class TechnicalException extends RuntimeException { + /** + * Constructs an exception with a message. + * @param message The description of the error met. + */ + public TechnicalException(final String message) { + super(message); + } + + /** + * Constructs an exception with a message and a code. + * @param message The description of the error met. + * @param cause The cause of the exception. + */ + public TechnicalException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/UnauthorizedException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/UnauthorizedException.java new file mode 100644 index 0000000..ec09317 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/exception/UnauthorizedException.java @@ -0,0 +1,22 @@ +package org.takiguchi.cerberus.cerberusapp.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Exception thrown when an anonymous user attempt to access to secured resource or if he failed to login. + */ +@ResponseStatus(value = HttpStatus.UNAUTHORIZED) +public class UnauthorizedException extends BusinessException { + public UnauthorizedException() { + super(); + } + + public UnauthorizedException(String message) { + super(message); + } + + public UnauthorizedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/model/Application.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/model/Application.java new file mode 100644 index 0000000..b7e1503 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/model/Application.java @@ -0,0 +1,73 @@ +package org.takiguchi.cerberus.cerberusapp.model; + +import java.util.UUID; + +public class Application { + private final UUID id; + /** The name to display. */ + private final String name; + /** The technical service name, like a docker container name or a system V service name. */ + private final String serviceName; + private final ServiceType serviceType; + + public static Builder anApplication() { + return new Builder(); + } + + private Application(UUID id, String name, String serviceName, ServiceType serviceType) { + this.id = id; + this.name = name; + this.serviceName = serviceName; + this.serviceType = serviceType; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public String getServiceName() { + return serviceName; + } + + public ServiceType getServiceType() { + return serviceType; + } + + public static class Builder { + private UUID id; + private String name; + private String serviceName; + private ServiceType serviceType; + + private Builder() { + } + + public Builder withId(UUID id) { + this.id = id; + return this; + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withServiceName(String serviceName) { + this.serviceName = serviceName; + return this; + } + + public Builder withServiceType(ServiceType serviceType) { + this.serviceType = serviceType; + return this; + } + + public Application build() { + return new Application(id, name, serviceName, serviceType); + } + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceStatus.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceStatus.java new file mode 100644 index 0000000..d736603 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceStatus.java @@ -0,0 +1,7 @@ +package org.takiguchi.cerberus.cerberusapp.model; + +public enum ServiceStatus { + STARTED, + STOPPED, + UNKNOWN; +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceType.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceType.java new file mode 100644 index 0000000..c2fb860 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/model/ServiceType.java @@ -0,0 +1,7 @@ +package org.takiguchi.cerberus.cerberusapp.model; + +public enum ServiceType { + FAKE, + SERVICE, + DOCKER; +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/adapter/ApplicationJpaRepositoryAdapter.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/adapter/ApplicationJpaRepositoryAdapter.java new file mode 100644 index 0000000..206df54 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/adapter/ApplicationJpaRepositoryAdapter.java @@ -0,0 +1,58 @@ +package org.takiguchi.cerberus.cerberusapp.persistence.adapter; + + +import org.springframework.stereotype.Component; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.persistence.mapper.ApplicationEntityMapper; +import org.takiguchi.cerberus.cerberusapp.persistence.model.ApplicationEntity; +import org.takiguchi.cerberus.cerberusapp.persistence.repository.ApplicationRepository; +import org.takiguchi.cerberus.cerberusapp.service.ApplicationRepositoryPort; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@Component +public class ApplicationJpaRepositoryAdapter implements ApplicationRepositoryPort { + private final ApplicationRepository repository; + private final ApplicationEntityMapper applicationMapper; + + public ApplicationJpaRepositoryAdapter(ApplicationRepository repository, + ApplicationEntityMapper applicationMapper) { + this.repository = repository; + this.applicationMapper = applicationMapper; + } + + @Override + public Optional getById(UUID applicationId) { + return repository.findById(applicationId) + .map(applicationMapper::mapToDomain); + } + + @Override + public List getAll() { + return repository.findAll() + .stream() + .map(applicationMapper::mapToDomain) + .collect(Collectors.toList()); + } + + @Override + public Application add(Application application) { + ApplicationEntity entityToSave = applicationMapper.mapToEntity(application); + ApplicationEntity savedApplication = repository.save(entityToSave); + return applicationMapper.mapToDomain(savedApplication); + } + + @Override + public void update(Application application) { + ApplicationEntity entityToSave = applicationMapper.mapToEntity(application); + repository.save(entityToSave); + } + + @Override + public void remove(UUID applicationId) { + repository.deleteById(applicationId); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/mapper/ApplicationEntityMapper.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/mapper/ApplicationEntityMapper.java new file mode 100644 index 0000000..9988eb3 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/mapper/ApplicationEntityMapper.java @@ -0,0 +1,29 @@ +package org.takiguchi.cerberus.cerberusapp.persistence.mapper; + +import org.springframework.stereotype.Component; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.persistence.model.ApplicationEntity; + +import static org.takiguchi.cerberus.cerberusapp.model.Application.anApplication; + +@Component +public class ApplicationEntityMapper { + + public Application mapToDomain(ApplicationEntity entity) { + return anApplication() + .withId(entity.getId()) + .withName(entity.getName()) + .withServiceName(entity.getServiceName()) + .withServiceType(entity.getServiceType()) + .build(); + } + + public ApplicationEntity mapToEntity(Application application) { + return new ApplicationEntity( + application.getId(), + application.getName(), + application.getServiceName(), + application.getServiceType() + ); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/model/ApplicationEntity.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/model/ApplicationEntity.java new file mode 100644 index 0000000..e0e6596 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/model/ApplicationEntity.java @@ -0,0 +1,63 @@ +package org.takiguchi.cerberus.cerberusapp.persistence.model; + +import com.fasterxml.jackson.annotation.JsonView; +import org.takiguchi.cerberus.cerberusapp.model.ServiceType; + +import javax.persistence.*; +import java.util.UUID; + +import static javax.persistence.EnumType.ORDINAL; + +@Entity +@Table(name = "application") +public class ApplicationEntity { + @Id + @GeneratedValue(generator = "system-uuid") + private UUID id; + private String name; + private String serviceName; + @Enumerated + private ServiceType serviceType; + + public ApplicationEntity() { + } + + public ApplicationEntity(UUID id, String name, String serviceName, ServiceType serviceType) { + this.id = id; + this.name = name; + this.serviceName = serviceName; + this.serviceType = serviceType; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public ServiceType getServiceType() { + return serviceType; + } + + public void setServiceType(ServiceType serviceType) { + this.serviceType = serviceType; + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/repository/ApplicationRepository.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/repository/ApplicationRepository.java new file mode 100644 index 0000000..222876b --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/persistence/repository/ApplicationRepository.java @@ -0,0 +1,12 @@ +package org.takiguchi.cerberus.cerberusapp.persistence.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.takiguchi.cerberus.cerberusapp.persistence.model.ApplicationEntity; + +import java.util.UUID; + +@Repository +public interface ApplicationRepository extends JpaRepository { + +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationRepositoryPort.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationRepositoryPort.java new file mode 100644 index 0000000..c9458cb --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationRepositoryPort.java @@ -0,0 +1,15 @@ +package org.takiguchi.cerberus.cerberusapp.service; + +import org.takiguchi.cerberus.cerberusapp.model.Application; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ApplicationRepositoryPort { + Optional getById(UUID applicationId); + List getAll(); + Application add(Application application); + void update(Application application); + void remove(UUID applicationId); +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationService.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationService.java new file mode 100644 index 0000000..4f98fa8 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationService.java @@ -0,0 +1,97 @@ +package org.takiguchi.cerberus.cerberusapp.service; + +import org.springframework.stereotype.Service; +import org.takiguchi.cerberus.cerberusapp.exception.NotFoundException; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.model.ServiceStatus; +import org.takiguchi.cerberus.cerberusapp.service.servicemanager.ServiceManagerProvider; +import org.takiguchi.cerberus.cerberusapp.service.validator.ApplicationValidator; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.takiguchi.cerberus.cerberusapp.model.Application.anApplication; + +@Service +public class ApplicationService { + private final ApplicationValidator applicationValidator; + private final ApplicationRepositoryPort applicationRepositoryPort; + private final ServiceManagerProvider serviceManagerProvider; + + public ApplicationService(ApplicationValidator applicationValidator, + ApplicationRepositoryPort applicationRepositoryPort, + ServiceManagerProvider serviceManagerProvider) { + this.applicationValidator = applicationValidator; + this.applicationRepositoryPort = applicationRepositoryPort; + this.serviceManagerProvider = serviceManagerProvider; + } + + public Optional getById(UUID applicationId) { + return applicationRepositoryPort.getById(applicationId); + } + + public List getAll() { + return applicationRepositoryPort.getAll(); + } + + public Application add(Application application) { + applicationValidator.validate(application); + return applicationRepositoryPort.add(application); + } + + public void update(Application application) { + applicationRepositoryPort.getById(application.getId()) + .map(existingApplication -> anApplication() + .withId(existingApplication.getId()) + .withName(application.getName()) + .withServiceName(application.getServiceName()) + .withServiceType(application.getServiceType()) + .build() + ) + .ifPresentOrElse(this::validateThenSave, NotFoundException::new); + } + + private void validateThenSave(Application updatedApplication) { + applicationValidator.validate(updatedApplication); + applicationRepositoryPort.update(updatedApplication); + } + + public void remove(UUID applicationId) { + applicationRepositoryPort.remove(applicationId); + } + + public ServiceStatus checkStatus(UUID applicationId) { + return applicationRepositoryPort.getById(applicationId) + .map(application -> serviceManagerProvider.getServiceManager(application) + .getStatus(application) + ).orElseThrow(NotFoundException::new); + } + + public void start(UUID applicationId) { + applicationRepositoryPort.getById(applicationId) + .ifPresentOrElse( + application -> serviceManagerProvider.getServiceManager(application) + .start(application), + NotFoundException::new + ); + } + + public void stop(UUID applicationId) { + applicationRepositoryPort.getById(applicationId) + .ifPresentOrElse( + application -> serviceManagerProvider.getServiceManager(application) + .stop(application), + NotFoundException::new + ); + } + + public void restart(UUID applicationId) { + applicationRepositoryPort.getById(applicationId) + .ifPresentOrElse( + application -> serviceManagerProvider.getServiceManager(application) + .restart(application), + NotFoundException::new + ); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/exception/ValidationException.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/exception/ValidationException.java new file mode 100644 index 0000000..d06e96d --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/exception/ValidationException.java @@ -0,0 +1,7 @@ +package org.takiguchi.cerberus.cerberusapp.service.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/DockerServiceManager.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/DockerServiceManager.java new file mode 100644 index 0000000..0eba9d2 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/DockerServiceManager.java @@ -0,0 +1,28 @@ +package org.takiguchi.cerberus.cerberusapp.service.servicemanager; + +import org.springframework.stereotype.Service; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.model.ServiceStatus; + +@Service +public class DockerServiceManager implements ServiceManager { + @Override + public ServiceStatus getStatus(Application application) { + throw new IllegalStateException("DockerServiceManager#getStatus not implemented."); + } + + @Override + public void start(Application application) { + throw new IllegalStateException("DockerServiceManager#start not implemented."); + } + + @Override + public void stop(Application application) { + throw new IllegalStateException("DockerServiceManager#stop not implemented."); + } + + @Override + public void restart(Application application) { + throw new IllegalStateException("DockerServiceManager#restart not implemented."); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/FakeServiceManager.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/FakeServiceManager.java new file mode 100644 index 0000000..6c2ae4c --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/FakeServiceManager.java @@ -0,0 +1,39 @@ +package org.takiguchi.cerberus.cerberusapp.service.servicemanager; + +import org.springframework.stereotype.Service; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.model.ServiceStatus; + +import java.util.*; + +import static org.takiguchi.cerberus.cerberusapp.model.ServiceStatus.*; + +@Service +public class FakeServiceManager implements ServiceManager { + private final Map handledApplications = new HashMap<>(); + + @Override + public ServiceStatus getStatus(Application application) { + return handledApplications.entrySet() + .stream() + .filter(entry -> entry.getKey().equals(application.getId())) + .findFirst() + .map(Map.Entry::getValue) + .orElse(STOPPED); + } + + @Override + public void start(Application application) { + handledApplications.put(application.getId(), STARTED); + } + + @Override + public void stop(Application application) { + handledApplications.put(application.getId(), STOPPED); + } + + @Override + public void restart(Application application) { + start(application); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/LinuxServiceManager.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/LinuxServiceManager.java new file mode 100644 index 0000000..f3c2b68 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/LinuxServiceManager.java @@ -0,0 +1,28 @@ +package org.takiguchi.cerberus.cerberusapp.service.servicemanager; + +import org.springframework.stereotype.Service; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.model.ServiceStatus; + +@Service +public class LinuxServiceManager implements ServiceManager { + @Override + public ServiceStatus getStatus(Application application) { + throw new IllegalStateException("LinuxServiceManager#getStatus not implemented."); + } + + @Override + public void start(Application application) { + throw new IllegalStateException("LinuxServiceManager#start not implemented."); + } + + @Override + public void stop(Application application) { + throw new IllegalStateException("LinuxServiceManager#stop not implemented."); + } + + @Override + public void restart(Application application) { + throw new IllegalStateException("LinuxServiceManager#restart not implemented."); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManager.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManager.java new file mode 100644 index 0000000..d82611c --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManager.java @@ -0,0 +1,11 @@ +package org.takiguchi.cerberus.cerberusapp.service.servicemanager; + +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.model.ServiceStatus; + +public interface ServiceManager { + ServiceStatus getStatus(Application application); + void start(Application application); + void stop(Application application); + void restart(Application application); +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManagerProvider.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManagerProvider.java new file mode 100644 index 0000000..d74e7c7 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/servicemanager/ServiceManagerProvider.java @@ -0,0 +1,42 @@ +package org.takiguchi.cerberus.cerberusapp.service.servicemanager; + +import org.springframework.stereotype.Component; +import org.takiguchi.cerberus.cerberusapp.exception.InternalServerErrorException; +import org.takiguchi.cerberus.cerberusapp.model.Application; + +import java.util.List; + +@Component +public class ServiceManagerProvider { + private final List serviceManagers; + + public ServiceManagerProvider(List serviceManagers) { + this.serviceManagers = serviceManagers; + } + + public ServiceManager getServiceManager(Application application) { + ServiceManager result; + switch (application.getServiceType()) { + case FAKE: + result = getServiceByClass(FakeServiceManager.class); + break; + case SERVICE: + result = getServiceByClass(LinuxServiceManager.class); + break; + case DOCKER: + result = getServiceByClass(DockerServiceManager.class); + break; + default: + throw new InternalServerErrorException(""); + } + + return result; + } + + private ServiceManager getServiceByClass(Class serviceManagerClass) { + return serviceManagers.stream() + .filter(serviceManagerClass::isInstance) + .findFirst() + .orElseThrow(InternalServerErrorException::new); + } +} diff --git a/src/main/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidator.java b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidator.java new file mode 100644 index 0000000..170dae3 --- /dev/null +++ b/src/main/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidator.java @@ -0,0 +1,24 @@ +package org.takiguchi.cerberus.cerberusapp.service.validator; + +import org.springframework.stereotype.Component; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.service.exception.ValidationException; + +@Component +public class ApplicationValidator { + + public void validate(Application application) { + if (application == null) { + throw new ValidationException("Application is null."); + } + if (application.getName() == null) { + throw new ValidationException("Application name is mandatory."); + } + if (application.getServiceName() == null) { + throw new ValidationException("Application service name is mandatory."); + } + if (application.getServiceType() == null) { + throw new ValidationException("Application service type is mandatory."); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..24e6042 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,15 @@ +server: + error: + whitelabel: + enabled: false # Disable html error responses. + include-stacktrace: never + +spring: + # ------------------------------------------------- + # Database configuration + # ------------------------------------------------- + datasource: + driverClassName: org.postgresql.Driver + url: jdbc:postgresql://localhost:50001/cerberus + username: h23 + password: P@ssword1 \ No newline at end of file diff --git a/src/main/sql/ddl.sql b/src/main/sql/ddl.sql new file mode 100644 index 0000000..709f32d --- /dev/null +++ b/src/main/sql/ddl.sql @@ -0,0 +1,9 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS application ( + id UUID DEFAULT uuid_generate_v4() NOT NULL, + name VARCHAR NOT NULL, + service_name VARCHAR NOT NULL, + service_type SMALLINT NOT NULL, + CONSTRAINT application_pk PRIMARY KEY (id) +); \ No newline at end of file diff --git a/src/test/java/org/takiguchi/cerberus/cerberusapp/CerberusApplicationTests.java b/src/test/java/org/takiguchi/cerberus/cerberusapp/CerberusApplicationTests.java new file mode 100644 index 0000000..f60cbaa --- /dev/null +++ b/src/test/java/org/takiguchi/cerberus/cerberusapp/CerberusApplicationTests.java @@ -0,0 +1,13 @@ +package org.takiguchi.cerberus.cerberusapp; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CerberusApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationServiceTest.java b/src/test/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationServiceTest.java new file mode 100644 index 0000000..4376ad0 --- /dev/null +++ b/src/test/java/org/takiguchi/cerberus/cerberusapp/service/ApplicationServiceTest.java @@ -0,0 +1,127 @@ +package org.takiguchi.cerberus.cerberusapp.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.service.servicemanager.ServiceManagerProvider; +import org.takiguchi.cerberus.cerberusapp.service.validator.ApplicationValidator; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.takiguchi.cerberus.cerberusapp.model.Application.anApplication; + +@ExtendWith(MockitoExtension.class) +class ApplicationServiceTest { + private ApplicationService service; + @Mock + private ApplicationRepositoryPort applicationRepositoryPort; + @Mock + private ApplicationValidator applicationValidator; + @Mock + private ServiceManagerProvider serviceManagerProvider; + @Captor + private ArgumentCaptor applicationCaptor; + + @BeforeEach + void setUp() { + service = new ApplicationService(applicationValidator, applicationRepositoryPort, serviceManagerProvider); + } + + @Test + void getById_should_return_the_application() { + // given + UUID applicationId = UUID.randomUUID(); + + Application application = anApplication().build(); + given(applicationRepositoryPort.getById(applicationId)).willReturn(Optional.of(application)); + + // when + Optional result = service.getById(applicationId); + + // then + assertThat(result).contains(application); + } + + @Test + void getAll_should_return_all_applications() { + // given + Application application1 = anApplication().build(); + Application application2 = anApplication().build(); + given(applicationRepositoryPort.getAll()).willReturn(List.of(application1, application2)); + + // when + List result = service.getAll(); + + // then + assertThat(result).contains(application1, application2); + } + + @Test + void add_should_validate_the_application_then_add_it() { + // given + Application application = anApplication().build(); + + InOrder inOrder = inOrder(applicationValidator, applicationRepositoryPort); + + Application addedApplication = anApplication().build(); + given(applicationRepositoryPort.add(application)).willReturn(addedApplication); + + // when + Application result = service.add(application); + + // then + assertThat(result).isEqualTo(addedApplication); + inOrder.verify(applicationValidator).validate(application); + inOrder.verify(applicationRepositoryPort).add(application); + } + + @Test + void update_should_retrieve_application_then_edit_some_fields_then_validate_it__then_update_it() { + // given + UUID newId = UUID.randomUUID(); + String newName = "new name"; + String newServiceName = "new service name"; + Application application = anApplication() + .withId(newId) + .withName(newName) + .withServiceName(newServiceName) + .build(); + + InOrder inOrder = inOrder(applicationRepositoryPort, applicationValidator, applicationRepositoryPort); + + UUID oldId = UUID.randomUUID(); + Application existingApplication = anApplication() + .withId(oldId) + .build(); + given(applicationRepositoryPort.getById(newId)).willReturn(Optional.of(existingApplication)); + + // when + service.update(application); + + // then + inOrder.verify(applicationRepositoryPort).getById(newId); + inOrder.verify(applicationValidator).validate(applicationCaptor.capture()); + Application updatedApplication = applicationCaptor.getValue(); + inOrder.verify(applicationRepositoryPort).update(updatedApplication); + + assertThat(updatedApplication).isNotNull() + .extracting( + Application::getId, + Application::getName, + Application::getServiceName + ) + .contains( + oldId, + newName, + newServiceName + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidatorTest.java b/src/test/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidatorTest.java new file mode 100644 index 0000000..5686b9f --- /dev/null +++ b/src/test/java/org/takiguchi/cerberus/cerberusapp/service/validator/ApplicationValidatorTest.java @@ -0,0 +1,96 @@ +package org.takiguchi.cerberus.cerberusapp.service.validator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.takiguchi.cerberus.cerberusapp.model.Application; +import org.takiguchi.cerberus.cerberusapp.model.ServiceType; +import org.takiguchi.cerberus.cerberusapp.service.exception.ValidationException; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.takiguchi.cerberus.cerberusapp.model.Application.anApplication; +import static org.takiguchi.cerberus.cerberusapp.model.ServiceType.FAKE; + +class ApplicationValidatorTest { + private ApplicationValidator validator; + + @BeforeEach + void setUp() { + validator = new ApplicationValidator(); + } + + @ParameterizedTest + @MethodSource("should_throw_a_validation_exception_data_provider") + void should_throw_a_validation_exception(Application application, String errorMessage) { + // when + ValidationException exception = catchThrowableOfType(() -> validator.validate(application), ValidationException.class); + + // then + assertThat(exception).isNotNull() + .hasMessage(errorMessage); + } + + private static Stream should_throw_a_validation_exception_data_provider() { + return Stream.of( + Arguments.of( + null, + "Application is null." + ), + Arguments.of( + anApplication().build(), + "Application name is mandatory." + ), + Arguments.of( + anApplication() + .withId(UUID.randomUUID()) + .build(), + "Application name is mandatory." + ), + Arguments.of( + anApplication() + .withName("name") + .build(), + "Application service name is mandatory." + ), + Arguments.of( + anApplication() + .withServiceName("serviceName") + .build(), + "Application name is mandatory." + ), + Arguments.of( + anApplication() + .withServiceType(FAKE) + .build(), + "Application name is mandatory." + ), + Arguments.of( + anApplication() + .withId(UUID.randomUUID()) + .withName("name") + .build(), + "Application service name is mandatory." + ), + Arguments.of( + anApplication() + .withId(UUID.randomUUID()) + .withServiceName("serviceName") + .build(), + "Application name is mandatory." + ), + Arguments.of( + anApplication() + .withId(UUID.randomUUID()) + .withName("name") + .withServiceName("serviceName") + .build(), + "Application service type is mandatory." + ) + ); + } +} \ No newline at end of file