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