mirror of
https://github.com/DerDavidBohl/dirigent-spring.git
synced 2025-12-23 20:29:58 -06:00
Refactored Process creation to avoid long living zombies
This commit is contained in:
@@ -85,6 +85,11 @@
|
||||
<artifactId>docker-java-transport-httpclient5</artifactId>
|
||||
<version>3.7.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-exec</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String> 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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
// }
|
||||
//}
|
||||
@@ -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");
|
||||
// }
|
||||
//}
|
||||
@@ -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) {
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.davidbohl.dirigent.utility.process;
|
||||
|
||||
public record ProcessResult(int exitCode, String stdout, String stderr) {}
|
||||
@@ -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<String> commandParts, long timeoutMs, Map<String, String> env)
|
||||
throws IOException {
|
||||
return executeInternal(commandParts, new File(System.getProperty("user.dir")), timeoutMs, env);
|
||||
}
|
||||
|
||||
public ProcessResult executeCommand(List<String> commandParts)
|
||||
throws IOException {
|
||||
return executeInternal(commandParts, new File(System.getProperty("user.dir")), 0, Map.of());
|
||||
}
|
||||
|
||||
public ProcessResult executeCommand(List<String> commandParts, File workingDirectory)
|
||||
throws IOException {
|
||||
return executeInternal(commandParts, workingDirectory, 0, Map.of());
|
||||
}
|
||||
|
||||
public ProcessResult executeCommand(List<String> commandParts, File workingDirectory, Map<String, String> env)
|
||||
throws IOException {
|
||||
return executeInternal(commandParts, workingDirectory, 0, env);
|
||||
}
|
||||
|
||||
private ProcessResult executeInternal(List<String> commandParts, File workingDirectory, long timeoutMs, Map<String, String> env)
|
||||
throws IOException {
|
||||
|
||||
Map<String, String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user