diff --git a/backend/pom.xml b/backend/pom.xml
index 4303b9a..825a2f8 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -85,6 +85,11 @@
docker-java-transport-httpclient5
3.7.0
+
+ org.apache.commons
+ commons-exec
+ 1.6.0
+
diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/config/DeploymentsConfigurationProvider.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/config/DeploymentsConfigurationProvider.java
index 8726cef..9952f13 100644
--- a/backend/src/main/java/org/davidbohl/dirigent/deployments/config/DeploymentsConfigurationProvider.java
+++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/config/DeploymentsConfigurationProvider.java
@@ -1,13 +1,14 @@
package org.davidbohl.dirigent.deployments.config;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import java.io.File;
+
import org.davidbohl.dirigent.deployments.models.DeploynentConfiguration;
-import org.davidbohl.dirigent.utility.GitService;
+import org.davidbohl.dirigent.utility.git.GitService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
-import java.io.File;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
@Service
public class DeploymentsConfigurationProvider {
diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java
index d149d2f..63d4e59 100644
--- a/backend/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java
+++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java
@@ -1,13 +1,35 @@
package org.davidbohl.dirigent.deployments.management;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
import org.davidbohl.dirigent.deployments.config.DeploymentsConfigurationProvider;
-import org.davidbohl.dirigent.deployments.events.*;
+import org.davidbohl.dirigent.deployments.events.AllDeploymentsStartRequestedEvent;
+import org.davidbohl.dirigent.deployments.events.DeploymentStateEvent;
+import org.davidbohl.dirigent.deployments.events.MultipleNamedDeploymentsStartRequestedEvent;
+import org.davidbohl.dirigent.deployments.events.NamedDeploymentStartRequestedEvent;
+import org.davidbohl.dirigent.deployments.events.NamedDeploymentStopRequestedEvent;
+import org.davidbohl.dirigent.deployments.events.RecreateAllDeploymentStatesEvent;
+import org.davidbohl.dirigent.deployments.events.SourceDeploymentStartRequestedEvent;
import org.davidbohl.dirigent.deployments.models.Deployment;
import org.davidbohl.dirigent.deployments.models.DeploynentConfiguration;
import org.davidbohl.dirigent.deployments.state.DeploymentState;
import org.davidbohl.dirigent.deployments.state.DeploymentStatePersistingService;
import org.davidbohl.dirigent.sercrets.SecretService;
-import org.davidbohl.dirigent.utility.GitService;
+import org.davidbohl.dirigent.utility.git.GitService;
+import org.davidbohl.dirigent.utility.process.ProcessResult;
+import org.davidbohl.dirigent.utility.process.ProcessRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@@ -15,17 +37,10 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.*;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
@Service
+@RequiredArgsConstructor()
public class DeploymentsService {
public static final String DEPLOYMENTS_DIR_NAME = "deployments";
@@ -35,23 +50,11 @@ public class DeploymentsService {
private final ApplicationEventPublisher applicationEventPublisher;
private final DeploymentStatePersistingService deploymentStatePersistingService;
private final SecretService secretService;
+ private final ProcessRunner processRunner;
@Value("${dirigent.compose.command}")
private String composeCommand;
- public DeploymentsService(
- DeploymentsConfigurationProvider deploymentsConfigurationProvider,
- GitService gitService,
- ApplicationEventPublisher applicationEventPublisher,
- DeploymentStatePersistingService deploymentStatePersistingService,
- SecretService secretService) {
- this.deploymentsConfigurationProvider = deploymentsConfigurationProvider;
- this.gitService = gitService;
- this.applicationEventPublisher = applicationEventPublisher;
- this.deploymentStatePersistingService = deploymentStatePersistingService;
- this.secretService = secretService;
- }
-
@EventListener(AllDeploymentsStartRequestedEvent.class)
public void onAllDeploymentsStartRequested(AllDeploymentsStartRequestedEvent event) {
@@ -188,26 +191,13 @@ public class DeploymentsService {
}
logger.info("Upping Compose for {}", deployment.name());
- ProcessBuilder builder = new ProcessBuilder(commandArgs)
- .directory(deploymentDir);
- builder.environment().putAll(secretService.getAllSecretsAsEnvironmentVariableMapByDeployment(deployment.name()));
-
- Process process = builder
- .start();
-
- BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
- StringBuilder errorOutput = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- errorOutput.append(line).append("\n");
- }
-
- int exitCode = process.waitFor();
- reader.close();
+ ProcessResult composeUp = processRunner.executeCommand(commandArgs,
+ deploymentDir,
+ secretService.getAllSecretsAsEnvironmentVariableMapByDeployment(deployment.name()));
- if ((exitCode != 0)) {
- applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.FAILED, errorOutput.toString()));
+ if ((composeUp.exitCode() != 0)) {
+ applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.FAILED, composeUp.stderr()));
return;
}
} catch (IOException | InterruptedException e) {
@@ -256,10 +246,8 @@ public class DeploymentsService {
List commandArgs = new ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList());
commandArgs.add("down");
- new ProcessBuilder(commandArgs)
- .directory(new File(DEPLOYMENTS_DIR_NAME + "/" + deploymentName))
- .start()
- .waitFor();
+
+ processRunner.executeCommand(commandArgs, new File(DEPLOYMENTS_DIR_NAME + "/" + deploymentName));
applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deploymentName, DeploymentState.State.STOPPED, "Deployment '%s' stopped".formatted(deploymentName)));
}
diff --git a/backend/src/main/java/org/davidbohl/dirigent/ui/SpaController.java b/backend/src/main/java/org/davidbohl/dirigent/ui/SpaController.java
deleted file mode 100644
index 703b888..0000000
--- a/backend/src/main/java/org/davidbohl/dirigent/ui/SpaController.java
+++ /dev/null
@@ -1,23 +0,0 @@
-//package org.davidbohl.dirigent.ui;
-//
-//import jakarta.servlet.http.HttpServletRequest;
-//import org.springframework.stereotype.Controller;
-//import org.springframework.web.bind.annotation.RequestMapping;
-//import org.springframework.web.servlet.HandlerMapping;
-//import org.springframework.web.util.UrlPathHelper;
-//
-//@Controller
-//public class SpaController {
-//
-// @RequestMapping(path = { "/ui/", "/ui/**"})
-// public String forward(HttpServletRequest request) {
-// UrlPathHelper pathHelper = new UrlPathHelper();
-// String path = pathHelper.getPathWithinApplication(request);
-//
-// if (path.contains(".")) {
-// return null;
-// }
-//
-// return "forward:/ui/index.html";
-// }
-//}
\ No newline at end of file
diff --git a/backend/src/main/java/org/davidbohl/dirigent/ui/WebConfig.java b/backend/src/main/java/org/davidbohl/dirigent/ui/WebConfig.java
deleted file mode 100644
index e81ff64..0000000
--- a/backend/src/main/java/org/davidbohl/dirigent/ui/WebConfig.java
+++ /dev/null
@@ -1,16 +0,0 @@
-//package org.davidbohl.dirigent.ui;
-//
-//import org.springframework.context.annotation.Configuration;
-//import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
-//import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
-//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-//
-//@Configuration
-//public class WebConfig implements WebMvcConfigurer {
-// @Override
-// public void addViewControllers(ViewControllerRegistry registry) {
-// // Forward only if it's NOT a request for a resource with a file extension (like .js, .css, .png)
-// registry.addViewController("/ui/{path:[^\\.]*}/**") // Match "/ui/something" or "/ui/something/else", excluding URLs with dots.
-// .setViewName("forward:/ui/index.html");
-// }
-//}
diff --git a/backend/src/main/java/org/davidbohl/dirigent/utility/GitService.java b/backend/src/main/java/org/davidbohl/dirigent/utility/git/GitService.java
similarity index 58%
rename from backend/src/main/java/org/davidbohl/dirigent/utility/GitService.java
rename to backend/src/main/java/org/davidbohl/dirigent/utility/git/GitService.java
index 2109e8f..80bbaeb 100644
--- a/backend/src/main/java/org/davidbohl/dirigent/utility/GitService.java
+++ b/backend/src/main/java/org/davidbohl/dirigent/utility/git/GitService.java
@@ -1,19 +1,25 @@
-package org.davidbohl.dirigent.utility;
+package org.davidbohl.dirigent.utility.git;
+import org.davidbohl.dirigent.utility.process.ProcessResult;
+import org.davidbohl.dirigent.utility.process.ProcessRunner;
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 lombok.RequiredArgsConstructor;
+
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
+import java.util.List;
import java.util.Objects;
@Service
+@RequiredArgsConstructor
public class GitService {
private final Logger logger = LoggerFactory.getLogger(GitService.class);
@@ -21,6 +27,8 @@ public class GitService {
@Value("${dirigent.git.authToken}")
private String authToken;
+ private final ProcessRunner processRunner;
+
public boolean updateRepo(String repoUrl, String targetDir, String rev) throws IOException, InterruptedException {
logger.info("Cloning or pulling git repository '{}' to dir '{}' @ rev '{}'", repoUrl, targetDir, rev);
@@ -51,14 +59,12 @@ public class GitService {
String currentHeadRev = getHeadRev(destinationDir);
- new ProcessBuilder("git", "reset", "--hard", "HEAD")
- .directory(destinationDir).start().waitFor();
- new ProcessBuilder("git", "fetch", "--all")
- .directory(destinationDir).start().waitFor();
- new ProcessBuilder("git", "checkout", rev)
- .directory(destinationDir).start().waitFor();
- new ProcessBuilder("git", "pull")
- .directory(destinationDir).start().waitFor();
+ processRunner.executeCommand(List.of("git", "reset", "--hard", "HEAD"), destinationDir);
+ processRunner.executeCommand(List.of("git", "fetch", "--all"), destinationDir);
+ processRunner.executeCommand(List.of("git", "checkout", rev), destinationDir);
+ processRunner.executeCommand(List.of("git", "pull"), destinationDir);
+
+
String newHeadRev = getHeadRev(destinationDir);
changed = !currentHeadRev.equals(newHeadRev);
@@ -66,18 +72,11 @@ public class GitService {
changed = true;
logger.debug("Local Repo does not exist. Cloning repository.");
ensureFileOrDirectoryIsDeletedRecursive(destinationDir);
- Process process = new ProcessBuilder("git", "clone", remoteGitUri, targetDir)
- .start();
- int exitCode = process.waitFor();
- if (exitCode != 0) {
- BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
- String line;
- StringBuilder stringBuilder = new StringBuilder();
- while ((line = reader.readLine()) != null) {
- stringBuilder.append(line).append(System.lineSeparator());
- }
- throw new IOException("Git clone failed with exit code " + exitCode + ": " + stringBuilder);
+ ProcessResult gitClone = processRunner.executeCommand(List.of("git", "clone", remoteGitUri, targetDir));
+
+ if (gitClone.exitCode() != 0) {
+ throw new IOException("Git clone failed with exit code " + gitClone.exitCode() + ": " + gitClone.stderr());
}
}
@@ -95,30 +94,15 @@ public class GitService {
return uriComponentsBuilder.toUriString();
}
- private static String getCurrentGitRemoteUrl(File destinationDir) throws IOException, InterruptedException {
- Process process = new ProcessBuilder("git", "config", "--get", "remote.origin.url")
- .directory(destinationDir).start();
- return getStdOutFromProcess(process).trim();
+ private String getCurrentGitRemoteUrl(File destinationDir) throws IOException, InterruptedException {
+
+ return processRunner.executeCommand(List.of("git", "config", "--get", "remote.origin.url"), destinationDir).stdout().trim();
}
- private static String getHeadRev(File destinationDir) throws IOException, InterruptedException {
- Process process = new ProcessBuilder("git", "rev-parse", "HEAD")
- .directory(destinationDir).start();
- return getStdOutFromProcess(process);
+ private String getHeadRev(File destinationDir) throws IOException, InterruptedException {
+ return processRunner.executeCommand(List.of("git", "rev-parse", "HEAD"), destinationDir).stdout().trim();
}
-
- private static String getStdOutFromProcess(Process process) throws IOException, InterruptedException {
- BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
- String line;
- StringBuilder stringBuilder = new StringBuilder();
- while ((line = reader.readLine()) != null) {
- stringBuilder.append(line);
- }
- String currentRev = stringBuilder.toString();
- process.waitFor();
- return currentRev;
- }
-
+
private static void ensureFileOrDirectoryIsDeletedRecursive(File directoryOrFileToBeDeleted) {
File[] allContents = directoryOrFileToBeDeleted.listFiles();
if (allContents != null) {
diff --git a/backend/src/main/java/org/davidbohl/dirigent/utility/process/ProcessResult.java b/backend/src/main/java/org/davidbohl/dirigent/utility/process/ProcessResult.java
new file mode 100644
index 0000000..09a5d6d
--- /dev/null
+++ b/backend/src/main/java/org/davidbohl/dirigent/utility/process/ProcessResult.java
@@ -0,0 +1,3 @@
+package org.davidbohl.dirigent.utility.process;
+
+public record ProcessResult(int exitCode, String stdout, String stderr) {}
\ No newline at end of file
diff --git a/backend/src/main/java/org/davidbohl/dirigent/utility/process/ProcessRunner.java b/backend/src/main/java/org/davidbohl/dirigent/utility/process/ProcessRunner.java
new file mode 100644
index 0000000..28ba526
--- /dev/null
+++ b/backend/src/main/java/org/davidbohl/dirigent/utility/process/ProcessRunner.java
@@ -0,0 +1,100 @@
+package org.davidbohl.dirigent.utility.process;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+import org.apache.commons.exec.ExecuteWatchdog;
+import org.apache.commons.exec.Executor;
+import org.apache.commons.exec.PumpStreamHandler;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ProcessRunner {
+
+ public ProcessResult executeCommand(List commandParts, long timeoutMs, Map env)
+ throws IOException {
+ return executeInternal(commandParts, new File(System.getProperty("user.dir")), timeoutMs, env);
+ }
+
+ public ProcessResult executeCommand(List commandParts)
+ throws IOException {
+ return executeInternal(commandParts, new File(System.getProperty("user.dir")), 0, Map.of());
+ }
+
+ public ProcessResult executeCommand(List commandParts, File workingDirectory)
+ throws IOException {
+ return executeInternal(commandParts, workingDirectory, 0, Map.of());
+ }
+
+ public ProcessResult executeCommand(List commandParts, File workingDirectory, Map env)
+ throws IOException {
+ return executeInternal(commandParts, workingDirectory, 0, env);
+ }
+
+ private ProcessResult executeInternal(List commandParts, File workingDirectory, long timeoutMs, Map env)
+ throws IOException {
+
+ Map finalEnv = System.getenv();
+
+ if(env != null && env.size() > 0)
+ finalEnv.putAll(env);
+
+ CommandLine command = new CommandLine(commandParts.get(0));
+ for (int i = 1; i < commandParts.size(); i++) {
+ command.addArgument(commandParts.get(i));
+ }
+
+ ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+ PumpStreamHandler streamHandler = new PumpStreamHandler(stdout, stderr);
+
+ Executor executor = DefaultExecutor.builder().get();
+ executor.setExitValue(0);
+ executor.setStreamHandler(streamHandler);
+ executor.setWorkingDirectory(workingDirectory);
+
+ ExecuteWatchdog watchdog = null;
+ if (timeoutMs > 0) {
+ watchdog = ExecuteWatchdog.builder()
+ .setTimeout(Duration.ofMillis(timeoutMs))
+ .get();
+ executor.setWatchdog(watchdog);
+ }
+
+ int exitCode = -1;
+ try {
+ exitCode = executor.execute(command, finalEnv);
+ } catch (ExecuteException e) {
+ exitCode = e.getExitValue();
+ } catch (InterruptedIOException e) {
+ throw e;
+ } finally {
+
+ streamHandler.stop();
+
+ try {
+ stdout.close();
+ } catch (IOException ignored) {
+ }
+ try {
+ stderr.close();
+ } catch (IOException ignored) {
+ }
+ }
+
+ String stdoutString = stdout.toString(StandardCharsets.UTF_8);
+ String stderrString = stderr.toString(StandardCharsets.UTF_8);
+
+ return new ProcessResult(exitCode, stdoutString, stderrString);
+ }
+
+}