feat: implement DockerRuntimeOrchestrator with volume-mount JAR deployment
- DockerRuntimeOrchestrator: docker-java based container lifecycle - DisabledRuntimeOrchestrator: no-op for observability-only mode - RuntimeOrchestratorAutoConfig: auto-detects Docker socket availability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.cameleer3.server.app.runtime;
|
||||
|
||||
import com.cameleer3.server.core.runtime.ContainerRequest;
|
||||
import com.cameleer3.server.core.runtime.ContainerStatus;
|
||||
import com.cameleer3.server.core.runtime.RuntimeOrchestrator;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class DisabledRuntimeOrchestrator implements RuntimeOrchestrator {
|
||||
@Override public boolean isEnabled() { return false; }
|
||||
@Override public String startContainer(ContainerRequest r) { throw new UnsupportedOperationException("Runtime management disabled"); }
|
||||
@Override public void stopContainer(String id) { throw new UnsupportedOperationException("Runtime management disabled"); }
|
||||
@Override public void removeContainer(String id) { throw new UnsupportedOperationException("Runtime management disabled"); }
|
||||
@Override public ContainerStatus getContainerStatus(String id) { return ContainerStatus.notFound(); }
|
||||
@Override public Stream<String> getLogs(String id, int tail) { return Stream.empty(); }
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.cameleer3.server.app.runtime;
|
||||
|
||||
import com.cameleer3.server.core.runtime.ContainerRequest;
|
||||
import com.cameleer3.server.core.runtime.ContainerStatus;
|
||||
import com.cameleer3.server.core.runtime.RuntimeOrchestrator;
|
||||
import com.github.dockerjava.api.DockerClient;
|
||||
import com.github.dockerjava.api.async.ResultCallback;
|
||||
import com.github.dockerjava.api.model.AccessMode;
|
||||
import com.github.dockerjava.api.model.Bind;
|
||||
import com.github.dockerjava.api.model.Frame;
|
||||
import com.github.dockerjava.api.model.HealthCheck;
|
||||
import com.github.dockerjava.api.model.HostConfig;
|
||||
import com.github.dockerjava.api.model.Volume;
|
||||
import com.github.dockerjava.core.DefaultDockerClientConfig;
|
||||
import com.github.dockerjava.core.DockerClientImpl;
|
||||
import com.github.dockerjava.zerodep.ZerodepDockerHttpClient;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DockerRuntimeOrchestrator.class);
|
||||
private DockerClient dockerClient;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
var config = DefaultDockerClientConfig.createDefaultConfigBuilder()
|
||||
.withDockerHost("unix:///var/run/docker.sock")
|
||||
.build();
|
||||
var httpClient = new ZerodepDockerHttpClient.Builder()
|
||||
.dockerHost(config.getDockerHost())
|
||||
.build();
|
||||
dockerClient = DockerClientImpl.getInstance(config, httpClient);
|
||||
log.info("Docker client initialized, host: {}", config.getDockerHost());
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() throws IOException {
|
||||
if (dockerClient != null) {
|
||||
dockerClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String startContainer(ContainerRequest request) {
|
||||
List<String> envList = request.envVars().entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue()).toList();
|
||||
|
||||
// Volume bind: mount JAR into container
|
||||
Bind jarBind = new Bind(request.jarPath(), new Volume("/app/app.jar"), AccessMode.ro);
|
||||
|
||||
HostConfig hostConfig = HostConfig.newHostConfig()
|
||||
.withMemory(request.memoryLimitBytes())
|
||||
.withMemorySwap(request.memoryLimitBytes())
|
||||
.withCpuShares(request.cpuShares())
|
||||
.withNetworkMode(request.network())
|
||||
.withBinds(jarBind);
|
||||
|
||||
var container = dockerClient.createContainerCmd(request.baseImage())
|
||||
.withName(request.containerName())
|
||||
.withEnv(envList)
|
||||
.withLabels(request.labels() != null ? request.labels() : Map.of())
|
||||
.withHostConfig(hostConfig)
|
||||
.withHealthcheck(new HealthCheck()
|
||||
.withTest(List.of("CMD-SHELL",
|
||||
"wget -qO- http://localhost:" + request.healthCheckPort() + "/cameleer/health || exit 1"))
|
||||
.withInterval(10_000_000_000L) // 10s
|
||||
.withTimeout(5_000_000_000L) // 5s
|
||||
.withRetries(3)
|
||||
.withStartPeriod(30_000_000_000L)) // 30s
|
||||
.exec();
|
||||
|
||||
dockerClient.startContainerCmd(container.getId()).exec();
|
||||
log.info("Started container {} ({})", request.containerName(), container.getId());
|
||||
return container.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopContainer(String containerId) {
|
||||
try {
|
||||
dockerClient.stopContainerCmd(containerId).withTimeout(30).exec();
|
||||
log.info("Stopped container {}", containerId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to stop container {}: {}", containerId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeContainer(String containerId) {
|
||||
try {
|
||||
dockerClient.removeContainerCmd(containerId).withForce(true).exec();
|
||||
log.info("Removed container {}", containerId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to remove container {}: {}", containerId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContainerStatus getContainerStatus(String containerId) {
|
||||
try {
|
||||
var inspection = dockerClient.inspectContainerCmd(containerId).exec();
|
||||
var state = inspection.getState();
|
||||
var health = state.getHealth();
|
||||
var healthStatus = health != null ? health.getStatus() : null;
|
||||
// Use health status if available, otherwise fall back to container state
|
||||
var effectiveState = healthStatus != null ? healthStatus : state.getStatus();
|
||||
return new ContainerStatus(
|
||||
effectiveState,
|
||||
Boolean.TRUE.equals(state.getRunning()),
|
||||
state.getExitCodeLong() != null ? state.getExitCodeLong().intValue() : 0,
|
||||
state.getError());
|
||||
} catch (Exception e) {
|
||||
return new ContainerStatus("not_found", false, -1, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<String> getLogs(String containerId, int tailLines) {
|
||||
List<String> logLines = new ArrayList<>();
|
||||
try {
|
||||
dockerClient.logContainerCmd(containerId)
|
||||
.withStdOut(true)
|
||||
.withStdErr(true)
|
||||
.withTail(tailLines)
|
||||
.withTimestamps(true)
|
||||
.exec(new ResultCallback.Adapter<Frame>() {
|
||||
@Override
|
||||
public void onNext(Frame frame) {
|
||||
logLines.add(new String(frame.getPayload()).trim());
|
||||
}
|
||||
}).awaitCompletion();
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to get logs for container {}: {}", containerId, e.getMessage());
|
||||
}
|
||||
return logLines.stream();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.cameleer3.server.app.runtime;
|
||||
|
||||
import com.cameleer3.server.core.runtime.RuntimeOrchestrator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Configuration
|
||||
public class RuntimeOrchestratorAutoConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RuntimeOrchestratorAutoConfig.class);
|
||||
|
||||
@Bean
|
||||
public RuntimeOrchestrator runtimeOrchestrator() {
|
||||
// Auto-detect: Docker socket available?
|
||||
if (Files.exists(Path.of("/var/run/docker.sock"))) {
|
||||
log.info("Docker socket detected - enabling Docker runtime orchestrator");
|
||||
return new DockerRuntimeOrchestrator();
|
||||
}
|
||||
// TODO: K8s detection (check for service account token)
|
||||
log.info("No Docker socket or K8s detected - runtime management disabled (observability-only mode)");
|
||||
return new DisabledRuntimeOrchestrator();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user