From f8dd289a1086fa3302a4e3f7a64c98cbf005359f Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 29 Sep 2025 14:25:07 +0200 Subject: [PATCH 01/25] Use SQLite as DB --- backend/pom.xml | 9 ++++++--- backend/src/main/resources/application.properties | 9 +++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 9ec23f0..1e12945 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -63,9 +63,12 @@ 4.5.14 - com.h2database - h2 - runtime + org.xerial + sqlite-jdbc + + + org.hibernate.orm + hibernate-community-dialects org.projectlombok diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 9ea7fa8..d3464f5 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,11 +1,8 @@ spring.application.name=dirigent 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.datasource.url=jdbc:sqlite:./data/dirigent.db +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect spring.jpa.hibernate.ddl-auto=update dirigent.git.authToken= From 2f703661ca1164ff8a8cf397f367528e86f0e1f5 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 29 Sep 2025 15:31:19 +0200 Subject: [PATCH 02/25] State and event improvements --- .../api/DeploymentsController.java | 8 ++---- .../AllDeploymentsStartRequestedEvent.java | 4 +-- .../NamedDeploymentStartRequestedEvent.java | 6 ++--- .../management/DeploymentsService.java | 26 +++++++++---------- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java index d9504bb..ce54183 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java @@ -43,12 +43,8 @@ public class DeploymentsController { } @PostMapping("/all/start") - 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)); + public void startAllDeployments(@RequestParam(required = false) boolean forceRecreate) { + applicationEventPublisher.publishEvent(new AllDeploymentsStartRequestedEvent(this, forceRecreate)); } @GetMapping() diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java index bd54c6d..73f54f6 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/events/AllDeploymentsStartRequestedEvent.java @@ -6,12 +6,10 @@ import org.springframework.context.ApplicationEvent; @Getter public class AllDeploymentsStartRequestedEvent extends ApplicationEvent { - private final boolean forceRun; private final boolean forceRecreate; - public AllDeploymentsStartRequestedEvent(Object source, boolean forceRun, boolean forceRecreate) { + public AllDeploymentsStartRequestedEvent(Object source, boolean forceRecreate) { super(source); - this.forceRun = forceRun; this.forceRecreate = forceRecreate; } } diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java index 8fc79fc..4942271 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/events/NamedDeploymentStartRequestedEvent.java @@ -7,12 +7,12 @@ import org.springframework.context.ApplicationEvent; public class NamedDeploymentStartRequestedEvent extends ApplicationEvent { private final String name; - private final boolean forced; + private final boolean forceRecreate; - public NamedDeploymentStartRequestedEvent(Object source, String name, boolean forced) { + public NamedDeploymentStartRequestedEvent(Object source, String name, boolean forceRecreate) { super(source); this.name = name; - this.forced = forced; + this.forceRecreate = forceRecreate; } } 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 b608b3f..f37dff2 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 @@ -51,7 +51,7 @@ public class DeploymentsService { makeDeploymentsDir(); DeploynentConfiguration deploymentsConfiguration = tryGetConfiguration(); - deployListOfDeployments(deploymentsConfiguration.deployments(), event.isForceRun(), event.isForceRecreate()); + deployListOfDeployments(deploymentsConfiguration.deployments(), event.isForceRecreate()); stopNotConfiguredDeployments(deploymentsConfiguration.deployments()); } @@ -71,7 +71,7 @@ public class DeploymentsService { if (first.isEmpty()) throw new DeploymentNameNotFoundException(event.getName()); - deploy(first.get(), event.isForced(), event.isForced()); + deploy(first.get(), event.isForceRecreate()); } @EventListener(SourceDeploymentStartRequestedEvent.class) @@ -84,7 +84,7 @@ public class DeploymentsService { .filter(d -> Objects.equals(d.source(), event.getDeploymentSource())) .collect(Collectors.toList()); - deployListOfDeployments(deployments, true, true); + deployListOfDeployments(deployments, true); } @EventListener(NamedDeploymentStopRequestedEvent.class) @@ -115,22 +115,21 @@ public class DeploymentsService { .filter(d -> !stoppedDeployments.contains(d.name())) .toList(); - deployListOfDeployments(deployments, true, false); + deployListOfDeployments(deployments, false); } - private void deploy(Deployment deployment, boolean forceRun, boolean forceRecreate) { + private void deploy(Deployment deployment, boolean forceRecreate) { logger.info("Deploying {}", deployment.name()); File deploymentDir = new File("deployments/" + deployment.name()); try { + + applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.STARTING, "Deployment '%s' updated".formatted(deployment.name()))); + + boolean updated = gitService.updateRepo(deployment.source(), deploymentDir.getAbsolutePath()); - - if (updated) { - applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.UPDATED, "Deployment '%s' updated".formatted(deployment.name()))); - } - - if (!updated && !forceRun) { + if (!updated && !forceRecreate) { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.RUNNING, "Deployment '%s' successfully started".formatted(deployment.name()))); logger.info("No changes in deployment. Skipping {}", deployment.name()); return; @@ -194,6 +193,7 @@ public class DeploymentsService { private void stopDeployment(String deploymentName) throws InterruptedException, IOException { logger.info("Stopping deployment {}", deploymentName); + applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deploymentName, DeploymentState.State.STOPPING, "Stopping deployment '%s'".formatted(deploymentName))); List commandArgs = new ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList()); commandArgs.add("down"); new ProcessBuilder(commandArgs) @@ -216,7 +216,7 @@ public class DeploymentsService { throw new RuntimeException("Could not delete directory " + directoryToBeDeleted); } - private void deployListOfDeployments(List deployments, boolean forceRun, boolean forceRecreate) { + private void deployListOfDeployments(List deployments, boolean forceRecreate) { makeDeploymentsDir(); @@ -233,7 +233,7 @@ public class DeploymentsService { ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); for (Deployment deployment : deploymentsOrderUnit) { - executorService.submit(() -> deploy(deployment, forceRun, forceRecreate)); + executorService.submit(() -> deploy(deployment, forceRecreate)); } executorService.shutdown(); From 36b3489395a9c9a79f93e0b1e0affb10137e3186 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 29 Sep 2025 15:52:08 +0200 Subject: [PATCH 03/25] improve state management --- .../management/DeploymentsService.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) 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 f37dff2..faae5bb 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 @@ -122,19 +122,28 @@ public class DeploymentsService { logger.info("Deploying {}", deployment.name()); File deploymentDir = new File("deployments/" + deployment.name()); - + try { - applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.STARTING, "Deployment '%s' updated".formatted(deployment.name()))); - - boolean updated = gitService.updateRepo(deployment.source(), deploymentDir.getAbsolutePath()); - if (!updated && !forceRecreate) { + Optional optionalState= deploymentStatePersistingService.getDeploymentStates().stream() + .filter(state -> state.getName().equals(deployment.name())) + .findFirst(); + + boolean deployWouldChangeState = optionalState.isEmpty() || optionalState.get().getState() != DeploymentState.State.RUNNING; + + + if (!updated && !forceRecreate && !deployWouldChangeState) { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.RUNNING, "Deployment '%s' successfully started".formatted(deployment.name()))); logger.info("No changes in deployment. Skipping {}", deployment.name()); return; } + if (updated) { + applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.UPDATED, "Deployment '%s' updated".formatted(deployment.name()))); + } + applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.STARTING, "Starting Deployment '%s'".formatted(deployment.name()))); + List commandArgs = new java.util.ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList()); commandArgs.add("up"); commandArgs.add("-d"); From 94a7d8413b5c47c4d0c8426ecd190261e2245d4f Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 29 Sep 2025 15:52:56 +0200 Subject: [PATCH 04/25] Logging improved --- .../dirigent/deployments/management/DeploymentsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 faae5bb..cbe9861 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 @@ -135,7 +135,7 @@ public class DeploymentsService { if (!updated && !forceRecreate && !deployWouldChangeState) { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.RUNNING, "Deployment '%s' successfully started".formatted(deployment.name()))); - logger.info("No changes in deployment. Skipping {}", deployment.name()); + logger.info("No update, forced recreation or changed states in deployment. Skipping {}", deployment.name()); return; } From eae6467b7549f7b153db5f7f8b3875cf8d0e2b9f Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 29 Sep 2025 16:03:32 +0200 Subject: [PATCH 05/25] Update API --- .../dirigent/deployments/management/DeploymentsService.java | 2 +- frontend/src/app/overview/api.service.ts | 2 +- frontend/src/app/overview/overview.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 cbe9861..ef17a31 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 @@ -132,7 +132,7 @@ public class DeploymentsService { boolean deployWouldChangeState = optionalState.isEmpty() || optionalState.get().getState() != DeploymentState.State.RUNNING; - + if (!updated && !forceRecreate && !deployWouldChangeState) { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.RUNNING, "Deployment '%s' successfully started".formatted(deployment.name()))); logger.info("No update, forced recreation or changed states in deployment. Skipping {}", deployment.name()); diff --git a/frontend/src/app/overview/api.service.ts b/frontend/src/app/overview/api.service.ts index effb270..504d43b 100644 --- a/frontend/src/app/overview/api.service.ts +++ b/frontend/src/app/overview/api.service.ts @@ -35,6 +35,6 @@ export class ApiService { } startDeployment(deploymentState: Deployment, force: boolean): Observable { - return this.http.post(`api/v1/deployments/${deploymentState.name}/start?force=${force}`, {}); + return this.http.post(`api/v1/deployments/${deploymentState.name}/start?forceRecreate=${force}`, {}); } } diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index f610cf9..703eb26 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -120,7 +120,7 @@ export class OverviewComponent implements OnInit { } ngOnInit(): void { - this.selectedFilterValues$.next(['RUNNING', 'FAILED', 'STOPPED']); + this.selectedFilterValues$.next(['RUNNING', 'STOPPED', 'FAILED', 'UPDATED', 'UNKNOWN', 'STARTING', 'STOPPING']); this.sort$.next({active: 'name', direction: 'asc'}); this.search$.next(''); } From 2771aeb75b72139067fc43347d830cbdaba2ab32 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 29 Sep 2025 16:14:30 +0200 Subject: [PATCH 06/25] Do not notify on stopped stopped --- .../deployments/api/DeploymentsController.java | 4 ++-- .../deployments/management/DeploymentsService.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java index ce54183..036998d 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/api/DeploymentsController.java @@ -33,8 +33,8 @@ public class DeploymentsController { } @PostMapping("/{name}/start") - public void startDeployment(@PathVariable String name, @RequestParam(required = false) boolean force) { - applicationEventPublisher.publishEvent(new NamedDeploymentStartRequestedEvent(this, name, force)); + public void startDeployment(@PathVariable String name, @RequestParam(required = false) boolean forceRecreate) { + applicationEventPublisher.publishEvent(new NamedDeploymentStartRequestedEvent(this, name, forceRecreate)); } @PostMapping("/{name}/stop") 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 ef17a31..21f125e 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 @@ -202,7 +202,18 @@ public class DeploymentsService { private void stopDeployment(String deploymentName) throws InterruptedException, IOException { logger.info("Stopping deployment {}", deploymentName); - applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deploymentName, DeploymentState.State.STOPPING, "Stopping deployment '%s'".formatted(deploymentName))); + + + Optional optionalState= deploymentStatePersistingService.getDeploymentStates().stream() + .filter(state -> state.getName().equals(deploymentName)) + .findFirst(); + + boolean stopWouldChangeState = optionalState.isEmpty() || optionalState.get().getState() != DeploymentState.State.STOPPED; + + if(stopWouldChangeState) { + applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deploymentName, DeploymentState.State.STOPPING, "Stopping deployment '%s'".formatted(deploymentName))); + } + List commandArgs = new ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList()); commandArgs.add("down"); new ProcessBuilder(commandArgs) From 5c420efa92f52aeace4e075c8b139c1eecb304fc Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 29 Sep 2025 16:56:23 +0200 Subject: [PATCH 07/25] Updated README.md --- .github/workflows/build.yml | 1 + README.md | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 074c0be..66aaa45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,6 @@ name: Build Docker Image on: + workflow_dispatch: pull_request: types: - opened diff --git a/README.md b/README.md index b8dca5d..9071d91 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,7 @@ Store all your repositories for one host in one gitea organization. This way you | 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) | +| `forceRecreate=trueu` | forces recreation of the targeted deployment(s) | ##### All Deployments: From f3f6e580072fa6e2fafabe7a85c1289022448fd4 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Thu, 2 Oct 2025 15:37:19 +0200 Subject: [PATCH 08/25] Implemented Secrets in Backend --- README.md | 6 +- backend/pom.xml | 2 +- .../management/DeploymentsService.java | 18 +++- .../davidbohl/dirigent/sercrets/Secret.java | 31 +++++++ .../dirigent/sercrets/SecretController.java | 32 +++++++ .../dirigent/sercrets/SecretDto.java | 8 ++ .../dirigent/sercrets/SecretRepository.java | 14 ++++ .../dirigent/sercrets/SecretService.java | 84 +++++++++++++++++++ .../application-local.properties.template | 1 + 9 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java create mode 100644 backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java create mode 100644 backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java create mode 100644 backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java create mode 100644 backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java diff --git a/README.md b/README.md index 9071d91..e12a3fd 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ services: restart: unless-stopped environment: - DIRIGENT_DEPLOYMENTS_GIT_URL= # required + - DIRIGENT_SECRETS_ENCRYPTION_KEY= # Has to be 16 Chars. Can be generated with `openssl rand -base64 12` - DIRIGENT_COMPOSE_COMMAND= # optional - DIRIGENT_GIT_AUTHTOKEN= # optional - DIRIGENT_START_ALL_ON_STARTUP= # optional @@ -89,6 +90,7 @@ services: docker run -d \ --name=dirigent \ -e DIRIGENT_DEPLOYMENTS_GIT_URL= \ + -e DIRIGENT_SECRETS_ENCRYPTION_KEY= \ # Has to be 16 Chars. Can be generated with `openssl rand -base64 12` #optional -e DIRIGENT_COMPOSE_COMMAND= \ #optional @@ -105,6 +107,7 @@ docker run -d \ -e DIRIGENT_GOTIFY_TOKEN= \ #optional but recommended -e DIRIGENT_INSTANCENAME= \ + -v /path/to/config:/app/config \ -v /path/to/deployments:/app/deployments \ -v /path/to/data:/app/data \ @@ -117,6 +120,7 @@ docker run -d \ | Variable | Description | Default | |---------------------------------------|-------------------------------------------------------------------------------------------------------|------------------| | DIRIGENT_DEPLOYMENTS_GIT_URL | URL to your deployments git repository | | +| DIRIGENT_SECRETS_ENCRYPTION_KEY | Encryption Key to save encrypted secrets. Has to be 16 Chars. Can be generated with `openssl rand -base64 12` | | | DIRIGENT_COMPOSE_COMMAND | Command to run your docker-compose files | `docker compose` | | DIRIGENT_GIT_AUTHTOKEN | Auth token with access to your repos | | | DIRIGENT_START_ALL_ON_STARTUP | Start all deployments on startup | `true` | @@ -190,7 +194,7 @@ Store all your repositories for one host in one gitea organization. This way you | Parameter | Description | |-----------------|------------------------------------------------------| -| `forceRecreate=trueu` | forces recreation of the targeted deployment(s) | +| `forceRecreate=true` | forces recreation of the targeted deployment(s) | ##### All Deployments: diff --git a/backend/pom.xml b/backend/pom.xml index 1e12945..cce48a5 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -10,7 +10,7 @@ org.davidbohl dirigent - 0.5.0-SNAPSHOT + 1.0.0-SNAPSHOT dirigent Helper for Docker Composes 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 21f125e..9045e52 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 @@ -6,6 +6,7 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,17 +33,22 @@ public class DeploymentsService { private final Logger logger = LoggerFactory.getLogger(DeploymentsService.class); private final ApplicationEventPublisher applicationEventPublisher; private final DeploymentStatePersistingService deploymentStatePersistingService; + private final SecretService secretService; @Value("${dirigent.compose.command}") private String composeCommand; public DeploymentsService( DeploymentsConfigurationProvider deploymentsConfigurationProvider, - GitService gitService, ApplicationEventPublisher applicationEventPublisher, DeploymentStatePersistingService deploymentStatePersistingService) { + 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) @@ -154,8 +160,12 @@ public class DeploymentsService { } logger.info("Upping Compose for {}", deployment.name()); - Process process = new ProcessBuilder(commandArgs) - .directory(deploymentDir) + 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())); @@ -223,7 +233,7 @@ public class DeploymentsService { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deploymentName, DeploymentState.State.STOPPED, "Deployment '%s' stopped".formatted(deploymentName))); } - void deleteDirectory(File directoryToBeDeleted) { + private void deleteDirectory(File directoryToBeDeleted) { File[] allContents = directoryToBeDeleted.listFiles(); if (allContents != null) { for (File file : allContents) { diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java new file mode 100644 index 0000000..294c443 --- /dev/null +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java @@ -0,0 +1,31 @@ +package org.davidbohl.dirigent.sercrets; + +import java.util.List; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Secret { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String environmentVariable; + + private String encryptedValue; + + private List deployments; + +} \ No newline at end of file diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java new file mode 100644 index 0000000..c3f769d --- /dev/null +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java @@ -0,0 +1,32 @@ +package org.davidbohl.dirigent.sercrets; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController() +@RequestMapping(path = "/api/v1/secrets") +public class SecretController { + + private final SecretService secretService; + + + public SecretController(SecretService secretService) { + this.secretService = secretService; + } + + @PutMapping + public void saveSecret(SecretDto secret) { + this.secretService.saveSecret(secret.environmentVariable(), secret.value(), secret.deployments()); + } + + @GetMapping + public List getSecrets() { + return this.secretService.getAllSecretsWithoutValues(); + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java new file mode 100644 index 0000000..663f5e7 --- /dev/null +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java @@ -0,0 +1,8 @@ +package org.davidbohl.dirigent.sercrets; + +import java.util.List; + +public record SecretDto(String environmentVariable, String value, List deployments) { + +} + diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java new file mode 100644 index 0000000..054c71e --- /dev/null +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java @@ -0,0 +1,14 @@ +package org.davidbohl.dirigent.sercrets; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SecretRepository extends JpaRepository { + + Optional findByKey(String key); + + List findByDeploymentsContaining(String deployment); + +} \ No newline at end of file diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java new file mode 100644 index 0000000..dc575f8 --- /dev/null +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -0,0 +1,84 @@ +package org.davidbohl.dirigent.sercrets; + +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + + +@Service +public class SecretService { + + private static final String ALGORITHM = "AES"; + + private final String encryptionKey; + private final SecretRepository secretRepository; + + public SecretService(@Value("${dirigent.secrets.encryption.key}") String encryptionKey, SecretRepository secretRepository) { + + if (encryptionKey == null || encryptionKey.length() != 16) { + throw new IllegalArgumentException("SECRET_ENCRYPTION_KEY muss 16 Zeichen lang sein!<" + encryptionKey + ">"); + } + + this.encryptionKey = encryptionKey; + this.secretRepository = secretRepository; + } + + public void saveSecret(String environmentVariable, String value, List deployments) { + try { + String encrypted = encrypt(value); + Secret secret = new Secret(null, environmentVariable, encrypted, deployments); + secretRepository.save(secret); + } catch (Exception e) { + throw new RuntimeException("Saving Secret failed", e); + } + } + + public Map getAllSecretsAsEnvironmentVariableMapByDeployment(String deployment) { + List secrets = secretRepository.findByDeploymentsContaining(deployment); + Map result = new HashMap<>(); + + for (Secret secret : secrets) { + result.put(secret.getEnvironmentVariable(), getSecret(secret.getEncryptedValue())); + } + + return result; + } + + public List getAllSecretsWithoutValues() { + return secretRepository.findAll().stream().map( + s -> new SecretDto(s.getEnvironmentVariable(), null, s.getDeployments()) + ).toList(); + } + + private String getSecret(String key) { + try { + Secret secret = secretRepository.findByKey(key).orElseThrow(); + return decrypt(secret.getEncryptedValue()); + } catch (Exception e) { + throw new RuntimeException("Reading Secret failed", e); + } + } + + private String encrypt(String value) throws Exception { + SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(), ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + return Base64.getEncoder().encodeToString(cipher.doFinal(value.getBytes())); + } + + private String decrypt(String encrypted) throws Exception { + SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(), ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + return new String(cipher.doFinal(Base64.getDecoder().decode(encrypted))); + } + +} + diff --git a/backend/src/main/resources/application-local.properties.template b/backend/src/main/resources/application-local.properties.template index 14d6b23..77c3b69 100644 --- a/backend/src/main/resources/application-local.properties.template +++ b/backend/src/main/resources/application-local.properties.template @@ -1,3 +1,4 @@ dirigent.deployments.git.url= dirigent.git.authToken= spring.h2.console.enabled=true +dirigent.secrets.encryption.key= From a2f34b837d23eb6cc12e0a6e9f01bf6bd83304ab Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Thu, 2 Oct 2025 16:31:54 +0200 Subject: [PATCH 09/25] Refacotred Secrets --- .../davidbohl/dirigent/sercrets/Secret.java | 8 ++---- .../dirigent/sercrets/SecretController.java | 11 ++++++-- .../dirigent/sercrets/SecretDto.java | 2 +- .../dirigent/sercrets/SecretRepository.java | 8 ++---- .../dirigent/sercrets/SecretService.java | 28 +++++++++---------- 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java index 294c443..de85369 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java @@ -2,10 +2,8 @@ package org.davidbohl.dirigent.sercrets; import java.util.List; -import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Getter; @@ -19,13 +17,13 @@ import lombok.Setter; @Entity public class Secret { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private String key; private String environmentVariable; private String encryptedValue; + @ElementCollection private List deployments; } \ No newline at end of file diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java index c3f769d..a78f4a9 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java @@ -3,12 +3,17 @@ package org.davidbohl.dirigent.sercrets; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import lombok.extern.slf4j.Slf4j; + @RestController() @RequestMapping(path = "/api/v1/secrets") +@Slf4j public class SecretController { private final SecretService secretService; @@ -18,9 +23,9 @@ public class SecretController { this.secretService = secretService; } - @PutMapping - public void saveSecret(SecretDto secret) { - this.secretService.saveSecret(secret.environmentVariable(), secret.value(), secret.deployments()); + @PutMapping("{key}") + public void saveSecret(@RequestBody SecretDto secret, @PathVariable String key) { + this.secretService.saveSecret(key, secret.environmentVariable(), secret.value(), secret.deployments()); } @GetMapping diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java index 663f5e7..683a181 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretDto.java @@ -2,7 +2,7 @@ package org.davidbohl.dirigent.sercrets; import java.util.List; -public record SecretDto(String environmentVariable, String value, List deployments) { +public record SecretDto(String key, String environmentVariable, String value, List deployments) { } diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java index 054c71e..d7316ba 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretRepository.java @@ -1,14 +1,12 @@ package org.davidbohl.dirigent.sercrets; import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface SecretRepository extends JpaRepository { +public interface SecretRepository extends JpaRepository { - Optional findByKey(String key); - - List findByDeploymentsContaining(String deployment); + List findAllByDeploymentsContaining(String deployment); + List findAllByEnvironmentVariableAndDeploymentsContaining(String environmentVariable, String deployment); } \ No newline at end of file diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java index dc575f8..9914a31 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -11,8 +11,11 @@ import javax.crypto.spec.SecretKeySpec; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + @Service +@Slf4j public class SecretService { private static final String ALGORITHM = "AES"; @@ -30,10 +33,11 @@ public class SecretService { this.secretRepository = secretRepository; } - public void saveSecret(String environmentVariable, String value, List deployments) { + public void saveSecret(String key, String environmentVariable, String value, List deployments) { try { + String encrypted = encrypt(value); - Secret secret = new Secret(null, environmentVariable, encrypted, deployments); + Secret secret = new Secret(key, environmentVariable, encrypted, deployments); secretRepository.save(secret); } catch (Exception e) { throw new RuntimeException("Saving Secret failed", e); @@ -41,11 +45,16 @@ public class SecretService { } public Map getAllSecretsAsEnvironmentVariableMapByDeployment(String deployment) { - List secrets = secretRepository.findByDeploymentsContaining(deployment); + List secrets = secretRepository.findAllByDeploymentsContaining(deployment); Map result = new HashMap<>(); for (Secret secret : secrets) { - result.put(secret.getEnvironmentVariable(), getSecret(secret.getEncryptedValue())); + try { + result.put(secret.getEnvironmentVariable(), decrypt(secret.getEncryptedValue())); + } catch(Exception ex) { + log.error("Failed to decrypt secret <" + secret.getKey() + "> for Env Var <" + secret.getEnvironmentVariable() + "> and Deployment <" + deployment + ">."); + throw new RuntimeException(ex); + } } return result; @@ -53,19 +62,10 @@ public class SecretService { public List getAllSecretsWithoutValues() { return secretRepository.findAll().stream().map( - s -> new SecretDto(s.getEnvironmentVariable(), null, s.getDeployments()) + s -> new SecretDto(s.getKey(), s.getEnvironmentVariable(), null, s.getDeployments()) ).toList(); } - private String getSecret(String key) { - try { - Secret secret = secretRepository.findByKey(key).orElseThrow(); - return decrypt(secret.getEncryptedValue()); - } catch (Exception e) { - throw new RuntimeException("Reading Secret failed", e); - } - } - private String encrypt(String value) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(), ALGORITHM); Cipher cipher = Cipher.getInstance(ALGORITHM); From d15e3617ea772f9a95d5d07f1aa89ecc7b96104f Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Thu, 2 Oct 2025 16:45:05 +0200 Subject: [PATCH 10/25] Added Secret Deletion --- .../org/davidbohl/dirigent/sercrets/SecretController.java | 6 ++++++ .../java/org/davidbohl/dirigent/sercrets/SecretService.java | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java index a78f4a9..50ac049 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java @@ -2,6 +2,7 @@ package org.davidbohl.dirigent.sercrets; import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; @@ -28,6 +29,11 @@ public class SecretController { this.secretService.saveSecret(key, secret.environmentVariable(), secret.value(), secret.deployments()); } + @DeleteMapping("{key}") + public void deleteSecret(@PathVariable String key) { + this.secretService.deleteSecret(key); + } + @GetMapping public List getSecrets() { return this.secretService.getAllSecretsWithoutValues(); diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java index 9914a31..7e76fde 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -66,6 +66,10 @@ public class SecretService { ).toList(); } + public void deleteSecret(String key) { + secretRepository.deleteById(key); + } + private String encrypt(String value) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(), ALGORITHM); Cipher cipher = Cipher.getInstance(ALGORITHM); From cc2c06851e80aec49c80629f5a00cb40637d12be Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 00:27:06 +0200 Subject: [PATCH 11/25] Added Frontend for Secrets --- .../dirigent/sercrets/SecretService.java | 13 +++- .../src/app/{overview => api}/api.service.ts | 30 ++++++- .../deployment.d.ts} | 0 frontend/src/app/api/secret.d.ts | 6 ++ .../{overview => api}/system-information.d.ts | 0 frontend/src/app/app.component.html | 5 +- frontend/src/app/app.component.ts | 6 +- frontend/src/app/app.routes.ts | 4 +- .../deployments.component.css} | 0 .../deployments.component.html} | 0 .../deployments.component.ts} | 14 ++-- .../start-dialog/start-dialog.component.css | 0 .../start-dialog/start-dialog.component.html | 0 .../start-dialog/start-dialog.component.ts | 0 .../edit-secret-dialog.component.css} | 0 .../edit-secret-dialog.component.html | 59 ++++++++++++++ .../edit-secret-dialog.component.ts | 78 +++++++++++++++++++ .../src/app/secrets/secrets.component.css | 1 + .../src/app/secrets/secrets.component.html | 45 +++++++++++ frontend/src/app/secrets/secrets.component.ts | 74 ++++++++++++++++++ 20 files changed, 317 insertions(+), 18 deletions(-) rename frontend/src/app/{overview => api}/api.service.ts (57%) rename frontend/src/app/{overview/deploymentState.d.ts => api/deployment.d.ts} (100%) create mode 100644 frontend/src/app/api/secret.d.ts rename frontend/src/app/{overview => api}/system-information.d.ts (100%) rename frontend/src/app/{app.component.spec.ts => deployments/deployments.component.css} (100%) rename frontend/src/app/{overview/overview.component.html => deployments/deployments.component.html} (100%) rename frontend/src/app/{overview/overview.component.ts => deployments/deployments.component.ts} (93%) rename frontend/src/app/{overview => deployments}/start-dialog/start-dialog.component.css (100%) rename frontend/src/app/{overview => deployments}/start-dialog/start-dialog.component.html (100%) rename frontend/src/app/{overview => deployments}/start-dialog/start-dialog.component.ts (100%) rename frontend/src/app/{overview/overview.component.css => secrets/edit-secret-dialog/edit-secret-dialog.component.css} (100%) create mode 100644 frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html create mode 100644 frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts create mode 100644 frontend/src/app/secrets/secrets.component.css create mode 100644 frontend/src/app/secrets/secrets.component.html create mode 100644 frontend/src/app/secrets/secrets.component.ts diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java index 7e76fde..d0cf96d 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -36,8 +36,11 @@ public class SecretService { public void saveSecret(String key, String environmentVariable, String value, List deployments) { try { - String encrypted = encrypt(value); - Secret secret = new Secret(key, environmentVariable, encrypted, deployments); + Secret secret = secretRepository.findById(key).orElseGet(() -> new Secret(key, environmentVariable, value, deployments)); + + if(value != null ) + secret.setEncryptedValue(encrypt(value)); + secretRepository.save(secret); } catch (Exception e) { throw new RuntimeException("Saving Secret failed", e); @@ -52,7 +55,7 @@ public class SecretService { try { result.put(secret.getEnvironmentVariable(), decrypt(secret.getEncryptedValue())); } catch(Exception ex) { - log.error("Failed to decrypt secret <" + secret.getKey() + "> for Env Var <" + secret.getEnvironmentVariable() + "> and Deployment <" + deployment + ">."); + log.error("Failed to decrypt secret <{}> for Env Var <{}> and Deployment <{}>.", secret.getKey(), secret.getEnvironmentVariable(), deployment); throw new RuntimeException(ex); } } @@ -78,6 +81,10 @@ public class SecretService { } private String decrypt(String encrypted) throws Exception { + + if(encrypted == null) + return null; + SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(), ALGORITHM); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, keySpec); diff --git a/frontend/src/app/overview/api.service.ts b/frontend/src/app/api/api.service.ts similarity index 57% rename from frontend/src/app/overview/api.service.ts rename to frontend/src/app/api/api.service.ts index 504d43b..cf4fb28 100644 --- a/frontend/src/app/overview/api.service.ts +++ b/frontend/src/app/api/api.service.ts @@ -1,8 +1,9 @@ import {Injectable} from '@angular/core'; -import {Observable, ReplaySubject} from 'rxjs'; -import {Deployment} from './deploymentState'; +import {Observable, ReplaySubject, tap} from 'rxjs'; +import {Deployment} from './deployment'; import {HttpClient} from '@angular/common/http'; -import { SystemInformation } from './system-information'; +import {Secret} from './secret'; +import {SystemInformation} from './system-information'; @Injectable({ providedIn: 'root' @@ -10,6 +11,7 @@ import { SystemInformation } from './system-information'; export class ApiService { private _deploymentStates: ReplaySubject> = new ReplaySubject>(1); + private _secrets: ReplaySubject> = new ReplaySubject>(1); constructor(private http: HttpClient) { } @@ -18,6 +20,10 @@ export class ApiService { return this._deploymentStates.asObservable(); } + get secrets$(): Observable> { + return this._secrets.asObservable(); + } + updateDeploymentStates(): void { this.getAllDeploymentStates().subscribe(r => this._deploymentStates.next(r)); } @@ -37,4 +43,22 @@ export class ApiService { startDeployment(deploymentState: Deployment, force: boolean): Observable { return this.http.post(`api/v1/deployments/${deploymentState.name}/start?forceRecreate=${force}`, {}); } + + getSecrets(): Observable> { + return this.http.get>('api/v1/secrets'); + } + + reloadSecrets(): void { + this.getSecrets().subscribe((r) => this._secrets.next(r)); + } + + putSecret(secret: Secret): Observable { + return this.http.put(`api/v1/secrets/${secret.key}`, secret).pipe( + tap(() => this.reloadSecrets()) + ); + } + + deleteSecret(secret: Secret): Observable { + return this.http.delete(`api/v1/secrets/${secret.key}`); + } } diff --git a/frontend/src/app/overview/deploymentState.d.ts b/frontend/src/app/api/deployment.d.ts similarity index 100% rename from frontend/src/app/overview/deploymentState.d.ts rename to frontend/src/app/api/deployment.d.ts diff --git a/frontend/src/app/api/secret.d.ts b/frontend/src/app/api/secret.d.ts new file mode 100644 index 0000000..5fe4817 --- /dev/null +++ b/frontend/src/app/api/secret.d.ts @@ -0,0 +1,6 @@ +export interface Secret { + key: string | null; + value: string | null; + environmentVariable: string; + deployments: Array; +} diff --git a/frontend/src/app/overview/system-information.d.ts b/frontend/src/app/api/system-information.d.ts similarity index 100% rename from frontend/src/app/overview/system-information.d.ts rename to frontend/src/app/api/system-information.d.ts diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 7f2200e..aee6c26 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -3,5 +3,8 @@ Icon Dirigent - + + + + diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9db3c14..fb2e037 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,9 +1,11 @@ import {Component} from '@angular/core'; -import {RouterOutlet} from '@angular/router'; +import {MatTab, MatTabGroup} from '@angular/material/tabs'; +import {DeploymentsComponent} from './deployments/deployments.component'; +import {SecretsComponent} from './secrets/secrets.component'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [MatTabGroup, MatTab, DeploymentsComponent, SecretsComponent], templateUrl: './app.component.html', styleUrl: './app.component.css' }) diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index fe4326c..1168fbd 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,9 +1,9 @@ import {Routes} from '@angular/router'; -import {OverviewComponent} from './overview/overview.component'; +import {DeploymentsComponent} from './deployments/deployments.component'; export const routes: Routes = [ { path: '', - component: OverviewComponent + component: DeploymentsComponent } ]; diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/deployments/deployments.component.css similarity index 100% rename from frontend/src/app/app.component.spec.ts rename to frontend/src/app/deployments/deployments.component.css diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/deployments/deployments.component.html similarity index 100% rename from frontend/src/app/overview/overview.component.html rename to frontend/src/app/deployments/deployments.component.html diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/deployments/deployments.component.ts similarity index 93% rename from frontend/src/app/overview/overview.component.ts rename to frontend/src/app/deployments/deployments.component.ts index 703eb26..0b37522 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/deployments/deployments.component.ts @@ -11,8 +11,8 @@ import { MatRowDef, MatTable } from '@angular/material/table'; -import {Deployment} from './deploymentState'; -import {ApiService} from './api.service'; +import {Deployment} from '../api/deployment'; +import {ApiService} from '../api/api.service'; import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu'; import {MatIcon} from '@angular/material/icon'; import {MatChip, MatChipListbox, MatChipListboxChange, MatChipOption} from '@angular/material/chips'; @@ -25,10 +25,10 @@ import {AsyncPipe} from '@angular/common'; import {FormsModule} from '@angular/forms'; import {MatSort, MatSortHeader, Sort} from '@angular/material/sort'; import {MatFormField, MatInput, MatLabel} from '@angular/material/input'; -import { SystemInformation } from './system-information'; +import { SystemInformation } from '../api/system-information'; @Component({ - selector: 'app-overview', + selector: 'app-deployments', imports: [ MatTable, MatColumnDef, @@ -56,10 +56,10 @@ import { SystemInformation } from './system-information'; MatInput, MatFormField ], - templateUrl: './overview.component.html', - styleUrl: './overview.component.css', + templateUrl: './deployments.component.html', + styleUrl: './deployments.component.css', }) -export class OverviewComponent implements OnInit { +export class DeploymentsComponent implements OnInit { selectedFilterValues$ = new ReplaySubject>(1); sort$ = new ReplaySubject(1); diff --git a/frontend/src/app/overview/start-dialog/start-dialog.component.css b/frontend/src/app/deployments/start-dialog/start-dialog.component.css similarity index 100% rename from frontend/src/app/overview/start-dialog/start-dialog.component.css rename to frontend/src/app/deployments/start-dialog/start-dialog.component.css diff --git a/frontend/src/app/overview/start-dialog/start-dialog.component.html b/frontend/src/app/deployments/start-dialog/start-dialog.component.html similarity index 100% rename from frontend/src/app/overview/start-dialog/start-dialog.component.html rename to frontend/src/app/deployments/start-dialog/start-dialog.component.html diff --git a/frontend/src/app/overview/start-dialog/start-dialog.component.ts b/frontend/src/app/deployments/start-dialog/start-dialog.component.ts similarity index 100% rename from frontend/src/app/overview/start-dialog/start-dialog.component.ts rename to frontend/src/app/deployments/start-dialog/start-dialog.component.ts diff --git a/frontend/src/app/overview/overview.component.css b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.css similarity index 100% rename from frontend/src/app/overview/overview.component.css rename to frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.css diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html new file mode 100644 index 0000000..80a2ae4 --- /dev/null +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html @@ -0,0 +1,59 @@ + + + @if (originalSecret.key) { +

