-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
feat(core): detect WSL Containers (wslc) as a Docker environment #11988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
DavidTavoularis
wants to merge
1
commit into
testcontainers:main
Choose a base branch
from
DavidTavoularis:feat/wslc-client-provider-strategy
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
core/src/main/java/org/testcontainers/dockerclient/WslcSocketClientProviderStrategy.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| package org.testcontainers.dockerclient; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.apache.commons.lang3.SystemUtils; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.net.URI; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| /** | ||
| * Auto-detects a WSL Containers (wslc) Docker daemon when no {@code DOCKER_HOST} is configured and no | ||
| * real Docker/Podman endpoint is available. wslc exposes neither a Windows named pipe nor a TCP port; | ||
| * its daemon is reached through a stdio bridge (see the {@code wslc://} docker-java transport). | ||
| * <p> | ||
| * The priority is below {@link NpipeSocketClientProviderStrategy} so an existing Docker Desktop or | ||
| * Podman named pipe always wins; wslc is only used as a fallback, and only on Windows when the | ||
| * {@code wslc} CLI is actually present. | ||
| * | ||
| * @deprecated this class is used by the SPI and should not be used directly | ||
| */ | ||
| @Slf4j | ||
| @Deprecated | ||
| public final class WslcSocketClientProviderStrategy extends DockerClientProviderStrategy { | ||
|
|
||
| private static final String SOCKET_LOCATION = "wslc://localhost"; | ||
|
|
||
| public static final int PRIORITY = NpipeSocketClientProviderStrategy.PRIORITY - 10; | ||
|
|
||
| static final String WSLC_EXECUTABLE_ENV = "WSLC_EXECUTABLE"; | ||
|
|
||
| static final String DEFAULT_EXECUTABLE = "wslc.exe"; | ||
|
|
||
| static final long PROBE_TIMEOUT_MS = 10_000; | ||
|
|
||
| @Override | ||
| public TransportConfig getTransportConfig() { | ||
| return TransportConfig.builder().dockerHost(URI.create(SOCKET_LOCATION)).build(); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| @Override | ||
| protected boolean isApplicable() { | ||
| return applies(SystemUtils.IS_OS_WINDOWS); | ||
| } | ||
|
|
||
| @Override | ||
| public String getDescription() { | ||
| return "WSL Containers (wslc) session (" + SOCKET_LOCATION + ")"; | ||
| } | ||
|
|
||
| @Override | ||
| protected int getPriority() { | ||
| return PRIORITY; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Never remembered in {@code ~/.testcontainers.properties}. A persisted strategy is loaded ahead | ||
| * of the priority-sorted ones by {@code getFirstValidStrategy}, so recording this one would let | ||
| * wslc keep winning on later runs even once a Docker Desktop or Podman named pipe is available — | ||
| * defeating the priority that is supposed to keep it a fallback. | ||
| */ | ||
| @Override | ||
| protected boolean isPersistable() { | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Split from {@link #isApplicable()} so the Windows gate can be exercised on any platform: the | ||
| * OS cannot be varied at runtime. | ||
| */ | ||
| static boolean applies(boolean windows) { | ||
| return windows && isWslcAvailable(); | ||
| } | ||
|
|
||
| private static boolean isWslcAvailable() { | ||
| String executable = resolveExecutable(System.getenv(WSLC_EXECUTABLE_ENV)); | ||
| return probe(probeCommand(executable, SystemUtils.IS_OS_WINDOWS), PROBE_TIMEOUT_MS); | ||
| } | ||
|
|
||
| /** | ||
| * @return {@link #DEFAULT_EXECUTABLE} unless {@code configured} names something; a blank | ||
| * {@code WSLC_EXECUTABLE} is treated as unset rather than passed on to fail as a | ||
| * missing executable | ||
| */ | ||
| static String resolveExecutable(String configured) { | ||
| return StringUtils.isBlank(configured) ? DEFAULT_EXECUTABLE : configured; | ||
| } | ||
|
|
||
| /** | ||
| * {@code wslc version} is metadata only: unlike most subcommands it does not start the container | ||
| * VM, which is what makes it usable on the strategy-discovery path. | ||
| * <p> | ||
| * Both streams are discarded, and merged first, so the child can never block on a pipe nobody is | ||
| * draining. {@code Redirect.DISCARD} would say this more directly but is Java 9+, and core main | ||
| * sources compile at {@code release 8}. | ||
| */ | ||
| static ProcessBuilder probeCommand(String executable, boolean windows) { | ||
| return new ProcessBuilder(executable, "version") | ||
| .redirectErrorStream(true) | ||
| .redirectOutput(ProcessBuilder.Redirect.to(new File(windows ? "NUL" : "/dev/null"))); | ||
| } | ||
|
|
||
| /** | ||
| * @return true only if the command ran to completion within {@code timeoutMillis} and exited 0. | ||
| * Every other outcome is a reason not to claim this strategy, and is logged at debug so | ||
| * a half-installed wslc does not fail silently. | ||
| */ | ||
| static boolean probe(ProcessBuilder builder, long timeoutMillis) { | ||
| Process process = null; | ||
| try { | ||
| process = builder.start(); | ||
| if (!process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) { | ||
| log.debug("wslc probe {} did not finish within {}ms", builder.command(), timeoutMillis); | ||
| return false; | ||
| } | ||
| int exitValue = process.exitValue(); | ||
| if (exitValue != 0) { | ||
| log.debug("wslc probe {} exited with {}", builder.command(), exitValue); | ||
| return false; | ||
| } | ||
| return true; | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| log.debug("wslc probe {} was interrupted", builder.command(), e); | ||
| return false; | ||
| } catch (IOException | RuntimeException e) { | ||
| log.debug("wslc probe {} could not be run", builder.command(), e); | ||
| return false; | ||
| } finally { | ||
| if (process != null) { | ||
| // A no-op once the process has exited, so this needs no isAlive() guard. | ||
| process.destroyForcibly(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
core/src/test/java/org/testcontainers/dockerclient/WslcSocketClientProviderStrategyTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| package org.testcontainers.dockerclient; | ||
|
|
||
| import org.apache.commons.lang3.SystemUtils; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.nio.file.Paths; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatNoException; | ||
| import static org.assertj.core.api.Assumptions.assumeThat; | ||
|
|
||
| class WslcSocketClientProviderStrategyTest { | ||
|
|
||
| private final WslcSocketClientProviderStrategy strategy = new WslcSocketClientProviderStrategy(); | ||
|
|
||
| @Test | ||
| void resolvesToTheWslcDockerHost() { | ||
| assertThat(strategy.getTransportConfig().getDockerHost()).hasToString("wslc://localhost"); | ||
| } | ||
|
|
||
| @Test | ||
| void describesItselfWithTheEndpoint() { | ||
| assertThat(strategy.getDescription()).contains("wslc://localhost"); | ||
| } | ||
|
|
||
| @Test | ||
| void sitsBelowTheNamedPipeStrategySoDockerDesktopAlwaysWins() { | ||
| assertThat(strategy.getPriority()) | ||
| .isEqualTo(WslcSocketClientProviderStrategy.PRIORITY) | ||
| .isLessThan(NpipeSocketClientProviderStrategy.PRIORITY); | ||
| } | ||
|
|
||
| @Test | ||
| void neverAppliesOffWindows() { | ||
| assertThat(WslcSocketClientProviderStrategy.applies(false)).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void probesTheEnvironmentWhenOnWindows() { | ||
| // The outcome depends on whether wslc is installed on the machine running the build, so this | ||
| // pins only what is environment-independent: the probe is reached and completes. | ||
| assertThatNoException().isThrownBy(() -> WslcSocketClientProviderStrategy.applies(true)); | ||
| } | ||
|
|
||
| @Test | ||
| void isApplicableCompletesOnAnyPlatform() { | ||
| assertThatNoException().isThrownBy(strategy::isApplicable); | ||
| } | ||
|
|
||
| @Test | ||
| void isNotApplicableOffWindowsEvenIfSomethingCalledWslcExists() { | ||
| assumeThat(SystemUtils.IS_OS_WINDOWS).isFalse(); | ||
| assertThat(strategy.isApplicable()).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void isNotPersistedSoALaterRunCannotSkipTheNamedPipeStrategy() { | ||
| // A persisted strategy is loaded ahead of the priority-sorted ones, which would let wslc | ||
| // keep winning after a Docker Desktop or Podman pipe becomes available. | ||
| assertThat(strategy.isPersistable()).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void skipsTheSocketProbeBecauseThereIsNoConnectableEndpoint() { | ||
| // A stdio bridge has neither a socket file nor a port to connect() to, so test() must defer | ||
| // to the daemon ping in tryOutStrategy rather than warn about an unrecognised scheme. | ||
| assertThat(strategy.test()).isTrue(); | ||
| } | ||
|
|
||
| @Test | ||
| void defaultsTheExecutableWhenTheEnvironmentDoesNotNameOne() { | ||
| assertThat(WslcSocketClientProviderStrategy.resolveExecutable(null)) | ||
| .isEqualTo(WslcSocketClientProviderStrategy.DEFAULT_EXECUTABLE); | ||
| assertThat(WslcSocketClientProviderStrategy.resolveExecutable(" ")) | ||
| .isEqualTo(WslcSocketClientProviderStrategy.DEFAULT_EXECUTABLE); | ||
| } | ||
|
|
||
| @Test | ||
| void honoursAConfiguredExecutable() { | ||
| assertThat(WslcSocketClientProviderStrategy.resolveExecutable("C:\\tools\\wslc.exe")) | ||
| .isEqualTo("C:\\tools\\wslc.exe"); | ||
| } | ||
|
|
||
| @Test | ||
| void asksWslcOnlyForItsVersion() { | ||
| assertThat(WslcSocketClientProviderStrategy.probeCommand("wslc.exe", true).command()) | ||
| .containsExactly("wslc.exe", "version"); | ||
| } | ||
|
|
||
| @Test | ||
| void discardsProbeOutputToThePlatformNullDevice() { | ||
| ProcessBuilder onWindows = WslcSocketClientProviderStrategy.probeCommand("wslc.exe", true); | ||
| ProcessBuilder elsewhere = WslcSocketClientProviderStrategy.probeCommand("wslc", false); | ||
|
|
||
| assertThat(onWindows.redirectOutput().file()).hasName("NUL"); | ||
| assertThat(elsewhere.redirectOutput().file()).hasName("null"); | ||
| assertThat(onWindows.redirectErrorStream()).isTrue(); | ||
| } | ||
|
|
||
| @Test | ||
| void probeSucceedsWhenTheCommandExitsZero() { | ||
| assertThat(WslcSocketClientProviderStrategy.probe(java("-version"), 30_000)).isTrue(); | ||
| } | ||
|
|
||
| @Test | ||
| void probeFailsWhenTheCommandExitsNonZero() { | ||
| assertThat(WslcSocketClientProviderStrategy.probe(java("-XXdefinitelyNotAValidFlag"), 30_000)).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void probeFailsWhenTheExecutableCannotBeRun() { | ||
| ProcessBuilder missing = new ProcessBuilder("wslc-does-not-exist-" + UUID.randomUUID()); | ||
|
|
||
| assertThat(WslcSocketClientProviderStrategy.probe(missing, 30_000)).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void probeFailsWhenTheCommandOutlivesTheTimeout() { | ||
| assertThat(WslcSocketClientProviderStrategy.probe(sleeper(), 200)).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void probeFailsAndRestoresTheInterruptFlagWhenInterrupted() { | ||
| Thread.currentThread().interrupt(); | ||
| try { | ||
| assertThat(WslcSocketClientProviderStrategy.probe(sleeper(), 30_000)).isFalse(); | ||
| assertThat(Thread.currentThread().isInterrupted()) | ||
| .as("interrupt flag restored rather than swallowed") | ||
| .isTrue(); | ||
| } finally { | ||
| // Clear it so the rest of the suite is unaffected. | ||
| Thread.interrupted(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The JVM running the build, which exists on every platform the suite runs on. | ||
| */ | ||
| private static ProcessBuilder java(String... args) { | ||
| String executable = SystemUtils.IS_OS_WINDOWS ? "java.exe" : "java"; | ||
| List<String> command = new ArrayList<>(); | ||
| command.add(Paths.get(System.getProperty("java.home"), "bin", executable).toString()); | ||
| command.addAll(Arrays.asList(args)); | ||
| return discarding(new ProcessBuilder(command)); | ||
| } | ||
|
|
||
| /** | ||
| * A command that outlives any timeout these tests use, so the timeout and interrupt paths are | ||
| * reached deterministically rather than by racing process startup. | ||
| */ | ||
| private static ProcessBuilder sleeper() { | ||
| return discarding( | ||
| SystemUtils.IS_OS_WINDOWS | ||
| ? new ProcessBuilder("cmd", "/c", "ping", "-n", "10", "127.0.0.1") | ||
| : new ProcessBuilder("sleep", "5") | ||
| ); | ||
| } | ||
|
|
||
| private static ProcessBuilder discarding(ProcessBuilder builder) { | ||
| return builder.redirectErrorStream(true).redirectOutput(ProcessBuilder.Redirect.DISCARD); | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: testcontainers/testcontainers-java
Length of output: 625
🏁 Script executed:
Repository: testcontainers/testcontainers-java
Length of output: 35717
🏁 Script executed:
Repository: testcontainers/testcontainers-java
Length of output: 24107
Do not merge while docker-java
3.7.1lacks thewslc://transport.On Windows, a successful
wslc.exe versionprobe makesWslcSocketClientProviderStrategyapplicable. Itstest()path then reachesinfoCmd().exec(), where docker-java3.7.1cannot openwslc://localhost. Update both BOM coordinates when a compatible release is available. Otherwise, do not register the strategy.🤖 Prompt for AI Agents