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