From b5d9c56681fc575eccdf66727547ad18dbbc1493 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Thu, 9 Jan 2025 14:25:09 +0100 Subject: [PATCH] added basic functionality --- .gitignore | 2 + Test.http | 2 + pom.xml | 9 +++ .../dirigent/DirigentApplication.java | 4 +- .../dirigent/deployments/CachingConfig.java | 33 +++++++++ .../DeploymentsConfigurationProvider.java | 49 +++++++++++++ .../deployments/DeploymentsController.java | 37 ++++++++++ .../deployments/DeploymentsService.java | 69 +++++++++++++++++++ .../dirigent/deployments/GitService.java | 61 ++++++++++++++++ .../deployments/models/Deployment.java | 10 +++ .../models/DeploynentConfiguration.java | 7 ++ src/main/resources/application.properties | 3 + 12 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 Test.http create mode 100644 src/main/java/org/davidbohl/dirigent/deployments/CachingConfig.java create mode 100644 src/main/java/org/davidbohl/dirigent/deployments/DeploymentsConfigurationProvider.java create mode 100644 src/main/java/org/davidbohl/dirigent/deployments/DeploymentsController.java create mode 100644 src/main/java/org/davidbohl/dirigent/deployments/DeploymentsService.java create mode 100644 src/main/java/org/davidbohl/dirigent/deployments/GitService.java create mode 100644 src/main/java/org/davidbohl/dirigent/deployments/models/Deployment.java create mode 100644 src/main/java/org/davidbohl/dirigent/deployments/models/DeploynentConfiguration.java diff --git a/.gitignore b/.gitignore index 549e00a..1e5955d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ build/ ### VS Code ### .vscode/ +/deployments/ +/config/ diff --git a/Test.http b/Test.http new file mode 100644 index 0000000..fa95ebd --- /dev/null +++ b/Test.http @@ -0,0 +1,2 @@ + +POST http://localhost:8080/deployments/all/start \ No newline at end of file diff --git a/pom.xml b/pom.xml index 87844a0..8b855e7 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,15 @@ spring-boot-starter-test test + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.apache.httpcomponents + httpclient + 4.5.14 + diff --git a/src/main/java/org/davidbohl/dirigent/DirigentApplication.java b/src/main/java/org/davidbohl/dirigent/DirigentApplication.java index 29d06f2..4e7cbb6 100644 --- a/src/main/java/org/davidbohl/dirigent/DirigentApplication.java +++ b/src/main/java/org/davidbohl/dirigent/DirigentApplication.java @@ -7,6 +7,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ConfigurableApplicationContext; +import java.io.IOException; + @SpringBootApplication @EnableConfigurationProperties @@ -14,7 +16,7 @@ public class DirigentApplication { static Logger logger = LoggerFactory.getLogger(DirigentApplication.class); - public static void main(String[] args) { + public static void main(String[] args) throws IOException, InterruptedException { ConfigurableApplicationContext context = SpringApplication.run(DirigentApplication.class, args); String composeCommand = context.getEnvironment().getProperty("dirigent.compose.command"); if(!isComposeInstalled(composeCommand)) { diff --git a/src/main/java/org/davidbohl/dirigent/deployments/CachingConfig.java b/src/main/java/org/davidbohl/dirigent/deployments/CachingConfig.java new file mode 100644 index 0000000..667157a --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/CachingConfig.java @@ -0,0 +1,33 @@ +package org.davidbohl.dirigent.deployments; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + + +@Configuration +@EnableCaching +@EnableScheduling +public class CachingConfig { + + private final Logger logger = LoggerFactory.getLogger(CachingConfig.class); + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("deployments"); + } + + @CacheEvict(value = "deployments", allEntries = true) + @Scheduled(fixedRateString = "${dirigent.deployments.cache.evict.interval}") + public void emptyDeploymentsCache() { + logger.info("emptying deployments cache"); + } + +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsConfigurationProvider.java b/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsConfigurationProvider.java new file mode 100644 index 0000000..3f1e3dd --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsConfigurationProvider.java @@ -0,0 +1,49 @@ +package org.davidbohl.dirigent.deployments; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.davidbohl.dirigent.deployments.models.DeploynentConfiguration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +@Service +public class DeploymentsConfigurationProvider implements CacheManagerCustomizer { + + private final GitService gitService; + + @Value("${dirigent.deployments.git.url}") + private String gitUrl; + + public DeploymentsConfigurationProvider(GitService gitService) { + this.gitService = gitService; + } + + @Cacheable("deployments") + public DeploynentConfiguration getConfiguration() throws IOException, InterruptedException { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + gitService.cloneOrPull(gitUrl, "config"); + + File configFile = new File("config/deployments.yml"); + + try { + return objectMapper.readValue(configFile, DeploynentConfiguration.class); + } catch (IOException e) { + e.printStackTrace(); + throw e; + } + } + + @Override + public void customize(ConcurrentMapCacheManager cacheManager) { + cacheManager.setCacheNames(Arrays.asList("deployments")); + } +} + diff --git a/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsController.java b/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsController.java new file mode 100644 index 0000000..d5b7918 --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsController.java @@ -0,0 +1,37 @@ +package org.davidbohl.dirigent.deployments; + +import org.davidbohl.dirigent.deployments.models.DeploynentConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +@RestController() +@RequestMapping(path = "/deployments") +public class DeploymentsController { + private final DeploymentsConfigurationProvider deploymentsConfigurationProvider; + private final DeploymentsService deploymentsService; + private static final Logger logger = LoggerFactory.getLogger(DeploymentsController.class); + + public DeploymentsController(DeploymentsConfigurationProvider deploymentsConfigurationProvider, DeploymentsService deploymentsService) { + this.deploymentsConfigurationProvider = deploymentsConfigurationProvider; + this.deploymentsService = deploymentsService; + } + + @GetMapping + public DeploynentConfiguration getDeployments() throws IOException, InterruptedException { + logger.info("Getting deployments"); + return deploymentsConfigurationProvider.getConfiguration(); + } + + @PostMapping("/all/start") + public void startAllDeployments() { + logger.info("Starting all deployments"); + deploymentsService.startAllDeployments(); + logger.info("Deployments started"); + } +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsService.java b/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsService.java new file mode 100644 index 0000000..be010be --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/DeploymentsService.java @@ -0,0 +1,69 @@ +package org.davidbohl.dirigent.deployments; + +import org.davidbohl.dirigent.deployments.models.Deployment; +import org.davidbohl.dirigent.deployments.models.DeploynentConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class DeploymentsService { + + private final DeploymentsConfigurationProvider deploymentsConfigurationProvider; + private final GitService gitService; + private final Logger logger = LoggerFactory.getLogger(DeploymentsService.class); + + @Value("${dirigent.compose.command}") + private String composeCommand; + + public DeploymentsService( + @Autowired DeploymentsConfigurationProvider deploymentsConfigurationProvider, + @Autowired GitService gitService) { + this.deploymentsConfigurationProvider = deploymentsConfigurationProvider; + this.gitService = gitService; + } + + public void startAllDeployments() { + + new File("deployments").mkdirs(); + + try { + DeploynentConfiguration deploymentsConfiguration = deploymentsConfigurationProvider.getConfiguration(); + + Map> deploymentsByOrder = deploymentsConfiguration.deployments().stream() + .sorted(Comparator.comparingInt(Deployment::order)) + .collect(Collectors.groupingBy(Deployment::order)); + + for (Deployment deployment : deploymentsConfiguration.deployments()) { + deploy(deployment); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + private void deploy(Deployment deployment) throws IOException, InterruptedException { + logger.info("Deploying {}", deployment.name()); + + File deploymentDir = new File("deployments/" + deployment.name()); + + gitService.cloneOrPull(deployment.source(), deploymentDir.getAbsolutePath()); + + new ProcessBuilder(composeCommand, "up", "-d", "--remove-orphans") + .directory(deploymentDir) + .start().waitFor(); + + logger.info("Deployment {} started", deployment.name()); + } + +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/GitService.java b/src/main/java/org/davidbohl/dirigent/deployments/GitService.java new file mode 100644 index 0000000..846fed8 --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/GitService.java @@ -0,0 +1,61 @@ +package org.davidbohl.dirigent.deployments; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +@Service +public class GitService { + + private final Logger logger = LoggerFactory.getLogger(GitService.class); + + @Value("${dirigent.git.authToken}") + private String authToken; + + public void cloneOrPull(String source, String destination) throws IOException, InterruptedException { + + logger.info("Cloning or pulling git repository '{}' to dir '{}'", source, destination); + + File destinationDir = new File(destination); + + if (destinationDir.exists()) { + logger.debug("Local Repo exists. Pulling latest changes."); + new ProcessBuilder("git", "fetch", "--all") + .directory(destinationDir).start().waitFor(); + new ProcessBuilder("git", "reset", "--hard", "HEAD") + .directory(destinationDir).start().waitFor(); + new ProcessBuilder("git", "pull") + .directory(destinationDir).start().waitFor(); + } else { + logger.debug("Local Repo does not exist. Cloning repository."); + deleteDirectory(destinationDir); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(source); + + if (uriComponentsBuilder.build().getUserInfo() == null || Objects.requireNonNull(uriComponentsBuilder.build().getUserInfo()).isEmpty()) { + uriComponentsBuilder = uriComponentsBuilder.userInfo(authToken); + } + + + new ProcessBuilder("git", "clone", uriComponentsBuilder.toUriString(), destination) + .start().waitFor(); + } + } + + boolean deleteDirectory(File directoryToBeDeleted) { + File[] allContents = directoryToBeDeleted.listFiles(); + if (allContents != null) { + for (File file : allContents) { + deleteDirectory(file); + } + } + return directoryToBeDeleted.delete(); + } +} + + diff --git a/src/main/java/org/davidbohl/dirigent/deployments/models/Deployment.java b/src/main/java/org/davidbohl/dirigent/deployments/models/Deployment.java new file mode 100644 index 0000000..15bbc1b --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/models/Deployment.java @@ -0,0 +1,10 @@ +package org.davidbohl.dirigent.deployments.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public record Deployment ( + String name, // ToDo: Validate name is not "all", cause its used in controller + String source, + @JsonIgnore int order, + @JsonIgnore boolean restartOnDeployment) { +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/models/DeploynentConfiguration.java b/src/main/java/org/davidbohl/dirigent/deployments/models/DeploynentConfiguration.java new file mode 100644 index 0000000..5dc4a3d --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/models/DeploynentConfiguration.java @@ -0,0 +1,7 @@ +package org.davidbohl.dirigent.deployments.models; + +import java.util.List; + +public record DeploynentConfiguration(List deployments) { +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 87f59fa..7165885 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,5 @@ spring.application.name=dirigent dirigent.compose.command=podman-compose +dirigent.deployments.git.url= +dirigent.git.authToken= +dirigent.deployments.cache.evict.interval=60000 \ No newline at end of file