AbstractStackPortRuntime.java
package com.cloudforge.core.local;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
/**
* Shared Docker lifecycle for target-owned StackPort resource browsers.
*/
public abstract class AbstractStackPortRuntime implements StackPortRuntime {
private final StackPortSpec spec;
protected AbstractStackPortRuntime(StackPortSpec spec) {
this.spec = spec;
}
protected StackPortSpec spec() {
return spec;
}
@Override
public DeploymentTarget target() {
return spec.target();
}
@Override
public String containerName() {
return spec.containerName();
}
@Override
public URI browserUrl() {
return spec.browserUrl();
}
@Override
public void start() throws IOException {
stopConflicting();
String network = resolveStackPortNetwork();
Path dataDir = prepareDataDir(false);
if (DockerEmulatorSupport.isContainerRunning(spec.containerName())) {
ensureEmulatorNetworkConnected();
if (isHealthy()) {
System.out.println(spec.displayName() + " already running: " + browserUrl());
return;
}
DockerEmulatorSupport.stopContainer(spec.containerName());
}
if (DockerEmulatorSupport.containerExists(spec.containerName())) {
DockerEmulatorSupport.startExistingContainer(spec.containerName());
ensureEmulatorNetworkConnected();
} else {
DockerEmulatorSupport.runDetached(dockerCreateArgs(network, dataDir));
ensureEmulatorNetworkConnected();
}
DockerEmulatorSupport.waitForHealthy(browserUrl());
printStarted(dataDir, network);
}
@Override
public void stop() throws IOException {
DockerEmulatorSupport.stopContainer(spec.containerName());
System.out.println(spec.displayName() + " stopped");
}
@Override
public void rebuild() throws IOException {
stopConflicting();
stop();
DockerEmulatorSupport.removeContainer(spec.containerName());
String network = resolveStackPortNetwork();
Path dataDir = prepareDataDir(true);
DockerEmulatorSupport.pullImage(spec.image());
DockerEmulatorSupport.runDetached(dockerCreateArgs(network, dataDir));
ensureEmulatorNetworkConnected();
DockerEmulatorSupport.waitForHealthy(browserUrl());
printStarted(dataDir, network);
System.out.println(spec.displayName() + " rebuilt (endpoint config reset)");
}
@Override
public boolean isRunning() throws IOException {
return DockerEmulatorSupport.isContainerRunning(spec.containerName());
}
@Override
public boolean isHealthy() {
return DockerEmulatorSupport.isHttpHealthy(browserUrl());
}
protected List<String> dockerCreateArgs(String network, Path dataDir) throws IOException {
String emulatorUrl = resolveAwsEndpointUrl();
String endpointsEnv = resolveStackPortEndpointsEnv();
List<String> args = new ArrayList<>();
args.addAll(List.of(
"run", "-d",
"--name", spec.containerName(),
"--network", network,
"-p", spec.hostPort() + ":" + spec.containerPort(),
"-e", "AWS_ENDPOINT_URL=" + emulatorUrl,
"-e", LocalEmulatorDefaults.STACKPORT_ENDPOINTS_ENV + "=" + endpointsEnv,
"-e", "AWS_ACCESS_KEY_ID=test",
"-e", "AWS_SECRET_ACCESS_KEY=test",
"-e", "AWS_REGION=" + System.getenv().getOrDefault("AWS_DEFAULT_REGION", "us-east-1"),
"-e", "STACKPORT_ALLOW_WRITES=true",
"-e", LocalEmulatorDefaults.STACKPORT_DATA_DIR_ENV + "=/stackport-data",
"-v", dataDir.toAbsolutePath() + ":/stackport-data",
spec.image()));
return args;
}
protected String resolveAwsEndpointUrl() {
String override = System.getenv(LocalEmulatorDefaults.STACKPORT_ENDPOINT_ENV);
if (override != null && !override.isBlank()) {
return override.trim();
}
return spec.emulatorEndpointUrl();
}
protected String resolveStackPortEndpointsEnv() {
String override = System.getenv(LocalEmulatorDefaults.STACKPORT_ENDPOINTS_ENV);
if (override != null && !override.isBlank()) {
return override.trim();
}
return spec.stackPortEndpointsEnvValue();
}
private String resolveStackPortNetwork() throws IOException {
String emulatorNetwork = DockerEmulatorSupport.primaryContainerNetwork(spec.emulatorContainerName());
if (emulatorNetwork != null && !emulatorNetwork.isBlank()) {
return emulatorNetwork;
}
DockerEmulatorSupport.ensureNetwork(LocalEmulatorDefaults.DOCKER_NETWORK);
return LocalEmulatorDefaults.DOCKER_NETWORK;
}
private void ensureEmulatorNetworkConnected() throws IOException {
String emulatorNetwork = DockerEmulatorSupport.primaryContainerNetwork(spec.emulatorContainerName());
if (emulatorNetwork != null && !emulatorNetwork.isBlank()) {
DockerEmulatorSupport.connectNetwork(spec.containerName(), emulatorNetwork);
}
}
private Path prepareDataDir(boolean reset) throws IOException {
Path dataDir = LocalEmulatorPaths.stackPortVolumeDir(spec.target());
if (reset && Files.exists(dataDir)) {
try (Stream<Path> walk = Files.walk(dataDir)) {
walk.sorted(Comparator.reverseOrder())
.filter(path -> !path.equals(dataDir))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new IllegalStateException("Failed to reset StackPort data: " + path, e);
}
});
}
}
Files.createDirectories(dataDir);
return dataDir;
}
private void printStarted(Path dataDir, String network) {
System.out.println(spec.displayName() + " started: " + browserUrl());
System.out.println(" Docker network: " + network);
System.out.println(" Host emulator gateway: http://localhost:"
+ LocalEmulatorDefaults.GATEWAY_PORT);
System.out.println(" StackPort endpoint name: " + spec.endpointConfigName());
System.out.println(" StackPort AWS endpoint: " + resolveAwsEndpointUrl());
System.out.println(" StackPort config volume: " + dataDir.toAbsolutePath());
System.out.println(" Note: In StackPort Settings, use the '" + spec.endpointConfigName()
+ "' endpoint — not http://localhost:" + LocalEmulatorDefaults.GATEWAY_PORT
+ " (localhost is for your Mac, not inside the StackPort container)");
}
private void stopConflicting() throws IOException {
if (spec.conflictingContainerName() != null && !spec.conflictingContainerName().isBlank()) {
DockerEmulatorSupport.stopContainer(spec.conflictingContainerName());
}
}
}