diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bc3acc..de78516 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,7 @@ on: push: branches: - 'latest' + - 'experimental' tags: - 'v*' diff --git a/.gitignore b/.gitignore index 26e1da1..6e80684 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ build/ ### Config Files ### /src/main/resources/application.properties /src/main/resources/application-local.properties +/data/ diff --git a/README.md b/README.md index dde2e99..c65205d 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ Tool to manage your docker compose deployments via git. ## Table of Contents -- [Dirigent](#dirigent) -- [Table of Contents](#table-of-contents) - [Setup](#setup) - [docker-compose](#docker-compose) - [docker CLI](#docker-cli) @@ -16,11 +14,16 @@ Tool to manage your docker compose deployments via git. - [API](#api) - [Gitea Webhook](#gitea-webhook) - [Deployments](#deployments) - - [Start All Deployments](#start-all-deployments) - - [Start Deployment by name](#start-deployment-by-name) + - [Start](#start) + - [All Deployments](#all-deployments) + - [Deployment by name](#deployment-by-name) + - [Stop](#stop) + - [Deployment by name](#deployment-by-name-1) + - [State](#state) - [Develop](#develop) - [Setup for local Tests](#setup-for-local-tests) + ## Setup ### docker-compose @@ -45,6 +48,7 @@ services: volumes: - /path/to/config:/app/config - /path/to/deployments:/app/deployments + - /path/to/data:/app/data - /var/run/docker.sock:/var/run/docker.sock ``` @@ -70,6 +74,7 @@ docker run -d \ -e DIRIGENT_GOTIFY_TOKEN= \ -v /path/to/config:/app/config \ -v /path/to/deployments:/app/deployments \ + -v /path/to/data:/app/data \ -v /var/run/docker.sock:/var/run/docker.sock \ ghcr.io/derdavidbohl/dirigent-spring:latest ``` @@ -104,11 +109,12 @@ deployments: ### Volumes -| Volume | Description | -|----------------------|------------------------------------| -| /app/config | Config directory for Dirigent | -| /app/deployments | Deployments directory for Dirigent | -| /var/run/docker.sock | Docker socket for Dirigent | +| Volume | Description | +|----------------------|----------------------------------------| +| /app/config | Config directory for Dirigent | +| /app/deployments | Deployments directory for Dirigent | +| /app/data | Data directory containing the database | +| /var/run/docker.sock | Docker socket for Dirigent | ### Step by Step (Gitea) @@ -144,13 +150,33 @@ Store all your repositories for one host in one gitea organization. This way you ### Deployments -#### Start All Deployments: +#### Start -`POST` to `/api/v1/deployments/all/start` optional add `force=true` if you want to force deployment and recreation of containers. +**Parameters** -#### Start Deployment by name: +| Parameter | Description | +|-----------------|------------------------------------------------------| +| `force=true` | forces Recreation and Run of targeted deployment(s) | +| `forceRun=true` | only forces run of targeted deployment(s) | +| `forceRecreate` | only forces recreation of the targeted deployment(s) | -`POST` to `/api/v1/deployments/{name}/start` optional add `force=true` if you want to force deployment and recreation of containers. +##### All Deployments: + +`POST` to `/api/v1/deployments/all/start` + +##### Deployment by name: + +`POST` to `/api/v1/deployments/{name}/start` + +#### Stop + +##### Deployment by name: + +`POST` to `/api/v1/deployments/{name}/stop` + +#### State + +`GET` to `/api/v1/deployment-states` ## Develop diff --git a/Test.http b/Test.http index 4a5c1af..9b2e648 100644 --- a/Test.http +++ b/Test.http @@ -1,2 +1,9 @@ +POST http://localhost:8080/api/v1/deployments/test2/stop + +### POST http://localhost:8080/api/v1/deployments/all/start + +### + +GET http://localhost:8080/api/v1/deployment-states diff --git a/pom.xml b/pom.xml index ccee58d..24334d8 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.davidbohl dirigent - 0.2.0-SNAPSHOT + 0.3.0-SNAPSHOT dirigent Helper for Docker Composes @@ -63,6 +63,11 @@ h2 runtime + + org.projectlombok + lombok + provided + diff --git a/src/main/java/org/davidbohl/dirigent/DirigentApplication.java b/src/main/java/org/davidbohl/dirigent/DirigentApplication.java index 370fc0d..2dcc955 100644 --- a/src/main/java/org/davidbohl/dirigent/DirigentApplication.java +++ b/src/main/java/org/davidbohl/dirigent/DirigentApplication.java @@ -7,8 +7,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ConfigurableApplicationContext; -import java.io.IOException; - @SpringBootApplication @EnableConfigurationProperties @@ -17,7 +15,7 @@ public class DirigentApplication { static Logger logger = LoggerFactory.getLogger(DirigentApplication.class); - public static void main(String[] args) throws IOException, InterruptedException { + public static void main(String[] args) { 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/api/DeploymentStatesController.java b/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentStatesController.java new file mode 100644 index 0000000..eeb67ba --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentStatesController.java @@ -0,0 +1,26 @@ +package org.davidbohl.dirigent.deployments.api; + +import org.davidbohl.dirigent.deployments.state.DeploymentState; +import org.davidbohl.dirigent.deployments.state.DeploymentStatePersistingService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController() +@RequestMapping(path = "/api/v1/deployment-states") +public class DeploymentStatesController { + + private final DeploymentStatePersistingService deploymentStatePersistingService; + + public DeploymentStatesController(DeploymentStatePersistingService deploymentStatePersistingService) { + this.deploymentStatePersistingService = deploymentStatePersistingService; + } + + @GetMapping + public List getDeploymentStates() { + return deploymentStatePersistingService.getDeploymentStates(); + } + +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java b/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java index ae23a14..52c4585 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java @@ -2,6 +2,7 @@ package org.davidbohl.dirigent.deployments.api; import org.davidbohl.dirigent.deployments.events.AllDeploymentsStartRequestedEvent; import org.davidbohl.dirigent.deployments.events.NamedDeploymentStartRequestedEvent; +import org.davidbohl.dirigent.deployments.events.NamedDeploymentStopRequestedEvent; import org.davidbohl.dirigent.deployments.management.DeploymentNameNotFoundException; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.ProblemDetail; @@ -23,9 +24,18 @@ public class DeploymentsController { applicationEventPublisher.publishEvent(new NamedDeploymentStartRequestedEvent(this, name, force)); } + @PostMapping("/{name}/stop") + public void stopDeployment(@PathVariable String name) { + applicationEventPublisher.publishEvent(new NamedDeploymentStopRequestedEvent(this, name)); + } + @PostMapping("/all/start") - public void startAllDeployments(@RequestParam(required = false) boolean force) { - applicationEventPublisher.publishEvent(new AllDeploymentsStartRequestedEvent(this, force)); + public void startAllDeployments(@RequestParam(required = false) boolean force, + @RequestParam(required = false) boolean forceRun, + @RequestParam(required = false) boolean forceRecreate) { + applicationEventPublisher.publishEvent(new AllDeploymentsStartRequestedEvent(this, + force || forceRun, + force || forceRecreate)); } @ExceptionHandler(DeploymentNameNotFoundException.class) diff --git a/src/main/java/org/davidbohl/dirigent/deployments/api/GiteaDeploymentsController.java b/src/main/java/org/davidbohl/dirigent/deployments/api/GiteaDeploymentsController.java index cee01d6..709709d 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/api/GiteaDeploymentsController.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/api/GiteaDeploymentsController.java @@ -2,7 +2,6 @@ package org.davidbohl.dirigent.deployments.api; import org.davidbohl.dirigent.deployments.events.AllDeploymentsStartRequestedEvent; import org.davidbohl.dirigent.deployments.events.SourceDeploymentStartRequestedEvent; -import org.davidbohl.dirigent.deployments.management.DeploymentsService; import org.davidbohl.dirigent.deployments.models.GiteaRequestBody; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -20,7 +19,7 @@ public class GiteaDeploymentsController { @Value("${dirigent.deployments.git.url}") private String configUrl; - public GiteaDeploymentsController(DeploymentsService deploymentsService, ApplicationEventPublisher applicationEventPublisher) { + public GiteaDeploymentsController(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } @@ -28,7 +27,7 @@ public class GiteaDeploymentsController { public void webHook(@RequestBody GiteaRequestBody body) { if(body.repository().cloneUrl().equals(configUrl)) { - applicationEventPublisher.publishEvent(new AllDeploymentsStartRequestedEvent(this, false)); + applicationEventPublisher.publishEvent(new AllDeploymentsStartRequestedEvent(this, true, true)); return; } diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java index 0a922ba..bd54c6d 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java @@ -1,17 +1,17 @@ package org.davidbohl.dirigent.deployments.events; +import lombok.Getter; import org.springframework.context.ApplicationEvent; +@Getter public class AllDeploymentsStartRequestedEvent extends ApplicationEvent { - private final boolean forced; + private final boolean forceRun; + private final boolean forceRecreate; - public AllDeploymentsStartRequestedEvent(Object source, boolean forced) { + public AllDeploymentsStartRequestedEvent(Object source, boolean forceRun, boolean forceRecreate) { super(source); - this.forced = forced; - } - - public boolean isForced() { - return forced; + this.forceRun = forceRun; + this.forceRecreate = forceRecreate; } } diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStartFailedEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStartFailedEvent.java deleted file mode 100644 index 7a92f0e..0000000 --- a/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStartFailedEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.davidbohl.dirigent.deployments.events; - -import org.springframework.context.ApplicationEvent; - -public class DeploymentStartFailedEvent extends ApplicationEvent { - - - private final String deploymentName; - private final String message; - - public DeploymentStartFailedEvent(Object source, String deploymentName, String string) { - super(source); - this.deploymentName = deploymentName; - this.message = string; - } - - public String getMessage() { - return message; - } - - public String getDeploymentName() { - return deploymentName; - } -} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStartSucceededEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStartSucceededEvent.java deleted file mode 100644 index 99a0852..0000000 --- a/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStartSucceededEvent.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.davidbohl.dirigent.deployments.events; - -import org.springframework.context.ApplicationEvent; - -public class DeploymentStartSucceededEvent extends ApplicationEvent { - private final String deploymentName; - - public DeploymentStartSucceededEvent(Object source, String deploymentName) { - super(source); - this.deploymentName = deploymentName; - } - - public String getDeploymentName() { - return deploymentName; - } -} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStateChangedEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStateChangedEvent.java new file mode 100644 index 0000000..87fd08c --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/events/DeploymentStateChangedEvent.java @@ -0,0 +1,21 @@ +package org.davidbohl.dirigent.deployments.events; + +import lombok.Getter; +import org.davidbohl.dirigent.deployments.state.DeploymentState; +import org.springframework.context.ApplicationEvent; + +@Getter +public class DeploymentStateChangedEvent extends ApplicationEvent { + + final String deploymentName; + final DeploymentState.State state; + final String context; + + public DeploymentStateChangedEvent(Object source, String deploymentName, DeploymentState.State state, String context) { + super(source); + this.deploymentName = deploymentName; + this.state = state; + this.context = context; + } + +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java index d395933..8fc79fc 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java @@ -1,7 +1,9 @@ package org.davidbohl.dirigent.deployments.events; +import lombok.Getter; import org.springframework.context.ApplicationEvent; +@Getter public class NamedDeploymentStartRequestedEvent extends ApplicationEvent { private final String name; @@ -13,11 +15,4 @@ public class NamedDeploymentStartRequestedEvent extends ApplicationEvent { this.forced = forced; } - public String getName() { - return name; - } - - public boolean isForced() { - return forced; - } } diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStopRequestedEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStopRequestedEvent.java new file mode 100644 index 0000000..77166b2 --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStopRequestedEvent.java @@ -0,0 +1,16 @@ +package org.davidbohl.dirigent.deployments.events; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class NamedDeploymentStopRequestedEvent extends ApplicationEvent { + + private final String name; + + public NamedDeploymentStopRequestedEvent(Object source, String name) { + super(source); + this.name = name; + } + +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/NotConfiguredDeploymentStopped.java b/src/main/java/org/davidbohl/dirigent/deployments/events/NotConfiguredDeploymentStopped.java deleted file mode 100644 index b11ae1f..0000000 --- a/src/main/java/org/davidbohl/dirigent/deployments/events/NotConfiguredDeploymentStopped.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.davidbohl.dirigent.deployments.events; - -import org.springframework.context.ApplicationEvent; - -public class NotConfiguredDeploymentStopped extends ApplicationEvent { - private final String deploymentName; - - public NotConfiguredDeploymentStopped(Object source, String deploymentName) { - super(source); - this.deploymentName = deploymentName; - } - - public String getDeploymentName() { - return deploymentName; - } -} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/RecreateAllDeploymentStatesEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/RecreateAllDeploymentStatesEvent.java new file mode 100644 index 0000000..bad4cce --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/events/RecreateAllDeploymentStatesEvent.java @@ -0,0 +1,9 @@ +package org.davidbohl.dirigent.deployments.events; + +import org.springframework.context.ApplicationEvent; + +public class RecreateAllDeploymentStatesEvent extends ApplicationEvent { + public RecreateAllDeploymentStatesEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/events/SourceDeploymentStartRequestedEvent.java b/src/main/java/org/davidbohl/dirigent/deployments/events/SourceDeploymentStartRequestedEvent.java index c5d633a..b1ab9c0 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/events/SourceDeploymentStartRequestedEvent.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/events/SourceDeploymentStartRequestedEvent.java @@ -1,17 +1,16 @@ package org.davidbohl.dirigent.deployments.events; +import lombok.Getter; import org.springframework.context.ApplicationEvent; +@Getter public class SourceDeploymentStartRequestedEvent extends ApplicationEvent { - private String deploymentSource; + private final String deploymentSource; public SourceDeploymentStartRequestedEvent(Object source, String deploymentSource) { super(source); this.deploymentSource = deploymentSource; } - public String getDeploymentSource() { - return deploymentSource; - } } diff --git a/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentScheduler.java b/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentScheduler.java index cd5baec..23dd7d5 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentScheduler.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentScheduler.java @@ -1,6 +1,6 @@ package org.davidbohl.dirigent.deployments.management; -import org.davidbohl.dirigent.deployments.events.AllDeploymentsStartRequestedEvent; +import org.davidbohl.dirigent.deployments.events.RecreateAllDeploymentStatesEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -33,14 +33,14 @@ public class DeploymentScheduler { void runScheduledDeployments() { if(enabled) { logger.info("Starting all deployments scheduled"); - this.applicationEventPublisher.publishEvent(new AllDeploymentsStartRequestedEvent(this, false)); + this.applicationEventPublisher.publishEvent(new RecreateAllDeploymentStatesEvent(this)); } } @EventListener(ContextRefreshedEvent.class) public void onContextRefreshed() { if(startAllDeploymentsOnStartup) - applicationEventPublisher.publishEvent(new AllDeploymentsStartRequestedEvent(this, false)); + applicationEventPublisher.publishEvent(new RecreateAllDeploymentStatesEvent(this)); } } diff --git a/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java b/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java index 98e831f..d57a510 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/management/DeploymentsService.java @@ -4,6 +4,8 @@ import org.davidbohl.dirigent.deployments.config.DeploymentsConfigurationProvide import org.davidbohl.dirigent.deployments.events.*; 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.deployments.utility.GitService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,16 +31,18 @@ public class DeploymentsService { private final DeploymentsConfigurationProvider deploymentsConfigurationProvider; private final Logger logger = LoggerFactory.getLogger(DeploymentsService.class); private final ApplicationEventPublisher applicationEventPublisher; + private final DeploymentStatePersistingService deploymentStatePersistingService; @Value("${dirigent.compose.command}") private String composeCommand; public DeploymentsService( DeploymentsConfigurationProvider deploymentsConfigurationProvider, - GitService gitService, ApplicationEventPublisher applicationEventPublisher) { + GitService gitService, ApplicationEventPublisher applicationEventPublisher, DeploymentStatePersistingService deploymentStatePersistingService) { this.deploymentsConfigurationProvider = deploymentsConfigurationProvider; this.gitService = gitService; this.applicationEventPublisher = applicationEventPublisher; + this.deploymentStatePersistingService = deploymentStatePersistingService; } @EventListener(AllDeploymentsStartRequestedEvent.class) @@ -47,7 +51,7 @@ public class DeploymentsService { makeDeploymentsDir(); DeploynentConfiguration deploymentsConfiguration = tryGetConfiguration(); - deployListOfDeployments(deploymentsConfiguration.deployments(), event.isForced()); + deployListOfDeployments(deploymentsConfiguration.deployments(), event.isForceRun(), event.isForceRecreate()); stopNotConfiguredDeployments(deploymentsConfiguration.deployments()); } @@ -64,10 +68,10 @@ public class DeploymentsService { Optional first = deploynentConfiguration.deployments().stream().filter(d -> Objects.equals(d.name(), event.getName())).findFirst(); - if(first.isEmpty()) + if (first.isEmpty()) throw new DeploymentNameNotFoundException(event.getName()); - deploy(first.get(), event.isForced()); + deploy(first.get(), event.isForced(), event.isForced()); } @EventListener(SourceDeploymentStartRequestedEvent.class) @@ -80,10 +84,40 @@ public class DeploymentsService { .filter(d -> Objects.equals(d.source(), event.getDeploymentSource())) .collect(Collectors.toList()); - deployListOfDeployments(deployments, true); + deployListOfDeployments(deployments, true, true); } - private void deploy(Deployment deployment, boolean force) { + @EventListener(NamedDeploymentStopRequestedEvent.class) + public void onNamedDeploymentStopRequested(NamedDeploymentStopRequestedEvent event) throws IOException, InterruptedException { + makeDeploymentsDir(); + stopDeployment(event.getName()); + } + + @EventListener(RecreateAllDeploymentStatesEvent.class) + public void onRecreateAllDeploymentStatesEvent() { + makeDeploymentsDir(); + DeploynentConfiguration deploynentConfiguration = tryGetConfiguration(); + + List stoppedDeployments = deploymentStatePersistingService.getDeploymentStates().stream() + .filter(d -> d.getState() == DeploymentState.State.STOPPED) + .map(DeploymentState::getName) + .toList(); + stoppedDeployments.forEach(d -> { + try { + stopDeployment(d); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + }); + + List deployments = deploynentConfiguration.deployments().stream() + .filter(d -> !stoppedDeployments.contains(d.name())) + .toList(); + + deployListOfDeployments(deployments, true, false); + } + + private void deploy(Deployment deployment, boolean forceRun, boolean forceRecreate) { logger.info("Deploying {}", deployment.name()); File deploymentDir = new File("deployments/" + deployment.name()); @@ -91,7 +125,8 @@ public class DeploymentsService { try { boolean updated = gitService.updateRepo(deployment.source(), deploymentDir.getAbsolutePath()); - if(!updated && !force) { + if (!updated && !forceRun) { + applicationEventPublisher.publishEvent(new DeploymentStateChangedEvent(this, deployment.name(), DeploymentState.State.STARTED, "Deployment '%s' successfully started".formatted(deployment.name()))); logger.info("No changes in deployment. Skipping {}", deployment.name()); return; } @@ -101,7 +136,7 @@ public class DeploymentsService { commandArgs.add("-d"); commandArgs.add("--remove-orphans"); - if(force) + if (forceRecreate) commandArgs.add("--force-recreate"); Process process = new ProcessBuilder(commandArgs) @@ -109,22 +144,23 @@ public class DeploymentsService { .start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream())); - StringBuilder output = new StringBuilder(); + StringBuilder errorOutput = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); + errorOutput.append(line).append("\n"); } - int exitVal = process.waitFor(); - if (exitVal != 0) { - applicationEventPublisher.publishEvent(new DeploymentStartFailedEvent(this, deployment.name(), output.toString())); + int exitCode = process.waitFor(); + if ((exitCode != 0)) { + applicationEventPublisher.publishEvent(new DeploymentStateChangedEvent(this, deployment.name(), DeploymentState.State.FAILED, errorOutput.toString())); + return; } } catch (IOException | InterruptedException e) { - applicationEventPublisher.publishEvent(new DeploymentStartFailedEvent(this, deployment.name(), e.getMessage())); + applicationEventPublisher.publishEvent(new DeploymentStateChangedEvent(this, deployment.name(), DeploymentState.State.FAILED, e.getMessage())); return; } - applicationEventPublisher.publishEvent(new DeploymentStartSucceededEvent(this, deployment.name())); + applicationEventPublisher.publishEvent(new DeploymentStateChangedEvent(this, deployment.name(), DeploymentState.State.STARTED, "Deployment '%s' successfully started".formatted(deployment.name()))); } private void stopNotConfiguredDeployments(List deployments) { @@ -132,21 +168,15 @@ public class DeploymentsService { File deploymentsDir = new File("deployments"); File[] files = deploymentsDir.listFiles(); - if(files == null) + if (files == null) return; for (File file : files) { if (file.isDirectory() && deployments.stream().noneMatch(d -> d.name().equals(file.getName()))) { try { - logger.info("Stopping deployment {}", file.getName()); - List commandArgs = new java.util.ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList()); - commandArgs.add("down"); - new ProcessBuilder(commandArgs) - .directory(file) - .start() - .waitFor(); + stopDeployment(file.getName()); deleteDirectory(file); - applicationEventPublisher.publishEvent(new NotConfiguredDeploymentStopped(this, file.getName())); + applicationEventPublisher.publishEvent(new DeploymentStateChangedEvent(this, file.getName(), DeploymentState.State.REMOVED, "Deployment '%s' removed (Not configured)".formatted(file.getName()))); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } @@ -155,6 +185,17 @@ public class DeploymentsService { logger.info("Not configured deployments stopped"); } + private void stopDeployment(String deploymentName) throws InterruptedException, IOException { + logger.info("Stopping deployment {}", deploymentName); + List commandArgs = new ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList()); + commandArgs.add("down"); + new ProcessBuilder(commandArgs) + .directory(new File(DEPLOYMENTS_DIR_NAME + "/" + deploymentName)) + .start() + .waitFor(); + applicationEventPublisher.publishEvent(new DeploymentStateChangedEvent(this, deploymentName, DeploymentState.State.STOPPED, "Deployment '%s' stopped".formatted(deploymentName))); + } + void deleteDirectory(File directoryToBeDeleted) { File[] allContents = directoryToBeDeleted.listFiles(); if (allContents != null) { @@ -164,11 +205,11 @@ public class DeploymentsService { } boolean deleted = directoryToBeDeleted.delete(); - if(!deleted) + if (!deleted) throw new RuntimeException("Could not delete directory " + directoryToBeDeleted); } - private void deployListOfDeployments(List deployments, boolean force) { + private void deployListOfDeployments(List deployments, boolean forceRun, boolean forceRecreate) { makeDeploymentsDir(); @@ -185,7 +226,7 @@ public class DeploymentsService { ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); for (Deployment deployment : deploymentsOrderUnit) { - executorService.submit(() -> deploy(deployment, force)); + executorService.submit(() -> deploy(deployment, forceRun, forceRecreate)); } executorService.shutdown(); diff --git a/src/main/java/org/davidbohl/dirigent/deployments/notification/NotificationService.java b/src/main/java/org/davidbohl/dirigent/deployments/notification/NotificationService.java index f723c7d..0b9e60a 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/notification/NotificationService.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/notification/NotificationService.java @@ -1,17 +1,14 @@ package org.davidbohl.dirigent.deployments.notification; -import org.davidbohl.dirigent.deployments.events.DeploymentStartFailedEvent; -import org.davidbohl.dirigent.deployments.events.DeploymentStartSucceededEvent; -import org.davidbohl.dirigent.deployments.events.NotConfiguredDeploymentStopped; -import org.davidbohl.dirigent.deployments.management.DeploymentsService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; +import org.davidbohl.dirigent.deployments.events.DeploymentStateChangedEvent; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service +@Slf4j public class NotificationService { @Value("${dirigent.gotify.baseUrl:}") @@ -20,28 +17,13 @@ public class NotificationService { @Value("${dirigent.gotify.token:}") private String gotifyToken; - private final Logger logger = LoggerFactory.getLogger(DeploymentsService.class); + @EventListener(DeploymentStateChangedEvent.class) + public void onDeploymentStateChanged(DeploymentStateChangedEvent event) { + String title = "Deployment \"%s\" state changed to %s".formatted(event.getDeploymentName(), event.getState()); + String context = event.getContext(); + sendGotifyMessage(title, context); - @EventListener(DeploymentStartFailedEvent.class) - public void onDeploymentStartFailed(DeploymentStartFailedEvent event) { - sendGotifyMessage(event.getMessage(), "Deployment \"%s\" Failed".formatted(event.getDeploymentName())); - - logger.warn("Deployment '{}' failed. Error: {}", event.getDeploymentName(), event.getMessage()); - - } - - @EventListener(DeploymentStartSucceededEvent.class) - public void onDeploymentStartSucceeded(DeploymentStartSucceededEvent event) { - sendGotifyMessage("Deployment succeeded", "Deployment \"%s\" Succeeded".formatted(event.getDeploymentName())); - - logger.info("Deployment '{}' succeeded.", event.getDeploymentName()); - } - - @EventListener(NotConfiguredDeploymentStopped.class) - public void onNotConfiguredDeploymentStopped(NotConfiguredDeploymentStopped event) { - sendGotifyMessage("Deployment stopped", "Deployment \"%s\" stopped because it is not configured".formatted(event.getDeploymentName())); - - logger.info("Deployment '{}' stopped because it is not configured.", event.getDeploymentName()); + log.info("Deployment '{}' state changed to {}. Context: {}", event.getDeploymentName(), event.getState(), context); } private void sendGotifyMessage(String title, String message) { diff --git a/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentState.java b/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentState.java new file mode 100644 index 0000000..51f0d4b --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentState.java @@ -0,0 +1,27 @@ +package org.davidbohl.dirigent.deployments.state; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Entity +public class DeploymentState { + @Id + private String name; + + @Enumerated(EnumType.STRING) + private State state; + + @Column(length = 65535) + private String message; + + public enum State { + STARTED, STOPPED, FAILED, REMOVED + } +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java b/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java new file mode 100644 index 0000000..3f96f55 --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java @@ -0,0 +1,31 @@ +package org.davidbohl.dirigent.deployments.state; + +import org.davidbohl.dirigent.deployments.events.DeploymentStateChangedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.StreamSupport; + +@Service +public class DeploymentStatePersistingService { + + final DeploymentStateRepository deploymentStateRepository; + + public DeploymentStatePersistingService(DeploymentStateRepository deploymentStateRepository) { + this.deploymentStateRepository = deploymentStateRepository; + } + + @EventListener(DeploymentStateChangedEvent.class) + public void handleDeploymentStateChangedEvent(DeploymentStateChangedEvent event) { + DeploymentState deploymentState = new DeploymentState(event.getDeploymentName(), event.getState(), event.getContext()); + deploymentStateRepository.save(deploymentState); + + } + + public List getDeploymentStates() { + return StreamSupport.stream(deploymentStateRepository.findAll().spliterator(), false) + .toList(); + } + +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStateRepository.java b/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStateRepository.java new file mode 100644 index 0000000..4acd328 --- /dev/null +++ b/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStateRepository.java @@ -0,0 +1,6 @@ +package org.davidbohl.dirigent.deployments.state; + +import org.springframework.data.repository.CrudRepository; + +public interface DeploymentStateRepository extends CrudRepository { +} diff --git a/src/main/java/org/davidbohl/dirigent/deployments/utility/GitService.java b/src/main/java/org/davidbohl/dirigent/deployments/utility/GitService.java index 20a500e..1189edf 100644 --- a/src/main/java/org/davidbohl/dirigent/deployments/utility/GitService.java +++ b/src/main/java/org/davidbohl/dirigent/deployments/utility/GitService.java @@ -27,7 +27,7 @@ public class GitService { File destinationDir = new File(destination); - boolean changed = false; + boolean changed; if (destinationDir.exists() && Arrays.asList(Objects.requireNonNull(destinationDir.list())).contains(".git")) { logger.debug("Local Repo exists. Pulling latest changes."); diff --git a/src/main/resources/application-local.properties.template b/src/main/resources/application-local.properties.template index f862051..14d6b23 100644 --- a/src/main/resources/application-local.properties.template +++ b/src/main/resources/application-local.properties.template @@ -1,2 +1,3 @@ dirigent.deployments.git.url= dirigent.git.authToken= +spring.h2.console.enabled=true diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1bbd11b..8f168ec 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,15 @@ spring.application.name=dirigent -dirigent.compose.command=docker compose -dirigent.deployments.cache.evict.interval=60000 -dirigent.start.all.on.startup=true spring.profiles.active=local +spring.datasource.url=jdbc:h2:file:./data/statedb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=false +spring.jpa.hibernate.ddl-auto=update + +dirigent.compose.command=docker compose +dirigent.start.all.on.startup=true dirigent.git.authToken= dirigent.delpoyments.schedule.enabled=true -dirigent.delpoyments.schedule.cron=0 */5 * * * * \ No newline at end of file +dirigent.delpoyments.schedule.cron=0 */5 * * * *