Skip to content

Commit 5547e0f

Browse files
committed
Add Windows named pipe transport
1 parent 1aac376 commit 5547e0f

26 files changed

Lines changed: 1863 additions & 11 deletions

.github/workflows/maven.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,34 @@ jobs:
8484
EOF
8585
- name: Build with Maven
8686
run: ./mvnw -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false -Djunit.jupiter.execution.parallel.enabled=false -Dhc.build.toolchain.version="${HC_BUILD_TOOLCHAIN_VERSION}" -Pdocker
87+
88+
# Windows Named Pipe tests — requires Windows for \\.\pipe\... support
89+
windows-npipe:
90+
91+
runs-on: windows-latest
92+
strategy:
93+
matrix:
94+
java: [ 17, 21 ]
95+
fail-fast: false
96+
97+
steps:
98+
- uses: actions/checkout@v5
99+
- uses: actions/cache@v4
100+
with:
101+
path: ~/.m2/repository
102+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
103+
restore-keys: |
104+
${{ runner.os }}-maven-
105+
- name: Set up JDK ${{ matrix.java }}
106+
uses: actions/setup-java@v5
107+
with:
108+
distribution: 'temurin'
109+
java-version: ${{ matrix.java }}
110+
- name: Build all modules (compile only)
111+
run: ./mvnw.cmd -V --file pom.xml --no-transfer-progress -P-use-toolchains install -DskipTests
112+
- name: Run Named Pipe unit tests
113+
timeout-minutes: 10
114+
run: ./mvnw.cmd -V --file pom.xml --no-transfer-progress -P-use-toolchains -pl httpclient5-testing "-DtrimStackTrace=false" "-Djunit.jupiter.execution.parallel.enabled=false" "-Dtest=NpipeIntegrationTests" test
115+
- name: Run Named Pipe Docker integration tests
116+
timeout-minutes: 10
117+
run: ./mvnw.cmd -V --file pom.xml --no-transfer-progress -P-use-toolchains -pl httpclient5-testing "-DtrimStackTrace=false" "-Djunit.jupiter.execution.parallel.enabled=false" "-Dtest=WindowsNamedPipeDockerIT" -Pdocker verify

