Skip to content

Commit 5ae88dd

Browse files
committed
Added Windows and Mac versions
1 parent 7f26859 commit 5ae88dd

4 files changed

Lines changed: 320 additions & 51 deletions

File tree

src/main/java/io/github/intisy/docker/EmbeddedDockerManager.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,17 @@ public class EmbeddedDockerManager {
2020
private String containerId;
2121

2222
public EmbeddedDockerManager() {
23+
this.dockerProvider = getProvider();
24+
}
25+
26+
public static DockerProvider getProvider() {
2327
String os = System.getProperty("os.name").toLowerCase();
24-
if (os.contains("linux")) {
25-
this.dockerProvider = new LinuxDockerProvider();
28+
if (os.contains("win")) {
29+
return new WindowsDockerProvider();
30+
} else if (os.contains("nix") || os.contains("nux") || os.contains("aix")) {
31+
return new LinuxDockerProvider();
32+
} else if (os.contains("mac")) {
33+
return new MacDockerProvider();
2634
} else {
2735
throw new UnsupportedOperationException("Unsupported operating system: " + os);
2836
}

src/main/java/io/github/intisy/docker/LinuxDockerProvider.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
/**
2626
* @author Finn Birich
2727
*/
28+
@SuppressWarnings("ResultOfMethodCallIgnored")
2829
public class LinuxDockerProvider implements DockerProvider {
2930
private static final String ROOTLESSKIT_VERSION = "v2.1.1";
3031
private static final String ROOTLESSKIT_DOWNLOAD_URL = "https://github.com/rootless-containers/rootlesskit/releases/download/%s/rootlesskit-%s.tar.gz";
3132
private static final String DOCKER_ROOTLESS_SCRIPT_URL = "https://raw.githubusercontent.com/moby/moby/master/contrib/dockerd-rootless.sh";
33+
private static final String DOCKER_DOWNLOAD_URL = "https://download.docker.com/linux/static/stable/%s/docker-%s.tgz";
3234
private static final Path DOCKER_DIR = Path.of(System.getProperty("user.home"), ".docker-java");
3335
private static final Path DOCKER_PATH = DOCKER_DIR.resolve("docker/dockerd");
3436
private static final Path ROOTLESSKIT_PATH = DOCKER_DIR.resolve("rootlesskit");
@@ -51,6 +53,7 @@ public void ensureInstalled() throws IOException, InterruptedException {
5153
ensureSlirp4netnsInstalled();
5254
}
5355

56+
@SuppressWarnings("ResultOfMethodCallIgnored")
5457
private void ensureDockerInstalled() throws IOException, InterruptedException {
5558
boolean autoUpdate = Boolean.parseBoolean(System.getProperty("docker.auto.update", "true"));
5659
String latestVersion = DockerVersionFetcher.getLatestVersion();
@@ -78,27 +81,24 @@ private void ensureDockerInstalled() throws IOException, InterruptedException {
7881
try (java.util.stream.Stream<Path> walk = Files.walk(dockerInstallDir)) {
7982
walk.sorted(java.util.Comparator.reverseOrder())
8083
.map(Path::toFile)
81-
.forEach(java.io.File::delete);
84+
.forEach(File::delete);
8285
}
8386
}
8487

8588
String arch = getArch();
86-
String dockerUrl = String.format("https://download.docker.com/linux/static/stable/%s/docker-%s.tgz", arch, latestVersion);
89+
String dockerUrl = String.format(DOCKER_DOWNLOAD_URL, arch, latestVersion);
8790
downloadAndExtract(dockerUrl, DOCKER_DIR);
8891
Files.writeString(DOCKER_VERSION_FILE, latestVersion);
8992
}
9093
}
9194

9295
private String getArch() {
9396
String osArch = System.getProperty("os.arch");
94-
switch (osArch) {
95-
case "amd64":
96-
return "x86_64";
97-
case "aarch64":
98-
return "aarch64";
99-
default:
100-
throw new UnsupportedOperationException("Unsupported architecture: " + osArch);
101-
}
97+
return switch (osArch) {
98+
case "amd64" -> "x86_64";
99+
case "aarch64" -> "aarch64";
100+
default -> throw new UnsupportedOperationException("Unsupported architecture: " + osArch);
101+
};
102102
}
103103

104104
private void ensureRootlessScriptInstalled() throws IOException, InterruptedException {
@@ -141,7 +141,7 @@ public void start() throws IOException, InterruptedException {
141141
ProcessBuilder pb;
142142
if (isRoot() && !forceRootless) {
143143
System.out.println("Running as root, starting dockerd with sudo.");
144-
pb = new ProcessBuilder("sudo", DOCKER_PATH.toString(), "-H", "unix://" + DOCKER_SOCKET_PATH.toString());
144+
pb = new ProcessBuilder("sudo", DOCKER_PATH.toString(), "-H", "unix://" + DOCKER_SOCKET_PATH);
145145
dockerProcess = pb.start();
146146
} else {
147147
System.out.println("Attempting to start in rootless mode using dockerd-rootless.sh.");
@@ -156,7 +156,7 @@ public void start() throws IOException, InterruptedException {
156156
pb = new ProcessBuilder(DOCKER_PATH.getParent().resolve("dockerd-rootless.sh").toString());
157157

158158
String path = pb.environment().getOrDefault("PATH", "");
159-
pb.environment().put("PATH", SLIRP4NETNS_DIR.toString() + File.pathSeparator + ROOTLESSKIT_PATH.toString() + File.pathSeparator + DOCKER_PATH.getParent().toString() + File.pathSeparator + path);
159+
pb.environment().put("PATH", SLIRP4NETNS_DIR + File.pathSeparator + ROOTLESSKIT_PATH + File.pathSeparator + DOCKER_PATH.getParent().toString() + File.pathSeparator + path);
160160
pb.environment().put("XDG_RUNTIME_DIR", runDir.toString());
161161
pb.environment().put("XDG_DATA_HOME", dataDir.toString());
162162
pb.environment().put("XDG_CONFIG_HOME", configDir.toString());
@@ -224,7 +224,8 @@ private void downloadFile(String urlString, Path destinationPath) throws IOExcep
224224
}
225225
}
226226

227-
private void downloadAndExtract(String urlString, Path destinationDir, String... toExtract) throws IOException, InterruptedException {
227+
@SuppressWarnings("deprecation")
228+
private void downloadAndExtract(String urlString, Path destinationDir) throws IOException, InterruptedException {
228229
System.out.println("Downloading and extracting " + urlString + "...");
229230
HttpClient client = HttpClient.newBuilder()
230231
.followRedirects(HttpClient.Redirect.NORMAL)
@@ -256,6 +257,7 @@ private void downloadAndExtract(String urlString, Path destinationDir, String...
256257
}
257258
}
258259

260+
@SuppressWarnings("BusyWait")
259261
private boolean waitForSocket() throws InterruptedException {
260262
System.out.println("Waiting for Docker socket to be available at " + DOCKER_SOCKET_PATH + "...");
261263
long timeoutMillis = TimeUnit.SECONDS.toMillis(30);
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package io.github.intisy.docker;
2+
3+
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.core.DefaultDockerClientConfig;
5+
import com.github.dockerjava.core.DockerClientImpl;
6+
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
7+
import com.github.dockerjava.transport.DockerHttpClient;
8+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
9+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
10+
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
11+
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.io.InputStream;
15+
import java.net.URI;
16+
import java.net.http.HttpClient;
17+
import java.net.http.HttpRequest;
18+
import java.net.http.HttpResponse;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.nio.file.StandardCopyOption;
22+
import java.util.concurrent.TimeUnit;
23+
24+
/**
25+
* @author Finn Birich
26+
*/
27+
@SuppressWarnings({"ResultOfMethodCallIgnored", "BusyWait"})
28+
public class MacDockerProvider implements DockerProvider {
29+
private static final String DOCKER_DOWNLOAD_URL = "https://download.docker.com/mac/static/stable/%s/docker-%s.tgz";
30+
private static final Path DOCKER_DIR = Path.of(System.getProperty("user.home"), ".docker-java");
31+
private static final Path DOCKER_PATH = DOCKER_DIR.resolve("docker/dockerd");
32+
private static final Path DOCKER_SOCKET_PATH = DOCKER_DIR.resolve("run/docker.sock");
33+
private static final Path DOCKER_VERSION_FILE = DOCKER_DIR.resolve(".docker-version");
34+
35+
private DockerClient dockerClient;
36+
private Process dockerProcess;
37+
38+
@Override
39+
public void ensureInstalled() throws IOException, InterruptedException {
40+
boolean autoUpdate = Boolean.parseBoolean(System.getProperty("docker.auto.update", "true"));
41+
String latestVersion = DockerVersionFetcher.getLatestVersion();
42+
boolean needsUpdate = true;
43+
44+
if (Files.exists(DOCKER_PATH)) {
45+
if (autoUpdate && Files.exists(DOCKER_VERSION_FILE)) {
46+
String installedVersion = Files.readString(DOCKER_VERSION_FILE).trim();
47+
if (!installedVersion.equals(latestVersion)) {
48+
System.out.println("Newer Docker version available. Updating from " + installedVersion + " to " + latestVersion);
49+
} else {
50+
System.out.println("Docker is up to date. (" + latestVersion + ")");
51+
needsUpdate = false;
52+
}
53+
} else if (!autoUpdate) {
54+
needsUpdate = false;
55+
}
56+
}
57+
58+
if (needsUpdate) {
59+
System.out.println("Docker installation is incomplete or outdated. Re-installing...");
60+
61+
Path dockerInstallDir = DOCKER_DIR.resolve("docker");
62+
if (Files.exists(dockerInstallDir)) {
63+
try (java.util.stream.Stream<Path> walk = Files.walk(dockerInstallDir)) {
64+
walk.sorted(java.util.Comparator.reverseOrder())
65+
.map(Path::toFile)
66+
.forEach(File::delete);
67+
}
68+
}
69+
70+
String arch = getArch();
71+
String dockerUrl = String.format(DOCKER_DOWNLOAD_URL, arch, latestVersion);
72+
downloadAndExtract(dockerUrl);
73+
Files.writeString(DOCKER_VERSION_FILE, latestVersion);
74+
}
75+
}
76+
77+
private String getArch() {
78+
String osArch = System.getProperty("os.arch");
79+
return switch (osArch) {
80+
case "amd64" -> "x86_64";
81+
case "aarch64" -> "aarch64";
82+
default -> throw new UnsupportedOperationException("Unsupported architecture: " + osArch);
83+
};
84+
}
85+
86+
@Override
87+
public void start() throws IOException, InterruptedException {
88+
Path runDir = DOCKER_DIR.resolve("run");
89+
Path dataDir = DOCKER_DIR.resolve("data");
90+
runDir.toFile().mkdirs();
91+
dataDir.toFile().mkdirs();
92+
93+
ProcessBuilder pb = new ProcessBuilder(DOCKER_PATH.toString(), "--config-file", DOCKER_DIR.resolve("config.json").toString());
94+
pb.environment().put("XDG_RUNTIME_DIR", runDir.toString());
95+
pb.environment().put("XDG_DATA_HOME", dataDir.toString());
96+
pb.redirectErrorStream(true);
97+
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
98+
dockerProcess = pb.start();
99+
100+
if (!waitForSocket()) {
101+
throw new RuntimeException("Docker daemon failed to create socket in time.");
102+
}
103+
}
104+
105+
@Override
106+
public DockerClient getClient() {
107+
if (this.dockerClient == null) {
108+
DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
109+
.withDockerHost("unix://" + DOCKER_SOCKET_PATH).build();
110+
111+
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
112+
.dockerHost(config.getDockerHost())
113+
.sslConfig(config.getSSLConfig())
114+
.build();
115+
116+
this.dockerClient = DockerClientImpl.getInstance(config, httpClient);
117+
}
118+
return this.dockerClient;
119+
}
120+
121+
@Override
122+
public void stop() {
123+
if (dockerProcess != null) {
124+
dockerProcess.destroy();
125+
try {
126+
dockerProcess.waitFor(10, TimeUnit.SECONDS);
127+
} catch (InterruptedException e) {
128+
dockerProcess.destroyForcibly();
129+
}
130+
}
131+
}
132+
133+
@SuppressWarnings("deprecation")
134+
private void downloadAndExtract(String urlString) throws IOException, InterruptedException {
135+
System.out.println("Downloading and extracting " + urlString + "...");
136+
HttpClient client = HttpClient.newBuilder()
137+
.followRedirects(HttpClient.Redirect.NORMAL)
138+
.build();
139+
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(urlString)).build();
140+
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
141+
142+
if (response.statusCode() != 200) {
143+
throw new IOException("Failed to download file from " + urlString + ". Status code: " + response.statusCode());
144+
}
145+
146+
try (InputStream is = response.body();
147+
GzipCompressorInputStream gzis = new GzipCompressorInputStream(is);
148+
TarArchiveInputStream tis = new TarArchiveInputStream(gzis)) {
149+
TarArchiveEntry entry;
150+
while ((entry = tis.getNextTarEntry()) != null) {
151+
if (!tis.canReadEntryData(entry)) continue;
152+
Path outputPath = MacDockerProvider.DOCKER_DIR.resolve(entry.getName());
153+
if (entry.isDirectory()) {
154+
Files.createDirectories(outputPath);
155+
} else {
156+
Files.createDirectories(outputPath.getParent());
157+
Files.copy(tis, outputPath, StandardCopyOption.REPLACE_EXISTING);
158+
if (entry.getName().endsWith("dockerd")) {
159+
outputPath.toFile().setExecutable(true);
160+
}
161+
}
162+
}
163+
}
164+
}
165+
166+
private boolean waitForSocket() throws InterruptedException {
167+
System.out.println("Waiting for Docker socket to be available at " + DOCKER_SOCKET_PATH + "...");
168+
long timeoutMillis = TimeUnit.SECONDS.toMillis(30);
169+
long startTime = System.currentTimeMillis();
170+
while (System.currentTimeMillis() - startTime < timeoutMillis) {
171+
if (Files.exists(DOCKER_SOCKET_PATH)) {
172+
System.out.println("Docker socket found.");
173+
return true;
174+
}
175+
Thread.sleep(500);
176+
}
177+
System.err.println("Timed out waiting for Docker socket.");
178+
return false;
179+
}
180+
}

0 commit comments

Comments
 (0)