Edit Secret

+ } + @if (!originalSecret.key) { + + Key + + + } + + + + Environment Variable + + + + + + Value + + + + + + + Deployments + + @for (deployment of secret.deployments; track deployment) { + + {{deployment}} + + + } + + + +
+ + + + + @if (originalSecret.key && !sureDelete) { + + } + @if (originalSecret.key && sureDelete) { + + } + + diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts new file mode 100644 index 0000000..7676c47 --- /dev/null +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts @@ -0,0 +1,78 @@ +import {ChangeDetectionStrategy, Component, inject, Inject} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef} from '@angular/material/dialog'; +import {Secret} from '../../api/secret'; +import {FormsModule} from '@angular/forms'; +import {MatFormField, MatInput, MatLabel} from '@angular/material/input'; +import {MatButton} from '@angular/material/button'; +import {MatChipGrid, MatChipInput, MatChipInputEvent, MatChipRow} from '@angular/material/chips'; +import {ENTER, SPACE} from '@angular/cdk/keycodes'; +import {MatIcon} from '@angular/material/icon'; +import {ApiService} from '../../api/api.service'; + +@Component({ + selector: 'app-edit-secret-dialog', + imports: [ + FormsModule, + MatInput, + MatFormField, + MatLabel, + MatButton, + MatDialogContent, + MatDialogActions, + MatChipGrid, + MatChipRow, + MatIcon, + MatChipInput + ], + templateUrl: './edit-secret-dialog.component.html', + styleUrl: './edit-secret-dialog.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EditSecretDialogComponent { + + protected readonly ENTER = ENTER; + protected readonly SPACE = SPACE; + readonly dialogRef = inject(MatDialogRef); + secret: Secret; + originalSecret: Secret; + sureDelete: boolean = false; + + constructor(@Inject(MAT_DIALOG_DATA) public data: Secret, private apiService: ApiService) { + this.secret = structuredClone(data); + this.originalSecret = structuredClone(data); + } + + changed(): boolean { + return JSON.stringify(this.secret) !== JSON.stringify(this.originalSecret); + } + + delete() { + + if (!this.sureDelete) { + this.sureDelete = true; + return; + } + this.apiService.deleteSecret(this.secret).subscribe(() => this.dialogRef.close()); + } + + save() { + this.apiService.putSecret(this.secret).subscribe(() => this.dialogRef.close()); + } + + cancel() { + this.dialogRef.close(); + } + + removeDeployment(deployment: string) { + this.secret.deployments = this.secret.deployments.filter(d => d !== deployment); + } + + addDeployment($event: MatChipInputEvent) { + + $event.chipInput.clear(); + + if (this.secret.deployments.includes($event.value)) return; + + this.secret.deployments.push($event.value); + } +} diff --git a/frontend/src/app/secrets/secrets.component.css b/frontend/src/app/secrets/secrets.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/app/secrets/secrets.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/secrets/secrets.component.html b/frontend/src/app/secrets/secrets.component.html new file mode 100644 index 0000000..f0fa15b --- /dev/null +++ b/frontend/src/app/secrets/secrets.component.html @@ -0,0 +1,45 @@ +

Your Secrets

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Actions + + Name {{ element.key }}Environment Variable {{ element.environmentVariable }}Deployments + + + @for (deployment of element.deployments; track deployment) { + + {{deployment}} + + } + + +
diff --git a/frontend/src/app/secrets/secrets.component.ts b/frontend/src/app/secrets/secrets.component.ts new file mode 100644 index 0000000..803dfd7 --- /dev/null +++ b/frontend/src/app/secrets/secrets.component.ts @@ -0,0 +1,74 @@ +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {ApiService} from '../api/api.service'; +import {Observable} from 'rxjs'; +import {Secret} from '../api/secret'; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatHeaderCellDef, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, + MatTable +} from '@angular/material/table'; +import {MatIcon} from '@angular/material/icon'; +import {MatChip, MatChipSet} from '@angular/material/chips'; +import {MatButton, MatIconButton} from '@angular/material/button'; +import {MatDialog} from '@angular/material/dialog'; +import {EditSecretDialogComponent} from './edit-secret-dialog/edit-secret-dialog.component'; + +@Component({ + selector: 'app-secrets', + imports: [ + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatIcon, + MatTable, + MatHeaderCellDef, + MatHeaderRowDef, + MatRowDef, + MatHeaderRow, + MatRow, + MatIconButton, + MatChipSet, + MatChip, + MatButton, + ], + templateUrl: './secrets.component.html', + styleUrl: './secrets.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SecretsComponent { + + secrets$: Observable>; + displayedColumns = ['actions', 'key', 'environmentVariable', 'deployments']; + + readonly dialog = inject(MatDialog); + + constructor(private apiService: ApiService) { + this.apiService.reloadSecrets(); + this.secrets$ = apiService.secrets$; + } + + edit(element: Secret) { + this.dialog.open(EditSecretDialogComponent, { + data: element + }); + } + + add() { + this.dialog.open(EditSecretDialogComponent, { + data: { + key: null, + value: null, + environmentVariable: '', + deployments: [] + } as Secret, + }); + } +} From c332e7f697c1ed216c4721fce08e55b895decc71 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 00:33:40 +0200 Subject: [PATCH 12/25] Reload after deletion --- frontend/src/app/api/api.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/api/api.service.ts b/frontend/src/app/api/api.service.ts index cf4fb28..4d9cbc7 100644 --- a/frontend/src/app/api/api.service.ts +++ b/frontend/src/app/api/api.service.ts @@ -59,6 +59,8 @@ export class ApiService { } deleteSecret(secret: Secret): Observable { - return this.http.delete(`api/v1/secrets/${secret.key}`); + return this.http.delete(`api/v1/secrets/${secret.key}`).pipe( + tap(() => this.reloadSecrets()) + ); } } From 0a8a94b737ee79071dc6177ec326f0e4b1fe3622 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 00:40:55 +0200 Subject: [PATCH 13/25] Reload after deletion - mobile friendly --- .../edit-secret-dialog/edit-secret-dialog.component.html | 7 +++++-- .../edit-secret-dialog/edit-secret-dialog.component.ts | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html index 80a2ae4..9908d6e 100644 --- a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html @@ -29,17 +29,20 @@ @for (deployment of secret.deployments; track deployment) { + [removable]="true" + (removed)="removeDeployment(deployment)"> + {{deployment}} + } diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts index 7676c47..5ac5967 100644 --- a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts @@ -4,7 +4,7 @@ import {Secret} from '../../api/secret'; import {FormsModule} from '@angular/forms'; import {MatFormField, MatInput, MatLabel} from '@angular/material/input'; import {MatButton} from '@angular/material/button'; -import {MatChipGrid, MatChipInput, MatChipInputEvent, MatChipRow} from '@angular/material/chips'; +import {MatChipGrid, MatChipInput, MatChipInputEvent, MatChipRemove, MatChipRow} from '@angular/material/chips'; import {ENTER, SPACE} from '@angular/cdk/keycodes'; import {MatIcon} from '@angular/material/icon'; import {ApiService} from '../../api/api.service'; @@ -22,7 +22,8 @@ import {ApiService} from '../../api/api.service'; MatChipGrid, MatChipRow, MatIcon, - MatChipInput + MatChipInput, + MatChipRemove ], templateUrl: './edit-secret-dialog.component.html', styleUrl: './edit-secret-dialog.component.css', From 08412fe5e9d129e9dfb730ed6d9ea3fc4d9fcecb Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 00:47:56 +0200 Subject: [PATCH 14/25] Update Deletion --- .../java/org/davidbohl/dirigent/sercrets/SecretService.java | 3 +++ .../secrets/edit-secret-dialog/edit-secret-dialog.component.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java index d0cf96d..cbed9f8 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -38,6 +38,9 @@ public class SecretService { Secret secret = secretRepository.findById(key).orElseGet(() -> new Secret(key, environmentVariable, value, deployments)); + secret.setDeployments(deployments); + secret.setEnvironmentVariable(environmentVariable); + if(value != null ) secret.setEncryptedValue(encrypt(value)); diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts index 5ac5967..76cb63f 100644 --- a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts @@ -70,6 +70,8 @@ export class EditSecretDialogComponent { addDeployment($event: MatChipInputEvent) { + if($event.value.trim().length === 0) return; + $event.chipInput.clear(); if (this.secret.deployments.includes($event.value)) return; From 020df6b718938c3d68d05fa82e788fb7fb580d24 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 14:56:59 +0200 Subject: [PATCH 15/25] Simplify Deployment Name input for Secrets --- frontend/package-lock.json | 29 ++-------- frontend/proxy.conf.json | 2 +- .../edit-secret-dialog.component.html | 10 +++- .../edit-secret-dialog.component.ts | 53 ++++++++++++++++--- 4 files changed, 60 insertions(+), 34 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 14226b7..c31d080 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -443,7 +443,6 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.19.tgz", "integrity": "sha512-PCpJagurPBqciqcq4Z8+3OtKLb7rSl4w/qBJoIMua8CgnrjvA1i+SWawhdtfI1zlY8FSwhzLwXV0CmWWfFzQPg==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^7.1.2", "tslib": "^2.3.0" @@ -493,7 +492,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.15.tgz", "integrity": "sha512-aVa/ctBYH/4qgA7r4sS7TV+/DzRYmcS+3d6l89pNKUXkI8gpmsd+r3FjccaemX4Wqru1QOrMvC+i+e7IBIVv0g==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -510,7 +508,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.15.tgz", "integrity": "sha512-hMHZU6/03xG0tbPDIm1hbVSTFLnRkGYfh+xdBwUMnIFYYTS0QJ2hdPfEZKCJIXm+fz9IAI5MPdDTfeyp0sgaHQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -524,7 +521,6 @@ "integrity": "sha512-4r5tvGA2Ok3o8wROZBkF9qNKS7L0AEpdBIkAVJbLw2rBY2SlyycFIRYyV2+D1lJ1jq/f9U7uN6oon0MjTvNYkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -601,7 +597,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.15.tgz", "integrity": "sha512-PxhzCwwm23N4Mq6oV7UPoYiJF4r6FzGhRSxOBBlEp322k7zEQbIxd/XO6F3eoG73qC1UsOXMYYv6GnQpx42y3A==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -618,7 +613,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.15.tgz", "integrity": "sha512-pZDElcYPmNzPxvWJpZQCIizsNApDIfk9xLJE4I8hzLISfWGbQvfjuuarDAuQZEXudeLXoDOstDXkDja40muLGg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -654,7 +648,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.15.tgz", "integrity": "sha512-OelQ6weCjon8kZD8kcqNzwugvZJurjS3uMJCwsA2vXmP/3zJ31SWtNqE2zLT1R2csVuwnp0h+nRMgq+pINU7Rg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -739,7 +732,6 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -3038,7 +3030,6 @@ "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.1.2", "@inquirer/confirm": "^5.1.6", @@ -5490,7 +5481,6 @@ "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.8.0" } @@ -5866,7 +5856,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6330,7 +6319,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -9319,8 +9307,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.11.0.tgz", "integrity": "sha512-MPJ8L5yyNul0F2SuEsLASwESXQjJvBXnKu31JWFyRZSvuv2B79K4GDWN3pSqvLheUNh7Fyb6dXwd4rsz95O2Kg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-worker": { "version": "27.5.1", @@ -9465,7 +9452,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -9824,7 +9810,6 @@ "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -11968,7 +11953,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12628,7 +12612,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13764,7 +13747,6 @@ "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -13917,8 +13899,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "3.0.1", @@ -13975,7 +13956,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14656,7 +14636,6 @@ "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -14734,7 +14713,6 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -15301,8 +15279,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json index 47e7bcd..82ccbdd 100644 --- a/frontend/proxy.conf.json +++ b/frontend/proxy.conf.json @@ -1,6 +1,6 @@ { "/api/**": { - "target": "http://localhost:8080", + "target": "http://services01:8080", "secure": false, "changeOrigin": true, "pathRewrite": { diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html index 9908d6e..8bad96b 100644 --- a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html @@ -40,11 +40,19 @@ } + + @for (deploymentName of $deploymentNames | async; track deploymentName) { + {{deploymentName}} + } + diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts index 76cb63f..b8796a3 100644 --- a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts @@ -8,6 +8,15 @@ import {MatChipGrid, MatChipInput, MatChipInputEvent, MatChipRemove, MatChipRow} import {ENTER, SPACE} from '@angular/cdk/keycodes'; import {MatIcon} from '@angular/material/icon'; import {ApiService} from '../../api/api.service'; +import {Observable, ReplaySubject} from 'rxjs'; +import {map, switchMap} from 'rxjs/operators'; +import { + MatAutocomplete, + MatAutocompleteSelectedEvent, + MatAutocompleteTrigger, + MatOption +} from '@angular/material/autocomplete'; +import {AsyncPipe} from '@angular/common'; @Component({ selector: 'app-edit-secret-dialog', @@ -23,7 +32,11 @@ import {ApiService} from '../../api/api.service'; MatChipRow, MatIcon, MatChipInput, - MatChipRemove + MatChipRemove, + MatAutocompleteTrigger, + MatAutocomplete, + AsyncPipe, + MatOption ], templateUrl: './edit-secret-dialog.component.html', styleUrl: './edit-secret-dialog.component.css', @@ -37,10 +50,24 @@ export class EditSecretDialogComponent { secret: Secret; originalSecret: Secret; sureDelete: boolean = false; + $deploymentNames: Observable>; + $deploymentNamesFilter: ReplaySubject = new ReplaySubject(1); constructor(@Inject(MAT_DIALOG_DATA) public data: Secret, private apiService: ApiService) { this.secret = structuredClone(data); this.originalSecret = structuredClone(data); + + this.$deploymentNames = apiService.deploymentStates$.pipe( + switchMap(ds => this.$deploymentNamesFilter.pipe(map(filter => ds.filter(ds => ds.name.toLowerCase().includes(filter.toLowerCase()))))), + map(ds => + ds.filter(ds => !this.secret.deployments.includes(ds.name)) + .map(ds => ds.name) + .sort((a, b) => a.localeCompare(b)) + ) + + ); + + this.$deploymentNamesFilter.next(''); } changed(): boolean { @@ -68,14 +95,28 @@ export class EditSecretDialogComponent { this.secret.deployments = this.secret.deployments.filter(d => d !== deployment); } - addDeployment($event: MatChipInputEvent) { - - if($event.value.trim().length === 0) return; + addDeploymentFromInput($event: MatChipInputEvent) { + const value = $event.value; $event.chipInput.clear(); + this.addDeployment(value); + } - if (this.secret.deployments.includes($event.value)) return; + private addDeployment(value: string) { + if (value.trim().length === 0) return; - this.secret.deployments.push($event.value); + + if (this.secret.deployments.includes(value)) return; + + this.secret.deployments.push(value); + } + + addDeploymentFromAutoComplete($event: MatAutocompleteSelectedEvent) { + this.addDeployment($event.option.viewValue); + $event.option.deselect(); + } + + filterDeployments($event: Event) { + this.$deploymentNamesFilter.next(($event.target as HTMLInputElement).value); } } From 147210d1aa4a56dee1c887b94959c4cde3f1a690 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 15:11:41 +0200 Subject: [PATCH 16/25] Recreate Deployments when Secrets change --- ...leNamedDeploymentsStartRequestedEvent.java | 18 +++++++ .../management/DeploymentsService.java | 50 +++++++++++++------ .../dirigent/sercrets/SecretService.java | 20 ++++++-- 3 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 backend/src/main/java/org/davidbohl/dirigent/deployments/events/MultipleNamedDeploymentsStartRequestedEvent.java diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/events/MultipleNamedDeploymentsStartRequestedEvent.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/events/MultipleNamedDeploymentsStartRequestedEvent.java new file mode 100644 index 0000000..9e8a122 --- /dev/null +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/events/MultipleNamedDeploymentsStartRequestedEvent.java @@ -0,0 +1,18 @@ +package org.davidbohl.dirigent.deployments.events; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +import java.util.List; + +@Getter +public class MultipleNamedDeploymentsStartRequestedEvent extends ApplicationEvent { + private final List names; + private final boolean forceRecreate; + + public MultipleNamedDeploymentsStartRequestedEvent(Object source, List names, boolean forceRecreate) { + super(source); + this.names = names; + this.forceRecreate = forceRecreate; + } +} 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 9045e52..6d2f063 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 @@ -40,9 +40,9 @@ public class DeploymentsService { public DeploymentsService( DeploymentsConfigurationProvider deploymentsConfigurationProvider, - GitService gitService, - ApplicationEventPublisher applicationEventPublisher, - DeploymentStatePersistingService deploymentStatePersistingService, + GitService gitService, + ApplicationEventPublisher applicationEventPublisher, + DeploymentStatePersistingService deploymentStatePersistingService, SecretService secretService) { this.deploymentsConfigurationProvider = deploymentsConfigurationProvider; this.gitService = gitService; @@ -67,6 +67,26 @@ public class DeploymentsService { throw new DeploymentsDirCouldNotBeCreatedException(); } + @EventListener(MultipleNamedDeploymentsStartRequestedEvent.class) + public void onMultipleNamedDeploymentsStartRequested(MultipleNamedDeploymentsStartRequestedEvent event) { + makeDeploymentsDir(); + DeploynentConfiguration deploynentConfiguration = tryGetConfiguration(); + + List relevantDeploymentStates = deploymentStatePersistingService.getDeploymentStates().stream() + .filter(ds -> event.getNames().contains(ds.getName()) && + (ds.getState() != DeploymentState.State.STOPPED && ds.getState() != DeploymentState.State.REMOVED)).toList(); + + List deployments = deploynentConfiguration.deployments().stream().filter( + d -> relevantDeploymentStates.stream().anyMatch(ds -> Objects.equals(ds.getName(), d.name())) + ).toList(); + + for (Deployment deployment : deployments) { + deploy(deployment, event.isForceRecreate()); + } + + + } + @EventListener(NamedDeploymentStartRequestedEvent.class) public void onNamedDeploymentStartRequested(NamedDeploymentStartRequestedEvent event) { makeDeploymentsDir(); @@ -128,17 +148,17 @@ public class DeploymentsService { logger.info("Deploying {}", deployment.name()); File deploymentDir = new File("deployments/" + deployment.name()); - + try { boolean updated = gitService.updateRepo(deployment.source(), deploymentDir.getAbsolutePath()); - Optional optionalState= deploymentStatePersistingService.getDeploymentStates().stream() - .filter(state -> state.getName().equals(deployment.name())) - .findFirst(); + Optional optionalState = deploymentStatePersistingService.getDeploymentStates().stream() + .filter(state -> state.getName().equals(deployment.name())) + .findFirst(); boolean deployWouldChangeState = optionalState.isEmpty() || optionalState.get().getState() != DeploymentState.State.RUNNING; - + if (!updated && !forceRecreate && !deployWouldChangeState) { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.RUNNING, "Deployment '%s' successfully started".formatted(deployment.name()))); logger.info("No update, forced recreation or changed states in deployment. Skipping {}", deployment.name()); @@ -147,7 +167,7 @@ public class DeploymentsService { if (updated) { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.UPDATED, "Deployment '%s' updated".formatted(deployment.name()))); - } + } applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deployment.name(), DeploymentState.State.STARTING, "Starting Deployment '%s'".formatted(deployment.name()))); List commandArgs = new java.util.ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList()); @@ -214,16 +234,16 @@ public class DeploymentsService { logger.info("Stopping deployment {}", deploymentName); - Optional optionalState= deploymentStatePersistingService.getDeploymentStates().stream() - .filter(state -> state.getName().equals(deploymentName)) - .findFirst(); + Optional optionalState = deploymentStatePersistingService.getDeploymentStates().stream() + .filter(state -> state.getName().equals(deploymentName)) + .findFirst(); boolean stopWouldChangeState = optionalState.isEmpty() || optionalState.get().getState() != DeploymentState.State.STOPPED; - - if(stopWouldChangeState) { + + if (stopWouldChangeState) { applicationEventPublisher.publishEvent(new DeploymentStateEvent(this, deploymentName, DeploymentState.State.STOPPING, "Stopping deployment '%s'".formatted(deploymentName))); } - + List commandArgs = new ArrayList<>(Arrays.stream(composeCommand.split(" ")).toList()); commandArgs.add("down"); new ProcessBuilder(commandArgs) diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java index cbed9f8..c32c47c 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -1,14 +1,13 @@ package org.davidbohl.dirigent.sercrets; -import java.util.Base64; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; +import org.davidbohl.dirigent.deployments.events.MultipleNamedDeploymentsStartRequestedEvent; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import lombok.extern.slf4j.Slf4j; @@ -22,8 +21,10 @@ public class SecretService { private final String encryptionKey; private final SecretRepository secretRepository; + private final ApplicationEventPublisher applicationEventPublisher; - public SecretService(@Value("${dirigent.secrets.encryption.key}") String encryptionKey, SecretRepository secretRepository) { + public SecretService(@Value("${dirigent.secrets.encryption.key}") String encryptionKey, SecretRepository secretRepository, ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; if (encryptionKey == null || encryptionKey.length() != 16) { throw new IllegalArgumentException("SECRET_ENCRYPTION_KEY muss 16 Zeichen lang sein!<" + encryptionKey + ">"); @@ -45,6 +46,9 @@ public class SecretService { secret.setEncryptedValue(encrypt(value)); secretRepository.save(secret); + + applicationEventPublisher.publishEvent(new MultipleNamedDeploymentsStartRequestedEvent(this, secret.getDeployments(), true)); + } catch (Exception e) { throw new RuntimeException("Saving Secret failed", e); } @@ -73,7 +77,13 @@ public class SecretService { } public void deleteSecret(String key) { + Optional byId = this.secretRepository.findById(key); + + if(byId.isEmpty()) return; + + Secret secret = byId.get(); secretRepository.deleteById(key); + applicationEventPublisher.publishEvent(new MultipleNamedDeploymentsStartRequestedEvent(this, secret.getDeployments(), true)); } private String encrypt(String value) throws Exception { From bd6e1c8d1d5aa7746fffd352c7681a2312b588c2 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 15:32:05 +0200 Subject: [PATCH 17/25] Recreate Deployments when Secrets change --- .../dirigent/deployments/management/DeploymentsService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 6d2f063..4731c13 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 @@ -72,7 +72,8 @@ public class DeploymentsService { makeDeploymentsDir(); DeploynentConfiguration deploynentConfiguration = tryGetConfiguration(); - List relevantDeploymentStates = deploymentStatePersistingService.getDeploymentStates().stream() + List deploymentStates = deploymentStatePersistingService.getDeploymentStates(); + List relevantDeploymentStates = deploymentStates.stream() .filter(ds -> event.getNames().contains(ds.getName()) && (ds.getState() != DeploymentState.State.STOPPED && ds.getState() != DeploymentState.State.REMOVED)).toList(); From c57f232cb902709eb5821e9d0f8de07fb68bcebb Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 15:43:38 +0200 Subject: [PATCH 18/25] Recreate Deployments when Secrets change --- .../state/DeploymentStatePersistingService.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java index 8115226..e90b77d 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java @@ -6,6 +6,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.StreamSupport; @@ -36,8 +37,15 @@ public class DeploymentStatePersistingService { } public List getDeploymentStates() { - return StreamSupport.stream(deploymentStateRepository.findAll().spliterator(), false) - .toList(); + Iterable all = deploymentStateRepository.findAll(); + + List result = new ArrayList<>(); + + for (DeploymentState deploymentState : all) { + result.add(deploymentState); + } + + return result; } } From 98a2b37afb7b7e96f5f29b6ddf2695af4d9a0d4d Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Fri, 3 Oct 2025 16:04:07 +0200 Subject: [PATCH 19/25] try fix --- .../dirigent/deployments/management/DeploymentsService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 4731c13..b6ffabd 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 @@ -72,10 +72,12 @@ public class DeploymentsService { makeDeploymentsDir(); DeploynentConfiguration deploynentConfiguration = tryGetConfiguration(); + List deploymentStates = deploymentStatePersistingService.getDeploymentStates(); List relevantDeploymentStates = deploymentStates.stream() - .filter(ds -> event.getNames().contains(ds.getName()) && - (ds.getState() != DeploymentState.State.STOPPED && ds.getState() != DeploymentState.State.REMOVED)).toList(); + .filter(ds -> event.getNames().stream().anyMatch(n -> n.equals(ds.getName()) && + (ds.getState() != DeploymentState.State.STOPPED && ds.getState() != DeploymentState.State.REMOVED))) + .toList(); List deployments = deploynentConfiguration.deployments().stream().filter( d -> relevantDeploymentStates.stream().anyMatch(ds -> Objects.equals(ds.getName(), d.name())) From e91ccb5df822df8a2b31857eee36d0006ec4bc3a Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Sat, 4 Oct 2025 14:18:40 +0200 Subject: [PATCH 20/25] Fixed Deltion of Secrets --- .../dirigent/deployments/state/DeploymentState.java | 1 + .../state/DeploymentStatePersistingService.java | 1 - .../java/org/davidbohl/dirigent/sercrets/Secret.java | 7 ++++--- .../davidbohl/dirigent/sercrets/SecretService.java | 12 +++++------- backend/src/main/resources/application.properties | 2 +- frontend/proxy.conf.json | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentState.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentState.java index b92a4fc..cea5308 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentState.java +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentState.java @@ -12,6 +12,7 @@ import lombok.Setter; @Setter @Entity public class DeploymentState { + @Id private String name; diff --git a/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java b/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java index e90b77d..5c03aa3 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/deployments/state/DeploymentStatePersistingService.java @@ -9,7 +9,6 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.stream.StreamSupport; @Service public class DeploymentStatePersistingService { diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java index de85369..05c065d 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/Secret.java @@ -1,15 +1,16 @@ package org.davidbohl.dirigent.sercrets; -import java.util.List; - import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.List; + @Getter @Setter @AllArgsConstructor @@ -23,7 +24,7 @@ public class Secret { private String encryptedValue; - @ElementCollection + @ElementCollection(fetch = FetchType.EAGER) private List deployments; } \ No newline at end of file diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java index c32c47c..f3d29ab 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -1,16 +1,14 @@ package org.davidbohl.dirigent.sercrets; -import java.util.*; - -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; - +import lombok.extern.slf4j.Slf4j; import org.davidbohl.dirigent.deployments.events.MultipleNamedDeploymentsStartRequestedEvent; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; -import lombok.extern.slf4j.Slf4j; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.util.*; @Service @@ -27,7 +25,7 @@ public class SecretService { this.applicationEventPublisher = applicationEventPublisher; if (encryptionKey == null || encryptionKey.length() != 16) { - throw new IllegalArgumentException("SECRET_ENCRYPTION_KEY muss 16 Zeichen lang sein!<" + encryptionKey + ">"); + throw new IllegalArgumentException("SECRET_ENCRYPTION_KEY must have a length of 16 characters!<" + encryptionKey + ">"); } this.encryptionKey = encryptionKey; diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index d3464f5..66ab50b 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=dirigent spring.profiles.active=local -spring.datasource.url=jdbc:sqlite:./data/dirigent.db +spring.datasource.url=jdbc:sqlite:data/dirigent.db spring.datasource.driver-class-name=org.sqlite.JDBC spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect spring.jpa.hibernate.ddl-auto=update diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json index 82ccbdd..47e7bcd 100644 --- a/frontend/proxy.conf.json +++ b/frontend/proxy.conf.json @@ -1,6 +1,6 @@ { "/api/**": { - "target": "http://services01:8080", + "target": "http://localhost:8080", "secure": false, "changeOrigin": true, "pathRewrite": { From d6263148af14d182d5072767f76a741f96180bbf Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Sat, 4 Oct 2025 15:22:05 +0200 Subject: [PATCH 21/25] upadted README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e12a3fd..7cd7635 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,10 @@ Store all your repositories for one host in one gitea organization. This way you `GET` to `/api/v1/deployment-states` +### Secrets + +TODO ;) + ## Develop ### Setup for local Tests From 3a0afdf3a5e933fbc63df7a2d076fc44f15a8d05 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Sat, 4 Oct 2025 15:26:20 +0200 Subject: [PATCH 22/25] upadted README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7cd7635..cb5a44e 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ Dirigent simplifies **GitOps for Docker Compose deployments** by automating the - **REST API**: Integrate with existing tools (CI/CD, monitoring). - **Gotify notifications**: Get alerts when deployments fail. +### Encrypted Secrets +- You can store encrypted Secrets that are injected as Environment Variable on execution + Ideal for Self-hosters managing multiple services (e.g., Nextcloud, Gitea, Vaultwarden). ## Setup From 8aa396c4aacdaf248735386c796b7f83668f8a94 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Sun, 5 Oct 2025 17:30:40 +0200 Subject: [PATCH 23/25] upadted README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb5a44e..a827049 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Dirigent simplifies **GitOps for Docker Compose deployments** by automating the - **REST API**: Integrate with existing tools (CI/CD, monitoring). - **Gotify notifications**: Get alerts when deployments fail. -### Encrypted Secrets +### 🔒 Encrypted Secrets - You can store encrypted Secrets that are injected as Environment Variable on execution Ideal for Self-hosters managing multiple services (e.g., Nextcloud, Gitea, Vaultwarden). From 804367b2f4f0e8e49a0731697101f590e371c4c0 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 6 Oct 2025 08:54:48 +0200 Subject: [PATCH 24/25] Restart Secrets that have been removed from a Secret --- .../dirigent/sercrets/SecretController.java | 9 ++++---- .../dirigent/sercrets/SecretService.java | 23 +++++++++++++++---- frontend/src/app/api/api.service.ts | 8 +++---- .../edit-secret-dialog.component.html | 2 ++ .../edit-secret-dialog.component.ts | 11 +++++---- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java index 50ac049..fc18b85 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import lombok.extern.slf4j.Slf4j; @@ -25,13 +26,13 @@ public class SecretController { } @PutMapping("{key}") - public void saveSecret(@RequestBody SecretDto secret, @PathVariable String key) { - this.secretService.saveSecret(key, secret.environmentVariable(), secret.value(), secret.deployments()); + public void saveSecret(@RequestBody SecretDto secret, @PathVariable String key, @RequestParam(required = false, defaultValue = "false") boolean restartDeployments) { + this.secretService.saveSecret(key, secret.environmentVariable(), secret.value(), secret.deployments(), restartDeployments); } @DeleteMapping("{key}") - public void deleteSecret(@PathVariable String key) { - this.secretService.deleteSecret(key); + public void deleteSecret(@PathVariable String key, @RequestParam(required = false, defaultValue = "false") boolean restartDeployments) { + this.secretService.deleteSecret(key, restartDeployments); } @GetMapping diff --git a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java index f3d29ab..a519866 100644 --- a/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java +++ b/backend/src/main/java/org/davidbohl/dirigent/sercrets/SecretService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.util.*; +import java.util.stream.Stream; @Service @@ -32,11 +33,13 @@ public class SecretService { this.secretRepository = secretRepository; } - public void saveSecret(String key, String environmentVariable, String value, List deployments) { + public void saveSecret(String key, String environmentVariable, String value, List deployments, boolean restartDeployments) { try { Secret secret = secretRepository.findById(key).orElseGet(() -> new Secret(key, environmentVariable, value, deployments)); + List oldDeployments = new ArrayList<>(secret.getDeployments()); + secret.setDeployments(deployments); secret.setEnvironmentVariable(environmentVariable); @@ -45,7 +48,15 @@ public class SecretService { secretRepository.save(secret); - applicationEventPublisher.publishEvent(new MultipleNamedDeploymentsStartRequestedEvent(this, secret.getDeployments(), true)); + log.info("Saved Secret <{}>", key); + + if(restartDeployments) { + + List deploymentsToRestart = new HashSet( + Stream.concat(oldDeployments.stream(), deployments.stream()).toList()).stream().toList(); + + applicationEventPublisher.publishEvent(new MultipleNamedDeploymentsStartRequestedEvent(this, deploymentsToRestart, true)); + } } catch (Exception e) { throw new RuntimeException("Saving Secret failed", e); @@ -74,14 +85,18 @@ public class SecretService { ).toList(); } - public void deleteSecret(String key) { + public void deleteSecret(String key, boolean restartDeployments) { Optional byId = this.secretRepository.findById(key); if(byId.isEmpty()) return; Secret secret = byId.get(); secretRepository.deleteById(key); - applicationEventPublisher.publishEvent(new MultipleNamedDeploymentsStartRequestedEvent(this, secret.getDeployments(), true)); + + log.debug("Deleted Secret <{}>", key); + + if(restartDeployments) + applicationEventPublisher.publishEvent(new MultipleNamedDeploymentsStartRequestedEvent(this, secret.getDeployments(), true)); } private String encrypt(String value) throws Exception { diff --git a/frontend/src/app/api/api.service.ts b/frontend/src/app/api/api.service.ts index 4d9cbc7..4eb530c 100644 --- a/frontend/src/app/api/api.service.ts +++ b/frontend/src/app/api/api.service.ts @@ -52,14 +52,14 @@ export class ApiService { this.getSecrets().subscribe((r) => this._secrets.next(r)); } - putSecret(secret: Secret): Observable { - return this.http.put(`api/v1/secrets/${secret.key}`, secret).pipe( + putSecret(secret: Secret, restartDeployments: boolean): Observable { + return this.http.put(`api/v1/secrets/${secret.key}?restartDeployments=${restartDeployments}`, secret).pipe( tap(() => this.reloadSecrets()) ); } - deleteSecret(secret: Secret): Observable { - return this.http.delete(`api/v1/secrets/${secret.key}`).pipe( + deleteSecret(secret: Secret, restartDeployments: boolean): Observable { + return this.http.delete(`api/v1/secrets/${secret.key}?restartDeployments=${restartDeployments}`).pipe( tap(() => this.reloadSecrets()) ); } diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html index 8bad96b..c4d9799 100644 --- a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.html @@ -55,9 +55,11 @@ + + Restart Deployments @if (originalSecret.key && !sureDelete) { diff --git a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts index b8796a3..fad8913 100644 --- a/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts +++ b/frontend/src/app/secrets/edit-secret-dialog/edit-secret-dialog.component.ts @@ -17,6 +17,7 @@ import { MatOption } from '@angular/material/autocomplete'; import {AsyncPipe} from '@angular/common'; +import { MatCheckbox } from "@angular/material/checkbox"; @Component({ selector: 'app-edit-secret-dialog', @@ -36,8 +37,9 @@ import {AsyncPipe} from '@angular/common'; MatAutocompleteTrigger, MatAutocomplete, AsyncPipe, - MatOption - ], + MatOption, + MatCheckbox +], templateUrl: './edit-secret-dialog.component.html', styleUrl: './edit-secret-dialog.component.css', changeDetection: ChangeDetectionStrategy.OnPush @@ -50,6 +52,7 @@ export class EditSecretDialogComponent { secret: Secret; originalSecret: Secret; sureDelete: boolean = false; + restartDeployments: boolean = false; $deploymentNames: Observable>; $deploymentNamesFilter: ReplaySubject = new ReplaySubject(1); @@ -80,11 +83,11 @@ export class EditSecretDialogComponent { this.sureDelete = true; return; } - this.apiService.deleteSecret(this.secret).subscribe(() => this.dialogRef.close()); + this.apiService.deleteSecret(this.secret, this.restartDeployments).subscribe(() => this.dialogRef.close()); } save() { - this.apiService.putSecret(this.secret).subscribe(() => this.dialogRef.close()); + this.apiService.putSecret(this.secret, this.restartDeployments).subscribe(() => this.dialogRef.close()); } cancel() { From 18be7226de93136c1c84d3fc8283191876820e44 Mon Sep 17 00:00:00 2001 From: DerDavidBohl Date: Mon, 6 Oct 2025 10:40:03 +0200 Subject: [PATCH 25/25] Updated README.md --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a827049..f9ec938 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,28 @@ Store all your repositories for one host in one gitea organization. This way you ### Secrets -TODO ;) +#### Model + +```json +{ + "key": "string", + "value": "string", + "environmentVariable": "string", + "deployments": ["string"] +} +``` + +#### Get Secrets + +`GET` to `/api/v1/secrets` + +#### Save Secret + +`PUT` to `/api/v1/secrets/{key}` + +#### Delete Secret + +`DELETE` to `/api/v1/secrets/{iey}` ## Develop