httpclient5-testing/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@
8383
<scope>test</scope>
8484
<type>pom</type>
8585
</dependency>
86+
<dependency>
87+
<groupId>net.java.dev.jna</groupId>
88+
<artifactId>jna-platform</artifactId>
89+
<scope>test</scope>
90+
</dependency>
8691
<dependency>
8792
<groupId>org.junit.jupiter</groupId>
8893
<artifactId>junit-jupiter</artifactId>
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.testing.compatibility;
28+
29+
import org.apache.hc.client5.http.classic.methods.HttpGet;
30+
import org.apache.hc.client5.http.config.RequestConfig;
31+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
32+
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
33+
import org.apache.hc.core5.http.HttpStatus;
34+
import org.apache.hc.core5.http.io.entity.EntityUtils;
35+
import org.junit.jupiter.api.Assertions;
36+
import org.junit.jupiter.api.DisplayName;
37+
import org.junit.jupiter.api.Test;
38+
import org.junit.jupiter.api.condition.EnabledOnOs;
39+
import org.junit.jupiter.api.condition.OS;
40+
import org.testcontainers.DockerClientFactory;
41+
import org.testcontainers.junit.jupiter.Testcontainers;
42+
43+
/**
44+
* Integration test that validates Windows Named Pipe (npipe://) connectivity
45+
* to Docker Desktop via {@code \\.\pipe\docker_engine}.
46+
* <p>
47+
* This test requires:
48+
* <ul>
49+
* <li>Windows OS</li>
50+
* <li>Docker Desktop running (which exposes {@code \\.\pipe\docker_engine})</li>
51+
* </ul>
52+
* <p>
53+
* The test uses Testcontainers to verify Docker is available, then exercises the
54+
* HttpClient Named Pipe transport by querying Docker's REST API directly through
55+
* the named pipe — the same way Docker CLI communicates with the daemon on Windows.
56+
* </p>
57+
* <p>
58+
* This is the Windows equivalent of connecting to {@code /var/run/docker.sock}
59+
* on Linux/macOS.
60+
* </p>
61+
*
62+
* @since 5.7
63+
*/
64+
@Testcontainers(disabledWithoutDocker = true)
65+
@EnabledOnOs(OS.WINDOWS)
66+
class WindowsNamedPipeDockerIT {
67+
68+
private static final String DOCKER_PIPE = "\\\\.\\pipe\\docker_engine";
69+
70+
@Test
71+
@DisplayName("GET /version via named pipe to Docker Desktop")
72+
void testDockerVersionViaNpipe() throws Exception {
73+
// Verify Docker is actually available (Testcontainers check)
74+
Assertions.assertTrue(DockerClientFactory.instance().isDockerAvailable(),
75+
"Docker must be available for this test");
76+
77+
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
78+
final HttpGet httpGet = new HttpGet("http://localhost/version");
79+
httpGet.setConfig(RequestConfig.custom()
80+
.setNamedPipe(DOCKER_PIPE)
81+
.build());
82+
83+
client.execute(httpGet, response -> {
84+
Assertions.assertEquals(HttpStatus.SC_OK, response.getCode(),
85+
"Docker /version should return 200 OK");
86+
final String body = EntityUtils.toString(response.getEntity());
87+
Assertions.assertNotNull(body, "Response body should not be null");
88+
Assertions.assertTrue(body.contains("ApiVersion"),
89+
"Docker /version response should contain ApiVersion");
90+
return null;
91+
});
92+
}
93+
}
94+
95+
@Test
96+
@DisplayName("GET /info via named pipe to Docker Desktop")
97+
void testDockerInfoViaNpipe() throws Exception {
98+
Assertions.assertTrue(DockerClientFactory.instance().isDockerAvailable(),
99+
"Docker must be available for this test");
100+
101+
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
102+
final HttpGet httpGet = new HttpGet("http://localhost/info");
103+
httpGet.setConfig(RequestConfig.custom()
104+
.setNamedPipe(DOCKER_PIPE)
105+
.build());
106+
107+
client.execute(httpGet, response -> {
108+
Assertions.assertEquals(HttpStatus.SC_OK, response.getCode(),
109+
"Docker /info should return 200 OK");
110+
final String body = EntityUtils.toString(response.getEntity());
111+
Assertions.assertNotNull(body, "Response body should not be null");
112+
Assertions.assertTrue(body.contains("Containers"),
113+
"Docker /info response should contain Containers field");
114+
return null;
115+
});
116+
}
117+
}
118+
119+
@Test
120+
@DisplayName("GET /containers/json via named pipe to Docker Desktop")
121+
void testDockerContainersViaNpipe() throws Exception {
122+
Assertions.assertTrue(DockerClientFactory.instance().isDockerAvailable(),
123+
"Docker must be available for this test");
124+
125+
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
126+
final HttpGet httpGet = new HttpGet("http://localhost/containers/json");
127+
httpGet.setConfig(RequestConfig.custom()
128+
.setNamedPipe(DOCKER_PIPE)
129+
.build());
130+
131+
client.execute(httpGet, response -> {
132+
Assertions.assertEquals(HttpStatus.SC_OK, response.getCode(),
133+
"Docker /containers/json should return 200 OK");
134+
final String body = EntityUtils.toString(response.getEntity());
135+
Assertions.assertNotNull(body, "Response body should not be null");
136+
// Should be a JSON array (possibly empty)
137+
Assertions.assertTrue(body.startsWith("["),
138+
"Docker /containers/json should return a JSON array");
139+
return null;
140+
});
141+
}
142+
}
143+
144+
@Test
145+
@DisplayName("Start Testcontainer and verify it appears via named pipe")
146+
void testTestcontainersViaNpipe() throws Exception {
147+
Assertions.assertTrue(DockerClientFactory.instance().isDockerAvailable(),
148+
"Docker must be available for this test");
149+
150+
// Start a simple container via Testcontainers
151+
try (final org.testcontainers.containers.GenericContainer<?> container =
152+
new org.testcontainers.containers.GenericContainer<>("alpine:3.19")
153+
.withCommand("sleep", "30")) {
154+
container.start();
155+
final String containerId = container.getContainerId();
156+
157+
// Now query Docker API through our named pipe transport
158+
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
159+
final HttpGet httpGet = new HttpGet(
160+
"http://localhost/containers/" + containerId + "/json");
161+
httpGet.setConfig(RequestConfig.custom()
162+
.setNamedPipe(DOCKER_PIPE)
163+
.build());
164+
165+
client.execute(httpGet, response -> {
166+
Assertions.assertEquals(HttpStatus.SC_OK, response.getCode(),
167+
"Container inspect should return 200 OK");
168+
final String body = EntityUtils.toString(response.getEntity());
169+
Assertions.assertTrue(body.contains(containerId),
170+
"Container inspect should reference the container ID");
171+
Assertions.assertTrue(body.contains("Running"),
172+
"Container should be in Running state");
173+
return null;
174+
});
175+
}
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)