feat: build Docker entrypoint per runtime type with custom args support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-12 13:06:54 +02:00
parent f66c8b6d18
commit e941256e6e
4 changed files with 55 additions and 20 deletions

View File

@@ -104,6 +104,28 @@ public class DeploymentExecutor {
updateStage(deployment.id(), DeployStage.PRE_FLIGHT);
preFlightChecks(jarPath, config);
// Resolve runtime type
String resolvedRuntimeType = config.runtimeType();
String mainClass = null;
if ("auto".equalsIgnoreCase(resolvedRuntimeType)) {
AppVersion appVersion = appService.getVersion(deployment.appVersionId());
if (appVersion.detectedRuntimeType() == null) {
throw new IllegalStateException(
"Could not detect runtime type for JAR '" + appVersion.jarFilename() +
"'. Set runtimeType explicitly in app configuration.");
}
resolvedRuntimeType = appVersion.detectedRuntimeType();
mainClass = appVersion.detectedMainClass();
} else if ("plain-java".equals(resolvedRuntimeType)) {
AppVersion appVersion = appService.getVersion(deployment.appVersionId());
mainClass = appVersion.detectedMainClass();
if (mainClass == null) {
throw new IllegalStateException(
"Runtime type 'plain-java' requires a Main-Class in the JAR manifest, " +
"but none was detected for '" + appVersion.jarFilename() + "'.");
}
}
// === PULL IMAGE ===
updateStage(deployment.id(), DeployStage.PULL_IMAGE);
// Docker pulls on create if not present locally
@@ -147,7 +169,8 @@ public class DeploymentExecutor {
config.memoryLimitBytes(), config.memoryReserveBytes(),
config.dockerCpuShares(), config.dockerCpuQuota(),
config.exposedPorts(), agentHealthPort,
"on-failure", 3
"on-failure", 3,
resolvedRuntimeType, config.customArgs(), mainClass
);
String containerId = orchestrator.startContainer(request);
@@ -277,6 +300,8 @@ public class DeploymentExecutor {
envVars.put("CAMELEER_AGENT_EXPORT_ENDPOINT", config.serverUrl());
envVars.put("CAMELEER_AGENT_ROUTECONTROL_ENABLED", String.valueOf(config.routeControlEnabled()));
envVars.put("CAMELEER_AGENT_REPLAY_ENABLED", String.valueOf(config.replayEnabled()));
envVars.put("CAMELEER_AGENT_HEALTH_ENABLED", "true");
envVars.put("CAMELEER_AGENT_HEALTH_PORT", String.valueOf(agentHealthPort));
if (bootstrapToken != null && !bootstrapToken.isBlank()) {
envVars.put("CAMELEER_AGENT_AUTH_TOKEN", bootstrapToken);
}
@@ -343,6 +368,8 @@ public class DeploymentExecutor {
map.put("serverUrl", config.serverUrl());
map.put("replicas", config.replicas());
map.put("deploymentStrategy", config.deploymentStrategy());
map.put("runtimeType", config.runtimeType());
map.put("customArgs", config.customArgs());
return map;
}
}

View File

@@ -84,10 +84,12 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
hostConfig.withCpuQuota(request.cpuQuota());
}
// When using volume mount, the JAR is at the original path, not /app/app.jar
// Resolve the JAR path for the entrypoint
String appJarPath;
if (request.jarVolumeName() != null && !request.jarVolumeName().isBlank()) {
envList = new ArrayList<>(envList);
envList.add("CAMELEER_APP_JAR=" + request.jarPath());
appJarPath = request.jarPath();
} else {
appJarPath = "/app/app.jar";
}
var createCmd = dockerClient.createContainerCmd(request.baseImage())
@@ -103,21 +105,19 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
.withRetries(3)
.withStartPeriod(30_000_000_000L));
// Override entrypoint to use the volume-mounted JAR path
if (request.jarVolumeName() != null && !request.jarVolumeName().isBlank()) {
createCmd.withEntrypoint("sh", "-c",
"exec java -javaagent:/app/agent.jar " +
"-Dcameleer.export.type=${CAMELEER_EXPORT_TYPE:-HTTP} " +
"-Dcameleer.export.endpoint=${CAMELEER_SERVER_URL} " +
"-Dcameleer.agent.name=${HOSTNAME} " +
"-Dcameleer.agent.application=${CAMELEER_APPLICATION_ID:-default} " +
"-Dcameleer.agent.environment=${CAMELEER_ENVIRONMENT_ID:-default} " +
"-Dcameleer.routeControl.enabled=${CAMELEER_ROUTE_CONTROL_ENABLED:-false} " +
"-Dcameleer.replay.enabled=${CAMELEER_REPLAY_ENABLED:-false} " +
"-Dcameleer.health.enabled=true " +
"-Dcameleer.health.port=9464 " +
"-jar ${CAMELEER_APP_JAR}");
}
// Build entrypoint based on runtime type
String customArgs = request.customArgs() != null && !request.customArgs().isBlank()
? " " + request.customArgs() : "";
String entrypoint = switch (request.runtimeType()) {
case "quarkus" -> "exec java -javaagent:/app/agent.jar" + customArgs + " -jar " + appJarPath;
case "plain-java" -> "exec java -javaagent:/app/agent.jar -cp " + appJarPath +
":/app/cameleer3-log-appender.jar" + customArgs + " " + request.mainClass();
case "native" -> "exec " + appJarPath + customArgs;
default -> // spring-boot (default)
"exec java -javaagent:/app/agent.jar -Dloader.path=/app/cameleer3-log-appender.jar" +
customArgs + " -cp " + appJarPath + " org.springframework.boot.loader.launch.PropertiesLauncher";
};
createCmd.withEntrypoint("sh", "-c", entrypoint);
if (request.exposedPorts() != null && !request.exposedPorts().isEmpty()) {
var ports = request.exposedPorts().stream()

View File

@@ -32,6 +32,11 @@ public class AppService {
public App getBySlug(String slug) { return appRepo.findBySlug(slug).orElseThrow(() -> new IllegalArgumentException("App not found: " + slug)); }
public List<AppVersion> listVersions(UUID appId) { return versionRepo.findByAppId(appId); }
public AppVersion getVersion(UUID versionId) {
return versionRepo.findById(versionId)
.orElseThrow(() -> new IllegalArgumentException("AppVersion not found: " + versionId));
}
public void updateContainerConfig(UUID id, Map<String, Object> containerConfig) {
getById(id); // verify exists
appRepo.updateContainerConfig(id, containerConfig);

View File

@@ -20,5 +20,8 @@ public record ContainerRequest(
List<Integer> exposedPorts,
int healthCheckPort,
String restartPolicyName,
int restartPolicyMaxRetries
int restartPolicyMaxRetries,
String runtimeType,
String customArgs,
String mainClass
) {}