Compare commits

...

10 Commits

Author SHA1 Message Date
668ebfcc84 adding ci 2025-12-24 14:12:03 +00:00
ab0f9ceb50 remove application-dev yaml 2025-12-24 14:01:48 +00:00
d4491a6c05 remove unecessary code 2025-12-24 13:59:54 +00:00
72a8501d37 updating webhook controller 2025-12-24 13:51:09 +00:00
66965e0ff2 adding infisical service 2025-12-24 13:50:56 +00:00
21d99f9923 adding dokploy client 2025-12-24 13:49:50 +00:00
816177c2ec enabling feign clients 2025-12-24 13:48:25 +00:00
dbdfea671e updating infisical properties 2025-12-24 13:47:59 +00:00
cde10a4b3e adding dokploy properties 2025-12-24 13:47:34 +00:00
bc9c2da372 adding app config 2025-12-24 11:26:25 +00:00
14 changed files with 234 additions and 58 deletions

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM gradle:8.10.2-jdk17 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:17-jdk-alpine
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

View File

@@ -25,51 +25,22 @@ repositories {
}
extra["springCloudVersion"] = "2025.1.0"
extra["springModulithVersion"] = "2.0.1"
extra["infisicalVersion"] = "3.0.5"
dependencies {
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-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
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")
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.projectlombok:lombok")
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-thymeleaf-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-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")
// https://mvnrepository.com/artifact/com.infisical/sdk
@@ -78,7 +49,6 @@ dependencies {
dependencyManagement {
imports {
mavenBom("org.springframework.modulith:spring-modulith-bom:${property("springModulithVersion")}")
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}

11
docker-compose.yml Normal file
View 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}

View File

@@ -2,12 +2,14 @@ package com.abnov.infisicalbridge;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class InfisicalBridgeApplication {
public static void main(String[] args) {
SpringApplication.run(InfisicalBridgeApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(InfisicalBridgeApplication.class, args);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,14 @@
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.DokployComposeUpdateRequest;
@FeignClient(name = "dokployClient", url = "${dokploy.api-url}", configuration = DokployFeignConfig.class)
public interface DokployClient {
@PostMapping("/compose.update")
void updateCompose(@RequestBody DokployComposeUpdateRequest request);
}

View File

@@ -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());
};
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,6 @@
package com.abnov.infisicalbridge.dto;
public record DokployComposeUpdateRequest(
String composeId,
String env) {
}

View File

@@ -21,4 +21,7 @@ public class InfisicalProperties {
@NotBlank(message = "Infisical client secret is required")
private String clientSecret;
@NotBlank(message = "Infisical webhook secret is required")
private String webhookSecret;
}

View File

@@ -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);
}
}
}

View File

@@ -1,11 +1,26 @@
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.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.abnov.infisicalbridge.dokploy.DokployClient;
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.extern.slf4j.Slf4j;
@@ -15,23 +30,64 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class InfisicalWebhookController {
private final InfisicalSignatureVerifier signatureVerifier;
private final InfisicalService service;
private final InfisicalProperties infisicalProperties;
private final ObjectMapper objectMapper;
private final DokployClient dokployClient;
@PostMapping
public void handleWebhook(
public ResponseEntity<Void> handleWebhook(
@RequestBody String payload,
@RequestHeader(value = "X-Infisical-Signature", required = false) String signature) {
// Check if signature header is present
@RequestParam String dokployComposeId,
@RequestHeader(value = "X-Infisical-Signature", required = false) String signature)
throws InfisicalException {
if (signature == null || signature.isEmpty()) {
log.error("Missing signature header");
return;
log.warn("Missing X-Infisical-Signature header");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// Verify the signature
if (!signatureVerifier.verifySignature(payload, signature, "demoa")) {
log.error("Invalid signature");
return;
if (!signatureVerifier.verifySignature(payload, signature, infisicalProperties.getWebhookSecret())) {
log.warn("Invalid webhook signature");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
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"));
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();
}
return ResponseEntity.ok().build();
}
}

View File

@@ -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

View File

@@ -6,3 +6,8 @@ infisical:
api-url: ${INFISICAL_API_URL}
client-id: ${INFISICAL_CLIENT_ID}
client-secret: ${INFISICAL_CLIENT_SECRET}
webhook-secret: ${INFISICAL_WEBHOOK_SECRET}
dokploy:
api-url: ${DOKPLOY_API_URL}
api-key: ${DOKPLOY_API_KEY}