Compare commits
25 Commits
a63540a01b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b1c6e54b1 | |||
| c4568a2f23 | |||
| 6000582d55 | |||
| 0a2937ad55 | |||
| 22a244832e | |||
| ef2b9667fc | |||
| c98900a9ff | |||
| 1684099f74 | |||
| 32cb5b57d7 | |||
| a65b2dbeb0 | |||
| 6037788dbd | |||
| aef24c809b | |||
| 706631c57b | |||
| 1ee7979091 | |||
| 12263fe0cb | |||
| 668ebfcc84 | |||
| ab0f9ceb50 | |||
| d4491a6c05 | |||
| 72a8501d37 | |||
| 66965e0ff2 | |||
| 21d99f9923 | |||
| 816177c2ec | |||
| dbdfea671e | |||
| cde10a4b3e | |||
| bc9c2da372 |
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM gradle:8.10.2-jdk21 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY build.gradle.kts settings.gradle.kts gradlew ./
|
||||||
|
COPY gradle ./gradle
|
||||||
|
|
||||||
|
RUN ./gradlew dependencies --no-daemon || true
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN ./gradlew clean bootJar --no-daemon
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||||
|
|
||||||
143
README.md
Normal file
143
README.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# Infisical ↔ Dokploy Bridge
|
||||||
|
|
||||||
|
A Spring Boot (Java 21) application acting as a secure bridge between Infisical and Dokploy, enabling automated synchronization and deployment of secrets through APIs and webhooks.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Secure integration with Infisical
|
||||||
|
- Automated updates via Dokploy API
|
||||||
|
- Webhook-driven synchronization
|
||||||
|
- Docker and Docker Compose ready
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
Infisical
|
||||||
|
↓ (Webhook / API)
|
||||||
|
Infisical–Dokploy Bridge (Spring Boot)
|
||||||
|
↓ (Dokploy API)
|
||||||
|
Dokploy
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Java 21
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- Infisical account
|
||||||
|
- Dokploy instance with API access
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Infisical
|
||||||
|
|
||||||
|
- INFISICAL_API_URL: Base URL of Infisical API
|
||||||
|
- INFISICAL_CLIENT_ID: Infisical service client ID
|
||||||
|
- INFISICAL_CLIENT_SECRET: Infisical service client secret
|
||||||
|
- INFISICAL_WEBHOOK_SECRET: Webhook signature validation secret
|
||||||
|
|
||||||
|
### Dokploy
|
||||||
|
|
||||||
|
- DOKPLOY_API_URL: Base URL of Dokploy API
|
||||||
|
- DOKPLOY_API_KEY: Dokploy API key
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
```txt
|
||||||
|
services:
|
||||||
|
infisical-bridge:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
INFISICAL_API_URL: ${INFISICAL_API_URL}
|
||||||
|
INFISICAL_CLIENT_ID: ${INFISICAL_CLIENT_ID}
|
||||||
|
INFISICAL_CLIENT_SECRET: ${INFISICAL_CLIENT_SECRET}
|
||||||
|
INFISICAL_WEBHOOK_SECRET: ${INFISICAL_WEBHOOK_SECRET}
|
||||||
|
DOKPLOY_API_URL: ${DOKPLOY_API_URL}
|
||||||
|
DOKPLOY_API_KEY: ${DOKPLOY_API_KEY}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
With Docker Compose:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Local development:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew bootRun
|
||||||
|
```
|
||||||
|
|
||||||
|
Application runs on http://localhost:8080
|
||||||
|
|
||||||
|
Use a service like ngrok.
|
||||||
|
|
||||||
|
## Infisical Webhook Configuration
|
||||||
|
|
||||||
|
When creating a webhook in Infisical, the following rules must be respected.
|
||||||
|
|
||||||
|
### Webhook URL Formats
|
||||||
|
|
||||||
|
Infisical bridge supports two webhook URL formats, depending on the Dokploy resource you want to update.
|
||||||
|
|
||||||
|
#### Dokploy Compose Webhook
|
||||||
|
|
||||||
|
`${INFISICAL_API_URL}/webhook?dokployComposeId=${DOKPLOY_COMPOSE_ID}`
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- dokployComposeId (required):
|
||||||
|
The identifier of the target Dokploy Compose.
|
||||||
|
This value is used to determine which Dokploy compose service should be updated when the webhook is triggered.
|
||||||
|
|
||||||
|
#### Dokploy Application Webhook
|
||||||
|
|
||||||
|
`${INFISICAL_API_URL}/webhook?dokployApplicationId=${DOKPLOY_APPLICATION_ID}`
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- dokployApplicationId (required):
|
||||||
|
The identifier of the target Dokploy Application.
|
||||||
|
This value is used to determine which Dokploy application should be updated when the webhook is triggered.
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
- Exactly one identifier must be provided per webhook URL.
|
||||||
|
- If no identifier or multiple identifiers are provided, the webhook request will be rejected.
|
||||||
|
- Ensure the provided ID matches an existing Dokploy resource.
|
||||||
|
|
||||||
|
### Webhook Secret
|
||||||
|
|
||||||
|
The webhook secret **must exactly match**:
|
||||||
|
|
||||||
|
`${INFISICAL_WEBHOOK_SECRET}`
|
||||||
|
|
||||||
|
Requests with an invalid or missing secret will be rejected.
|
||||||
|
|
||||||
|
## Webhooks Behavior
|
||||||
|
|
||||||
|
- Incoming webhook signatures are validated
|
||||||
|
- Secrets are fetched from Infisical
|
||||||
|
- Dokploy is updated using its API
|
||||||
|
- Invalid or unsigned requests are ignored
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Secrets are never persisted
|
||||||
|
- Configuration is environment-driven
|
||||||
|
- HTTPS is recommended in production
|
||||||
|
- Restrict network access to trusted sources only
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Java 21
|
||||||
|
- Spring Boot
|
||||||
|
- Gradle (Kotlin DSL)
|
||||||
|
- Docker / Docker Compose
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
@@ -25,51 +25,22 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extra["springCloudVersion"] = "2025.1.0"
|
extra["springCloudVersion"] = "2025.1.0"
|
||||||
extra["springModulithVersion"] = "2.0.1"
|
|
||||||
|
|
||||||
extra["infisicalVersion"] = "3.0.5"
|
extra["infisicalVersion"] = "3.0.5"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
// implementation("org.springframework.boot:spring-boot-starter-amqp")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-liquibase")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-quartz")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-restclient")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
||||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
|
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
|
||||||
implementation("org.springframework.modulith:spring-modulith-events-api")
|
|
||||||
implementation("org.springframework.modulith:spring-modulith-starter-core")
|
|
||||||
implementation("org.springframework.modulith:spring-modulith-starter-jpa")
|
|
||||||
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
|
|
||||||
compileOnly("org.projectlombok:lombok")
|
compileOnly("org.projectlombok:lombok")
|
||||||
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
|
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
|
||||||
runtimeOnly("org.springframework.modulith:spring-modulith-actuator")
|
|
||||||
// runtimeOnly("org.springframework.modulith:spring-modulith-events-amqp")
|
|
||||||
runtimeOnly("org.springframework.modulith:spring-modulith-observability")
|
|
||||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
annotationProcessor("org.projectlombok:lombok")
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
|
||||||
// testImplementation("org.springframework.boot:spring-boot-starter-amqp-test")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-liquibase-test")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-mail-test")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-quartz-test")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-restclient-test")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-thymeleaf-test")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-validation-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-validation-test")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
|
||||||
testImplementation("org.springframework.boot:spring-boot-testcontainers")
|
|
||||||
testImplementation("org.springframework.modulith:spring-modulith-starter-test")
|
|
||||||
testImplementation("org.testcontainers:testcontainers-junit-jupiter")
|
|
||||||
testImplementation("org.testcontainers:testcontainers-postgresql")
|
|
||||||
// testImplementation("org.testcontainers:testcontainers-rabbitmq")
|
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/com.infisical/sdk
|
// https://mvnrepository.com/artifact/com.infisical/sdk
|
||||||
@@ -78,7 +49,6 @@ dependencies {
|
|||||||
|
|
||||||
dependencyManagement {
|
dependencyManagement {
|
||||||
imports {
|
imports {
|
||||||
mavenBom("org.springframework.modulith:spring-modulith-bom:${property("springModulithVersion")}")
|
|
||||||
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
|
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
infisical-bridge:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
INFISICAL_API_URL: ${INFISICAL_API_URL}
|
||||||
|
INFISICAL_CLIENT_ID: ${INFISICAL_CLIENT_ID}
|
||||||
|
INFISICAL_CLIENT_SECRET: ${INFISICAL_CLIENT_SECRET}
|
||||||
|
INFISICAL_WEBHOOK_SECRET: ${INFISICAL_WEBHOOK_SECRET}
|
||||||
|
DOKPLOY_API_URL: ${DOKPLOY_API_URL}
|
||||||
|
DOKPLOY_API_KEY: ${DOKPLOY_API_KEY}
|
||||||
@@ -2,8 +2,10 @@ package com.abnov.infisicalbridge;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableFeignClients
|
||||||
public class InfisicalBridgeApplication {
|
public class InfisicalBridgeApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.abnov.infisicalbridge.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AppConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ObjectMapper objectMapper() {
|
||||||
|
return new ObjectMapper();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.abnov.infisicalbridge.dokploy;
|
||||||
|
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
|
import com.abnov.infisicalbridge.dto.DokployApplicationUpdateRequest;
|
||||||
|
import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest;
|
||||||
|
|
||||||
|
@FeignClient(name = "dokployClient", url = "${dokploy.api-url}", configuration = DokployFeignConfig.class)
|
||||||
|
public interface DokployClient {
|
||||||
|
|
||||||
|
@PostMapping("/compose.update")
|
||||||
|
void updateCompose(@RequestBody DokployComposeUpdateRequest request);
|
||||||
|
|
||||||
|
@PostMapping("/application.update")
|
||||||
|
void updateApplication(@RequestBody DokployApplicationUpdateRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.abnov.infisicalbridge.dokploy;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import feign.RequestInterceptor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class DokployFeignConfig {
|
||||||
|
|
||||||
|
private final DokployProperties properties;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RequestInterceptor dokployRequestInterceptor() {
|
||||||
|
return requestTemplate -> {
|
||||||
|
// Add API key to every request
|
||||||
|
requestTemplate.header("x-api-key", properties.getApiKey());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.abnov.infisicalbridge.dokploy;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "dokploy")
|
||||||
|
@Validated
|
||||||
|
public class DokployProperties {
|
||||||
|
|
||||||
|
@NotBlank(message = "Dokploy API URL is required")
|
||||||
|
private String apiUrl;
|
||||||
|
|
||||||
|
@NotBlank(message = "Dokploy API KEY is required")
|
||||||
|
private String apiKey;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.abnov.infisicalbridge.dto;
|
||||||
|
|
||||||
|
public record DokployApplicationUpdateRequest(
|
||||||
|
String applicationId,
|
||||||
|
String env) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.abnov.infisicalbridge.dto;
|
||||||
|
|
||||||
|
public record DokployComposeUpdateRequest(
|
||||||
|
String composeId,
|
||||||
|
String env) {
|
||||||
|
}
|
||||||
@@ -21,4 +21,7 @@ public class InfisicalProperties {
|
|||||||
|
|
||||||
@NotBlank(message = "Infisical client secret is required")
|
@NotBlank(message = "Infisical client secret is required")
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
|
|
||||||
|
@NotBlank(message = "Infisical webhook secret is required")
|
||||||
|
private String webhookSecret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.abnov.infisicalbridge.infisical;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.infisical.sdk.InfisicalSdk;
|
||||||
|
import com.infisical.sdk.config.SdkConfig;
|
||||||
|
import com.infisical.sdk.util.InfisicalException;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class InfisicalService {
|
||||||
|
private final InfisicalProperties properties;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final InfisicalSdk sdk;
|
||||||
|
|
||||||
|
public InfisicalService(InfisicalProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.sdk = initializeClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InfisicalSdk initializeClient() {
|
||||||
|
try {
|
||||||
|
log.info("Initializing Infisical SDK");
|
||||||
|
var sdkInstance = new InfisicalSdk(
|
||||||
|
new SdkConfig.Builder()
|
||||||
|
.withSiteUrl(properties.getApiUrl())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
sdkInstance.Auth().UniversalAuthLogin(
|
||||||
|
properties.getClientId(),
|
||||||
|
properties.getClientSecret());
|
||||||
|
|
||||||
|
log.info("Successfully authenticated with Infisical");
|
||||||
|
return sdkInstance;
|
||||||
|
|
||||||
|
} catch (InfisicalException e) {
|
||||||
|
log.error("Failed to initialize Infisical SDK", e);
|
||||||
|
throw new IllegalStateException("Failed to initialize Infisical SDK", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,27 @@
|
|||||||
package com.abnov.infisicalbridge.infisical;
|
package com.abnov.infisicalbridge.infisical;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.abnov.infisicalbridge.dokploy.DokployClient;
|
||||||
|
import com.abnov.infisicalbridge.dto.DokployApplicationUpdateRequest;
|
||||||
|
import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest;
|
||||||
|
import com.abnov.infisicalbridge.dto.InfisicalWebhookEventResponse;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.infisical.sdk.InfisicalSdk;
|
||||||
|
import com.infisical.sdk.models.Secret;
|
||||||
|
import com.infisical.sdk.util.InfisicalException;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -15,23 +31,77 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class InfisicalWebhookController {
|
public class InfisicalWebhookController {
|
||||||
private final InfisicalSignatureVerifier signatureVerifier;
|
private final InfisicalSignatureVerifier signatureVerifier;
|
||||||
|
private final InfisicalService service;
|
||||||
|
private final InfisicalProperties infisicalProperties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final DokployClient dokployClient;
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public void handleWebhook(
|
public ResponseEntity<Void> handleWebhook(
|
||||||
@RequestBody String payload,
|
@RequestBody String payload,
|
||||||
@RequestHeader(value = "X-Infisical-Signature", required = false) String signature) {
|
@RequestParam(required = false) String dokployComposeId,
|
||||||
// Check if signature header is present
|
@RequestParam(required = false) String dokployApplicationId,
|
||||||
|
@RequestHeader(value = "X-Infisical-Signature", required = false) String signature)
|
||||||
|
throws InfisicalException {
|
||||||
|
|
||||||
if (signature == null || signature.isEmpty()) {
|
if (signature == null || signature.isEmpty()) {
|
||||||
log.error("Missing signature header");
|
log.warn("Missing X-Infisical-Signature header");
|
||||||
return;
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature
|
if (!signatureVerifier.verifySignature(payload, signature, infisicalProperties.getWebhookSecret())) {
|
||||||
if (!signatureVerifier.verifySignature(payload, signature, "demoa")) {
|
log.warn("Invalid webhook signature");
|
||||||
log.error("Invalid signature");
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Webhook received and verified: {}", payload);
|
InfisicalWebhookEventResponse webhookEvent;
|
||||||
|
try {
|
||||||
|
webhookEvent = objectMapper.readValue(payload, InfisicalWebhookEventResponse.class);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Invalid webhook payload", e);
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalSdk sdk = service.getSdk();
|
||||||
|
List<Secret> secrets = sdk.Secrets().ListSecrets(
|
||||||
|
webhookEvent.project().projectId(),
|
||||||
|
webhookEvent.project().environment(),
|
||||||
|
webhookEvent.project().secretPath(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false);
|
||||||
|
|
||||||
|
if (secrets.isEmpty()) {
|
||||||
|
log.warn("No secrets found for project={} env={}",
|
||||||
|
webhookEvent.project().projectName(),
|
||||||
|
webhookEvent.project().environment());
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String envContent = secrets.stream()
|
||||||
|
.map(s -> s.getSecretKey() + "=" + s.getSecretValue())
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
|
||||||
|
if (dokployComposeId != null) {
|
||||||
|
try {
|
||||||
|
dokployClient.updateCompose(
|
||||||
|
new DokployComposeUpdateRequest(dokployComposeId, envContent));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to update Dokploy compose {}", dokployComposeId, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dokployApplicationId != null) {
|
||||||
|
try {
|
||||||
|
dokployClient.updateApplication(
|
||||||
|
new DokployApplicationUpdateRequest(dokployApplicationId, envContent));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to update Dokploy application {}", dokployApplicationId, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
spring:
|
|
||||||
liquibase:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
sql:
|
|
||||||
init:
|
|
||||||
mode: always
|
|
||||||
|
|
||||||
datasource:
|
|
||||||
initialization-mode: always
|
|
||||||
|
|
||||||
jpa:
|
|
||||||
show-sql: true
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: create-drop
|
|
||||||
@@ -6,3 +6,8 @@ infisical:
|
|||||||
api-url: ${INFISICAL_API_URL}
|
api-url: ${INFISICAL_API_URL}
|
||||||
client-id: ${INFISICAL_CLIENT_ID}
|
client-id: ${INFISICAL_CLIENT_ID}
|
||||||
client-secret: ${INFISICAL_CLIENT_SECRET}
|
client-secret: ${INFISICAL_CLIENT_SECRET}
|
||||||
|
webhook-secret: ${INFISICAL_WEBHOOK_SECRET}
|
||||||
|
|
||||||
|
dokploy:
|
||||||
|
api-url: ${DOKPLOY_API_URL}
|
||||||
|
api-key: ${DOKPLOY_API_KEY}
|
||||||
|
|||||||
Reference in New Issue
Block a user